ネットワークの最適化

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

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

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

持続的な接続を維持する

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

このセクションでは、HTTP / HTTPSAxiosSuperagent のパッケージの使用について、および Google API へのアクセスについて説明します。

HTTP / HTTPS パッケージを使用した HTTP リクエスト

このセクションでは、HTTP リクエストを作成する関数を HTTP / HTTPS パッケージを使って最適化する方法について、最適化の前と後の例を示して説明します。

最適化前

次の関数は、すべての関数呼び出しで新しい接続を実行します。

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

exports.function = functions.https.onRequest((request, response) => {
    req = http.request({
        host: '<HOST>',
        port: 80,
        path: '<PATH>',
        method: 'GET',
    }, (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();
});

最適化後

上記のコード スニペットを最適化した以下のバージョンでは、keep-alive オプション付きのカスタム HTTP エージェントを使用して永続的な接続を維持します。

const http = require('http');
const functions = require('firebase-functions');
const agent = new http.Agent({keepAlive: true});

exports.function = functions.https.onRequest((request, response) => {
    req = http.request({
        host: '<HOST>',
        port: 80,
        path: '<PATH>',
        method: 'GET',
        agent: agent,
    }, (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();
});

同じアプローチを HTTPS でも使用できますが、この場合は https.Agent だけを使用します。http.Agent ではありません。

Axios パッケージを使用した HTTP リクエスト

このセクションでは、HTTP リクエストを作成する関数を Axios パッケージを使って最適化する方法について、最適化の前と後の例を示して説明します。

最適化前

次の関数は、呼び出しごとに 1 つの接続と 2 つの DNS 解決が必要です。

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

exports.function = functions.https.onRequest((request, response) => {
    axios({
        method: 'get',
        url: '<URL>',
        responseType: 'text'
    }).then(function(response) {
        res.status(200).send(`Data: ${response.data}`);
    });
});

Package.json

{
    "dependencies": {
        "axios": "^0.15.3"
    }
}

最適化後

グローバル Axios インスタンスとカスタム HTTP エージェントを作成して持続的な接続を維持すると、接続数を減らすことができます。次に示す安定した状態では、関数呼び出しごとの接続や DNS クエリがありません。

const axios = require('axios');
const functions = require('firebase-functions');
const https = require('https');

const instance = axios.create({
    baseURL: '<URL>',
    timeout: 10000,
});

const requestConfig = {
  method: 'get',
  url: '',
  httpsAgent: new https.Agent({ keepAlive: true }),
};

exports.function = functions.https.onRequest((request, response) => {
    return instance.request(requestConfig).then((response) => {
        console.log(`Data: ${response.data}`);
        res.status(200).send(`Data: ${response.data}`);
    });
});

HTTP でも同様のアプローチを使用できます。

Superagent パッケージを使用した HTTP リクエスト

このセクションでは、HTTP リクエストを作成する関数を Superagent パッケージを使って最適化する方法について、最適化の前と後の例を示して説明します。

最適化前

次の関数は、呼び出しごとに 1 つの接続と 2 つの DNS解決を使用します。

const request = require('superagent');

exports.function = functions.https.onRequest((request, response) => {
    request
        .get('<URL>')
        .end((err, response) => {
            res.status(200).send(`Data: ${response.text}`);
    });
});

最適化後

永続的な接続を維持するには、リクエストごとにカスタム HTTP エージェントを指定するだけです。

const request = require('superagent');
const functions = require('firebase-functions');

exports.function = functions.https.onRequest((request, response) => {
    request
        .get('<URL>')
        .end((err, response) => {
            res.status(200).send(`Data: ${response.text}`);
    });
});

Google API へのアクセス

このセクションでは、Google API にアクセスする関数を最適化する方法について、最適化の前と後の例を示して説明します。

最適化前

この例では Cloud Pub/Sub を使用していますが、このアプローチは Language API クライアントCloud Spanner などの他のクライアント ライブラリでも使用できます。ただし、効果は特定のクライアント ライブラリの現在の実装状態によって変わることがあるので注意してください。

次のコードは、呼び出しごとに 1 つの接続と 2 つの DNS クエリを実行します。

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

exports.function = functions.https.onRequest((request, response) => {
    const pubsub = PubSub();
    const topic = pubsub.topic('<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');
        }
    });
});

最適化後

不要な接続と DNS クエリを削除するには、グローバル スコープで pubsub オブジェクトを作成するだけです。

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

exports.function = functions.https.onRequest((request, response) => {
    const topic = pubsub.topic('<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');
        }
    });
});

関数をテストする

関数で実行する接続の平均数を測定するには、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 コストを比較することもできます。

フィードバックを送信...

ご不明な点がありましたら、Google のサポートページをご覧ください。