Sugerencias generales de desarrollo

En esta guía, se proporcionan las prácticas recomendadas para diseñar, implementar y probar un servicio de Cloud Run. 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 y, también, implementar un servicio de Cloud Run.

Actividad en segundo plano

La actividad en segundo plano es todo lo que sucede después de que se entrega la respuesta HTTP. Para determinar si hay actividad en segundo plano no evidente en el servicio, revisa los registros para ver si hay algún registro después de la entrada de la solicitud HTTP.

Configura la CPU para que siempre se asigne a fin de usar actividades en segundo plano

Si deseas admitir actividades en segundo plano en tu servicio de Cloud Run, configura tu CPU de servicio de Cloud Run para que siempre se asigne a fin de que puedas ejecutar actividades en segundo plano fuera de las solicitudes. tienen acceso a la CPU

Evita las actividades en segundo plano si la CPU se asigna solo durante el procesamiento de la solicitud

Si necesitas configurar el servicio para asignar CPU solo durante el procesamiento de solicitudes, cuando el servicio de Cloud Run termine de controlar una solicitud, se inhabilitará el acceso de la instancia a la CPU. o muy limitada. No debes iniciar subprocesos o rutinas en segundo plano que se ejecuten fuera del alcance de los controladores de solicitudes si usas este tipo de asignación de CPU.

Revisa el código para asegurarte de que todas las operaciones asíncronas finalicen antes de entregar la respuesta.

La ejecución de subprocesos en segundo plano con este tipo de asignación de CPU puede provocar un comportamiento inesperado porque las solicitudes posteriores a la misma instancia de contenedor reanudan cualquier actividad en segundo plano suspendida.

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 tiempos de inicio lentos del contenedor.

Informa errores

Controla todas las excepciones y no permitas que el servicio falle en caso de errores. Una falla genera un inicio lento del contenedor mientras el tráfico se pone en cola para una instancia de reemplazo.

Consulta la guía de Error Reporting para obtener información sobre cómo informar errores de forma correcta.

Optimiza el rendimiento

Esta sección describe las recomendaciones para optimizar el rendimiento.

Inicia los contenedores con rapidez

Debido a que las instancias se escalan según sea necesario, su tiempo de inicio tiene un impacto en la latencia de tu servicio. Si bien Cloud Run separa el inicio de instancia del procesamiento de solicitudes, puede ocurrir que una solicitud deba esperar a que se inicie una nueva instancia. En particular, esto sucede cuando se escala desde cero.

La rutina de inicio consta de los siguientes pasos:

  • Descargar la imagen del contenedor (mediante la tecnología de transmisión de imágenes de contenedor de Cloud Run)
  • Inicia el contenedor mediante la ejecución del comando entrypoint.
  • Esperar a que el contenedor comience a escuchar en el puerto configurado.

Optimizar la velocidad de inicio del contenedor minimiza la latencia de procesamiento de solicitudes.

Usa el aumento de la CPU de inicio para reducir la latencia del inicio

Puedes habilitar el aumento de CPU de inicio para aumentar de forma temporal la asignación de CPU durante el inicio de la instancia a fin de reducir la latencia de inicio.

Usa instancias mínimas para reducir los tiempos de inicio de contenedores

Puedes configurar instancias mínimas y simultaneidad para minimizar los tiempos de inicio del contenedor. Por ejemplo, si usas un mínimo de instancias de 1, el servicio está listo para recibir hasta la cantidad de solicitudes simultáneas configuradas para el servicio sin necesidad de iniciar una instancia nueva.

Ten en cuenta que una solicitud que espera a que se inicie una instancia se mantendrá pendiente en una cola de la siguiente manera:

  • Si se inician instancias nuevas, como durante un escalamiento horizontal, las solicitudes admitirán al menos el tiempo de inicio promedio de las instancias de contenedor de este servicio. Esto incluye el momento en que la solicitud inicia un escalamiento horizontal, como cuando escalas desde cero.
  • Si el tiempo de inicio es inferior a 10 segundos, las solicitudes permanecerán hasta 10 segundos.
  • Si no hay instancias en el proceso de inicio y la solicitud no inicia un escalamiento horizontal, las solicitudes se almacenarán hasta por 10 segundos.

Usa las dependencias de manera 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 se suma a la latencia de inicio.

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 Cloud Run, no puedes suponer que el estado del servicio se conserva entre las solicitudes. Sin embargo, Cloud Run vuelve a usar las instancias 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 del contenedor. Usa la inicialización diferida para los objetos usados con poca frecuencia para diferir el costo del tiempo y disminuir los tiempos de inicio del contenedor.

Una desventaja de la inicialización diferida es un aumento de la latencia para las primeras solicitudes a instancias nuevas. Esto puede provocar una escalamiento excesivo y solicitudes perdidas cuando implementas una revisión nueva de un servicio que controla activamente muchas solicitudes.

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

Usa un entorno de ejecución diferente

Es posible que experimentes tiempos de inicio más rápidos mediante un entorno de ejecución diferente.

Optimiza la simultaneidad

Las instancias de Cloud Run 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.

Cloud Run ajusta de forma automática la simultaneidad hasta el máximo configurado.

La simultaneidad máxima predeterminada de 80 es una buena opción para muchas imágenes de contenedor. Sin embargo, debes hacer lo siguiente:

  • Redúcela si tu contenedor no puede procesar muchas solicitudes simultáneas.
  • Auméntala si tu contenedor puede manejar un gran volumen de solicitudes.

Ajusta la simultaneidad para tu servicio

La pila tecnológica 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.

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 Cloud Run para el 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.

Equilibrio entre la capacidad de procesamiento, la latencia y el costo

Ajustar la configuración de solicitudes simultáneas máximas puede ayudarte a equilibrar la compensación entre la capacidad de procesamiento, la latencia y el costo de tu servicio.

En general, una configuración más baja de solicitudes simultáneas máximas genera una latencia y una capacidad de procesamiento más bajas por instancia. Con una cantidad máxima de solicitudes simultáneas más baja, menos solicitudes compiten por los recursos dentro de cada instancia y cada solicitud logra un mejor rendimiento. Sin embargo, como cada instancia puede entregar menos solicitudes a la vez, la capacidad de procesamiento por instancia es menor y el servicio necesita más instancias para entregar el mismo tráfico.

En el sentido opuesto, una configuración más alta de solicitudes simultáneas generalmente genera una latencia y una capacidad de procesamiento más altas por instancia. Es posible que las solicitudes deban esperar el acceso a recursos como el ancho de banda de la CPU, la GPU y la memoria dentro de la instancia, lo que aumenta la latencia. Sin embargo, cada instancia puede procesar más solicitudes a la vez, de modo que el servicio necesita menos instancias en general para procesar el mismo tráfico.

Consideraciones de costo

La facturación de Cloud Run se realiza por tiempo de instancia. Si la CPU siempre se asigna, el tiempo de la instancia es la vida útil total de cada instancia. Si la CPU no siempre se asigna, el tiempo de la instancia es el tiempo que cada instancia dedica a procesar al menos una solicitud.

El impacto de la cantidad máxima de solicitudes simultáneas en la facturación depende de tu patrón de tráfico. Reducir la cantidad máxima de solicitudes simultáneas puede generar una factura más baja si el parámetro de configuración más bajo genera lo siguiente:

  • Disminución de la latencia
  • Instancias que completan su trabajo más rápido
  • Las instancias se cierran más rápido, incluso si se requieren más instancias en total

Sin embargo, también es posible lo contrario: reducir la cantidad máxima de solicitudes simultáneas puede aumentar la facturación si el aumento en la cantidad de instancias no supera la reducción en el tiempo que se ejecuta cada instancia, debido a la latencia mejorada.

La mejor manera de optimizar la facturación es mediante pruebas de carga con diferentes parámetros de configuración de solicitudes simultáneas máximas para identificar el parámetro que genera el tiempo de instancia facturable más bajo, como se ve en la métrica de supervisión container/billable_instance_time.

Seguridad de contenedores

Se aplican muchas prácticas de seguridad de software de uso general a los servicios 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.

  • Evita el uso de las funciones de versión preliminar con las políticas de la organización personalizadas.

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.

Compila imágenes de contenedor mínimas

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.

Debido a la tecnología de transmisión de imágenes de contenedor de Cloud Run, el tamaño de la imagen de contenedor no afecta los tiempos de inicio del contenedor ni el tiempo de procesamiento de solicitudes. Además, el tamaño de la imagen de contenedor 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: