Como otimizar redes

As funções do Cloud Functions são bem simples e permitem desenvolver um código rapidamente e executá-lo em um ambiente sem servidor. Em escala moderada, o custo de executar funções é baixo, e otimizar seu código pode não parecer uma prioridade tão urgente. No entanto, à medida que a implantação evolui, otimizar o código passa a ser cada vez mais importante.

Neste documento, descrevemos como otimizar a rede para as funções. Veja a seguir alguns dos benefícios da otimização de rede:

  • Reduz o tempo de CPU gasto para estabelecer novas conexões em cada chamada de função.
  • Reduz a probabilidade de ficar sem conexão ou cotas de DNS.

Como manter conexões permanentes

Nesta seção, você verá exemplos sobre como manter conexões permanentes em uma função. As cotas de conexão poderão se esgotar rapidamente caso você não faça isso.

Os seguintes cenários são abordados nesta seção:

  • HTTP/S
  • APIs do Google

Solicitações HTTP/S

Veja abaixo no snippet de código otimizado como manter conexões permanentes em vez de criar uma nova conexão a cada invocação de função:

Node.js

const axios = require('axios');

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 {
    const {data} = await axios.get('/', {httpAgent, httpsAgent});
    res.status(200).send(`Data: ${data}`);
  } 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")
}

Como acessar APIs do Google

O Cloud Pub/Sub foi usado neste exemplo, mas esta abordagem também funciona em outras bibliotecas de cliente, como a Cloud Natural Language ou a Cloud Spanner. As melhorias no desempenho podem depender da implementação atual de algumas bibliotecas de cliente.

Criar um objeto de cliente PubSub resulta em uma conexão e duas consultas DNS por invocação. Para evitar conexões e consultas DNS desnecessárias, crie o objeto de cliente PubSub no escopo global, como mostrado no exemplo abaixo:

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

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

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

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

// GCP_PROJECT is a user-set environment variable.
var projectID = os.Getenv("GCP_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)
}

Como testar a carga da sua função

Para analisar quantas conexões a função realiza em média, basta implantá-la como uma função HTTP e usar uma biblioteca de testes de desempenho para invocá-la em determinada taxa de consultas por segundo (QPS, na sigla em inglês). Uma opção possível é o Artillery, que pode ser invocado com uma única linha:

$ artillery quick -d 300 -r 30 URL

Este comando busca o URL fornecido em 30 QPS durante 300 segundos.

Depois de executar o teste, verifique o uso da cota de conexão na página de cota da API Cloud Functions no Console do Cloud. Se o uso for consistente em torno de 30 (ou múltiplos de 30), você está estabelecendo uma (ou várias) conexões a cada invocação. Depois de otimizar seu código, você verá algumas conexões (10 a 30) ocorrerem apenas no início do teste.

Nessa mesma página, você também pode comparar o custo de CPU antes e depois da otimização no gráfico de cota.