Optimizar la red (1.ª gen.)

La sencillez de las funciones de Cloud Run te permite desarrollar código rápidamente y ejecutarlo en un entorno sin servidor. A una escala moderada, el coste de ejecutar funciones es bajo, por lo que optimizar el código puede no parecer una prioridad alta. Sin embargo, a medida que tu implementación se amplía, optimizar el código se vuelve cada vez más importante.

En este documento se describe cómo optimizar las redes de tus funciones. Estas son algunas de las ventajas de optimizar la red:

  • Reduce el tiempo de CPU dedicado a establecer nuevas conexiones en cada llamada de función.
  • Reduce la probabilidad de agotar las cuotas de conexión o DNS.

Mantener conexiones persistentes

En esta sección se ofrecen ejemplos de cómo mantener conexiones persistentes en una función. Si no lo haces, es posible que agotes rápidamente las cuotas de conexión.

En esta sección se tratan los siguientes casos:

  • HTTP/S
  • APIs de Google

Solicitudes HTTP/S

El siguiente fragmento de código optimizado muestra cómo mantener conexiones persistentes en lugar de crear una conexión nueva cada vez que se invoca una función:

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

Te recomendamos que utilices el framework HTTP de PHP Guzzle para enviar solicitudes HTTP, ya que gestiona las conexiones persistentes automáticamente.

Acceder a las API de Google

En el siguiente ejemplo se usa Cloud Pub/Sub, pero este enfoque también funciona con otras bibliotecas de cliente, como Cloud Natural Language o Cloud Spanner. Ten en cuenta que las mejoras en el rendimiento pueden depender de la implementación actual de bibliotecas de cliente concretas.

Al crear un objeto de cliente de Pub/Sub, se genera una conexión y dos consultas DNS por invocación. Para evitar conexiones y consultas de DNS innecesarias, crea el objeto de cliente de Pub/Sub en el ámbito global, tal como se muestra en el siguiente ejemplo:

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

Restablecimientos de conexiones salientes

Las secuencias de conexión de tu función a la VPC y a Internet se pueden terminar y sustituir ocasionalmente cuando se reinicia o se actualiza la infraestructura subyacente. Si tu aplicación reutiliza conexiones de larga duración, te recomendamos que la configures para que restablezca las conexiones y así evitar que se reutilice una conexión inactiva.

Tiempos de espera de solicitudes salientes

Los sockets salientes se pueden recuperar después de 10 minutos de inactividad. Cualquier operación de socket mantiene el socket activo durante 10 minutos más.

Prueba de carga de tu función

Para medir cuántas conexiones realiza tu función de media, despliégala como función HTTP y usa un framework de pruebas de rendimiento para invocarla a un determinado número de consultas por segundo. Una opción es Artillery, que puedes invocar con una sola línea:

$ artillery quick -d 300 -r 30 URL

Este comando obtiene la URL indicada a 30 QPS durante 300 segundos.

Después de realizar la prueba, comprueba el uso de tu cuota de conexiones en la página de cuotas de la API de funciones de Cloud Run de la consola de Google Cloud . Si el uso es de aproximadamente 30 (o un múltiplo de este valor), estás estableciendo una (o varias) conexiones en cada invocación. Después de optimizar el código, deberías ver que se producen algunas conexiones (entre 10 y 30) solo al principio de la prueba.

También puede comparar el coste de CPU antes y después de la optimización en el gráfico de cuota de CPU de la misma página.