Советы

В этом документе описаны рекомендации по проектированию, реализации, тестированию и развертыванию Cloud Functions.

Правильность

В этом разделе приведены общие рекомендации по разработке и реализации Cloud Functions.

Как писать идемпотентные функции

Функции должны давать один и тот же результат, даже если они вызываются несколько раз. Это позволяет повторить вызов, если предыдущий вызов завершился ошибкой в середине кода. Дополнительную информацию можно найти в статье Повторные попытки выполнения функций, управляемых событиями.

Не запускать фоновые действия

Фоновые действия – это все, что происходит после завершения функции. Вызов функции завершается, когда она возвращает значение или иным образом сигнализирует о завершении, например путем вызова аргумента callback в управляемых событиями функциях Node.js. Любой код, запущенный после корректного завершения, не сможет получить доступ к ЦП и не будет выполняться.

Кроме того, когда в той же среде выполняется последующий вызов, фоновое действие возобновляется, мешая новому вызову. Это может привести к непредвиденным ошибкам, которые будет сложно диагностировать. Попытка доступа к сети после завершения функции обычно приводит к сбросу подключений (код ошибки ECONNRESET).

Фоновую активность часто можно обнаружить в журналах отдельных вызовов, найдя все, что регистрируется после строки, указывающей, что вызов завершен. Фоновые действия иногда могут быть скрыты глубже в коде, особенно при наличии асинхронных операций, таких как обратные вызовы или таймеры. Проверьте код и убедитесь, что все асинхронные операции завершаются до того, как вы завершите функцию.

Всегда удалять временные файлы

Локальное хранилище на диске во временном каталоге – это файловая система в памяти. Записываемые файлы занимают память, доступную для функции, и иногда сохраняются между вызовами. Если не удалять эти файлы, может возникнуть ошибка нехватки памяти и последующий холодный запуск.

Чтобы узнать, сколько памяти использует отдельная функция, выберите ее в списке функций в консоли Google Cloud и откройте диаграмму Использование памяти.

Если вам нужно долгосрочное хранилище, используйте Cloud Run точки монтирования с Cloud Storage или тома NFS.

Вы можете снизить требования к памяти при обработке больших файлов с помощью конвейеризации. Например, вы можете обработать файл в Cloud Storage, создав поток чтения, пропустив его через процесс на основе потока и записав выходной поток непосредственно в Cloud Storage.

Functions Framework

Чтобы обеспечить согласованную установку одних и тех же зависимостей в разных средах, рекомендуем включить библиотеку Functions Framework в менеджер пакетов и закрепить зависимость за определенной версией Functions Framework.

Для этого укажите нужную версию в соответствующем файле блокировки (например, package-lock.json для Node.js или requirements.txt для Python).

Если Functions Framework не указан в качестве зависимости, он будет автоматически добавлен в процессе сборки с использованием последней доступной версии.

Инструменты

В этом разделе приведены рекомендации по использованию инструментов для реализации, тестирования и взаимодействия с Cloud Functions.

Локальная разработка

Развертывание функции занимает некоторое время, поэтому часто быстрее протестировать код функции локально.

Разработчики Firebase могут использовать эмулятор Cloud Functions интерфейса командной строки Firebase.

Как избежать тайм-аутов развертывания во время инициализации

Если развертывание функции завершается ошибкой тайм-аута, это, скорее всего, означает, что выполнение кода глобальной области видимости функции во время процесса развертывания занимает слишком много времени.

В интерфейсе командной строки Firebase есть время ожидания по умолчанию для обнаружения функций во время развертывания. Если логика инициализации в исходном коде функций (загрузка модулей, выполнение сетевых вызовов и т. д.) превышает это время ожидания, развертывание может завершиться ошибкой.

Чтобы избежать этого, воспользуйтесь одним из следующих способов:

Используйте хук onInit(), чтобы код инициализации не выполнялся во время развертывания. Код внутри хука onInit() будет выполняться только при развертывании функции в Cloud Run, а не во время самого процесса развертывания.

Node.js

const { onInit } = require('firebase-functions/v2/core');
const { onRequest } = require('firebase-functions/v2/https');

// Example of a slow initialization task
function slowInitialization() {
  // Simulate a long-running operation (e.g., loading a large model, network request).
  return new Promise(resolve => {
      setTimeout(() => {
          console.log("Slow initialization complete");
          resolve("Initialized Value");
      }, 20000); // Simulate a 20-second delay
  });
}
let initializedValue;

onInit(async () => {
  initializedValue = await slowInitialization();
});

exports.myFunction = onRequest((req, res) => {
  // Access the initialized value. It will be ready after the first invocation.
  res.send(`Value: ${initializedValue}`);
});

Python

from firebase_functions.core import init
from firebase_functions import https_fn
import time

# Example of a slow initialization task
def _slow_initialization():
  time.sleep(20)  # Simulate a 20-second delay
  print("Slow initialization complete")
  return "Initialized Value"

_initialized_value = None

@init
def initialize():
  global _initialized_value
  _initialized_value = _slow_initialization()

@https_fn.on_request()
def my_function(req: https_fn.Request) -> https_fn.Response:
  # Access the initialized value. It will be ready after the first invocation.
  return https_fn.Response(f"Value: {_initialized_value}")

Альтернативный вариант: увеличьте время ожидания обнаружения

Если вы не можете провести рефакторинг кода, чтобы использовать onInit(), увеличьте время ожидания развертывания CLI с помощью переменной среды FUNCTIONS_DISCOVERY_TIMEOUT:

$ export FUNCTIONS_DISCOVERY_TIMEOUT=30
$ firebase deploy --only functions

Как отправлять электронные письма с помощью Sendgrid

Cloud Functions не разрешает исходящие подключения через порт 25, поэтому вы не можете устанавливать незащищенные подключения к SMTP-серверу. Рекомендуемый способ отправки электронных писем – использование стороннего сервиса, например SendGrid. Другие варианты отправки электронной почты описаны в руководстве Отправка электронной почты из экземпляра для Google Compute Engine.

Эффективность

В этом разделе описаны рекомендации по оптимизации эффективности.

Избегайте низкой параллельности

Поскольку холодные запуски требуют много ресурсов, возможность повторно использовать недавно запущенные экземпляры во время пиковой нагрузки – отличный способ оптимизации. Ограничение параллелизма ограничивает использование существующих экземпляров, что приводит к увеличению числа холодных запусков.

Увеличение параллелизма помогает отложить несколько запросов на экземпляр, что упрощает обработку пиков нагрузки.

Используйте зависимости с умом

Поскольку функции не сохраняют состояние, среда выполнения часто инициализируется с нуля (это называется холодным запуском). При холодном запуске оценивается глобальный контекст функции.

Если ваши функции импортируют модули, время загрузки этих модулей может увеличивать задержку вызова во время холодного запуска. Вы можете сократить эту задержку, а также время, необходимое для развертывания функции, правильно загрузив зависимости и не загружая зависимости, которые ваша функция не использует.

Используйте глобальные переменные, чтобы повторно использовать объекты в будущих вызовах

Нет гарантии, что состояние функции будет сохранено для будущих вызовов. Однако Cloud Functions часто повторно использует среду выполнения предыдущего вызова. Если вы объявите переменную в глобальной области видимости, ее значение можно будет повторно использовать в последующих вызовах без необходимости повторного вычисления.

Это позволяет кешировать объекты, воссоздание которых при каждом вызове функции может быть слишком затратным. Перемещение таких объектов из тела функции в глобальную область видимости может значительно повысить производительность. В примере ниже тяжелый объект создается только один раз для каждого экземпляра функции и используется во всех вызовах функции, достигающих данного экземпляра:

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

Эта HTTP-функция принимает объект запроса (flask.Request) и возвращает текст ответа или любой набор значений, который можно преобразовать в объект Response с помощью make_response.

Особенно важно кешировать сетевые подключения, ссылки на библиотеки и объекты API-клиентов в глобальной области видимости. Примеры приведены в разделе Оптимизация сети.

Как сократить количество холодных запусков, задав минимальное количество экземпляров

По умолчанию Cloud Functions масштабирует количество экземпляров в зависимости от количества входящих запросов. Вы можете изменить этот режим работы по умолчанию, задав минимальное количество экземпляров, которые Cloud Functions должна поддерживать в готовности к обслуживанию запросов. Указав минимальное количество экземпляров, вы сократите число холодных запусков приложения. Если ваше приложение чувствительно к задержкам, рекомендуем задать минимальное количество экземпляров и завершить инициализацию во время загрузки.

Подробнее о том, как управлять масштабированием…

Примечания о холодном запуске и инициализации

Глобальная инициализация происходит во время загрузки. Без него первому запросу потребуется завершить инициализацию и загрузить модули, что приведет к увеличению задержки.

Однако глобальная инициализация также влияет на холодные запуски. Чтобы минимизировать это влияние, инициализируйте только то, что необходимо для первого запроса, чтобы задержка первого запроса была как можно меньше.

Это особенно важно, если вы настроили минимальное количество экземпляров, как описано выше, для функции, чувствительной к задержкам. В этом случае завершение инициализации во время загрузки и кэширование полезных данных гарантирует, что первому запросу не нужно будет этого делать и он будет обслужен с низкой задержкой.

Если вы инициализируете переменные в глобальной области видимости, то в зависимости от языка длительное время инициализации может привести к двум вариантам поведения: - Для некоторых комбинаций языков и асинхронных библиотек фреймворк функций может работать асинхронно и возвращать результат немедленно, в результате чего код продолжает выполняться в фоновом режиме, что может вызвать такие проблемы, как невозможность доступа к ЦП. Чтобы избежать этого, заблокируйте инициализацию модуля, как описано ниже. Это также гарантирует, что запросы не будут обрабатываться до завершения инициализации. - С другой стороны, если инициализация синхронная, то длительное время инициализации приведет к более длительным холодным запускам, что может стать проблемой, особенно для функций с низкой параллельностью во время пиков нагрузки.

Пример предварительного прогрева асинхронной библиотеки node.js

Пример асинхронной библиотеки Node.js – Node.js с Firestore. Чтобы воспользоваться преимуществами min_instances, следующий код завершает загрузку и инициализацию во время загрузки, блокируя загрузку модуля.

Используется TLA, а значит, требуется ES6. Для этого нужно использовать расширение .mjs для кода node.js или добавить type: module в файл 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.
});

Примеры глобальной инициализации

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

В этой HTTP-функции используются глобальные переменные с отложенной инициализацией. Она принимает объект запроса (flask.Request) и возвращает текст ответа или любой набор значений, который можно преобразовать в объект Response с помощью make_response.

Это особенно важно, если вы определяете несколько функций в одном файле и разные функции используют разные переменные. Если вы не используете отложенную инициализацию, вы можете тратить ресурсы на переменные, которые инициализируются, но никогда не используются.

Дополнительные ресурсы

Подробнее об оптимизации производительности рассказывается в видео Google Cloud Performance Atlas Cloud Functions Cold Boot Time.