Neste documento, descrevemos as práticas recomendadas para projetar, implementar, testar e implantar o Cloud Functions.
Correção
Nesta seção, descrevemos as práticas recomendadas gerais para projetar e implementar o Cloud Functions.
Escreva funções idempotentes
As funções devem produzir o mesmo resultado, mesmo que sejam chamadas várias vezes. Isso permite que você tente executar uma invocação novamente caso a anterior falhe no seu código. Para mais informações, consulte Como repetir funções baseadas em eventos.
Não inicie atividades em segundo plano
Atividades em segundo plano são tudo que ocorre depois que a função é encerrada.
A invocação de uma função termina quando a função retorna ou sinaliza
a conclusão, por exemplo, chamando o argumento callback
nas funções do Node.js
baseadas em eventos. Qualquer código executado após a finalização normal não pode acessar a CPU e não progredirá.
Além disso, quando uma invocação subsequente é executada no mesmo ambiente, a atividade em segundo plano é retomada, interferindo na nova invocação. Isso pode levar a um comportamento inesperado e erros difíceis de diagnosticar. O acesso à rede depois que uma função é concluída normalmente causa a redefinição das conexões (código do erro ECONNRESET
).
Ela normalmente pode ser detectada em registros de invocações individuais, encontrando tudo o que será registrado depois da linha que informa sobre o término da invocação. Às vezes, a atividade em segundo plano pode ser aprofundada no código, especialmente quando operações assíncronas, como callbacks ou timers, estão presentes. Revise o código para verificar se todas as operações assíncronas foram concluídas antes de você finalizar a função.
Sempre exclua arquivos temporários
O armazenamento de disco local no diretório temporário é um sistema de arquivos na memória. Os arquivos que você grava consomem memória disponível para sua função e, às vezes, permanecem entre as chamadas. A não exclusão deles pode resultar em um erro de memória insuficiente e uma subsequente inicialização a frio.
Veja a memória usada por uma função individual selecionando-a na lista de funções do console do Google Cloud e escolhendo o gráfico Uso da memória.
Se você precisar de acesso ao armazenamento de longo prazo, use as montagens de volume Cloud Run com Cloud Storage ou volumes do NFS.
É possível reduzir os requisitos de memória ao processar arquivos maiores usando pipelines. Por exemplo, processe um arquivo no Cloud Storage criando um stream de leitura, transmitindo-o por um processo baseado em stream e gravando o stream de saída diretamente no Cloud Storage.
Functions Framework
Para garantir que as mesmas dependências sejam instaladas de maneira consistente em diferentes ambientes, recomendamos que você inclua a biblioteca do Functions Framework no gerenciador de pacotes e fixe a dependência a uma versão específica do Functions Framework.
Para isso, inclua a versão de sua preferência no arquivo de bloqueio relevante
(por exemplo, package-lock.json
para Node.js ou requirements.txt
para Python).
Se o Functions Framework não estiver listado explicitamente como uma dependência, ele será adicionado automaticamente durante o processo de build usando a versão mais recente disponível.
Ferramentas
Nesta seção, fornecemos diretrizes sobre como usar ferramentas para implementar, testar e interagir com o Cloud Functions.
Desenvolvimento local
A implantação de funções demora um pouco, então geralmente é mais rápido testar o código da sua função localmente.
Os desenvolvedores do Firebase podem usar o Emulador do Cloud Functions para a CLI do Firebase.Use SendGrid para enviar e-mails
Como o Cloud Functions não permite conexões de saída na porta 25, não é possível estabelecer conexões não seguras com um servidor SMTP. A maneira recomendada de enviar e-mails é usar um serviço de terceiros, como o SendGrid. Veja outras opções para enviar e-mails no tutorial Como enviar e-mails a partir de uma instância do Google Compute Engine.
Desempenho
Nesta seção, você verá as práticas recomendadas para otimizar o desempenho.
Evite baixa simultaneidade
Como as inicializações a frio são caras, a capacidade de reutilizar instâncias iniciadas recentemente durante um pico é uma ótima otimização para lidar com a carga. Limitar a contemporaneidade limita como as instâncias atuais podem ser aproveitadas, o que gera mais inicializações a frio.
O aumento da simultaneidade ajuda a adiar várias solicitações por instância, facilitando o processamento de picos de carga.Use dependências com sabedoria
Como as funções não têm estado, o ambiente de execução normalmente é inicializado do zero, o que é chamado de inicialização a frio. Quando ocorre uma inicialização a frio, o contexto global da função é avaliado.
Se suas funções importam módulos, o tempo de carregamento deles pode ser adicionado à latência de chamada durante uma inicialização a frio. É possível reduzir essa latência, bem como o tempo necessário para implantar sua função, carregando as dependências necessárias e não carregando as dependências que sua função não utiliza.
Use variáveis globais para reutilizar objetos em futuras invocações
Não há garantias de que o estado de uma função será preservado para invocações futuras. No entanto, o Cloud Functions geralmente recicla o ambiente de execução de uma invocação anterior. Se você declarar uma variável no escopo global, o valor dela pode ser reutilizado em invocações subsequentes sem necessidade de recálculo.
Dessa forma, é possível armazenar em cache os objetos cuja recriação em cada invocação de função pode ser cara. Mover esses objetos do corpo da função para o escopo global pode resultar em melhorias significativas de desempenho. No exemplo a seguir, um objeto pesado é criado apenas uma vez por instância de função e é compartilhado em todas as invocações da função que alcançam a instância determinada:
Node.js
console.log('Global scope'); const perInstance = heavyComputation(); const functions = require('firebase-functions'); exports.function = functions.https.onRequest((req, res) => { console.log('Function invocation'); const perFunction = lightweightComputation(); res.send(`Per instance: ${perInstance}, per function: ${perFunction}`); });
Python
import time from firebase_functions import https_fn # 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() @https_fn.on_request() def scope_demo(request): # Per-function scope # This computation runs every time this function is called function_var = light_computation() return https_fn.Response(f"Instance: {instance_var}; function: {function_var}")
Essa função HTTP recebe um objeto de solicitação (flask.Request
) e retorna
o texto de resposta ou qualquer conjunto de valores que possa ser transformado em um
objeto Response
usando
make_response
.
É muito importante fazer o armazenamento em cache de conexões de rede, referências de biblioteca e objetos de cliente de API em escopo global. Leia Como otimizar redes para ver exemplos.
Reduza a inicialização a frio definindo um número mínimo de instâncias
Por padrão, o Cloud Functions dimensiona o número de instâncias com base no número de solicitações de entrada. É possível alterar esse comportamento padrão definindo um número mínimo de instâncias que o Cloud Functions precisa manter prontas para exibir solicitações. Definir um número mínimo de instâncias reduz as inicializações a frio do aplicativo. Recomendamos definir um número mínimo de instâncias e concluir a inicialização no momento do carregamento se o aplicativo for sensível à latência.
Consulte Controlar o comportamento de escalonamento para mais informações sobre essas opções de ambiente de execução.Observações sobre inicialização a frio e inicialização
A inicialização global acontece no momento do carregamento. Sem ele, a primeira solicitação precisaria concluir a inicialização e carregar módulos, o que resultaria em uma latência maior.
No entanto, a inicialização global também tem um impacto nas inicializações a frio. Para minimizar esse impacto, inicialize apenas o que for necessário para a primeira solicitação, para manter a latência da primeira solicitação o mais baixa possível.
Isso é especialmente importante se você configurou instâncias mínimas, conforme descrito acima, para uma função sensível à latência. Nesse cenário, a conclusão da inicialização no momento do carregamento e o armazenamento em cache de dados úteis garante que a primeira solicitação não precise fazer isso e seja enviada com baixa latência.
Se você inicializar variáveis no escopo global, dependendo da linguagem, tempos de inicialização longos podem resultar em dois comportamentos: - Para algumas combinações de linguagens e bibliotecas assíncronas, o framework de função pode ser executado de forma assíncrona e retornar imediatamente, fazendo com que o código continue sendo executado em segundo plano, o que pode causar problemas, como não conseguir acessar a CPU. Para evitar isso, bloqueie a inicialização do módulo, conforme descrito abaixo. Isso também garante que as solicitações não sejam atendidas até que a inicialização seja concluída. - Por outro lado, se a inicialização for síncrona, o tempo de inicialização longo vai causar inicializações a frio mais longas, o que pode ser um problema, especialmente com funções de baixa simultaneidade durante picos de carga.
Exemplo de pré-aquecimento de uma biblioteca node.js assíncrona
O Node.js com o Firestore é um exemplo de biblioteca assíncrona do Node.js. Para aproveitar o min_instances, o código abaixo conclui o carregamento e a inicialização no momento do carregamento, bloqueando o carregamento do módulo.
O TLA é usado, o que significa que o ES6 é necessário, usando uma extensão .mjs
para
o código node.js ou adicionando type: module
ao arquivo package.json.
{ "main": "main.js", "type": "module", "dependencies": { "@google-cloud/firestore": "^7.10.0", "@google-cloud/functions-framework": "^3.4.5" } }
Node.js
import Firestore from '@google-cloud/firestore'; import * as functions from '@google-cloud/functions-framework'; const firestore = new Firestore({preferRest: true}); // Pre-warm firestore connection pool, and preload our global config // document in cache. In order to ensure no other request comes in, // block the module loading with a synchronous global request: const config = await firestore.collection('collection').doc('config').get(); functions.http('fetch', (req, res) => { // Do something with config and firestore client, which are now preloaded // and will execute at lower latency. });
Exemplos de inicialização global
Node.js
const functions = require('firebase-functions'); let myCostlyVariable; exports.function = functions.https.onRequest((req, res) => { doUsualWork(); if(unlikelyCondition()){ myCostlyVariable = myCostlyVariable || buildCostlyVariable(); } res.status(200).send('OK'); });
Python
from firebase_functions import https_fn # 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 @https_fn.on_request() def lazy_globals(request): 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 https_fn.Response(f"Lazy: {lazy_global}, non-lazy: {non_lazy_global}.")
Essa função HTTP usa globais inicializados de maneira lenta. Ela recebe um objeto de solicitação
(flask.Request
) e retorna o texto de resposta ou qualquer conjunto de valores que
possa ser transformado em um objeto Response
usando
make_response
.
Isso é importante principalmente se você definir várias funções em um único arquivo e diferentes funções usarem variáveis diferentes. A menos que você use a inicialização lenta, poderá desperdiçar recursos em variáveis que são inicializadas, mas nunca usadas.
Outros recursos
Saiba como otimizar o desempenho no vídeo Tempo de inicialização a frio do Cloud Functions do Atlas de desempenho do Google Cloud.