В этом документе описаны лучшие практики проектирования, внедрения, тестирования и развертывания Cloud Functions .
Правильность
В этом разделе описаны общие рекомендации по проектированию и внедрению Cloud Functions .
Напишите идемпотентные функции
Ваши функции должны давать один и тот же результат, даже если они вызываются несколько раз. Это позволяет повторить вызов, если предыдущий вызов завершился неудачей на полпути выполнения кода. Для получения дополнительной информации см. раздел «Повторные попытки вызова функций, управляемых событиями» .
Не запускайте фоновые процессы.
Фоновая активность — это всё, что происходит после завершения вашей функции. Вызов функции завершается, когда функция возвращает управление или иным образом сигнализирует о завершении, например, путем вызова аргумента callback в функциях Node.js, управляемых событиями. Любой код, выполняемый после корректного завершения, не может получить доступ к процессору и не будет продолжать работу.
Кроме того, при последующем вызове в той же среде возобновляется фоновая активность, что мешает новому вызову. Это может привести к неожиданному поведению и труднодиагностируемым ошибкам. Доступ к сети после завершения работы функции обычно приводит к сбросу соединений (код ошибки ECONNRESET ).
Фоновую активность часто можно обнаружить в логах отдельных вызовов, найдя все записи после строки, сообщающей о завершении вызова. Фоновая активность иногда может быть скрыта глубже в коде, особенно при наличии асинхронных операций, таких как коллбэки или таймеры. Проверьте свой код, чтобы убедиться, что все асинхронные операции завершились, прежде чем завершать функцию.
Всегда удаляйте временные файлы.
Локальное дисковое хранилище во временном каталоге представляет собой файловую систему в оперативной памяти. Файлы, которые вы записываете, потребляют память, доступную вашей функции, и иногда сохраняются между вызовами. Если вы явно не удалите эти файлы, это может в конечном итоге привести к ошибке нехватки памяти и последующему холодному запуску.
Вы можете просмотреть объем памяти, используемый отдельной функцией, выбрав ее в списке функций в консоли Google Cloud и выбрав график использования памяти .
Если вам требуется доступ к долговременному хранилищу, рассмотрите возможность использования монтирования томов Cloud Run с помощью Cloud Storage или томов NFS .
С помощью конвейерной обработки можно уменьшить требования к памяти при обработке больших файлов. Например, вы можете обработать файл в Cloud Storage, создав поток для чтения, пропустив его через потоковый процесс и записав выходной поток непосредственно в Cloud Storage.
Структура функций
Чтобы обеспечить согласованную установку одних и тех же зависимостей во всех средах, мы рекомендуем включить библиотеку Functions Framework в ваш менеджер пакетов и зафиксировать зависимость на определенной версии Functions Framework.
Для этого укажите предпочитаемую версию в соответствующем файле блокировки (например, package-lock.json для Node.js или requirements.txt для Python).
Если Functions Framework явно не указан в качестве зависимости, он будет автоматически добавлен в процессе сборки с использованием последней доступной версии.
Инструменты
В этом разделе представлены рекомендации по использованию инструментов для внедрения, тестирования и взаимодействия с Cloud Functions .
Местное развитие
Развертывание функции занимает некоторое время, поэтому зачастую быстрее протестировать код функции локально.
Разработчики Firebase могут использовать эмулятор Cloud Functions Firebase CLI .Избегайте превышения времени ожидания развертывания во время инициализации.
Если развертывание вашей функции завершается с ошибкой тайм-аута, это, вероятно, означает, что выполнение кода глобальной области видимости вашей функции занимает слишком много времени в процессе развертывания.
В Firebase CLI установлен таймаут по умолчанию для обнаружения ваших функций во время развертывания. Если логика инициализации в исходном коде ваших функций (загрузка модулей, выполнение сетевых запросов и т. д.) превышает этот таймаут, развертывание может завершиться неудачей.
Чтобы избежать истечения времени ожидания, используйте одну из следующих стратегий:
(Рекомендуется) использовать onInit() для отложенной инициализации.
Используйте хук onInit() , чтобы избежать выполнения кода инициализации во время развертывания. Код внутри хука onInit() будет выполняться только при развертывании функции в Cloud Run Functions, а не в процессе самого развертывания.
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, с расширением .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 .