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

Evita las actividades en segundo plano

Cuando una aplicación que se ejecuta en Cloud Run 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 (completamente administrado), 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.

Informa errores

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

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

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 punto de entrada 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 Cloud Run, no puedes suponer que el estado del servicio se conserva entre las solicitudes. Sin embargo, Cloud Run 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

// Global (instance-wide) scope
// This computation runs at instance cold-start
const instanceVar = heavyComputation();

/**
 * HTTP function that declares a variable.
 *
 * @param {Object} req request context.
 * @param {Object} res response context.
 */
exports.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

# Global (instance-wide) scope
# This computation runs at instance cold-start
instance_var = heavy_computation()

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>.
    """
    function_var = light_computation()
    return 'Per instance: {}, per function: {}'.format(
        instance_var, 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()
}

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

// 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.
 */
exports.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

# 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

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 'Lazy: {}, non-lazy: {}.'.format(lazy_global, 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"
)

// client is lazily initialized by LazyGlobal.
var client *storage.Client
var clientOnce sync.Once

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

Minimiza el tamaño de la imagen de contenedor

Un tamaño de imagen de contenedor más grande tiene varios efectos:

  • Mayor vulnerabilidad de seguridad, ya que más código implica una superficie de ataque más grande
  • Tiempo de compilación más lento para la imagen de contenedor mientras se descargan muchos archivos
  • Tiempo de implementación más lento para el servicio, ya que la imagen de contenedor está preparada para su uso en una revisión nueva
  • Aumento de los costos de salida de red con Container Registry si el depósito de almacenamiento del contenedor está lejos respecto a la geografía de la región de servicio

En Cloud Run, 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.

A continuación, puedes obtener más información sobre la seguridad de los contenedores.

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

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:

Usa la simultaneidad

Cloud Run admite la simultaneidad configurable, que tiene consideraciones especiales para el servicio. Esto es diferente a Cloud Functions, que no admite la simultaneidad.

Ajusta la simultaneidad para tu servicio

Puedes habilitar las instancias de contenedor de un servicio para entregar varias solicitudes de forma simultánea, es decir, “al mismo tiempo”. La cantidad de solicitudes simultáneas que puede entregar cada instancia de contenedor está limitada por technology stack y el uso de recursos compartidos, como variables globales y conexiones de bases de datos.

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.

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 administradas de Container Registry 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.

Análisis de seguridad automatizado

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

Si usas Cloud Run for Anthos en Google Cloud, puedes usar la autorización binaria para asegurarte de que solo se implementen imágenes de contenedor seguras.