最佳化網路連線 (第 1 代)

Cloud Run 函式簡單易用,可讓您快速開發程式碼,並在無伺服器環境中執行。在中等規模情況下,函式的執行費用較低,最佳化程式碼似乎顯得不那麼迫切。但隨著開發規模的增長,最佳化程式碼就會變得越來越重要。

本文件說明如何針對您的函式最佳化網路。最佳化網路的部分優點如下所述:

  • 可減少 CPU 在每次呼叫函式時建立新連線花費的時間。
  • 降低連線或 DNS 配額用盡的可能性。

維持持續連線

本節提供如何在函式中保持永久連線的範例。如果不能永久連線,會導致快速用盡連線配額。

本節介紹下列情境:

  • HTTP/S
  • Google API

HTTP/S 要求

下列最佳化程式碼片段說明如何維護持續性連線,而非在每次函式叫用時建立新連線:

Node.js

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

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

const functions = require('@google-cloud/functions-framework');

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.
 */
functions.http('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 fetchResponse = await fetch(url, {agent});
    const text = await fetchResponse.text();

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

Python

import functions_framework
import requests

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


@functions_framework.http
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"

	"github.com/GoogleCloudPlatform/functions-framework-go/functions"
)

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,
}

func init() {
	functions.HTTP("MakeRequest", MakeRequest)
}

// 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")
}

PHP

建議使用 Guzzle PHP HTTP 架構傳送 HTTP 要求,因為這個架構會自動處理持續性連線。

存取 Google API

下列範例使用 Cloud Pub/Sub,但這個方法也適用於其他用戶端程式庫,例如 Cloud Natural LanguageCloud Spanner。請注意,效能的提升可能取決於特定用戶端程式庫的目前實作。

建立 Pub/Sub 用戶端物件時,每次叫用都會產生一個連線和兩個 DNS 查詢。為避免不必要的連線和 DNS 查詢,請在全域範圍中建立 Pub/Sub 用戶端物件,如下列範例所示:

Node.js

const functions = require('@google-cloud/functions-framework');
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.
 */
functions.http('gcpApiCall', (req, res) => {
  const topic = pubsub.topic(req.body.topic);

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

Python

import os

import functions_framework
from google.cloud import pubsub_v1


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


@functions_framework.http
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>.
    """

    """
    The `GCP_PROJECT` environment variable is set automatically in the Python 3.7 runtime.
    In later runtimes, it must be specified by the user upon function deployment.
    See this page for more information:
        https://cloud.google.com/functions/docs/configuring/env-var#python_37_and_go_111
    """
    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 "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"
    "sync"
    "cloud.google.com/go/pubsub"
    "github.com/GoogleCloudPlatform/functions-framework-go/functions"
)
// client is a global Pub/Sub client, initialized once per instance.
var client *pubsub.Client
var once sync.Once
// createClient creates the global pubsub Client
func createClient() {
    // GOOGLE_CLOUD_PROJECT is a user-set environment variable.
    var projectID = os.Getenv("GOOGLE_CLOUD_PROJECT")
    // 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)
    }
}
func init() {
    // register http function
    functions.HTTP("PublishMessage", PublishMessage)
}
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) {
    // use of sync.Once ensures client is only created once.
    once.Do(createClient)
    // 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)
}

重設外送連線

函式與 VPC 和網際網路之間的連線串流,可能會在基礎架構重新啟動或更新時終止並取代。如果應用程式會重複使用長期連線,建議您將應用程式設為重新建立連線,避免重複使用已失效的連線。

傳出要求逾時

系統可在傳出通訊端閒置 10 分鐘後將其收回,如有任何通訊端相關作業,通訊端有效時間會延長 10 分鐘。

對函式執行負載測試

如要評估函式平均執行的連線數,請將函式部署為 HTTP 函式,並使用效能測試架構以特定 QPS 叫用函式。其中一個可能的選擇是 Artillery,您可以使用單行叫用:

$ artillery quick -d 300 -r 30 URL

這個指令會以 30 QPS 的頻率擷取指定網址 300 秒。

執行測試後,請前往 Google Cloud 控制台的 Cloud Run Functions API 配額頁面,查看連線配額用量。如果用量持續在 30 左右 (或 30 的倍數),表示您在每次呼叫時建立一或多個連線。最佳化程式碼之後,您應該會發現只有在測試開始時,建立了一些連線 (數目大概是10-30 個)。

您也可以在同一頁面的 CPU 配額圖中比較最佳化前後的 CPU 費用。