Entorno de ejecución de Cloud Functions

Cloud Functions trabaja en un entorno sin servidores totalmente administrado en el que Google se encarga de la infraestructura, los sistemas operativos y los entornos de ejecución. Cada función de Cloud Functions se ejecuta en un contexto propio, seguro y aislado, se escala automáticamente y cuenta con un ciclo de vida independiente de otras funciones.

En este documento, se explica cómo diseñar las funciones de Cloud Functions con base en estas limitaciones.

Entornos de ejecución

Cloud Functions admite varios lenguajes y entornos de ejecución. Las actualizaciones a los entornos de ejecución de los lenguajes generalmente se realizan automáticamente (a menos que se notifique algo distinto) y, además, incluyen cualquier cambio en la definición de la imagen base. Para obtener más información, consulta las páginas de los entornos de ejecución de Node.js 6, Node.js 8, Python o Go.

Simultaneidad

Cloud Functions puede iniciar varias instancias de función para aumentar la escala a fin de alcanzar la carga actual. Estas instancias se ejecutan paralelamente, lo que da como resultado poder tener más de una ejecución paralela de función.

Sin embargo, cada instancia de función maneja solo una solicitud simultánea a la vez. Esto significa que, mientras tu código está procesando una solicitud, no hay posibilidades de que una segunda solicitud se enrute a la misma instancia de función, por lo que la solicitud original puede usar la cantidad total de recursos (CPU y memoria) que solicitaste.

Puesto que diferentes instancias de función procesan las solicitudes simultáneas, estas no comparten variables ni memoria local. Esto se analiza en detalle más adelante en este documento.

Funciones sin estado

Cloud Functions implementa el paradigma sin servidores, en el que solo ejecutas el código sin preocuparte por la infraestructura subyacente, como los servidores o las máquinas virtuales. Para permitir que Google administre y escale automáticamente las funciones, estas no deben tener estado; la invocación de una función no debe basarse en un estado de la memoria configurado por una invocación previa. Sin embargo, el estado existente a menudo puede reutilizarse como una optimización del rendimiento; consulta la recomendación en Sugerencias y trucos para obtener detalles.

Por ejemplo, el valor del contador que muestra la siguiente función no corresponde al recuento total de invocación de funciones, ya que a estas podrían haberlas manejado instancias de función diferentes, que no comparten variables globales, memoria, sistemas de archivos ni otro estado:

Node.js

// Global variable, but only shared within function instance.
let count = 0;

/**
 * HTTP Cloud Function that counts how many times
 * it is executed within a specific instance.
 *
 * @param {Object} req Cloud Function request context.
 * @param {Object} res Cloud Function response context.
 */
exports.executionCount = (req, res) => {
  count++;

  // Note: the total function invocation count across
  // all instances may not be equal to this value!
  res.send(`Instance execution count: ${count}`);
};

Python (Beta)

# Global variable, modified within the function by using the global keyword.
count = 0

def statelessness(request):
    """
    HTTP Cloud Function that counts how many times it is executed
    within a specific instance.
    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 count
    count += 1

    # Note: the total function invocation count across
    # all instances may not be equal to this value!
    return 'Instance execution count: {}'.format(count)

Go (Beta)

// Package http provides a set of HTTP Cloud Function samples.
package http

import (
	"fmt"
	"net/http"
)

// count is a global variable, but only shared within a function instance.
var count = 0

// ExecutionCount is an HTTP Cloud Function that counts how many times it
// is executed within a specific instance.
func ExecutionCount(w http.ResponseWriter, r *http.Request) {
	count++

	// Note: the total function invocation count across
	// all instances may not be equal to this value!
	fmt.Fprintf(w, "Instance execution count: %d", count)
}

Si necesitas compartir estados en las invocaciones de función, tu función deberá usar un servicio como Cloud Datastore, Cloud Firestore o Cloud Storage para conservar los datos. Para ver una lista completa de las opciones de almacenamiento disponible, consulta Elige una opción de almacenamiento.

Inicios en frío

Una instancia de función nueva se inicia en dos casos:

  • Cuando implementas la función.

  • Cuando una instancia de función nueva se crea automáticamente para aumentar la escala hasta la carga o, de manera ocasional, a fin de reemplazar una instancia existente.

Iniciar una instancia de función nueva implica cargar el entorno de ejecución y tu código. Las solicitudes que incluyen el inicio de instancias de función (inicios en frío) pueden resultar más lentas que las que alcanzan instancias de función existentes. Sin embargo, si tu función recibe una carga constante, la cantidad de inicios en frío normalmente es insignificante, a menos que la función falle con frecuencia y requiera el reinicio del entorno de la función. Consulta Errores para aprender cómo solucionar los errores correctamente y evitar los inicios en frío.

Vida útil de la instancia de función

El entorno que ejecuta una instancia de función generalmente es resiliente y las invocaciones de función posteriores lo reutilizan, a menos que la cantidad de instancias se escale hacia abajo (debido a la falta de tráfico en curso) o la función falle. Esto significa que, cuando la ejecución de una función termina, la misma instancia de función puede manejar otra invocación de función. Por lo tanto, se recomienda almacenar en caché el estado en todas las invocaciones de alcance global cuando sea posible. Tu función aún debería estar preparada para trabajar sin esta caché disponible, ya que no hay garantía de que la siguiente invocación alcance la misma instancia de función (consulta Funciones sin estado).

Alcance de la función vs. alcance global

La invocación de una sola función da como resultado la ejecución del cuerpo de la función declarada solamente como el punto de entrada. El alcance global en el archivo de función, que se espera que contenga su definición, se ejecuta en cada inicio en frío, pero no si la instancia ya se inicializó.

Node.js

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

/**
 * HTTP Cloud Function that declares a variable.
 *
 * @param {Object} req Cloud Function request context.
 * @param {Object} res Cloud Function 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 (Beta)

# 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>.
    """

    # Per-function scope
    # This computation runs every time this function is called
    function_var = light_computation()
    return 'Instance: {}; function: {}'.format(instance_var, function_var)

Go (Beta)

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

Puedes suponer que el alcance global se ejecutó exactamente una vez antes de que se invocara el código de tu función en una instancia de función nueva (y en cada creación posterior de una instancia de función nueva). Sin embargo, no deberías depender de la cantidad total de ejecuciones de alcance global o de su tiempo, ya que estas dependen del ajuste de escala automático que administra Google.

Cronograma de ejecución de funciones

Una función tiene acceso a los recursos solicitados (CPU y memoria) solo por el tiempo que dura la ejecución de la función. El código que se ejecuta fuera de los períodos de ejecución no tiene garantía de ejecutarse y puede sufrir una detención en cualquier momento. Por lo tanto, deberías indicar siempre el final de la ejecución de tu función correctamente (consulta Funciones de HTTP y Funciones en segundo plano para obtener orientación) y evitar ejecutar cualquier código más allá de ese momento.

Por ejemplo, el código ejecutado después de enviar la respuesta de HTTP podría sufrir una interrupción en cualquier momento, como en el ejemplo:

Node.js

/**
 * HTTP Cloud Function that may not completely
 * execute due to early HTTP response
 *
 * @param {Object} req Cloud Function request context.
 * @param {Object} res Cloud Function response context.
 */
exports.afterResponse = (req, res) => {
  res.end();

  // This statement may not execute
  console.log('Function complete!');
};

Garantías de ejecución

Por lo general, las funciones se invocan una vez por cada evento entrante. Sin embargo, Cloud Functions no garantiza una sola invocación en todos los casos debido a las diferencias en las situaciones de error.

La cantidad máxima o mínima de veces que tu función se invocará en un solo evento depende del tipo de función.

  • Las funciones de HTTP se invocan máximo una vez. Esto se debe a la naturaleza síncrona de las llamadas a HTTP y quiere decir que se mostrará cualquier error en el manejo de la invocación de función sin reintentar el proceso. Se espera que el emisor de una función de HTTP corrija los errores y vuelva a intentar el proceso si es necesario.

  • Las funciones en segundo plano se invocan mínimo una vez. Esto se debe a su naturaleza asíncrona del manejo de eventos, en la que no hay ningún emisor que espere por la respuesta. En extrañas circunstancias, el sistema podría invocar una función más de una vez con el fin de asegurar el envío del evento. Si una función en segundo plano falla con un error, no se invocará de nuevo, a menos que se habiliten opciones a fin de reintentar en caso de error para esa función.

Para asegurarte de que tu función se comporta correctamente en los demás intentos de ejecución, debes hacerla idempotente mediante su implementación, de manera que un evento resulte como se espera (así como los efectos secundarios) incluso si se envía varias veces. En el caso de las funciones de HTTP, esto también significa mostrar el valor deseado incluso si el emisor vuelve a intentar las llamadas al extremo de la función de HTTP. Consulta Reintenta las funciones en segundo plano para obtener más información sobre cómo hacer tu función idempotente.

Errores

La forma recomendada para que una función indique un error depende del tipo de función, como se explica a continuación:

  • Las funciones de HTTP deben mostrar códigos de estado HTTP correctos que denoten un error. Consulta Funciones de HTTP para ver ejemplos.

  • Las funciones en segundo plano deben registrar y mostrar un mensaje de error. Consulta Funciones en segundo plano para ver ejemplos.

Si se muestra un error de la manera recomendada, la instancia de función que mostró el error se etiqueta como que tiene un comportamiento normal y puede entregar solicitudes futuras si se necesita.

Si tu código o cualquier otro código que llames arroja una excepción no detectada o hace que falle el proceso actual, la instancia de función puede reiniciarse antes de manejar la siguiente invocación. Esto puede generar más inicios en frío, lo que da como resultado una latencia más alta, y no es recomendable.

Consulta Genera informes de errores para ver más análisis de cómo informar errores en Cloud Functions.

Tiempo de espera

El tiempo de ejecución de la función está limitado por la duración del tiempo de espera, que puedes especificar en el momento de implementar la función. Según la configuración predeterminada, una función agota su tiempo de espera después de 1 minuto, pero puedes extender este período hasta 9 minutos Cuando la ejecución de una función supera el tiempo de espera, se muestra un estado de error de inmediato. Cualquier código restante que se esté ejecutando podría cancelarse.

Por ejemplo, el fragmento de abajo incluye código que está programado para ejecutarse 2 minutos después de que comienza la ejecución de la función. Si el tiempo de espera se configuró en 1 minuto, es posible que este código nunca se ejecute:

Node.js

/**
 * HTTP Cloud Function that may not completely
 * execute due to function execution timeout
 *
 * @param {Object} req Cloud Function request context.
 * @param {Object} res Cloud Function response context.
 */
exports.afterTimeout = (req, res) => {
  setTimeout(() => {
    // May not execute if function's timeout is <2 minutes
    console.log('Function running...');
    res.end();
  }, 120000); // 2 minute delay
};

Python (Beta)

def timeout(request):
    print('Function running...')
    time.sleep(120)

    # May not execute if function's timeout is <2 minutes
    print('Function completed!')
    return 'Function completed!'

Go (Beta)

// Package tips contains tips for writing Cloud Functions in Go.
package tips

import (
	"fmt"
	"log"
	"net/http"
	"time"
)

// Timeout sleeps for 2 minutes and may time out before finishing.
func Timeout(w http.ResponseWriter, r *http.Request) {
	log.Println("Function execution started...")
	time.Sleep(2 * time.Minute)
	log.Println("Function completed!")
	fmt.Fprintln(w, "Function completed!")
}

Para configurar el tiempo de ejecución máximo de una función con la herramienta de línea de comandos de gcloud, usa la marca --timeout en el momento de la implementación, como en el ejemplo:

gcloud functions deploy FUNCTION_NAME --timeout=TIMEOUT FLAGS...

En el comando anterior, FLAGS... se refiere a las otras opciones que pasas durante la implementación de la función. Para obtener una referencia completa del comando deploy, consulta gcloud functions deploy.

También puedes configurar el tiempo de espera durante la creación de la función en GCP Console con estos pasos:

  1. Ve a la página Descripción general de Cloud Functions en GCP Console.

  2. Haz clic en Crear función.

  3. Completa los campos obligatorios para la función.

  4. Haz clic en Más para ver la configuración avanzada.

  5. Ingresa un valor en el campo Tiempo de espera.

Sistema de archivos

El entorno de ejecución de funciones contiene un archivo ejecutable de función, además de archivos y directorios incluidos en el paquete de la función implementada, como las dependencias locales. Estos archivos están disponibles en un directorio de solo lectura que se puede determinar con base en la ubicación del archivo de función. Ten en cuenta que el directorio de la función puede ser diferente del directorio de trabajo actual.

El siguiente ejemplo presenta una lista de funciones ubicadas en el directorio de funciones:

Node.js

const fs = require('fs');

/**
 * HTTP Cloud Function that lists files in the function directory
 *
 * @param {Object} req Cloud Function request context.
 * @param {Object} res Cloud Function response context.
 */
exports.listFiles = (req, res) => {
  fs.readdir(__dirname, (err, files) => {
    if (err) {
      console.error(err);
      res.sendStatus(500);
    } else {
      console.log('Files', files);
      res.sendStatus(200);
    }
  });
};

Python (Beta)

def list_files(request):
    import os
    from os import path

    root = path.dirname(path.abspath(__file__))
    children = os.listdir(root)
    files = [c for c in children if path.isfile(path.join(root, c))]
    return 'Files: {}'.format(files)

Go (Beta)

// Package tips contains tips for writing Cloud Functions in Go.
package tips

import (
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
)

// ListFiles lists the files in the current directory.
func ListFiles(w http.ResponseWriter, r *http.Request) {
	files, err := ioutil.ReadDir("./")
	if err != nil {
		http.Error(w, "Unable to read files", http.StatusInternalServerError)
		log.Printf("ioutil.ListFiles: %v", err)
		return
	}
	fmt.Fprintln(w, "Files:")
	for _, f := range files {
		fmt.Fprintf(w, "\t%v\n", f.Name())
	}
}

También puedes cargar código desde otros archivos implementados con la función.

La única parte del sistema de archivos que admite operaciones de escritura es el directorio /tmp, que puedes usar para almacenar archivos temporales de una instancia de función. Se trata de un punto de activación de disco local, conocido como un volumen “tmpfs”, en el que los datos escritos se almacenan en la memoria. Ten en cuenta que este proceso consumirá recursos de memoria aprovisionados para la función.

El resto del sistema de archivos es de solo lectura y accesible para la función.

Red

Tu función puede acceder a la red pública de Internet con las bibliotecas estándar que ofrece el entorno de ejecución o los proveedores de terceros. Por ejemplo, puedes llamar a un extremo HTTP como se muestra abajo:

Node.js

const request = require('request');

/**
 * HTTP Cloud Function that makes an HTTP request
 *
 * @param {Object} req Cloud Function request context.
 * @param {Object} res Cloud Function response context.
 */
exports.makeRequest = (req, res) => {
  // The URL to send the request to
  const url = 'https://example.com';

  request(url, (err, response) => {
    if (!err && response.statusCode === 200) {
      res.sendStatus(200);
    } else {
      res.sendStatus(500);
    }
  });
};

Python (Beta)

def make_request(request):
    """
    HTTP Cloud Function that makes another HTTP request.
    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>.
    """
    import requests

    # The URL to send the request to
    url = 'http://example.com'

    # Process the request
    response = requests.get(url)
    response.raise_for_status()
    return 'Success!'

Go (Beta)

// Package http provides a set of HTTP Cloud Function samples.
package http

import (
	"fmt"
	"net/http"
	"time"
)

var urlString = "https://example.com"

// client is used to make HTTP requests with a 10 second timeout.
// http.Clients should be reused instead of created as needed.
var client = &http.Client{
	Timeout: 10 * time.Second,
}

// MakeRequest is an example of making an HTTP request. MakeRequest uses a
// single http.Client for all requests to take advantage of connection
// pooling and caching. See https://godoc.org/net/http#Client.
func MakeRequest(w http.ResponseWriter, r *http.Request) {
	resp, err := client.Get(urlString)
	if err != nil {
		http.Error(w, "Error making request", http.StatusInternalServerError)
		return
	}
	if resp.StatusCode != http.StatusOK {
		msg := fmt.Sprintf("Bad StatusCode: %d", resp.StatusCode)
		http.Error(w, msg, http.StatusInternalServerError)
		return
	}
	fmt.Fprintf(w, "ok")
}

Intenta volver a usar las conexiones de red en las invocaciones de funciones, como se describe en Optimiza las Herramientas de redes. Sin embargo, ten en cuenta que el sistema podría cerrar una conexión que permanece 2 minutos sin usarse y que los intentos adicionales para usar una conexión cerrada podrían dar como resultado un error de “restablecimiento de la conexión”. Tu código debe usar una biblioteca que administre bien las conexiones cerradas o que las administre de manera explícita si usa construcciones de herramientas de redes de nivel inferior.

Varias funciones

Cada función implementada se aísla de todas las demás funciones, incluso de aquellas implementadas desde el mismo archivo fuente. En particular, no comparten memoria, variables globales, sistemas de archivos ni otros estados.

Para compartir datos en todas las funciones implementadas, puedes usar los servicios de almacenamiento como Cloud Datastore, Cloud Firestore o Cloud Storage. De manera alternativa, puedes invocar una función desde otra, con los activadores apropiados. Por ejemplo, haz una solicitud HTTP al extremo de una función de HTTP o publica un mensaje en un tema de Cloud Pub/Sub para activar una función de Cloud Pub/Sub.

¿Te ha resultado útil esta página? Enviar comentarios:

Enviar comentarios sobre...

Documentación de Cloud Functions