Sugerencias generales de desarrollo

En esta guía, se proporcionan prácticas recomendadas para diseñar, implementar y probar un servicio de Knative serving. Para obtener más sugerencias, consulta Migra un servicio existente.

Escribe servicios eficaces

En esta sección, se describen las prácticas recomendadas generales para diseñar e implementar un servicio de Knative serving.

Evita las actividades en segundo plano

Cuando una aplicación que se ejecuta en Knative serving termina de administrar una solicitud, el acceso de la instancia del contenedor a la CPU se inhabilitará o se limitará de forma grave. Por lo tanto, no debes iniciar subprocesos o rutinas en segundo plano que se ejecuten fuera del alcance de los controladores de solicitudes.

La ejecución de subprocesos en segundo plano puede generar un comportamiento inesperado porque las solicitudes posteriores a la misma instancia de contenedor reanudan cualquier actividad en segundo plano suspendida.

La actividad en segundo plano es todo lo que sucede después de que se entrega la respuesta HTTP. Revisa el código para asegurarte de que todas las operaciones asíncronas finalicen antes de entregar la respuesta.

Si sospechas que puede haber actividad en segundo plano no evidente en el servicio, puedes revisar los registros: busca cualquier cosa registrada después de la entrada para la solicitud HTTP.

Borra archivos temporales

En el entorno de Cloud Run, el almacenamiento en disco es un sistema de archivos en la memoria. Los archivos escritos en el disco consumen memoria disponible para el servicio y pueden persistir entre invocaciones. Si no se borran, es posible que se produzca un error de memoria insuficiente y un inicio en frío posterior.

Optimiza el rendimiento

En esta sección, se describen las prácticas recomendadas para optimizar el rendimiento.

Inicia los servicios con rapidez

Debido a que las instancias de contenedor se escalan según sea necesario, inicializar el entorno de ejecución por completo es un método típico. Este tipo de inicialización se denomina “inicio en frío”. Si una solicitud del cliente activa un inicio en frío, el inicio de la instancia del contenedor genera latencia adicional.

La rutina de inicio consta de los siguientes pasos:

  • Inicio del servicio
    • Inicio del contenedor
    • Ejecución del comando de entrypoint para iniciar el servidor
  • Verificación del puerto de servicio abierto

La optimización de la velocidad de inicio del servicio minimiza la latencia que retrasa a una instancia de contenedor en la entrega de solicitudes.

Usa las dependencias de forma inteligente

Si usas un lenguaje dinámico con bibliotecas dependientes, como la importación de módulos en Node.js, el tiempo de carga de esos módulos agrega latencia durante un inicio en frío. Reduce la latencia de inicio de las siguientes maneras:

  • Minimiza la cantidad y el tamaño de las dependencias para compilar un servicio optimizado.
  • Carga de forma diferida el código que se usa con poca frecuencia, si tu lenguaje lo admite.
  • Usa optimizaciones de carga de código como la optimización del cargador automático de composer de PHP.

Usa variables globales

En Knative serving, no puedes suponer que el estado del servicio se conserva entre las solicitudes. Sin embargo, Knative serving vuelve a usar las instancias de contenedores individuales para entregar tráfico continuo, por lo que puedes declarar una variable en el permiso global a fin de permitir que el valor se vuelva a usar en invocaciones posteriores. No se puede saber con anticipación si alguna solicitud individual recibe el beneficio de esta reutilización.

También puedes almacenar objetos en la memoria caché si son costosos de volver a crear en cada solicitud de servicio. Esta migración de la lógica de la solicitud al permiso global da como resultado un mejor rendimiento.

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

Realiza una inicialización diferida de variables globales

La inicialización de las variables globales siempre ocurre durante el inicio, lo que aumenta el tiempo de inicio en frío. Usa la inicialización diferida para los objetos usados con poca frecuencia a fin de diferir el costo del tiempo y disminuir los tiempos de inicio en frío.

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

Optimiza la simultaneidad

Las instancias de Knative serving pueden entregar varias solicitudes de manera simultánea hasta una simultaneidad máxima configurable. Esto es diferente de Cloud Run Functions, que usa concurrency = 1.

Debes mantener la configuración de simultaneidad máxima predeterminada, a menos que tu código tenga requisitos de simultaneidad específicos.

Ajusta la simultaneidad para tu servicio

La technology stack y el uso de recursos compartidos, como variables y conexiones de bases de datos, pueden limitar la cantidad de solicitudes simultáneas que puede entregar cada instancia de contenedor.

Para optimizar el servicio a fin de obtener la máxima simultaneidad estable, sigue estos pasos:

  1. Optimiza el rendimiento del servicio.
  2. Establece el nivel de compatibilidad de simultaneidad esperado en cualquier configuración de simultaneidad a nivel de código. No todas las technology stacks requieren esa configuración.
  3. Implementa el servicio.
  4. Configura la simultaneidad de Knative serving para tu servicio en igual o inferior a cualquier configuración a nivel de código. Si no hay una configuración a nivel de código, usa la simultaneidad esperada.
  5. Usa herramientas de prueba de carga que admitan una simultaneidad configurable. Debes confirmar que el servicio se mantiene estable con la carga y la simultaneidad esperadas.
  6. Si el servicio funciona mal, ve al paso 1 para mejorar el servicio o al paso 2 para reducir la simultaneidad. Si el servicio funciona bien, vuelve al paso 2 y aumenta la simultaneidad

Continúa iterando hasta encontrar la simultaneidad estable máxima.

Coincidencia entre memoria y simultaneidad

Cada solicitud que el servicio administra requiere cierta cantidad de memoria adicional. Por lo tanto, cuando aumentes o disminuyas la simultaneidad, asegúrate de ajustar también el límite de memoria.

Evita el estado global mutable

Si deseas aprovechar el estado global mutable en un contexto de simultaneidad, realiza pasos adicionales en el código para asegurarte de que esto se haga de forma segura. Para minimizar la contención, limita las variables globales a una inicialización única y vuelve a usarlas como se describió antes en Rendimiento.

Si usas variables globales mutables en un servicio que entrega varias solicitudes al mismo tiempo, asegúrate de usar bloqueos o exclusiones mutuas para evitar condiciones de carrera.

Seguridad de contenedores

Se aplican muchas prácticas de seguridad de software de uso general a las aplicaciones en contenedores. Hay algunas prácticas que son específicas de los contenedores o que se alinean con su filosofía y arquitectura.

Para mejorar la seguridad de los contenedores, haz lo siguiente:

  • Usa imágenes base seguras y mantenidas de forma activa, como las imágenes base de Google o las imágenes oficiales de Docker Hub.

  • Aplica actualizaciones de seguridad a los servicios Para hacerlo, vuelve a compilar imágenes de contenedor con regularidad y vuelve a implementar los servicios.

  • Incluye en el contenedor solo lo necesario para ejecutar el servicio. El código, los paquetes y las herramientas adicionales son vulnerabilidades de seguridad potenciales. Consulta más arriba el impacto en el rendimiento relacionado.

  • Implementa un proceso de compilación determinista que incluya versiones específicas de software y bibliotecas. Esto evita que se incluya código sin verificar en el contenedor.

  • Configura el contenedor para que se ejecute como un usuario que no sea root con la declaración de USER de Dockerfile. Es posible que algunas imágenes de contenedor ya tengan un usuario específico configurado.

Automatiza el análisis de seguridad

Habilita el análisis de vulnerabilidades para el análisis de seguridad de las imágenes de contenedor almacenadas en Artifact Registry.

También puedes usar la autorización binaria para garantizar que solo se implementen imágenes de contenedor seguras.

Compila imágenes de contenedor mínimo

Es probable que las imágenes de contenedor grandes aumenten las vulnerabilidades de seguridad porque contienen más de lo que necesita el código.

En Knative serving, el tamaño de la imagen de contenedor no afecta el tiempo de procesamiento de solicitud o el inicio en frío y no se considera en la memoria disponible del contenedor.

Para compilar un contenedor mínimo, considera trabajar con una imagen base eficiente, como las siguientes:

Ubuntu es más grande, pero es una imagen base de uso común con un entorno de servidor integrado más completo.

Si el servicio tiene un proceso de compilación con muchas herramientas, considera usar compilaciones de varias etapas para mantener el contenedor simple en el tiempo de ejecución.

Estos recursos proporcionan más información sobre la creación de imágenes de contenedores eficientes: