Trucos y consejos
En este documento se describen las prácticas recomendadas para diseñar, implementar, probar y desplegar funciones de Cloud Run.
Corrección
En esta sección se describen las prácticas recomendadas generales para diseñar e implementar funciones de Cloud Run.
Escribir funciones idempotentes
Tus funciones deben producir el mismo resultado aunque se llamen varias veces. Esto te permite volver a intentar una invocación si la anterior falla a mitad del código. Para obtener más información, consulta Reintentar funciones controladas por eventos.
Asegurarse de que las funciones HTTP envían una respuesta HTTP
Si tu función se activa mediante HTTP, recuerda enviar una respuesta HTTP, como se muestra a continuación. Si no lo haces, la función se ejecutará hasta que se agote el tiempo de espera. Si esto ocurre, se te cobrará todo el tiempo de espera. Los tiempos de espera también pueden provocar comportamientos impredecibles o arranques en frío en invocaciones posteriores, lo que da lugar a comportamientos impredecibles o latencia adicional.
Node.js
Python
Go
Java
C#
using Google.Cloud.Functions.Framework; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using System.IO; using System.Text.Json; using System.Threading.Tasks; namespace HelloHttp; public class Function : IHttpFunction { private readonly ILogger _logger; public Function(ILogger<Function> logger) => _logger = logger; public async Task HandleAsync(HttpContext context) { HttpRequest request = context.Request; // Check URL parameters for "name" field // "world" is the default value string name = ((string) request.Query["name"]) ?? "world"; // If there's a body, parse it as JSON and check for "name" field. using TextReader reader = new StreamReader(request.Body); string text = await reader.ReadToEndAsync(); if (text.Length > 0) { try { JsonElement json = JsonSerializer.Deserialize<JsonElement>(text); if (json.TryGetProperty("name", out JsonElement nameElement) && nameElement.ValueKind == JsonValueKind.String) { name = nameElement.GetString(); } } catch (JsonException parseException) { _logger.LogError(parseException, "Error parsing JSON request"); } } await context.Response.WriteAsync($"Hello {name}!", context.RequestAborted); } }
Ruby
PHP
<?php use Google\CloudFunctions\FunctionsFramework; use Psr\Http\Message\ServerRequestInterface; // Register the function with Functions Framework. // This enables omitting the `FUNCTIONS_SIGNATURE_TYPE=http` environment // variable when deploying. The `FUNCTION_TARGET` environment variable should // match the first parameter. FunctionsFramework::http('helloHttp', 'helloHttp'); function helloHttp(ServerRequestInterface $request): string { $name = 'World'; $body = $request->getBody()->getContents(); if (!empty($body)) { $json = json_decode($body, true); if (json_last_error() != JSON_ERROR_NONE) { throw new RuntimeException(sprintf( 'Could not parse body: %s', json_last_error_msg() )); } $name = $json['name'] ?? $name; } $queryString = $request->getQueryParams(); $name = $queryString['name'] ?? $name; return sprintf('Hello, %s!', htmlspecialchars($name)); }
No iniciar actividades en segundo plano
La actividad en segundo plano es todo lo que ocurre después de que tu función haya finalizado.
Una invocación de función finaliza cuando la función devuelve un valor o indica que se ha completado de otro modo, como llamando al argumento callback
en las funciones basadas en eventos de Node.js. Cualquier código que se ejecute después de la finalización correcta no podrá acceder a la CPU y no avanzará.
Además, cuando se ejecuta una invocación posterior en el mismo entorno, tu actividad en segundo plano se reanuda, lo que interfiere con la nueva invocación. Esto puede provocar un comportamiento inesperado y errores difíciles de diagnosticar. Acceder a la red después de que finalice una función suele provocar que se restablezcan las conexiones (código de error ECONNRESET
).
La actividad en segundo plano se puede detectar en los registros de invocaciones individuales. Para ello, busca cualquier elemento que se registre después de la línea que indica que la invocación ha finalizado. A veces, la actividad en segundo plano puede estar más oculta en el código, sobre todo cuando hay operaciones asíncronas, como retrollamadas o temporizadores. Revisa el código para asegurarte de que todas las operaciones asíncronas finalicen antes de terminar la función.
Eliminar siempre los archivos temporales
El almacenamiento en disco local en el directorio temporal es un sistema de archivos en memoria. Los archivos que escribes consumen la memoria disponible para tu función y, a veces, se conservan entre invocaciones. Si no se eliminan explícitamente estos archivos, puede que se produzca un error de falta de memoria y un arranque en frío.
Para ver la memoria que usa una función concreta, selecciónala en la lista de funciones de laGoogle Cloud consola y elige el gráfico Uso de memoria.
No intentes escribir fuera del directorio temporal y asegúrate de usar métodos independientes de la plataforma o del SO para crear rutas de archivos.
Puedes reducir los requisitos de memoria al procesar archivos más grandes mediante el procesamiento en paralelo. Por ejemplo, puedes procesar un archivo en Cloud Storage creando un flujo de lectura, pasándolo por un proceso basado en flujos y escribiendo el flujo de salida directamente en Cloud Storage.
Framework de Functions
Cuando implementas una función, Functions Framework se añade automáticamente como dependencia con su versión actual. Para asegurarte de que las mismas dependencias se instalen de forma coherente en diferentes entornos, te recomendamos que fijes tu función a una versión específica de Functions Framework.
Para ello, incluye la versión que prefieras en el archivo de bloqueo correspondiente (por ejemplo, package-lock.json
para Node.js o requirements.txt
para Python).
Herramientas
En esta sección se proporcionan directrices sobre cómo usar herramientas para implementar, probar e interactuar con funciones de Cloud Run.
Desarrollo local
El despliegue de funciones lleva un poco de tiempo, por lo que suele ser más rápido probar el código de tu función de forma local.
Informes de errores
En los lenguajes que usan el control de excepciones, no se deben lanzar excepciones no detectadas, ya que fuerzan los inicios en frío en invocaciones futuras. Consulta la guía de Error Reporting para obtener información sobre cómo informar de errores correctamente.
No salgas manualmente
Si sales manualmente, puede que se produzca un comportamiento inesperado. En su lugar, usa las siguientes expresiones idiomáticas específicas de cada idioma:
Node.js
No uses process.exit()
. Las funciones HTTP deben enviar una respuesta con res.status(200).send(message)
, y las funciones basadas en eventos se cerrarán cuando devuelvan un valor (ya sea de forma implícita o explícita).
Python
No uses sys.exit()
. Las funciones HTTP deben devolver explícitamente una respuesta como una cadena, y las funciones basadas en eventos se cerrarán cuando devuelvan un valor (ya sea implícita o explícitamente).
Go
No uses os.Exit()
. Las funciones HTTP deben devolver explícitamente una respuesta como una cadena, y las funciones basadas en eventos se cerrarán cuando devuelvan un valor (ya sea implícita o explícitamente).
Java
No uses System.exit()
. Las funciones HTTP deben enviar una respuesta con response.getWriter().write(message)
, y las funciones basadas en eventos se cerrarán cuando devuelvan un valor (ya sea de forma implícita o explícita).
C#
No uses System.Environment.Exit()
. Las funciones HTTP deben enviar una respuesta con context.Response.WriteAsync(message)
, y las funciones basadas en eventos se cerrarán cuando devuelvan un valor (ya sea de forma implícita o explícita).
Ruby
No uses exit()
ni abort()
. Las funciones HTTP deben devolver explícitamente una respuesta como una cadena, y las funciones basadas en eventos se cerrarán cuando devuelvan un valor (ya sea implícita o explícitamente).
PHP
No uses exit()
ni die()
. Las funciones HTTP deben devolver explícitamente una respuesta como una cadena, y las funciones basadas en eventos se cerrarán cuando devuelvan un valor (ya sea implícita o explícitamente).
Usar SendGrid para enviar correos
Las funciones de Cloud Run no permiten conexiones salientes en el puerto 25, por lo que no puedes establecer conexiones no seguras con un servidor SMTP. La forma recomendada de enviar correos es usar SendGrid. Puedes consultar otras opciones para enviar correos en el tutorial Enviar correos desde una instancia de Compute Engine.
Rendimiento
En esta sección se describen las prácticas recomendadas para optimizar el rendimiento.
Usa las dependencias con cabeza
Como las funciones no tienen estado, el entorno de ejecución se inicializa a menudo desde cero (durante lo que se conoce como arranque en frío). Cuando se produce un arranque en frío, se evalúa el contexto global de la función.
Si tus funciones importan módulos, el tiempo de carga de esos módulos puede aumentar la latencia de invocación durante un arranque en frío. Puedes reducir esta latencia, así como el tiempo necesario para desplegar tu función, cargando las dependencias correctamente y no cargando las que no utilice.
Usar variables globales para reutilizar objetos en invocaciones futuras
No se garantiza que el estado de una función se conserve para invocaciones futuras. Sin embargo, las funciones de Cloud Run suelen reciclar el entorno de ejecución de una invocación anterior. Si declaras una variable en el ámbito global, su valor se puede reutilizar en invocaciones posteriores sin tener que volver a calcularse.
De esta forma, puedes almacenar en caché objetos que pueden ser caros de recrear en cada invocación de función. Mover estos objetos del cuerpo de la función al ámbito global puede mejorar significativamente el rendimiento. En el siguiente ejemplo, se crea un objeto pesado solo una vez por instancia de función y se comparte en todas las invocaciones de función que llegan a la instancia dada:
Node.js
Python
Go
Java
C#
using Google.Cloud.Functions.Framework; using Microsoft.AspNetCore.Http; using System.Linq; using System.Threading.Tasks; namespace Scopes; public class Function : IHttpFunction { // Global (server-wide) scope. // This computation runs at server cold-start. // Warning: Class variables used in functions code must be thread-safe. private static readonly int GlobalVariable = HeavyComputation(); // Note that one instance of this class (Function) is created per invocation, // so calling HeavyComputation in the constructor would not have the same // benefit. public async Task HandleAsync(HttpContext context) { // Per-function-invocation scope. // This computation runs every time this function is called. int functionVariable = LightComputation(); await context.Response.WriteAsync( $"Global: {GlobalVariable}; function: {functionVariable}", context.RequestAborted); } private static int LightComputation() { int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; return numbers.Sum(); } private static int HeavyComputation() { int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; return numbers.Aggregate((current, next) => current * next); } }
Ruby
PHP
use Psr\Http\Message\ServerRequestInterface; function scopeDemo(ServerRequestInterface $request): string { // Heavy computations should be cached between invocations. // The PHP runtime does NOT preserve variables between invocations, so we // must write their values to a file or otherwise cache them. // (All writable directories in Cloud Functions are in-memory, so // file-based caching operations are typically fast.) // You can also use PSR-6 caching libraries for this task: // https://packagist.org/providers/psr/cache-implementation $cachePath = sys_get_temp_dir() . '/cached_value.txt'; $response = ''; if (file_exists($cachePath)) { // Read cached value from file, using file locking to prevent race // conditions between function executions. $response .= 'Reading cached value.' . PHP_EOL; $fh = fopen($cachePath, 'r'); flock($fh, LOCK_EX); $instanceVar = stream_get_contents($fh); flock($fh, LOCK_UN); } else { // Compute cached value + write to file, using file locking to prevent // race conditions between function executions. $response .= 'Cache empty, computing value.' . PHP_EOL; $instanceVar = _heavyComputation(); file_put_contents($cachePath, $instanceVar, LOCK_EX); } // Lighter computations can re-run on each function invocation. $functionVar = _lightComputation(); $response .= 'Per instance: ' . $instanceVar . PHP_EOL; $response .= 'Per function: ' . $functionVar . PHP_EOL; return $response; }
Es especialmente importante almacenar en caché las conexiones de red, las referencias de bibliotecas y los objetos de cliente de API en el ámbito global. Consulta ejemplos en Optimizar la red.
Inicializar variables globales en diferido
Si inicializa variables en el ámbito global, el código de inicialización siempre se ejecutará mediante una invocación de arranque en frío, lo que aumentará la latencia de la función.
En algunos casos, esto provoca que los servicios a los que se llama sufran timeouts intermitentes
si no se gestionan correctamente en un bloque try
/catch
. Si algunos objetos no se usan en todas las rutas de código, considera la posibilidad de inicializarlos de forma diferida bajo demanda:
Node.js
Python
Go
Java
C#
using Google.Cloud.Functions.Framework; using Microsoft.AspNetCore.Http; using System; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace LazyFields; public class Function : IHttpFunction { // This computation runs at server cold-start. // Warning: Class variables used in functions code must be thread-safe. private static readonly int NonLazyGlobal = FileWideComputation(); // This variable is initialized at server cold-start, but the // computation is only performed when the function needs the result. private static readonly Lazy<int> LazyGlobal = new Lazy<int>( FunctionSpecificComputation, LazyThreadSafetyMode.ExecutionAndPublication); public async Task HandleAsync(HttpContext context) { // In a more complex function, there might be some paths that use LazyGlobal.Value, // and others that don't. The computation is only performed when necessary, and // only once per server. await context.Response.WriteAsync( $"Lazy global: {LazyGlobal.Value}; non-lazy global: {NonLazyGlobal}", context.RequestAborted); } private static int FunctionSpecificComputation() { int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; return numbers.Sum(); } private static int FileWideComputation() { int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; return numbers.Aggregate((current, next) => current * next); } }
Ruby
PHP
Las funciones de PHP no pueden conservar variables entre solicitudes. En el ejemplo de ámbitos anterior, se usa la carga diferida para almacenar en caché los valores de las variables globales en un archivo.
Esto es especialmente importante si defines varias funciones en un mismo archivo y cada una de ellas usa variables diferentes. Si no usas la inicialización diferida, puedes desperdiciar recursos en variables que se inicializan pero que nunca se usan.
Reduce los arranques en frío definiendo un número mínimo de instancias
De forma predeterminada, Cloud Run Functions escala el número de instancias en función del número de solicitudes entrantes. Puedes cambiar este comportamiento predeterminado definiendo un número mínimo de instancias que las funciones de Cloud Run deben mantener listas para atender solicitudes. Si defines un número mínimo de instancias, se reducirán los arranques en frío de tu aplicación. Te recomendamos que definas un número mínimo de instancias si tu aplicación es sensible a la latencia.
Para saber cómo definir un número mínimo de instancias, consulta Usar instancias mínimas.
Recursos adicionales
Consulta más información sobre cómo optimizar el rendimiento en el vídeo "Google Cloud Performance Atlas" (Atlas de rendimiento de Google Cloud) sobre el tiempo de arranque en frío de las funciones de Cloud Run.