В этом документе описаны рекомендации по проектированию, реализации, тестированию и развертыванию 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(), чтобы избежать запуска кода инициализации во время развертывания. Код внутри хука 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 с Firestore – пример асинхронной библиотеки Node.js. Чтобы использовать параметр min_instances, следующий код завершает загрузку и инициализацию во время загрузки, блокируя загрузку модуля.
Используется TLA, а значит, требуется ES6. Для кода node.js используется расширение .mjs или в файл package.json добавляется type: module.
{ "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 Время холодной загрузки.