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.

Como gravar serviços eficazes

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

Configurar a CPU para ser sempre alocada se você usar atividades 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 CPU para que seja sempre alocada

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

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

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

Como otimizar o desempenho

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

Como iniciar os serviços rapidamente

Como as instâncias de contêiner são escalonadas conforme necessário, um método tradicional é inicializar o ambiente de execução completamente. Esse tipo de inicialização é chamado de "inicialização a frio". Se uma solicitação do cliente acionar uma inicialização a frio, a inicialização da instância do contêiner resultará em latência adicional.

A rotina de inicialização engloba estas etapas:

  • Início do serviço
    • Inicialização do contêiner
    • Execução do comando entrypoint para iniciar seu servidor
  • Verificação da porta de serviço aberta

A otimização da velocidade de inicialização do serviço minimiza a latência que atrasa uma instância de contêiner no atendimento às solicitações.

Como 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, definir uma instância mínima como 1 significa que o serviço está pronto para receber o número de solicitações simultâneas configuradas para o serviço sem uma inicialização a frio. Portanto, o número mínimo de instâncias necessárias para eliminar uma inicialização a frio é o tamanho do pico de tráfego esperado dividido pela simultaneidade.

Como usar 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 adicionará latência durante uma inicialização a frio. 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.

Como 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

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

/**
 * HTTP function that declares a variable.
 *
 * @param {Object} req request context.
 * @param {Object} res response context.
 */
exports.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

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

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>.
    """
    function_var = light_computation()
    return 'Per instance: {}, per function: {}'.format(
        instance_var, 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()
}

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

Como realizar 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

// 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.
 */
exports.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

# 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

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 'Lazy: {}, non-lazy: {}.'.format(lazy_global, 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"
)

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

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

Como 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 do Cloud Functions, que usa concurrency = 1.

Continue usando a configuração de simultaneidade máxima padrão, a menos que seu código tenha requisitos específicos de simultaneidade.

Como ajustar a simultaneidade para seu 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.

Como corresponder a memória com a 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.

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

Segurança do contêiner

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 gerenciadas pelo Google ou as imagens oficiais (em inglês) 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.

Como automatizar a verificação de segurança

Ative o verificador de vulnerabilidades de imagem do Container Registry para a verificação de segurança de imagens de contêiner armazenadas no Container Registry.

Como criar imagens mínimas 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: