Советы и усилители; трюки

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

Корректность

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

Напишите идемпотентные функции

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

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

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

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

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

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

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

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

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

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

Функции Фреймворк

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

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

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

Инструменты

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

Местное развитие

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

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

Избегайте тайм-аутов развертывания во время инициализации

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

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

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

Используйте хук 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}`);
});

Питон

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}`);
});

Питон

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 с Firestore — пример асинхронной библиотеки Node.js. Чтобы воспользоваться преимуществом 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');
});

Питон

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 .