Consejos generales de desarrollo

En esta guía se incluyen prácticas recomendadas para diseñar, implementar, probar y desplegar un servicio de Cloud Run. Para ver más consejos, consulta Migrar un servicio actual.

Escribir servicios eficaces

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

Actividad en segundo plano

La actividad en segundo plano es cualquier acción que se produce después de que se haya enviado la respuesta HTTP. Para determinar si hay actividad en segundo plano en tu servicio que no sea evidente, consulta los registros para ver si hay algo registrado después de la entrada de la solicitud HTTP.

Configurar la facturación basada en instancias para usar actividades en segundo plano

Si quieres admitir actividades en segundo plano en tu servicio de Cloud Run, configura tu servicio de Cloud Run para que use la facturación basada en instancias. De esta forma, podrás ejecutar actividades en segundo plano fuera de las solicitudes y seguir teniendo acceso a la CPU.

Evita las actividades en segundo plano si usas la facturación basada en solicitudes

Si necesitas configurar tu servicio para que use la facturación basada en solicitudes, cuando el servicio de Cloud Run termine de gestionar una solicitud, se inhabilitará o se limitará considerablemente el acceso de la instancia a la CPU. No debes iniciar subprocesos ni rutinas en segundo plano que se ejecuten fuera del ámbito de los controladores de solicitudes si usas este tipo de facturación.

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

Si ejecutas subprocesos en segundo plano con la facturación basada en solicitudes habilitada, puede producirse un comportamiento inesperado, ya que cualquier solicitud posterior a la misma instancia de contenedor reanuda cualquier actividad en segundo plano suspendida.

Eliminar archivos temporales

En el entorno de Cloud Run, el almacenamiento en disco es un sistema de archivos en memoria. Los archivos escritos en el disco consumen memoria que, de lo contrario, estaría disponible para tu servicio y pueden conservarse entre invocaciones. Si no se eliminan estos archivos, se puede producir un error de falta de memoria y, posteriormente, el contenedor tardará en iniciarse.

Informar de errores

Gestiona todas las excepciones y no permitas que tu servicio falle debido a errores. Un fallo provoca que el contenedor tarde en iniciarse mientras el tráfico se pone en cola para una instancia de sustitución.

Consulta la guía de informes de errores para obtener información sobre cómo informar de errores correctamente.

Optimización del rendimiento

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

Iniciar contenedores rápidamente

Como las instancias se escalan según sea necesario, el tiempo de inicio influye en la latencia de tu servicio. Cloud Run desacopla el inicio de las instancias y el procesamiento de las solicitudes, por lo que, en algunos casos, una solicitud debe esperar a que se inicie una nueva instancia antes de procesarse. Esto suele ocurrir cuando un servicio se escala desde cero.

La rutina de inicio consta de lo siguiente:

  • Descargar la imagen de contenedor (con la tecnología de streaming de imágenes de contenedor de Cloud Run)
  • Inicia el contenedor ejecutando el comando entrypoint.
  • Esperando a que el contenedor empiece a escuchar en el puerto configurado.

Si optimizas la velocidad de inicio de los contenedores, se minimiza la latencia de procesamiento de las solicitudes.

Usar el aumento de CPU al inicio para reducir la latencia de inicio

Puedes habilitar el aumento de la CPU al inicio para aumentar temporalmente la asignación de CPU durante el inicio de la instancia y, de este modo, reducir la latencia de inicio.

Usar instancias mínimas para reducir el tiempo de inicio de los contenedores

Puedes configurar las instancias mínimas y la concurrencia para minimizar los tiempos de inicio de los contenedores. Por ejemplo, si usas un número mínimo de instancias de 1, significa que tu servicio está listo para recibir hasta el número de solicitudes simultáneas configurado para tu servicio sin necesidad de iniciar una nueva instancia.

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

Las solicitudes se quedarán pendientes hasta 3, 5 veces el tiempo medio de inicio de las instancias de contenedor de este servicio o 10 segundos, lo que sea mayor.

Usa las dependencias con cabeza

Si usas un lenguaje dinámico con bibliotecas dependientes, como importar módulos en Node.js, el tiempo de carga de esos módulos se añade a la latencia de inicio.

Para reducir la latencia de inicio, puedes hacer lo siguiente:

  • Minimiza el número y el tamaño de las dependencias para crear un servicio ligero.
  • Carga en diferido 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 de carga automática de Composer de PHP.

Usar variables globales

En Cloud Run, no puedes dar por hecho que el estado del servicio se conserva entre solicitudes. Sin embargo, Cloud Run reutiliza instancias individuales para servir el tráfico continuo, por lo que puedes declarar una variable en el ámbito global para permitir que su valor se reutilice en invocaciones posteriores. No se puede saber de antemano si una solicitud individual se beneficiará de esta reutilización.

También puedes almacenar objetos en caché en la memoria si es caro recrearlos en cada solicitud de servicio. Si se mueve de la lógica de la solicitud al ámbito global, el rendimiento será mejor.

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

Realizar la inicialización diferida de variables globales

La inicialización de las variables globales siempre se produce durante el inicio, lo que aumenta el tiempo de inicio del contenedor. Usa la inicialización diferida para los objetos que se usen con poca frecuencia para aplazar el coste de tiempo y reducir los tiempos de inicio de los contenedores.

Un inconveniente de la inicialización diferida es el aumento de la latencia de las primeras solicitudes a las nuevas instancias. Esto puede provocar un escalado excesivo y solicitudes descartadas cuando implementas una nueva revisión de un servicio que está gestionando 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  # 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}."

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

Usar un entorno de ejecución diferente

Puedes disfrutar de tiempos de inicio más rápidos si usas un entorno de ejecución diferente.

Optimizar la simultaneidad

Las instancias de Cloud Run pueden atender varias solicitudes simultáneamente, hasta un máximo de simultaneidad configurable.

Cloud Run ajusta automáticamente la simultaneidad hasta el máximo configurado.

La simultaneidad máxima predeterminada de 80 es adecuada para muchas imágenes de contenedor. Sin embargo, debes hacer lo siguiente:

  • Redúcelo si tu contenedor no puede procesar muchas solicitudes simultáneas.
  • Aumente este valor si su contenedor puede gestionar un gran volumen de solicitudes.

Ajustar la simultaneidad de tu servicio

El número de solicitudes simultáneas que puede atender cada instancia puede estar limitado por la pila tecnológica y el uso de recursos compartidos, como variables y conexiones de bases de datos.

Para optimizar tu servicio y conseguir la máxima simultaneidad estable, haz lo siguiente:

  1. Optimiza el rendimiento de tu servicio.
  2. Define el nivel de simultaneidad esperado en cualquier configuración de simultaneidad a nivel de código. No todas las pilas tecnológicas requieren este ajuste.
  3. Despliega tu servicio.
  4. Define la simultaneidad de Cloud Run de tu servicio para que sea igual o inferior a cualquier configuración a nivel de código. Si no hay ninguna configuración a nivel de código, usa la simultaneidad esperada.
  5. Usa herramientas de pruebas de carga que admitan una simultaneidad configurable. Debes confirmar que tu servicio sigue siendo estable con la carga y la simultaneidad esperadas.
  6. Si el servicio no funciona bien, ve al paso 1 para mejorarlo o al paso 2 para reducir la simultaneidad. Si el servicio funciona bien, vuelve al paso 2 y aumenta la simultaneidad.

Sigue iterando hasta que encuentres la simultaneidad estable máxima.

Asignar memoria a la simultaneidad

Cada solicitud que gestiona tu servicio requiere una 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 quieres usar un estado global mutable en un contexto simultáneo, toma medidas adicionales en tu código para asegurarte de que se hace de forma segura. Minimiza la contención limitando las variables globales a la inicialización única y reutilizándolas, tal como se ha descrito anteriormente en la sección Rendimiento.

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

Compensaciones entre el rendimiento, la latencia y el coste

Ajustar la configuración del número máximo de solicitudes simultáneas puede ayudarte a equilibrar el compromiso entre el rendimiento, la latencia y el coste de tu servicio.

Por lo general, si se establece un número máximo de solicitudes simultáneas más bajo, se reduce la latencia y el rendimiento por instancia. Si se reduce el número máximo de solicitudes simultáneas, habrá menos solicitudes que compitan por los recursos de cada instancia y cada solicitud tendrá un mejor rendimiento. Sin embargo, como cada instancia puede atender menos solicitudes a la vez, el rendimiento por instancia es menor y el servicio necesita más instancias para atender el mismo tráfico.

Por el contrario, si se configura un número máximo de solicitudes simultáneas más alto, generalmente se obtendrá una latencia y un rendimiento por instancia mayores. Es posible que las solicitudes tengan que esperar para acceder a recursos como la CPU, la GPU y el ancho de banda de la memoria dentro de la instancia, lo que provoca un aumento de la latencia. Sin embargo, cada instancia puede procesar más solicitudes a la vez, por lo que el servicio necesita menos instancias en general para procesar el mismo tráfico.

Consideraciones sobre el coste

Los precios de Cloud Run se calculan por tiempo de instancia. Si configuras la facturación basada en instancias, el tiempo de instancia es el tiempo total de vida útil de cada instancia. Si configuras la facturación basada en solicitudes, el tiempo de instancia es el tiempo que cada instancia dedica a procesar al menos una solicitud.

El impacto del número máximo de solicitudes simultáneas en la facturación depende de tu patrón de tráfico. Reducir el número máximo de solicitudes simultáneas puede dar lugar a una factura más baja si el ajuste inferior provoca lo siguiente:

  • Latencia reducida
  • Las instancias completan su trabajo más rápido
  • Las instancias se cierran más rápido, aunque se necesiten más instancias en total

Sin embargo, también puede ocurrir lo contrario: reducir el número máximo de solicitudes simultáneas puede aumentar la facturación si el aumento del número de instancias no compensa la reducción del tiempo que se ejecuta cada instancia debido a la mejora de la latencia.

La mejor forma de optimizar la facturación es realizar pruebas de carga con diferentes ajustes de solicitudes simultáneas máximas para identificar el ajuste que dé como resultado el tiempo de instancia facturable más bajo, tal como se muestra en la container/billable_instance_time métrica de monitorización.

Seguridad en contenedores

Muchas prácticas de seguridad de software de uso general se aplican a los servicios en contenedores. Hay algunas prácticas que son específicas de los contenedores o que se ajustan a la filosofía y la arquitectura de los contenedores.

Para mejorar la seguridad del contenedor, sigue estos pasos:

  • Usa imágenes base seguras y que se mantengan activamente, como las imágenes base de Google o las imágenes oficiales de Docker Hub.

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

  • Incluye en el contenedor solo lo necesario para ejecutar tu servicio. El código, los paquetes y las herramientas adicionales pueden suponer vulnerabilidades de seguridad. Consulta la sección anterior para ver el impacto en el rendimiento relacionado.

  • Implementa un proceso de compilación determinista que incluya versiones específicas de software y bibliotecas. De esta forma, se evita que se incluya código no verificado en tu contenedor.

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

  • Impide el uso de las funciones de vista previa mediante políticas de organización personalizadas.

Automatizar el análisis de seguridad

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

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

Gracias a la tecnología de streaming de imágenes de contenedor de Cloud Run, el tamaño de la imagen de contenedor no afecta al tiempo de inicio del contenedor ni al tiempo de procesamiento de las solicitudes. El tamaño de la imagen del contenedor tampoco se tiene en cuenta en la memoria disponible de tu contenedor.

Para crear un contenedor mínimo, puedes usar una imagen base ligera, como las siguientes:

Ubuntu tiene un tamaño mayor, pero es una imagen base que se usa con frecuencia y que ofrece un entorno de servidor más completo.

Si tu servicio tiene un proceso de compilación que requiere muchas herramientas, te recomendamos que utilices compilaciones multietapa para que tu contenedor sea ligero en el tiempo de ejecución.

En estos recursos encontrarás más información sobre cómo crear imágenes de contenedor ligeras: