Netzwerke optimieren

Die Einfachheit von Cloud Functions ermöglicht Ihnen, Code schnell zu entwickeln und in einer serverlosen Umgebung auszuführen. Bei mittlerer Skalierung sind die Kosten für das Ausführen von Funktionen gering und es besteht möglicherweise kein unmittelbarer Bedarf dafür, den Code zu optimieren. Je größer Ihre Bereitstellung jedoch wird, desto wichtiger wird auch die Optimierung des Codes.

In diesem Dokument wird beschrieben, wie Sie das Netzwerk für Ihre Funktionen optimieren. Eine Netzwerkoptimierung bieten unter anderem folgende Vorteile:

  • Sie können die CPU-Zeit reduzieren, die für das Erstellen neuer Verbindungen bei jedem Funktionsaufruf benötigt wird.
  • Sie können die Wahrscheinlichkeit verringern, dass Kontingente für Verbindungen oder DNS aufgebraucht werden.

Persistente Verbindungen aufrechterhalten

In diesem Abschnitt wird anhand von Beispielen gezeigt, wie Sie in einer Funktion persistente Verbindungen aufrechterhalten. Ohne persistente Verbindungen können die Verbindungskontingente schnell aufgebraucht sein.

Die folgenden Szenarien werden in diesem Abschnitt behandelt:

  • HTTP/S
  • Google APIs

HTTP/S-Anfragen

Mit dem folgenden optimierten Code-Snippet wird veranschaulicht, wie Sie persistente Verbindungen aufrechterhalten, anstatt bei jedem Funktionsaufruf eine neue Verbindung zu erstellen:

Node.js

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

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

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

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

Python

import requests

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

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

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

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

Auf Google APIs zugreifen

Im folgenden Beispiel wird Cloud Pub/Sub eingesetzt. Der Ansatz funktioniert jedoch auch für andere Clientbibliotheken, z. B. Cloud Natural Language oder Cloud Spanner. Die erzielten Leistungsverbesserungen können von der aktuellen Implementierung bestimmter Clientbibliotheken abhängen.

Beim Erstellen eines PubSub-Clientobjekts werden pro Aufruf eine Verbindung und zwei DNS-Abfragen erzeugt. Erstellen Sie das PubSub-Clientobjekt global, um unnötige Verbindungen und DNS-Abfragen zu vermeiden, wie im folgenden Beispiel gezeigt:

Node.js

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.
 */
exports.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
from google.cloud import pubsub_v1

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

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>.
    """

    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 = 'Test message'.encode('utf-8')
    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"

	"cloud.google.com/go/pubsub"
)

// GOOGLE_CLOUD_PROJECT is a user-set environment variable.
var projectID = os.Getenv("GOOGLE_CLOUD_PROJECT")

// client is a global Pub/Sub client, initialized once per instance.
var client *pubsub.Client

func init() {
	// 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)
	}
}

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

Lasttests für eine Funktion ausführen

Wenn Sie messen möchten, wie viele Verbindungen im Durchschnitt von einer Funktion erstellt werden, können Sie sie einfach als HTTP-Funktion bereitstellen und in einem Leistungstest-Framework bei einer bestimmten Anzahl von Abfragen pro Sekunde aufrufen. Dazu können Sie z. B. Artillery verwenden, das sich mit einer einzigen Zeile aufrufen lässt:

$ artillery quick -d 300 -r 30 URL

Mit diesem Befehl wird die angegebene URL 300 Sekunden lang bei 30 Abfragen pro Sekunde abgerufen.

Nach dem Test überprüfen Sie die Nutzungswerte Ihres Verbindungskontingents in der Cloud Console auf der Kontingentseite der Cloud Functions API. Wenn die Nutzung beständig bei 30 Abfragen oder einem Vielfachen davon liegt, erstellt die Funktion mit jedem Aufruf eine (oder mehrere) Verbindungen. Nach der Optimierung des Codes sollten nur zu Beginn des Tests ein paar wenige Verbindungen (10–30) erzeugt werden.

Auf derselben Seite können Sie im Diagramm für das CPU-Kontingent auch die CPU-Kosten vor und nach der Optimierung vergleichen.