Ambiente de execução do Cloud Functions

O Cloud Functions é executado em um ambiente sem servidor totalmente gerenciado, em que o Google processa totalmente infraestrutura, sistemas operacionais e ambientes de execução para você. Cada função do Cloud é executada no próprio contexto de execução seguro e isolado, com dimensionamento automático e um ciclo de vida independente de outras funções.

Ambientes de execução

O Cloud Functions é compatível com vários ambientes de execução da linguagem:

Runtime Imagem base
Node.js 6 (obsoleto) Debian 8
Node.js 8 (obsoleto) Ubuntu 18.04
Node.js 10 Ubuntu 18.04
Node.js 12 (Beta) Ubuntu 18.04
Python 3.7 Ubuntu 18.04
Python 3.8 (Beta) Ubuntu 18.04
Go 1.11 Ubuntu 18.04
Go 1.13 Ubuntu 18.04
Java 11 Ubuntu 18.04

As atualizações dos ambientes de execução geralmente são feitas de maneira automática, a menos que seja notificado de outra forma. Todos os ambientes de execução recebem atualizações automáticas da versão da linguagem à medida que são disponibilizados para a comunidade de linguagem. Da mesma forma, o Cloud Functions pode aplicar atualizações a outros aspectos do ambiente de execução, como o sistema operacional ou pacotes incluídos. Essas atualizações ajudam a manter a segurança da função.

Funções sem estado

O Cloud Functions implementa o paradigma sem servidor, onde você executa seu código sem se preocupar com a infraestrutura subjacente, como servidores e máquinas virtuais. Para permitir que o Google faça o gerenciamento e escalonamento automáticos das funções, elas precisam ser sem estado. Uma invocação de função não precisa depender do estado na memória definido por uma invocação anterior. No entanto, o estado existente pode ser reutilizado para otimização de desempenho. Consulte a recomendação em Dicas e sugestões para mais detalhes.

Por exemplo, o valor do contador retornado pela função a seguir não corresponde à contagem de invocação total porque as invocações podem ser manipuladas por instâncias de função diferentes, que não compartilham variáveis globais, memória, sistemas de arquivos ou outro 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

# 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


// Package http provides a set of HTTP Cloud Functions 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)
}

Java


import com.google.cloud.functions.HttpFunction;
import com.google.cloud.functions.HttpRequest;
import com.google.cloud.functions.HttpResponse;
import java.io.BufferedWriter;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicInteger;

public class ExecutionCount implements HttpFunction {

  private final AtomicInteger count = new AtomicInteger(0);

  @Override
  public void service(HttpRequest request, HttpResponse response)
      throws IOException {
    count.getAndIncrement();

    // Note: the total function invocation count across
    // all instances may not be equal to this value!
    BufferedWriter writer = response.getWriter();
    writer.write("Instance execution count: " + count);
  }
}

Se você precisa compartilhar estados em invocações de função, sua função deve usar um serviço como Datastore, Firestore ou Cloud Storage para manter os dados. Para uma lista completa de opção de armazenamento disponíveis, consulte Como escolher uma opção de armazenamento.

Escalonamento automático e simultaneidade

O Cloud Functions lida com as solicitações recebidas atribuindo-as a instâncias da sua função. Dependendo do volume de solicitações e do número de instâncias de função existentes, o Cloud Functions pode atribuir uma solicitação a uma instância existente ou criar uma nova.

Cada instância de função lida com uma solicitação simultânea por vez. Isso quer dizer que seu código está processando uma solicitação. Não há possibilidade de uma segunda solicitação ser encaminhada para a mesma instância. Portanto, a solicitação original pode usar a quantidade total de recursos (CPU e memória) que você solicitou.

Nos casos em que o volume de solicitações de entrada excede o número de instâncias existentes, o Cloud Functions pode iniciar várias novas instâncias para processar solicitações. Esse comportamento de escalonamento automático permite que o Cloud Functions lide com muitas solicitações em paralelo, cada uma usando uma instância diferente da sua função.

Como são processadas por instâncias de função diferentes, as solicitações simultâneas não compartilham variáveis nem memória local. Isso será abordado em detalhes posteriormente neste documento.

Como controlar o comportamento de escalonamento automático

O Cloud Functions permite definir um limite para o número total de instâncias de função que podem coexistir a qualquer momento. Em alguns casos, o escalonamento ilimitado não é desejável. Por exemplo, sua função pode depender de um recurso (como um banco de dados) que não pode ser escalonado no mesmo grau que o Cloud Functions. Um grande aumento no volume de solicitações pode fazer com que o Cloud Functions crie mais instâncias de função do que seu banco de dados pode tolerar.

Inicializações a frio

Uma nova instância de função é iniciada em dois casos:

  • Quando você implanta a função.

  • Quando uma nova instância de função é criada automaticamente para ser escalonada até a carga ou para substituir uma instância existente às vezes.

O início de uma nova instância de função envolve o carregamento do ambiente de execução e do código. As solicitações que incluem inicialização de instância de função (inicializações a frio) podem ser mais lentas do que as solicitações que atingem instâncias de função existentes. Porém, se a função receber carga constante, o número de inicializações a frio normalmente será insignificante, a menos que a função falhe sempre e exija a reinicialização do ambiente de função. Consulte Erros para aprender como processar erros corretamente e evitar inicializações a frio.

Tempo de vida da instância de função

O ambiente que executa uma instância de função costuma ser resiliente e reutilizado por invocações de função subsequentes, a menos que o número de instâncias esteja sendo reduzido (por causa da falta de tráfego em andamento) ou a função falhe. Isso significa que quando uma execução de função termina, outra invocação pode ser manipulada pela mesma instância. Por isso, é recomendável armazenar em cache o estado entre as invocações em escopo global, quando possível. A função ainda deve estar preparada para funcionar sem esse cache disponível, porque não há garantia de que a próxima invocação alcançará a mesma instância de função (consulte Funções sem estado).

Escopo de função versus escopo global

Uma única invocação de função resulta na execução apenas do corpo da função declarada como o ponto de entrada. O escopo global no arquivo de função, que conterá a definição de função, é executado em cada inicialização a frio, mas não se a instância já tiver sido inicializada.

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

    # 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


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

É possível pressupor que o escopo global tenha sido executado exatamente uma vez antes da invocação do código de função em uma nova instância de função (e em cada criação subsequente de uma nova instância de função). No entanto, você não precisa depender do número total ou do tempo das execuções de escopo global, porque elas dependem do escalonamento automático gerenciado pelo Google.

Linha do tempo da execução da função

Uma função só tem acesso aos recursos solicitados (CPU e memória) durante a execução da função. Não é garantido que o código seja executado fora do período de execução. Ele também pode ser interrompido a qualquer momento. Portanto, sempre sinalize o fim da execução da função corretamente e evite executar qualquer código além dela. Consulte Funções HTTP e Funções em segundo plano para orientações.

Por exemplo, o código executado após o envio da resposta HTTP pode ser interrompido a qualquer momento:

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!');
};

É importante considerar o cronograma de execução ao inicializar seu aplicativo. As tarefas em segundo plano não devem ser criadas no escopo global durante a inicialização, já que são executadas fora da duração de uma solicitação.

Garantias de execução

As funções normalmente são invocadas uma vez para cada evento recebido. No entanto, o Cloud Functions não garante uma única invocação em todos os casos por causa de diferenças em cenários de erro.

O número máximo ou mínimo de vezes em que a função será invocada em um único evento depende do tipo da função:

  • As funções HTTP são invocadas, no máximo, uma vez. Isso acontece por causa da natureza síncrona das chamadas HTTP, e isso significa que qualquer erro no processamento da invocação da função será retornado sem nova tentativa. O autor da chamada de uma função HTTP processará os erros e tentará novamente, se necessário.

  • As funções em segundo plano são invocadas pelo menos uma vez. Isso ocorre devido à natureza assíncrona de processar eventos, em que não há nenhum autor da chamada aguardando a resposta. O sistema pode, em raras circunstâncias, invocar uma função em segundo plano mais de uma vez para garantir a entrega do evento. Se uma invocação da função em segundo plano falhar com um erro, ela não é invocada de novo, a menos que a opção novas tentativas em caso de falha esteja ativada para essa função.

Para garantir que a função se comporte corretamente em tentativas de execução repetidas, você precisa torná-la idempotente implementando-a de maneira que um evento resulte nos resultados desejados (e efeitos colaterais), mesmo que ela seja entregue várias vezes. No caso de funções HTTP, isso também indicará o retorno do valor desejado mesmo se o autor da chamada tentar novamente chamadas para o endpoint da função HTTP. Consulte Como recuperar funções em segundo plano para mais informações sobre como tornar a função idempotente.

Erros

A maneira recomendada para uma função sinalizar um erro depende do tipo de função:

  • As funções HTTP retornarão códigos de status HTTP apropriados que denotarem um erro. Consulte Funções HTTP para exemplos.

  • As funções em segundo plano precisam registrar e retornar uma mensagem de erro. Consulte Funções em segundo plano para exemplos.

Se um erro for retornado da maneira recomendada, a instância da função que retornou o erro será identificada como se comportando normalmente e poderá disponibilizar solicitações futuras, se necessário.

Caso seu código ou qualquer outro código que você chamar gerar uma exceção não capturada ou travar o processo atual, a instância da função poderá ser reiniciada antes de processar a próxima invocação. Isso pode levar a mais inicializações a frio, resultando em maior latência e, portanto, essa prática não é recomendada.

Consulte Como gerar relatórios de erros para mais discussões sobre como informar erros no Cloud Functions.

Tempo limite

O ambiente de execução da função é limitado pela duração do tempo limite, que é possível especificar na hora de implantação da função. Por padrão, uma função expira após um minuto, mas é possível estender esse período para até nove minutos.

Quando a execução da função excede o tempo limite, um status de erro é retornado imediatamente ao autor da chamada. Os recursos de CPU usados pela instância de função expirada são limitados e o processamento da solicitação pode ser pausado imediatamente. Trabalhos pausados podem ou não prosseguir com solicitações subsequentes, o que pode causar efeitos colaterais inesperados.

O snippet abaixo inclui o código programado para execução dois minutos após o início da execução da função. Se o tempo limite for definido como um minuto, esse código talvez nunca seja executado:

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

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


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

Em algumas circunstâncias, o código acima pode ser executado com êxito, mas de maneira inesperada. Considere o cenário em que a função expira. A instância que está atendendo à solicitação está pausada (limitando a CPU). O trabalho pendente está pausado. Se uma solicitação subsequente for roteada para a mesma instância, o trabalho será retomado e Function running... será emitido para os Registros.

Um sintoma comum desse comportamento é a aparência que o trabalho e os registros de uma solicitação estão "vazando" em uma solicitação subsequente. Como não é possível saber se o trabalho pausado será retomado, você não deve confiar nesse comportamento. Em vez disso, sua função deve evitar tempos limite usando uma combinação das seguintes técnicas:

  1. Defina um tempo limite maior do que o ambiente de execução esperado da função.
  2. Rastreie o tempo restante durante a execução e realize a limpeza/saída antecipadamente.

Para definir o tempo máximo de execução de uma função usando a ferramenta de linha de comando gcloud, use a sinalização --timeout no momento da implantação:

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

No comando acima, FLAGS... se refere a outras opções passadas durante a implantação da função. Para obter uma referência completa para o comando deploy, consulte gcloud functions deploy.

Você também pode definir o tempo limite durante a criação da função no Console do Cloud da seguinte maneira:

  1. Acesse a página Visão geral do Cloud Functions no Console do Cloud.

  2. Clique em Criar função.

  3. Preencha os campos obrigatórios da função.

  4. Veja as configurações avançadas clicando em Mais.

  5. Digite um valor no campo Tempo limite.

Sistema de arquivos

O ambiente de execução da função contém um arquivo de função executável, além de arquivos e diretórios incluídos no pacote de funções implantadas, como dependências locais. Esses arquivos estão disponíveis em um diretório somente leitura, que pode ser determinado com base no local do arquivo de função. O diretório da função pode ser diferente do diretório de trabalho atual.

O exemplo a seguir lista arquivos localizados no diretório da função:

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

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


// 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.
// Uses directory "serverless_function_source_code" as defined in the Go
// Functions Framework Buildpack.
// See https://github.com/GoogleCloudPlatform/buildpacks/blob/56eaad4dfe6c7bd0ecc4a175de030d2cfab9ae1c/cmd/go/functions_framework/main.go#L38.
func ListFiles(w http.ResponseWriter, r *http.Request) {
	files, err := ioutil.ReadDir("./serverless_function_source_code")
	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())
	}
}

Java


import com.google.cloud.functions.HttpFunction;
import com.google.cloud.functions.HttpRequest;
import com.google.cloud.functions.HttpResponse;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;

public class FileSystem implements HttpFunction {

  // Lists the files in the current directory.
  @Override
  public void service(HttpRequest request, HttpResponse response)
      throws IOException {
    File currentDirectory = new File(".");
    File[] files = currentDirectory.listFiles();
    PrintWriter writer = new PrintWriter(response.getWriter());
    writer.println("Files:");
    for (File f : files) {
      writer.printf("\t%s%n", f.getName());
    }
  }
}

Também é possível carregar código de outros arquivos implantados com a função.

A única parte gravável do sistema de arquivos é o diretório /tmp, que pode ser usado para armazenar arquivos temporários em uma instância da função. Esse é um ponto de montagem do disco local conhecido como volume "tmpfs", em que os dados gravados no volume são armazenados na memória. Isso consumirá recursos de memória provisionados para a função.

O restante do sistema de arquivos é somente leitura e acessível à função.

Rede

A função pode acessar a Internet pública usando bibliotecas padrão oferecidas pelo ambiente de execução ou por provedores de terceiros. Por exemplo, é possível chamar um ponto de extremidade HTTP conforme mostrado abaixo:

Node.js

const fetch = require('node-fetch');

/**
 * HTTP Cloud Function that makes an HTTP request
 *
 * @param {Object} req Cloud Function request context.
 * @param {Object} res Cloud Function response context.
 */
exports.makeRequest = async (req, res) => {
  const url = 'https://example.com'; // URL to send the request to
  const externalRes = await fetch(url);
  res.sendStatus(externalRes.ok ? 200 : 500);
};

Python

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


// Package http provides a set of HTTP Cloud Functions 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")
}

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.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpResponse.BodyHandlers;
import java.time.Duration;

public class SendHttpRequest implements HttpFunction {

  // Create a client with some reasonable defaults. This client can be reused for multiple requests.
  // (java.net.httpClient also pools connections automatically by default.)
  private static HttpClient client =
      HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();

  @Override
  public void service(HttpRequest request, HttpResponse response)
      throws IOException, InterruptedException {
    // Create a GET sendHttpRequest to "http://example.com"
    String url = "http://example.com";
    var getRequest = java.net.http.HttpRequest.newBuilder().uri(URI.create(url)).GET().build();

    // Send the sendHttpRequest using the client
    var getResponse = client.send(getRequest, BodyHandlers.ofString());

    // Write the results to the output:
    var writer = new PrintWriter(response.getWriter());
    writer.printf("Received code '%s' from url '%s'.", getResponse.statusCode(), url);
  }
}

Tente reutilizar as conexões de rede entre as invocações de função, conforme descrito em Como otimizar a rede. No entanto, uma conexão que permanece sem uso por dois minutos pode ser fechada pelo sistema, e outras tentativas de usar uma conexão fechada resultam em um erro de "redefinição de conexão". O código precisa usar uma biblioteca que processe bem conexões fechadas ou processá-las explicitamente, caso esteja usando construtos de rede de baixo nível.

Várias funções

Cada função implantada permanece isolada de todas as outras funções, mesmo as implantadas pelo mesmo arquivo de origem. Em especial, elas não compartilham memória, variáveis globais, sistemas de arquivos ou outros estados.

Para compartilhar dados em funções implantadas, é possível usar serviços de armazenamento como Datastore, Firestore ou Cloud Storage. Como alternativa, é possível invocar uma função a partir de outra, usando os gatilhos apropriados. Por exemplo, faça uma solicitação HTTP para o endpoint de uma função HTTP ou publique uma mensagem em um tópico Pub/Sub para acionar uma função Pub/Sub.