네트워킹 최적화

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

함수의 부하 테스트

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

$ artillery quick -d 300 -r 30 URL

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

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

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