אופטימיזציה של הרשת (דור ראשון)

הפשטות של פונקציות Cloud Run מאפשרת לכם לפתח קוד במהירות ולהריץ אותו בסביבה ללא שרת. בסביבה מתונה, העלות של הפעלת פונקציות נמוכה, ולכן יכול להיות שלא תראו חשיבות רבה לאופטימיזציה של הקוד. אבל ככל שהפריסה גדלה, חשוב יותר לבצע אופטימיזציה של הקוד.

במאמר הזה מוסבר איך לבצע אופטימיזציה של הרשתות עבור הפונקציות. אלה כמה מהיתרונות של אופטימיזציה של הרשת:

  • צמצום הזמן שנדרש למעבד כדי ליצור חיבורים חדשים בכל קריאה לפונקציה.
  • כדי להקטין את הסיכוי לחריגה ממכסות החיבור או ה-DNS.

שמירה על חיבורים מתמידים

בקטע הזה מופיעות דוגמאות לאופן שבו אפשר לשמור על חיבורים מתמשכים בפונקציה. אם לא תעשו את זה, יכול להיות שתגיעו במהירות למכסת החיבורים.

בקטע הזה מוסבר על התרחישים הבאים:

  • HTTP/S
  • Google APIs

בקשות 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!"

המשך


// 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 Framework כדי לשלוח בקשות HTTP, כי הוא מטפל בחיבורים קבועים באופן אוטומטי.

גישה אל Google APIs

בדוגמה הבאה נעשה שימוש ב-Cloud Pub/Sub, אבל הגישה הזו מתאימה גם לספריות לקוח אחרות, למשל Cloud Natural Language או Cloud 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"

המשך

// 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 דקות של חוסר פעילות. כל פעולת socket שומרת על ה-socket פעיל למשך 10 דקות נוספות.

בדיקת טעינה של הפונקציה

כדי למדוד כמה חיבורים הפונקציה מבצעת בממוצע, צריך לפרוס אותה כפונקציית HTTP ולהשתמש במסגרת לבדיקת ביצועים כדי להפעיל אותה ב-QPS מסוים. אפשרות אחת היא Artillery, שאפשר להפעיל אותה באמצעות שורה אחת:

$ artillery quick -d 300 -r 30 URL

הפקודה הזו מאחזרת את כתובת ה-URL הנתונה ב-30 QPS למשך 300 שניות.

אחרי שמבצעים את הבדיקה, בודקים את השימוש במכסת החיבורים בדף המכסות של Cloud Run functions API במסוף Google Cloud . אם השימוש הוא באופן עקבי בסביבות 30 (או כפולה של 30), אתם יוצרים חיבור אחד (או כמה חיבורים) בכל הפעלה. אחרי שמבצעים אופטימיזציה של הקוד, אמורות להתרחש כמה (10-30) התחברויות רק בתחילת הבדיקה.

אפשר גם להשוות את עלות השימוש במעבד לפני ואחרי האופטימיזציה בתרשים של מכסת השימוש במעבד באותו דף.