Dicas gerais de desenvolvimento

Este guia fornece as práticas recomendadas para projetar, implementar, testar e implantar um serviço de exibição Knative. Para mais dicas, consulte Como migrar um serviço atual.

Como gravar serviços eficazes

Esta seção descreve as práticas recomendadas gerais para projetar e implementar um serviço de veiculação Knative.

Como evitar atividades em segundo plano

Quando um aplicativo em execução no Knative termina de processar uma solicitação, o acesso da instância do contêiner à CPU é desativado ou severamente limitado. Portanto, não inicie linhas de execução ou rotinas em segundo plano que sejam executadas fora do escopo dos gerenciadores de solicitações.

O uso de linhas de execução em segundo plano pode resultar em um comportamento inesperado, porque qualquer solicitação seguinte à mesma instância de contêiner retoma qualquer atividade em segundo plano suspensa.

Uma atividade em segundo plano é qualquer evento ocorrido depois que sua resposta HTTP foi entregue. Revise seu código para certificar-se de que todas as operações assíncronas terminem antes de entregar sua resposta.

Se você suspeita que pode haver uma atividade em segundo plano no serviço que não seja imediatamente aparente, é possível verificar seus registros: procure por qualquer coisa que esteja registrada após a entrada para a solicitação HTTP.

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 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:

  • Iniciar o 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 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

Na exibição do Knative, não é possível presumir que o estado do serviço seja preservado entre as solicitações. No entanto, a exibição do Knative reutiliza instâncias de contêiner individuais para exibir tráfego contínuo. Assim, você pode declarar uma variável no escopo global para permitir que o valor seja reutilizado em invocações 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 objetos em cache na memória se a recriação for cara a cada solicitação de serviço. Mover isso da lógica de solicitação para o escopo global resulta em um 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}"

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

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

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

Como otimizar a simultaneidade

As instâncias de exibição do Knative podem atender a várias solicitações simultaneamente, "simultaneamente", até uma simultaneidade máxima configurável. 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 o serviço;
  4. Defina a simultaneidade de veiculação do Knative 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 a 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 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.

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

Também é possível usar a autorização binária para garantir que apenas imagens de contêiner seguras sejam implantadas.

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.

Na exibição do Knative, o tamanho da imagem do contêiner não afeta a inicialização a frio ou o tempo de processamento da solicitação e não é contabilizado na memória disponível do 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: