Sugestões gerais de desenvolvimento

Este guia fornece práticas recomendadas para conceber, implementar, testar e implementar um serviço do Cloud Run. Para mais sugestões, consulte o artigo Migrar um serviço existente.

Escreva serviços eficazes

Esta secção descreve as práticas recomendadas gerais para conceber e implementar um serviço do Cloud Run.

Atividade em segundo plano

A atividade em segundo plano é tudo o que acontece depois de a resposta HTTP ter sido enviada. Para determinar se existe atividade em segundo plano no seu serviço que não seja imediatamente evidente, verifique os registos para ver se existe algo registado após a entrada do pedido HTTP.

Configure a faturação baseada em instâncias para usar atividades em segundo plano

Se quiser suportar atividades em segundo plano no seu serviço do Cloud Run, defina o serviço do Cloud Run para faturação baseada em instâncias, para que possa executar atividades em segundo plano fora dos pedidos e continuar a ter acesso à CPU.

Evite atividades em segundo plano se usar a faturação baseada em pedidos

Se precisar de definir o seu serviço para a faturação baseada em pedidos, quando o serviço Cloud Run terminar de processar um pedido, o acesso da instância à CPU é desativado ou severamente limitado. Não deve iniciar threads ou rotinas em segundo plano que sejam executadas fora do âmbito dos processadores de pedidos se usar este tipo de faturação.

Reveja o seu código para se certificar de que todas as operações assíncronas terminam antes de enviar a resposta.

A execução de threads em segundo plano com a faturação baseada em pedidos ativada pode resultar num comportamento inesperado, porque qualquer pedido subsequente à mesma instância do contentor retoma qualquer atividade em segundo plano suspensa.

Elimine ficheiros temporários

No ambiente do Cloud Run, o armazenamento em disco é um sistema de ficheiros na memória. Os ficheiros escritos no disco consomem memória que, de outra forma, estaria disponível para o seu serviço e podem persistir entre invocações. Se não eliminar estes ficheiros, pode ocorrer um erro de falta de memória e, posteriormente, os contentores podem demorar mais tempo a iniciar.

Comunique erros

Trate todas as exceções e não permita que o seu serviço falhe devido a erros. Uma falha do sistema leva a um início lento do contentor enquanto o tráfego está em fila para uma instância de substituição.

Consulte o guia de relatórios de erros para obter informações sobre como comunicar corretamente os erros.

Otimize o desempenho

Esta secção descreve as práticas recomendadas para otimizar o desempenho.

Inicie contentores rapidamente

Uma vez que as instâncias são dimensionadas conforme necessário, o respetivo tempo de arranque tem impacto na latência do seu serviço. O Cloud Run desvincula o arranque da instância e o processamento de pedidos, pelo que, em alguns casos, um pedido tem de aguardar que uma nova instância seja iniciada antes de ser processado. Isto acontece frequentemente quando um serviço é dimensionado a partir de zero.

A rotina de arranque consiste no seguinte:

  • Transferir a imagem do contentor (através da tecnologia de streaming de imagens de contentores do Cloud Run)
  • Iniciar o contentor executando o comando entrypoint
  • A aguardar que o contentor comece a ouvir na porta configurada.

A otimização da velocidade de arranque do contentor minimiza a latência de processamento de pedidos.

Use o aumento da CPU de arranque para reduzir a latência de arranque

Pode ativar o aumento da CPU no arranque para aumentar temporariamente a atribuição da CPU durante o arranque da instância de modo a reduzir a latência do arranque.

Use instâncias mínimas para reduzir os tempos de arranque dos contentores

Pode configurar instâncias mínimas e a concorrência para minimizar os tempos de início do contentor. Por exemplo, usar um mínimo de 1 instância significa que o seu serviço está pronto para receber até ao número de pedidos simultâneos configurados para o seu serviço sem precisar de iniciar uma nova instância.

Tenha em atenção que um pedido que aguarda o início de uma instância é mantido pendente numa fila da seguinte forma:

Os pedidos ficam pendentes até 3, 5 vezes o tempo de arranque médio das instâncias de contentores deste serviço ou 10 segundos, consoante o que for superior.

Use as dependências de forma inteligente

Se usar uma linguagem dinâmica com bibliotecas dependentes, como importar módulos no Node.js, o tempo de carregamento desses módulos é adicionado à latência de arranque.

Reduza a latência do arranque das seguintes formas:

  • Minimize o número e o tamanho das dependências para criar um serviço simples.
  • Carregue em diferido o código que é usado com pouca frequência, se o seu idioma o suportar.
  • Use otimizações de carregamento de código, como a otimização do carregamento automático do Composer do PHP.

Use variáveis globais

No Cloud Run, não pode assumir que o estado do serviço é preservado entre pedidos. No entanto, o Cloud Run reutiliza instâncias individuais para publicar tráfego contínuo, pelo que pode declarar uma variável no âmbito global para permitir que o respetivo valor seja reutilizado em invocações subsequentes. Não é possível saber antecipadamente se um pedido individual vai receber o benefício desta reutilização.

Também pode colocar objetos em cache na memória se for dispendioso recriá-los em cada pedido de serviço. Mover isto da lógica de pedido para o âmbito global resulta num melhor desempenho.

Node.js

const functions = require('@google-cloud/functions-framework');

// TODO(developer): Define your own computations
const {lightComputation, heavyComputation} = require('./computations');

// Global (instance-wide) scope
// This computation runs once (at instance cold-start)
const instanceVar = heavyComputation();

/**
 * HTTP function that declares a variable.
 *
 * @param {Object} req request context.
 * @param {Object} res response context.
 */
functions.http('scopeDemo', (req, res) => {
  // Per-function scope
  // This computation runs every time this function is called
  const functionVar = lightComputation();

  res.send(`Per instance: ${instanceVar}, per function: ${functionVar}`);
});

Python

import time

import functions_framework


# Placeholder
def heavy_computation():
    return time.time()


# Placeholder
def light_computation():
    return time.time()


# Global (instance-wide) scope
# This computation runs at instance cold-start
instance_var = heavy_computation()


@functions_framework.http
def scope_demo(request):
    """
    HTTP Cloud Function that declares a variable.
    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>.
    """

    # Per-function scope
    # This computation runs every time this function is called
    function_var = light_computation()
    return f"Instance: {instance_var}; function: {function_var}"

Ir


// h is in the global (instance-wide) scope.
var h string

// init runs during package initialization. So, this will only run during an
// an instance's cold start.
func init() {
	h = heavyComputation()
	functions.HTTP("ScopeDemo", ScopeDemo)
}

// ScopeDemo is an example of using globally and locally
// scoped variables in a function.
func ScopeDemo(w http.ResponseWriter, r *http.Request) {
	l := lightComputation()
	fmt.Fprintf(w, "Global: %q, Local: %q", h, l)
}

Java


import com.google.cloud.functions.HttpFunction;
import com.google.cloud.functions.HttpRequest;
import com.google.cloud.functions.HttpResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;

public class Scopes implements HttpFunction {
  // Global (instance-wide) scope
  // This computation runs at instance cold-start.
  // Warning: Class variables used in functions code must be thread-safe.
  private static final int INSTANCE_VAR = heavyComputation();

  @Override
  public void service(HttpRequest request, HttpResponse response)
      throws IOException {
    // Per-function scope
    // This computation runs every time this function is called
    int functionVar = lightComputation();

    var writer = new PrintWriter(response.getWriter());
    writer.printf("Instance: %s; function: %s", INSTANCE_VAR, functionVar);
  }

  private static int lightComputation() {
    int[] numbers = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    return Arrays.stream(numbers).sum();
  }

  private static int heavyComputation() {
    int[] numbers = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    return Arrays.stream(numbers).reduce((t, x) -> t * x).getAsInt();
  }
}

Realize a inicialização em diferido de variáveis globais

A inicialização das variáveis globais ocorre sempre durante o arranque, o que aumenta o tempo de arranque do contentor. Use a inicialização tardia para objetos usados com pouca frequência para diferir o custo de tempo e diminuir os tempos de arranque do contentor.

Uma desvantagem da inicialização tardia é o aumento da latência para os primeiros pedidos a novas instâncias. Isto pode causar um aumento excessivo da escala e pedidos ignorados quando implementa uma nova revisão de um serviço que está a processar ativamente muitos pedidos.

Node.js

const functions = require('@google-cloud/functions-framework');

// Always initialized (at cold-start)
const nonLazyGlobal = fileWideComputation();

// Declared at cold-start, but only initialized if/when the function executes
let lazyGlobal;

/**
 * HTTP function that uses lazy-initialized globals
 *
 * @param {Object} req request context.
 * @param {Object} res response context.
 */
functions.http('lazyGlobals', (req, res) => {
  // This value is initialized only if (and when) the function is called
  lazyGlobal = lazyGlobal || functionSpecificComputation();

  res.send(`Lazy global: ${lazyGlobal}, non-lazy global: ${nonLazyGlobal}`);
});

Python

import functions_framework

# Always initialized (at cold-start)
non_lazy_global = file_wide_computation()

# Declared at cold-start, but only initialized if/when the function executes
lazy_global = None


@functions_framework.http
def lazy_globals(request):
    """
    HTTP Cloud Function that uses lazily-initialized globals.
    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>.
    """
    global lazy_global, non_lazy_global  # noqa: F824

    # This value is initialized only if (and when) the function is called
    if not lazy_global:
        lazy_global = function_specific_computation()

    return f"Lazy: {lazy_global}, non-lazy: {non_lazy_global}."

Ir


// Package tips contains tips for writing Cloud Functions in Go.
package tips

import (
	"context"
	"log"
	"net/http"
	"sync"

	"cloud.google.com/go/storage"
	"github.com/GoogleCloudPlatform/functions-framework-go/functions"
)

// client is lazily initialized by LazyGlobal.
var client *storage.Client
var clientOnce sync.Once

func init() {
	functions.HTTP("LazyGlobal", LazyGlobal)
}

// LazyGlobal is an example of lazily initializing a Google Cloud Storage client.
func LazyGlobal(w http.ResponseWriter, r *http.Request) {
	// You may wish to add different checks to see if the client is needed for
	// this request.
	clientOnce.Do(func() {
		// Pre-declare an err variable to avoid shadowing client.
		var err error
		client, err = storage.NewClient(context.Background())
		if err != nil {
			http.Error(w, "Internal error", http.StatusInternalServerError)
			log.Printf("storage.NewClient: %v", err)
			return
		}
	})
	// Use client.
}

Java


import com.google.cloud.functions.HttpFunction;
import com.google.cloud.functions.HttpRequest;
import com.google.cloud.functions.HttpResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;

public class LazyFields implements HttpFunction {
  // Always initialized (at cold-start)
  // Warning: Class variables used in Servlet classes must be thread-safe,
  // or else might introduce race conditions in your code.
  private static final int NON_LAZY_GLOBAL = fileWideComputation();

  // Declared at cold-start, but only initialized if/when the function executes
  // Uses the "initialization-on-demand holder" idiom
  // More information: https://en.wikipedia.org/wiki/Initialization-on-demand_holder_idiom
  private static class LazyGlobalHolder {
    // Making the default constructor private prohibits instantiation of this class
    private LazyGlobalHolder() {}

    // This value is initialized only if (and when) the getLazyGlobal() function below is called
    private static final Integer INSTANCE = functionSpecificComputation();

    private static Integer getInstance() {
      return LazyGlobalHolder.INSTANCE;
    }
  }

  @Override
  public void service(HttpRequest request, HttpResponse response)
      throws IOException {
    Integer lazyGlobal = LazyGlobalHolder.getInstance();

    var writer = new PrintWriter(response.getWriter());
    writer.printf("Lazy global: %s; non-lazy global: %s%n", lazyGlobal, NON_LAZY_GLOBAL);
  }

  private static int functionSpecificComputation() {
    int[] numbers = new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9};
    return Arrays.stream(numbers).sum();
  }

  private static int fileWideComputation() {
    int[] numbers = new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9};
    return Arrays.stream(numbers).reduce((t, x) -> t * x).getAsInt();
  }
}

Use um ambiente de execução diferente

Pode ter tempos de arranque mais rápidos se usar um ambiente de execução diferente.

Otimize a simultaneidade

As instâncias do Cloud Run podem processar vários pedidos em simultâneo, "em simultâneo", até uma simultaneidade máxima configurável.

O Cloud Run ajusta automaticamente a concorrência até ao máximo configurado.

A simultaneidade máxima predefinida de 80 é adequada para muitas imagens de contentores. No entanto, deve:

  • Diminua-o se o seu contentor não conseguir processar muitos pedidos simultâneos.
  • Aumente-o se o seu contentor conseguir processar um grande volume de pedidos.

Ajuste a simultaneidade do seu serviço

O número de pedidos simultâneos que cada instância pode publicar pode ser limitado pela pilha de tecnologia e pela utilização de recursos partilhados, como variáveis e ligações à base de dados.

Para otimizar o seu serviço em função da simultaneidade estável máxima:

  1. Otimize o desempenho do seu serviço.
  2. Defina o nível esperado de suporte de simultaneidade em qualquer configuração de simultaneidade ao nível do código. Nem todas as stacks de tecnologia requerem esta definição.
  3. Implemente o seu serviço.
  4. Defina a simultaneidade do Cloud Run para o seu serviço igual ou inferior a qualquer configuração ao nível do código. Se não existir uma configuração ao nível do código, use a concorrência esperada.
  5. Use ferramentas de testes de carga que suportem uma concorrência configurável. Tem de confirmar que o seu serviço permanece estável sob a carga e a concorrência esperadas.
  6. Se o serviço tiver um desempenho fraco, avance para o passo 1 para melhorar o serviço ou para o passo 2 para reduzir a simultaneidade. Se o serviço tiver um bom desempenho, regresse ao passo 2 e aumente a simultaneidade.

Continue a iterar até encontrar a concorrência estável máxima.

Faça corresponder a memória à simultaneidade

Cada pedido que o seu serviço processa requer uma certa quantidade de memória adicional. Por isso, quando ajustar a concorrência para cima ou para baixo, certifique-se de que também ajusta o limite de memória.

Evite o estado global mutável

Se quiser tirar partido do estado global mutável num contexto concorrente, tome medidas adicionais no seu código para garantir que isto é feito em segurança. Minimize a contenção limitando as variáveis globais à inicialização única e à reutilização, conforme descrito acima em Desempenho.

Se usar variáveis globais mutáveis num serviço que processa vários pedidos ao mesmo tempo, certifique-se de que usa bloqueios ou exclusões mútuas para evitar condições de corrida.

Equilíbrios entre o débito, a latência e o custo

Ajustar a definição de pedidos simultâneos máximos pode ajudar a equilibrar a relação de compromisso entre o débito, a latência e o custo do seu serviço.

Em geral, uma definição de pedidos concorrentes máximos mais baixa resulta numa latência mais baixa e num débito mais baixo por instância. Com um número máximo de pedidos simultâneos inferior, menos pedidos competem por recursos em cada instância e cada pedido alcança um melhor desempenho. No entanto, como cada instância pode publicar menos pedidos de cada vez, a taxa de transferência por instância é inferior e o serviço precisa de mais instâncias para publicar o mesmo tráfego.

Na direção oposta, uma definição de pedidos simultâneos máximos mais elevada resulta geralmente numa latência mais elevada e num débito mais elevado por instância. Os pedidos podem ter de aguardar o acesso a recursos como a CPU, a GPU e a largura de banda da memória na instância, o que leva a um aumento da latência. No entanto, cada instância pode processar mais pedidos em simultâneo, pelo que o serviço precisa de menos instâncias no geral para processar o mesmo tráfego.

Considerações sobre o custo

A preçário do Cloud Run é por tempo de instância. Se definir a faturação baseada em instâncias, o tempo de instância é a duração total de cada instância. Se definir a faturação baseada em pedidos, o tempo de instância é o tempo que cada instância passa a processar, pelo menos, um pedido.

O impacto dos pedidos simultâneos máximos na faturação depende do seu padrão de tráfego. Diminuir os pedidos simultâneos máximos pode resultar numa fatura mais baixa se a definição mais baixa levar a

  • Latência reduzida
  • As instâncias concluem o respetivo trabalho mais rapidamente
  • As instâncias encerram mais rapidamente, mesmo que sejam necessárias mais instâncias no total

No entanto, também é possível o contrário: diminuir os pedidos simultâneos máximos pode aumentar a faturação se o aumento no número de instâncias não for compensado pela redução no tempo de execução de cada instância, devido à latência melhorada.

A melhor forma de otimizar a faturação é através de testes de carga com diferentes definições de pedidos simultâneos máximos para identificar a definição que resulta no menor tempo de instância faturável, conforme apresentado na container/billable_instance_time métrica de monitorização.

Segurança do contentor

Muitas práticas de segurança de software de uso geral aplicam-se a serviços contentorizados. Existem algumas práticas específicas dos contentores ou que se alinham com a filosofia e a arquitetura dos contentores.

Para melhorar a segurança do contentor:

  • Use imagens base seguras e ativamente mantidas, como as imagens base da Google ou as imagens oficiais do Docker Hub.

  • Aplique atualizações de segurança aos seus serviços recriando regularmente imagens de contentores e reimplementando os seus serviços.

  • Inclua no contentor apenas o que for necessário para executar o seu serviço. O código, os pacotes e as ferramentas adicionais são potenciais vulnerabilidades de segurança. Consulte acima o impacto no desempenho relacionado.

  • Implemente um processo de compilação determinístico que inclua versões específicas de software e bibliotecas. Isto impede a inclusão de código não validado no seu contentor.

  • Defina o contentor para ser executado como um utilizador diferente de root com a declaração Dockerfile USER. Algumas imagens de contentores podem já ter um utilizador específico configurado.

  • Impeça a utilização de funcionalidades de pré-visualização através de políticas de organização personalizadas.

Automatize a análise de segurança

Ative a análise de vulnerabilidades para a análise de segurança de imagens de contentores armazenadas no Artifact Registry.

Crie imagens de contentores mínimas

As imagens de contentores grandes aumentam provavelmente as vulnerabilidades de segurança porque contêm mais do que o código precisa.

Devido à tecnologia de streaming de imagens de contentores do Cloud Run, o tamanho da sua imagem de contentor não afeta os tempos de arranque do contentor nem o tempo de processamento de pedidos. O tamanho da imagem do contentor também não é contabilizado na memória disponível do contentor.

Para criar um contentor minimalista, considere trabalhar a partir de uma imagem de base simples, como:

O Ubuntu tem um tamanho maior, mas é uma imagem base usada com frequência com um ambiente de servidor mais completo pronto a usar.

Se o seu serviço tiver um processo de compilação com muitas ferramentas, considere usar compilações de várias fases para manter o seu contentor leve no tempo de execução.

Estes recursos oferecem mais informações sobre a criação de imagens de contentores otimizadas: