Dicas gerais de desenvolvimento

Neste guia, apresentamos as práticas recomendadas para projetar, implementar, testar e implantar um serviço do Cloud Run. Para mais dicas, consulte Como migrar um serviço atual.

Criar serviços eficazes

Nesta seção, descrevemos as práticas recomendadas gerais para projetar e implementar um serviço do Cloud Run.

Atividade em segundo plano

Uma atividade em segundo plano é qualquer evento ocorrido depois que sua resposta HTTP foi entregue. Para determinar se há atividade em segundo plano no serviço que não seja imediatamente aparente, verifique nos registros tudo que está registrado após a entrada para a solicitação HTTP.

Configurar a CPU para ser sempre alocada para usar atividades em segundo plano

Se você quiser oferecer suporte a atividades em segundo plano no serviço do Cloud Run, defina a CPU do serviço do Cloud Run como sempre alocada para que você possa executar atividades em segundo plano fora das solicitações e ainda assim têm acesso à CPU.

Evitar atividades em segundo plano se a CPU for alocada somente durante o processamento da solicitação

Se você precisar definir seu serviço para alocar CPU apenas durante o processamento da solicitação, quando o serviço do Cloud Run terminar de processar uma solicitação, o acesso da instância do contêiner à CPU será desativado ou gravemente limitado. Não inicie linhas de execução ou rotinas em segundo plano que sejam executadas fora do escopo dos gerenciadores de solicitações se você usar esse tipo de alocação de CPU.

Revise seu código para certificar-se de que todas as operações assíncronas terminem antes de entregar sua resposta.

A execução de threads em segundo plano com esse tipo de alocação de CPU pode resultar em um comportamento inesperado, pois qualquer solicitação subsequente à mesma instância de contêiner retoma qualquer atividade em segundo plano suspensa.

Excluir arquivos temporários

No ambiente do Cloud Run, o armazenamento em disco é um sistema de arquivos na memória. Os arquivos gravados no disco consomem memória de outra forma disponível para seu serviço e podem persistir entre as chamadas. Deixar de excluir esses arquivos pode resultar em um erro de memória insuficiente e uma inicialização a frio subsequente.

Relatar erros

Gerencie todas as exceções e não deixe seu serviço falhar em erros. Uma falha leva a uma inicialização a frio, enquanto o tráfego fica na fila para uma instância de contêiner de substituição.

Consulte o guia do Error Reporting para informações sobre como relatar erros corretamente.

Otimizar o desempenho

Nesta seção, você verá as práticas recomendadas para otimizar o desempenho.

Iniciar contêineres rapidamente

Como as instâncias de contêiner são escalonadas conforme necessário, o tempo de inicialização tem impacto na latência do serviço. Embora o Cloud Run desassocie a inicialização da instância e o processamento da solicitação, pode acontecer de uma solicitação aguardar até que uma nova instância de contêiner seja iniciada para ser processada, o que acontece principalmente durante o escalonamento a partir de zero. Isso é chamado de "inicialização a frio".

A rotina de inicialização engloba estas etapas:

  • Fazer o download da imagem do contêiner (usando a tecnologia de streaming de imagem do contêiner do Cloud Run)
  • Iniciar o contêiner executando o comando entrypoint.
  • Aguardar o contêiner começar a detectar na porta configurada.

A otimização da velocidade de inicialização do contêiner minimiza a latência do processamento de solicitações.

Usar a otimização da CPU de inicialização para reduzir a latência da inicialização

É possível ativar a otimização da CPU de inicialização para aumentar temporariamente a alocação de CPU durante a inicialização de instâncias para reduzir a latência da inicialização.

Usar instâncias mínimas para reduzir inicializações a frio

É possível configurar instâncias mínimas e simultaneidade para minimizar inicializações a frio. Por exemplo, usar no mínimo uma instância significa que o serviço está pronto para receber até o número de solicitações simultâneas configuradas para o serviço sem a necessidade de iniciar uma nova instância de contêiner.

Observe que uma solicitação que aguarda o início de uma instância será mantida pendente em uma fila da seguinte maneira:

  • Se novas instâncias estiverem inicializando, como durante um escalonamento horizontal, as solicitações ficarão pendentes pelo menos durante o tempo médio de inicialização das instâncias de contêiner deste serviço. Isso inclui quando a solicitação inicia um escalonamento horizontal, como ao escalonar do zero.
  • Se o tempo de inicialização for menor que 10 segundos, as solicitações ficarão pendentes por até 10 segundos.
  • Se não houver instâncias no processo de inicialização e a solicitação não iniciar um escalonamento horizontal, as solicitações ficarão pendentes por até 10 segundos.

Use dependências com sabedoria

Se você usar uma linguagem dinâmica com bibliotecas dependentes, como a importação de módulos no Node.js, o tempo de carregamento desses módulos será adicionado à latência de inicialização.

Reduza a latência de inicialização das maneiras a seguir:

  • Minimize o número e o tamanho das dependências para criar um serviço enxuto.
  • Desacelere o código de carregamento que é usado com pouca frequência, se a linguagem for compatível com ele.
  • Use otimizações de carregamento de código, como a otimização do carregador automático do composer do PHP.

Usar variáveis globais

No Cloud Run, não é possível presumir que o estado do serviço seja preservado entre as solicitações. No entanto, o Cloud Run reutiliza instâncias de contêiner individuais para veicular o tráfego em andamento, para você declarar uma variável no escopo global e permitir que o valor seja reutilizado em chamadas subsequentes. Não é possível saber com antecedência se alguma solicitação individual recebe o benefício dessa reutilização.

Também é possível armazenar em cache objetos na memória, se eles são caros para recriar em cada solicitação de serviço. Mover isso da lógica de solicitação para o escopo global melhora o 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}"

Go


// 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();
  }
}

Executar a inicialização lenta de variáveis globais

A inicialização de variáveis globais sempre ocorre durante a inicialização, o que aumenta o tempo de inicialização a frio. Use a inicialização lenta para objetos usados com pouca frequência para adiar a cobrança relacionada ao tempo e diminuir os tempos de inicialização a frio.

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

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

Go


// 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();
  }
}

Usar um ambiente de execução diferente

Os tempos de inicialização podem ficar mais rápidos usando um ambiente de execução diferente.

Otimizar a simultaneidade

As instâncias do Cloud Run podem atender a várias solicitações ao mesmo tempo até atingir uma simultaneidade configurável máxima. Isso é diferente das funções do Cloud Run, que usa concurrency = 1.

O Cloud Run ajusta automaticamente a simultaneidade até o máximo configurado.

A simultaneidade máxima padrão de 80 é uma boa opção para muitas imagens de contêiner. No entanto, faça o seguinte:

  • Reduza-a se o contêiner não conseguir processar várias solicitações simultâneas.
  • Aumente-o se o contêiner puder lidar com um grande volume de solicitações.

Ajustar a simultaneidade do serviço

É possível que o número de solicitações simultâneas que cada instância de contêiner pode exibir seja limitado pela pilha de tecnologia e pelo uso de recursos compartilhados, como variáveis e conexões de banco de dados.

Para otimizar seu serviço e ter máxima simultaneidade estável:

  1. otimize seu desempenho de serviço;
  2. defina o nível esperado de suporte de simultaneidade em qualquer configuração de simultaneidade no nível de código. Nem todas as pilhas de tecnologia exigem essa configuração;
  3. implante seu serviço;
  4. defina a simultaneidade do Cloud Run para seu serviço como igual ou menor que qualquer configuração no nível de código. Se não houver configuração no nível de código, use sua simultaneidade esperada;
  5. Use ferramentas de teste de carga que sejam compatíveis com uma simultaneidade configurável. Confirme que seu serviço permanece estável sob a carga e simultaneidade esperadas;
  6. se o serviço não funcionar bem, vá para a etapa 1 para melhorá-lo ou para a etapa 2 para reduzir a simultaneidade. Se o serviço funcionar bem, volte para a etapa 2 e aumente a simultaneidade.

Continue iterando até encontrar a simultaneidade estável máxima.

Associar memória à simultaneidade

Cada solicitação que o serviço gerencia requer uma quantidade adicional de memória. Então, quando você ajusta a simultaneidade para cima ou para baixo, ajuste seu limite de memória também.

Evitar o estado global mutável

Se você quiser aproveitar o estado global mutável em um contexto simultâneo, execute etapas adicionais no código para garantir que isso seja feito com segurança. Minimize a contenção limitando as variáveis globais à inicialização única e reutilize conforme descrito acima em Desempenho.

Se você usar variáveis globais mutáveis em um serviço que atenda a várias solicitações ao mesmo tempo, certifique-se de usar bloqueios ou mutexes para evitar disputas.

Compensações entre capacidade de processamento, latência e custo

Ajustar a configuração de solicitações simultâneas máximas pode ajudar a equilibrar a troca entre capacidade de processamento, latência e custo do serviço.

Em geral, uma configuração de solicitações simultâneas máxima mais baixa resulta em latência e capacidade de processamento menores por instância. Com um número máximo menor de solicitações simultâneas, menos solicitações competem por recursos em cada instância, e cada solicitação consegue um desempenho melhor. No entanto, como cada instância pode atender menos solicitações de uma vez, a capacidade de processamento por instância é menor e o serviço precisa de mais instâncias para atender ao mesmo tráfego.

Na direção oposta, uma configuração de solicitações simultâneas máxima maior geralmente resulta em latência e capacidade de processamento maiores por instância. As solicitações podem precisar esperar pelo acesso a recursos como CPU, GPU e largura de banda de memória dentro da instância, o que leva a um aumento da latência. No entanto, cada instância pode processar mais solicitações de uma só vez, de modo que o serviço precisa de menos instâncias no geral para processar o mesmo tráfego.

Considerações sobre o custo

O faturamento do Cloud Run é por instância. Se a CPU for sempre alocada, o tempo de instância será o ciclo de vida total de cada instância. Se a CPU não for sempre alocada, o tempo de instância será o tempo que cada instância leva para processar pelo menos uma solicitação.

O impacto das solicitações simultâneas máximas no faturamento depende do seu padrão de tráfego. A redução do número máximo de solicitações simultâneas pode resultar em uma conta menor se a configuração mais baixa levar a

  • Latência menor
  • Instâncias que concluem o trabalho mais rápido
  • Instâncias sendo encerradas mais rapidamente, mesmo que mais instâncias sejam necessárias

Mas o oposto também é possível: reduzir o número máximo de solicitações simultâneas pode aumentar a cobrança 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 maneira de otimizar o faturamento é por meio de testes de carga, usando diferentes configurações de solicitações simultâneas máximas para identificar a configuração que resulta no menor tempo de instância faturável, conforme mostrado na métrica de monitoramento container/billable_instance_time.

Segurança de contêineres

Muitas práticas de segurança de software de uso geral se aplicam a aplicativos em contêiner. Há algumas práticas que são específicas para contêineres ou que se alinham à filosofia e arquitetura deles.

Para melhorar a segurança do contêiner, siga estas recomendações:

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

  • Aplique atualizações de segurança aos serviços, reconstruindo regularmente as imagens de contêiner e reimplantando os serviços.

  • Inclua no contêiner apenas o que for necessário para executar seu serviço. Códigos, pacotes e ferramentas extras podem deixar a segurança mais vulnerável. Confira acima o impacto relacionado ao desempenho.

  • Implemente um processo de compilação determinista que inclua versões específicas de software e biblioteca. Isso impede que códigos não verificados sejam incluídos no contêiner.

  • Defina o contêiner para ser executado como um usuário diferente de root com a instrução USER do Dockerfile (em inglês). Algumas imagens de contêiner talvez já tenham um usuário específico configurado.

  • Impeça o uso de recursos em fase de pré-lançamento usando políticas personalizadas da organização.

Automatizar a verificação de segurança

Ative a verificação de vulnerabilidades para a verificação de segurança de imagens de contêiner armazenadas no Artifact Registry.

Criar o mínimo de imagens de contêiner

É possível que imagens grandes de contêiner aumentem as vulnerabilidades de segurança porque elas contêm mais do que o código precisa.

Devido à tecnologia de streaming de imagens de contêiner do Cloud Run, o tamanho da imagem do contêiner não afeta a inicialização a frio ou o tempo de processamento da solicitação. O tamanho da imagem do contêiner também não é contabilizado na memória disponível do seu contêiner.

Para criar um contêiner mínimo, utilize uma imagem básica enxuta, como estas:

O Ubuntu é maior em tamanho, mas é uma imagem de base comumente usada com um ambiente de servidor pronto para uso mais completo.

Se seu serviço tem um processo de criação de ferramentas pesadas, considere o uso de compilações em vários estágios para manter o contêiner leve no ambiente de execução.

Estes recursos fornecem informações adicionais sobre a criação de imagens de contêineres enxutas: