ネットワークの最適化

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

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

  • 各関数呼び出しで新しいアウトバウンド接続を確立するために要する CPU 時間を短縮する。
  • 接続や DNS 割り当てが不足する可能性を減らす。

持続的な接続を維持する

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

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

  • HTTP/S
  • Google API

HTTP/S リクエスト

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

Node.js

const http = require('http');
const functions = require('firebase-functions');

// Setting the `keepAlive` option to `true` keeps
// connections open between function invocations
const agent = new http.Agent({keepAlive: true});

exports.function = functions.https.onRequest((request, response) => {
    req = http.request({
        host: '',
        port: 80,
        path: '',
        method: 'GET',
        agent: agent, // Holds the connection open after the first invocation
    }, res => {
        let rawData = '';
        res.setEncoding('utf8');
        res.on('data', chunk => { rawData += chunk; });
        res.on('end', () => {
            response.status(200).send(`Data: ${rawData}`);
        });
    });
    req.on('error', e => {
        response.status(500).send(`Error: ${e.message}`);
    });
    req.end();
});

Python

from firebase_functions import https_fn
import requests

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

@https_fn.on_request()
def connection_pooling(request):

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

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

この HTTP 関数は、接続プールを使用して HTTP リクエストを行います。 リクエスト オブジェクト(flask.Request)を受け取り、レスポンス テキストを返します。または、make_response を使用して Response オブジェクトに変換できる値のセットを返します。

Google API へのアクセス

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

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

node.js

const PubSub = require('@google-cloud/pubsub');
const functions = require('firebase-functions');
const pubsub = PubSub();

exports.function = functions.https.onRequest((req, res) => {
    const topic = pubsub.topic('');

    topic.publish('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 firebase_functions import https_fn
from google.cloud import pubsub_v1

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

@https_fn.on_request()
def gcp_api_call(request):

    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 = b"Test message"
    pubsub.publish(topic_path, data=data)

    return https_fn.Response("1 message published")
    

この HTTP 関数は、キャッシュに保存されたクライアント ライブラリ インスタンスを使用して、関数の呼び出しごとに必要な接続数を減らします。 リクエスト オブジェクト(flask.Request)を受け取り、レスポンス テキストを返します。または、make_response を使用して Response オブジェクトに変換できる値のセットを返します。

GCP_PROJECT 環境変数は、Python 3.7 ランタイムで自動的に設定されます。以降のランタイムでは、関数のデプロイ時に必ず指定してください。環境変数を構成するをご覧ください。

送信接続

アウトバウンド リクエストのタイムアウト

関数から VPC ネットワークへのリクエストでは、10 分間のアイドル時間の後にタイムアウトが発生します。関数からインターネットへのリクエストの場合は、20 分間のアイドル時間の後にタイムアウトが発生します。

アウトバウンド接続のリセット

関数から VPC とインターネットの両方への接続ストリームは、基盤となるインフラストラクチャの再起動や更新が発生すると、停止して置き替えられる場合があります。アプリケーションで長期間の接続を再利用する場合は、切断された接続の再利用を避けるため、接続を再確立してアプリケーションを構成することをおすすめします。

関数の負荷テスト

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

$ artillery quick -d 300 -r 30 URL

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

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

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