Sugestões gerais de desenvolvimento

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

Escrever serviços eficazes

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

Evitar atividades em segundo plano

Quando uma aplicação em execução no Knative serving termina o processamento de um pedido, o acesso da instância do contentor ao CPU é desativado ou severamente limitado. Por conseguinte, não deve iniciar threads ou rotinas em segundo plano que sejam executadas fora do âmbito dos controladores de pedidos.

A execução de threads em segundo plano pode resultar num comportamento inesperado, uma vez que qualquer pedido subsequente à mesma instância do contentor retoma qualquer atividade em segundo plano suspensa.

A atividade em segundo plano é tudo o que acontece depois de a resposta HTTP ter sido enviada. Reveja o seu código para se certificar de que todas as operações assíncronas terminam antes de enviar a resposta.

Se suspeitar que pode existir atividade em segundo plano no seu serviço que não seja facilmente visível, pode verificar os registos: procure tudo o que seja registado após a entrada do pedido HTTP.

Eliminar 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 um arranque a frio subsequente.

Otimizar o desempenho

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

Iniciar serviços rapidamente

Uma vez que as instâncias de contentores são dimensionadas conforme necessário, um método típico consiste em inicializar completamente o ambiente de execução. Este tipo de inicialização é denominado "arranque a frio". Se um pedido do cliente acionar um início a frio, o início da instância do contentor resulta numa latência adicional.

A rotina de arranque consiste no seguinte:

  • A iniciar o serviço
    • A iniciar o contentor
    • Executar o comando entrypoint para iniciar o servidor.
  • Verificar a porta de serviço aberta.

A otimização para a velocidade de arranque do serviço minimiza a latência que atrasa uma instância de contentor a servir pedidos.

Usar 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 adiciona latência durante um arranque a frio. 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.

Usar variáveis globais

No Knative serving, não pode assumir que o estado do serviço é preservado entre pedidos. No entanto, o Knative serving reutiliza instâncias de contentores 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();
  }
}

Execução da inicialização tardia de variáveis globais

A inicialização de variáveis globais ocorre sempre durante o arranque, o que aumenta o tempo de início a frio. Use a inicialização tardia para objetos usados com pouca frequência para adiar o custo de tempo e diminuir os tempos de início 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  # 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();
  }
}

Otimizar a simultaneidade

As instâncias de publicação do Knative podem publicar vários pedidos em simultâneo, "em simultâneo", até uma simultaneidade máxima configurável. Isto é diferente das funções do Cloud Run, que usam concurrency = 1.

Deve manter a predefinição de simultaneidade máxima, a menos que o seu código tenha requisitos de simultaneidade específicos.

Ajustar a simultaneidade do seu serviço

O número de pedidos simultâneos que cada instância do contentor 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 plataformas tecnológicas requerem esta definição.
  3. Implemente o seu serviço.
  4. Defina a simultaneidade de publicação do Knative 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 teste 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.

Fazer corresponder a memória à concorrência

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.

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

Segurança do contentor

Muitas práticas de segurança de software de fins gerais aplicam-se a aplicações contentorizadas. 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.

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

Também pode usar a autorização binária para garantir que apenas são implementadas imagens de contentores seguras.

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

No Knative Serving, o tamanho da imagem do contentor não afeta o arranque a frio nem o tempo de processamento de pedidos e não conta para a 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 fornecem mais informações sobre a criação de imagens de contentores otimizadas: