ネットワークの最適化

Cloud Functions を使用すると、サーバーレス環境でコードをすばやく開発して実行できます。中程度の規模では、ファンクションのランニング コストが低いため、コードの最適化の重要性が見過ごされがちです。しかし、デプロイが拡大するにつれて、コードの最適化の重要性が高まります。

このドキュメントでは、関数用にネットワークを最適化する方法について説明します。ネットワークの最適化には次のようなメリットがあります。

  • 各ファンクション呼び出しで新しい接続を確立するために要する CPU 時間を短縮する。
  • 接続や DNS 割り当てが不足する可能性を減らす。

持続的な接続を維持する

このセクションでは、ファンクション内で持続的な接続を維持する方法の例を示して説明します。持続的な接続を維持できないと、接続割り当てがすぐに使い果たされてしまう場合があります。

このセクションでは、次のシナリオを取り上げます。

  • HTTP/S
  • Google API

HTTP/S リクエスト

以下の最適化コード スニペットでは、関数が呼び出されるたびに新しい接続を作成するのではなく、永続的な接続を維持する方法を示しています。

Node.js

const fetch = require('node-fetch');

const http = require('http');
const https = require('https');

const httpAgent = new http.Agent({keepAlive: true});
const httpsAgent = new https.Agent({keepAlive: true});

/**
 * HTTP Cloud Function that caches an HTTP agent to pool HTTP connections.
 *
 * @param {Object} req Cloud Function request context.
 * @param {Object} res Cloud Function response context.
 */
exports.connectionPooling = async (req, res) => {
  try {
    // TODO(optional): replace this with your own URL.
    const url = 'https://www.example.com/';

    // Select the appropriate agent to use based on the URL.
    const agent = url.includes('https') ? httpsAgent : httpAgent;

    const {data} = await fetch(url, {agent});
    const text = await data.text();

    res.status(200).send(`Data: ${text}`);
  } catch (err) {
    res.status(500).send(`Error: ${err.message}`);
  }
};

Python

import requests

# Create a global HTTP session (which provides connection pooling)
session = requests.Session()

def connection_pooling(request):
    """
    HTTP Cloud Function that uses a connection pool to make HTTP requests.
    Args:
        request (flask.Request): The request object.
        <http://flask.pocoo.org/docs/1.0/api/#flask.Request>
    Returns:
        The response text, or any set of values that can be turned into a
        Response object using `make_response`
        <http://flask.pocoo.org/docs/1.0/api/#flask.Flask.make_response>.
    """

    # The URL to send the request to
    url = 'http://example.com'

    # Process the request
    response = session.get(url)
    response.raise_for_status()
    return 'Success!'

Go


// Package http provides a set of HTTP Cloud Functions samples.
package http

import (
	"fmt"
	"net/http"
	"time"
)

var urlString = "https://example.com"

// client is used to make HTTP requests with a 10 second timeout.
// http.Clients should be reused instead of created as needed.
var client = &http.Client{
	Timeout: 10 * time.Second,
}

// MakeRequest is an example of making an HTTP request. MakeRequest uses a
// single http.Client for all requests to take advantage of connection
// pooling and caching. See https://godoc.org/net/http#Client.
func MakeRequest(w http.ResponseWriter, r *http.Request) {
	resp, err := client.Get(urlString)
	if err != nil {
		http.Error(w, "Error making request", http.StatusInternalServerError)
		return
	}
	if resp.StatusCode != http.StatusOK {
		msg := fmt.Sprintf("Bad StatusCode: %d", resp.StatusCode)
		http.Error(w, msg, http.StatusInternalServerError)
		return
	}
	fmt.Fprintf(w, "ok")
}

Google API へのアクセス

以下の例では Cloud Pub/Sub を使用していますが、このアプローチは Cloud Natural LanguageCloud Spanner など、他のクライアント ライブラリでも使用できます。パフォーマンスの改善度は、特定のクライアント ライブラリの現在の実装状態によって変わることがあります。

PubSub クライアント オブジェクトを作成すると、呼び出しごとに 1 つの接続と 2 つの DNS クエリが行われます。不要な接続と DNS クエリを行わないようにするには、下の例のように、グローバル スコープで PubSub クライアント オブジェクトを作成します。

Node.js

const {PubSub} = require('@google-cloud/pubsub');
const pubsub = new PubSub();

