Dicas gerais de desenvolvimento

Este guia apresenta as práticas recomendadas para projetar, implementar, testar e implantar um serviço do Knative serving. 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 Knative serving.

Como evitar atividades em segundo plano

Quando um aplicativo em execução no Knative serving termina de processar uma solicitação, o acesso da instância do contêiner à CPU é desativado ou gravemente 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:

  • 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 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 Knative serving, não é possível presumir que o estado do serviço seja preservado entre solicitações. No entanto, o Knative serving 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();
  }
}

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 do Knative serving podem atender a várias solicitações ao mesmo tempo, "simultaneamente", até uma simultaneidade máxima configurável. Isso é diferente das funções do Cloud Run, 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 Knative serving no seu serviço como igual ou menor que qualquer outra configuração no nível do 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 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.

No Knative serving, 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 conta para a 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: