네트워킹 최적화

Cloud Run 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 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 Language 또는 Cloud Spanner)에서도 같은 방식을 사용할 수 있습니다. 특정 클라이언트 라이브러리의 현재 구현 방식에 따라 성능 개선의 정도가 다를 수 있습니다.

Pub/Sub 클라이언트 객체를 만들면 호출당 연결은 1회, DNS 쿼리는 2회가 발생합니다. 불필요한 연결과 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 및 인터넷으로의 연결 스트림은 기본 인프라가 다시 시작되거나 업데이트될 때 종료될 수 있습니다. 애플리케이션이 장기적으로 지속되는 연결을 재사용하는 경우 끊어진 연결을 재사용하지 않도록 연결을 다시 설정하도록 애플리케이션을 구성하는 것이 좋습니다.

함수의 부하 테스트

함수에서 평균적으로 수행하는 연결 수를 측정하려면 HTTP 함수로 배포하고 성능 테스트 프레임워크를 사용하여 특정 QPS에서 호출하면 됩니다. 사용 가능한 방법 중 하나는 Artillery이며 한 줄로 호출할 수 있습니다.

$ artillery quick -d 300 -r 30 URL

이 명령어는 300초 동안 30QPS로 해당 URL을 가져옵니다.

테스트를 수행한 후 Cloud 콘솔의 Cloud Run Functions API 할당량 페이지에서 연결 할당량의 사용량을 확인하세요. 사용량이 지속적으로 30 내외라면 호출할 때마다 1번 연결하는 것이고, 30의 배수라면 여러 번 연결하는 것입니다. 코드를 최적화한 후에는 테스트를 시작할 때만 10~30 정도의 적은 연결 수가 표시되어야 합니다.

또한 같은 페이지의 CPU 할당량 플롯에서 최적화 전후의 CPU 비용을 비교할 수 있습니다.