/**
 * HTTP Cloud Function that uses a cached client library instance to
 * reduce the number of connections required per function invocation.
 *
 * @param {Object} req Cloud Function request context.
 * @param {Object} req.body Cloud Function request context body.
 * @param {String} req.body.topic The Cloud Pub/Sub topic to publish to.
 * @param {Object} res Cloud Function response context.
 */
exports.gcpApiCall = (req, res) => {
  const topic = pubsub.topic(req.body.topic);

  topic.publish(Buffer.from('Test message'), err => {
    if (err) {
      res.status(500).send(`Error publishing the message: ${err}`);
    } else {
      res.status(200).send('1 message published');
    }
  });
};

Python

import os
from google.cloud import pubsub_v1

# Create a global Pub/Sub client to avoid unneeded network activity
pubsub = pubsub_v1.PublisherClient()

def gcp_api_call(request):
    """
    HTTP Cloud Function that uses a cached client library instance to
    reduce the number of connections required per function invocation.
    Args:
        request (flask.Request): The request object.
    Returns:
        The response text, or any set of values that can be turned into a
        Response object using `make_response`
        <http://flask.pocoo.org/docs/1.0/api/#flask.Flask.make_response>.
    """

    project = os.getenv('GCP_PROJECT')
    request_json = request.get_json()

    topic_name = request_json['topic']
    topic_path = pubsub.topic_path(project, topic_name)

    # Process the request
    data = 'Test message'.encode('utf-8')
    pubsub.publish(topic_path, data=data)

    return '1 message published'

Go


// Package contexttip is an example of how to use Pub/Sub and context.Context in
// a Cloud Function.
package contexttip

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"

	"cloud.google.com/go/pubsub"
)

// GOOGLE_CLOUD_PROJECT is a user-set environment variable.
var projectID = os.Getenv("GOOGLE_CLOUD_PROJECT")

// client is a global Pub/Sub client, initialized once per instance.
var client *pubsub.Client

func init() {
	// err is pre-declared to avoid shadowing client.
	var err error

	// client is initialized with context.Background() because it should
	// persist between function invocations.
	client, err = pubsub.NewClient(context.Background(), projectID)
	if err != nil {
		log.Fatalf("pubsub.NewClient: %v", err)
	}
}

type publishRequest struct {
	Topic   string `json:"topic"`
	Message string `json:"message"`
}

// PublishMessage publishes a message to Pub/Sub. PublishMessage only works
// with topics that already exist.
func PublishMessage(w http.ResponseWriter, r *http.Request) {
	// Parse the request body to get the topic name and message.
	p := publishRequest{}

	if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
		log.Printf("json.NewDecoder: %v", err)
		http.Error(w, "Error parsing request", http.StatusBadRequest)
		return
	}

	if p.Topic == "" || p.Message == "" {
		s := "missing 'topic' or 'message' parameter"
		log.Println(s)
		http.Error(w, s, http.StatusBadRequest)
		return
	}

	m := &pubsub.Message{
		Data: []byte(p.Message),
	}
	// Publish and Get use r.Context() because they are only needed for this
	// function invocation. If this were a background function, they would use
	// the ctx passed as an argument.
	id, err := client.Topic(p.Topic).Publish(r.Context(), m).Get(r.Context())
	if err != nil {
		log.Printf("topic(%s).Publish.Get: %v", p.Topic, err)
		http.Error(w, "Error publishing message", http.StatusInternalServerError)
		return
	}
	fmt.Fprintf(w, "Message published: %v", id)
}

関数の負荷テスト

関数で実行する接続の平均数を測定するには、HTTP 関数としてデプロイしてから、パフォーマンス テスト フレームワークを使用して特定の QPS で呼び出します。1 つの選択肢として Artillery があります。これは次のように 1 行で呼び出すことができます。

$ artillery quick -d 300 -r 30 URL

このコマンドは、指定された URL を 30 QPS で 300 秒間フェッチします。

テストを実行した後、Cloud Console の Cloud Functions API 割り当てページで接続割り当ての使用状況を確認します。使用状況が常に 30(またはその倍数)前後の場合、すべての呼び出しで 1 つ(または複数)の接続が確立されています。コードを最適化した後は、いくつかの接続(10〜30)がテストの開始時にのみ発生していることがわかります。

同じページの CPU 割り当てプロットで、最適化前後の CPU コストを比較することもできます。