Optimizar las Herramientas de redes

La simplicidad de Cloud Functions te permite programar código rápidamente y ejecutarlo en un entorno sin servidores. A una escala moderada, el costo de ejecutar funciones es bajo, y puedes considerar que optimizar tu código no es una prioridad importante. Sin embargo, a medida que tu implementación aumenta su escala, optimizar tu código se vuelve cada vez más relevante.

En este documento, se describe cómo optimizar las Herramientas de redes para tus funciones. Algunos de los beneficios de optimizar las Herramientas de redes son los siguientes:

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

Mantén conexiones continuas

Esta sección muestra ejemplos sobre cómo mantener conexiones continuas en una función. No hacerlo puede causar que agotes tu cuota de conexión rápidamente.

En esta sección, se abordan los siguientes casos:

  • HTTP/S
  • API de Google

Solicitudes HTTP(S)

El fragmento de código optimizado de más abajo muestra cómo mantener la coherencia de las conexiones en lugar de crear una conexión nueva para cada invocación de función:

Node.js

const http = require('http');
const agent = new http.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 = (req, res) => {
  req = http.request(
    {
      host: '',
      port: 80,
      path: '',
      method: 'GET',
      agent: agent,
    },
    resInner => {
      let rawData = '';
      resInner.setEncoding('utf8');
      resInner.on('data', chunk => {
        rawData += chunk;
      });
      resInner.on('end', () => {
        res.status(200).send(`Data: ${rawData}`);
      });
    }
  );
  req.on('error', e => {
    res.status(500).send(`Error: ${e.message}`);
  });
  req.end();
};

Python (Beta)

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 (Beta)

// Package http provides a set of HTTP Cloud Function 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")
}

Acceso a las API de Google

En el ejemplo de más abajo, se usa Cloud Pub/Sub, pero este enfoque también sirve para otras bibliotecas cliente, como Cloud Natural Language o Cloud Spanner. Ten en cuenta que es posible que las mejoras de rendimiento dependan de la implementación actual de algunas bibliotecas cliente en particular.

Si se crea un objeto de cliente de PubSub se genera una conexión y dos consultas de DNS por invocación. Para evitar consultas de DNS y conexiones innecesarias, crea el objeto de cliente de PubSub en alcance global, como se indica en el siguiente ejemplo:

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 publisher = topic.publisher();

  publisher.publish(Buffer.from('Test message'), err => {
    if (err) {
      res.status(500).send(`Error publishing the message: ${err}`);
    } else {
      res.status(200).send('1 message published');
    }
  });
};

Python (Beta)

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 (Beta)

// 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"
	"io/ioutil"
	"log"
	"net/http"
	"os"

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

// projectID is set from the GOOGLE_CLOUD_PROJECT environment variable, which is
// automatically set by the Cloud Functions runtime.
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"`
}

// PublishMessage publishes a message to Pub/Sub. PublishMessage only works
// with topics that already exist.
func PublishMessage(w http.ResponseWriter, r *http.Request) {
	// Read the request body.
	data, err := ioutil.ReadAll(r.Body)
	if err != nil {
		log.Printf("ioutil.ReadAll: %v", err)
		http.Error(w, "Error reading request", http.StatusBadRequest)
		return
	}

	// Parse the request body to get the topic name.
	p := publishRequest{}
	if err := json.Unmarshal(data, &p); err != nil {
		log.Printf("json.Unmarshal: %v", err)
		http.Error(w, "Error parsing request", http.StatusBadRequest)
		return
	}

	m := &pubsub.Message{
		Data: []byte("Test 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, "Published msg: %v", id)
}

Prueba de carga de tu función

Para medir cuántas conexiones ejecuta en promedio tu función, solo debes implementarla como una función de HTTP y usar un marco de trabajo de prueba de rendimiento para invocarla con una cierta cantidad de QPS. Una opción posible es Artillery, que puedes invocar con una sola línea:

$ artillery quick -d 300 -r 30 URL

Este comando recupera la URL dada a 30 QPS por 300 segundos.

Después de ejecutar la prueba, verifica el uso de tu cuota de conexión en la página de cuotas de la API de Cloud Functions en Cloud Console. Si el uso se mantiene alrededor de 30 (o sus múltiplos), estás estableciendo una (o varias) conexiones en cada invocación. Después de optimizar el código, deberías ver que se ejecutan unas pocas conexiones (entre 10 y 30) solo al comienzo de la prueba.

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

¿Te ha resultado útil esta página? Enviar comentarios:

Enviar comentarios sobre...

Documentación de Cloud Functions