Некоторые особенности Google Cloud функций
Материал представляет собой адаптированный перевод выдержек из документации Google Cloud Functions, которые стоит принимать во внимание при разработке. Использование облачных функций кажется чрезвычайно простым и очевидным, однако порой их поведение может оказаться неожиданным, если не знать некоторые нюансы которые я собрал в этом положении.
Cloud Functions реализует serverles-парадигму, в которой вы просто запускаете свой код, не беспокоясь о нижележащей (базовой) инфраструктуре, такой как серверы или виртуальные машины. Чтобы позволить Google автоматически управлять и масштабировать функции, они должны быть stateless (лишенные состояния) - один вызов функции не должен зависеть от состояния в памяти, установленного предыдущим вызовом. Тем не менее, существующее состояние часто может быть повторно использовано для оптимизации производительности.
Не существует никакой гарантии, что состояние облачной функции будет сохранено для будущих вызовов. Однако облачные функции часто повторно используют среду выполнения предыдущего вызова. Если вы объявляете переменную в глобальной области видимости ее значение можно переиспользовать в последующих вызовах без необходимости повторного ее вычисления.
Таким образом, вы можете кэшировать объекты, которые могут быть дорогостоящими для воссоздания при каждом вызове функции. Перемещение таких объектов из тела функции в глобальную область может привести к значительному повышению производительности.
В следующем примере тяжелая вычислительная задача производится только один раз для каждого экземпляра функции и распределяется между всеми вызовами функций, достигающими данного экземпляра:
Если вы инициализируете переменные в глобальной области видимости, код инициализации всегда будет выполняться через вызов холодного запуска, увеличивая задержку вашей функции. Если некоторые объекты используются не во всех путях кода, рассмотрите возможность их отложенной инициализации по требованию:
// Всегда инициализирован при холодном запуске
const nonLazyGlobal = fileWideComputation();
// Объявлен при холодном запуске, но только инициализируется, если/когда функция выполняется
let lazyGlobal;
/**
* HTTP-функция, использующая глобальные переменные
* с отложенной инициализацией
*
* @param {Object} req Контекст запроса.
* @param {Object} res Контекст ответа.
*/
exports.lazyGlobals = (req, res) => {
// Это значение инициализируется только если (и когда) вызывается функция
lazyGlobal = lazyGlobal || functionSpecificComputation();
res.send(`Lazy global: ${lazyGlobal}, non-lazy global: ${nonLazyGlobal}`);
};
Это особенно важно, если вы определяете несколько функций в одном файле, а разные функции используют разные переменные. Если вы не используете отложенную инициализацию, вы можете тратить ресурсы на переменные, которые инициализируются, но никогда не используются.
Облачные функции обрабатывают входящие запросы, назначая их экземплярам вашей функции. В зависимости от объема запросов, а также количества существующих экземпляров функций, облачные функции могут назначить запрос существующему экземпляру или создать новый.
Каждый экземпляр функции обрабатывает только один параллельный запрос за раз. Это означает, что пока ваш код выполняет один запрос, второй запрос не может быть обработан в том же экземпляре. Таким образом, исходный запрос может использовать весь объем ресурсов (процессор и память), которые вы запросили.
В случаях, когда объем входящих запросов превышает количество существующих экземпляров, облачные функции могут запускать новые экземпляры для обработки запросов. Такое автоматическое масштабирование позволяет облачным функциям обрабатывать множество запросов параллельно, каждый из которых использует отдельный экземпляр вашей функции.
Окружение, в которой запущен экземпляр функции, обычно устойчиво и повторно используется последующими вызовами функций, если только число экземпляров не уменьшается (из-за отсутствия текущего трафика) или происходит сбой вашей функции. Это означает, что когда выполнение одной функции завершается, другой вызов функции может быть обработан тем же экземпляром функции. Поэтому рекомендуется по возможности кэшировать состояние между вызовами в глобальной области видимости. Однако ваша функция должна быть готова к работе без упомянутого кэша, поскольку нет гарантии, что следующий вызов достигнет того же экземпляра функции.
Новый экземпляр функции запускается в двух случаях:
/**
* HTTP-функция исполнение которой может быть прервано до ее
* заверщения в связи с досрочным ответом HTTP
*
* @param {Object} req Контекст запроса.
* @param {Object} res Контекст ответа.
*/
exports.afterResponse = (req, res) => {
res.end();
// Это выражение может быть не достигнуто
console.log('Function complete!');
};
// Эти сообщение будут доставленны в Stackdriver Error Reporting
console.error(new Error('I failed you'));
console.error('I failed you', new Error('I failed you too'));
throw new Error('I failed you'); // Will cause a cold start if not caught
// These will NOT be reported to Stackdriver Error Reporting
console.info(new Error('I failed you')); // Logging an Error object at the info level
console.error('I failed you'); // Logging something other than an Error object
throw 1; // Throwing something other than an Error object
callback('I failed you');
res.status(500).send('I failed you');
/**
* HTTP-функция исполнение которой может не завершиться
* при превышении тайм-аута функции
*
* @param {Object} req Контекст запроса.
* @param {Object} res Контекст ответа.
*/
exports.afterTimeout = (req, res) => {
setTimeout(() => {
// Может не выполниться если тайм-аут < 2 минут
console.log('Function running...');
res.end();
}, 120000); // задержка в 2 минуты
};
Вы также можете загрузить код из других файлов, развернутых с помощью функции.
Единственная директория позволяющая запись /tmp , которая может быть использована для хранения временных файлов в экземпляре функции. Это точка монтирования локального диска, известная как том «tmpfs», в котором данные, записанные на том, хранятся в памяти. Обратите внимание, что это будет потреблять ресурсы памяти, выделенные для функции.
Остальная часть файловой системы доступна только для чтения.
Ваша функция может получить доступ к всемирной сети с помощью стандартных библиотек, предлагаемых средой выполнения или сторонними поставщиками. Например, вы можете сделать HTTP запрос используя модуль node-fetch для Node.js функций.
Старайтесь переиспользовать сетевые подключения, как описано в разделе Оптимизация сетевых подключений. Однако обратите внимание, что соединение, которое не используется в течение 2 минут, может быть принудительно закрыто системой, и дальнейшие попытки использовать это соединение приведут к ошибке «сброс соединения». Ваш код должен либо использовать библиотеку, умеет правильно обрабатывать закрытые соединения, либо явно управлять ими при использовании низкоуровневых сетевых конструкций.
Каждая развернутая функция изолирована от всех других функций, даже если они развернуты из одного и того же исходного файла. В частности, они не разделяют память, глобальные переменные, файловые системы или другие состояния.
Для обмена данными между развернутыми функциями вы можете использовать службы хранения, такие как Datastore, Firestore или Cloud Storage. Кроме того, вы можете вызывать одну функцию из другой, используя соответствующие триггеры. Например, можно сделать HTTP-запрос к другой функции HTTP или опубликовать сообщение через Pub/Sub, чтобы вызвать другую функцию.
Cloud Functions реализует serverles-парадигму, в которой вы просто запускаете свой код, не беспокоясь о нижележащей (базовой) инфраструктуре, такой как серверы или виртуальные машины. Чтобы позволить Google автоматически управлять и масштабировать функции, они должны быть stateless (лишенные состояния) - один вызов функции не должен зависеть от состояния в памяти, установленного предыдущим вызовом. Тем не менее, существующее состояние часто может быть повторно использовано для оптимизации производительности.
Глобальные переменные
Не существует никакой гарантии, что состояние облачной функции будет сохранено для будущих вызовов. Однако облачные функции часто повторно используют среду выполнения предыдущего вызова. Если вы объявляете переменную в глобальной области видимости ее значение можно переиспользовать в последующих вызовах без необходимости повторного ее вычисления.
Таким образом, вы можете кэшировать объекты, которые могут быть дорогостоящими для воссоздания при каждом вызове функции. Перемещение таких объектов из тела функции в глобальную область может привести к значительному повышению производительности.
В следующем примере тяжелая вычислительная задача производится только один раз для каждого экземпляра функции и распределяется между всеми вызовами функций, достигающими данного экземпляра:
// Глобальная область видимости
// Вычисление выполняется при холодном запуске
const instanceVar = heavyComputation();
/**
* HTTP-функция,которая объявляет переменную
*
* @param {Object} req Контекст запроса.
* @param {Object} res Контекст ответа.
*/
exports.scopeDemo = (req, res) => {
// Область видимости функции
// Следующее вычисление производится при каждом вызове функции
const functionVar = lightComputation();
res.send(`Per instance: ${instanceVar}, per function: ${functionVar}`); };
// Вычисление выполняется при холодном запуске
const instanceVar = heavyComputation();
/**
* HTTP-функция,которая объявляет переменную
*
* @param {Object} req Контекст запроса.
* @param {Object} res Контекст ответа.
*/
exports.scopeDemo = (req, res) => {
// Область видимости функции
// Следующее вычисление производится при каждом вызове функции
const functionVar = lightComputation();
res.send(`Per instance: ${instanceVar}, per function: ${functionVar}`); };
Если вы инициализируете переменные в глобальной области видимости, код инициализации всегда будет выполняться через вызов холодного запуска, увеличивая задержку вашей функции. Если некоторые объекты используются не во всех путях кода, рассмотрите возможность их отложенной инициализации по требованию:
// Всегда инициализирован при холодном запуске
const nonLazyGlobal = fileWideComputation();
// Объявлен при холодном запуске, но только инициализируется, если/когда функция выполняется
let lazyGlobal;
/**
* HTTP-функция, использующая глобальные переменные
* с отложенной инициализацией
*
* @param {Object} req Контекст запроса.
* @param {Object} res Контекст ответа.
*/
exports.lazyGlobals = (req, res) => {
// Это значение инициализируется только если (и когда) вызывается функция
lazyGlobal = lazyGlobal || functionSpecificComputation();
res.send(`Lazy global: ${lazyGlobal}, non-lazy global: ${nonLazyGlobal}`);
};
Это особенно важно, если вы определяете несколько функций в одном файле, а разные функции используют разные переменные. Если вы не используете отложенную инициализацию, вы можете тратить ресурсы на переменные, которые инициализируются, но никогда не используются.
Автоматическое масштабирование и параллелизм
Облачные функции обрабатывают входящие запросы, назначая их экземплярам вашей функции. В зависимости от объема запросов, а также количества существующих экземпляров функций, облачные функции могут назначить запрос существующему экземпляру или создать новый.
Каждый экземпляр функции обрабатывает только один параллельный запрос за раз. Это означает, что пока ваш код выполняет один запрос, второй запрос не может быть обработан в том же экземпляре. Таким образом, исходный запрос может использовать весь объем ресурсов (процессор и память), которые вы запросили.
В случаях, когда объем входящих запросов превышает количество существующих экземпляров, облачные функции могут запускать новые экземпляры для обработки запросов. Такое автоматическое масштабирование позволяет облачным функциям обрабатывать множество запросов параллельно, каждый из которых использует отдельный экземпляр вашей функции.
Срок службы экземпляра функции
Окружение, в которой запущен экземпляр функции, обычно устойчиво и повторно используется последующими вызовами функций, если только число экземпляров не уменьшается (из-за отсутствия текущего трафика) или происходит сбой вашей функции. Это означает, что когда выполнение одной функции завершается, другой вызов функции может быть обработан тем же экземпляром функции. Поэтому рекомендуется по возможности кэшировать состояние между вызовами в глобальной области видимости. Однако ваша функция должна быть готова к работе без упомянутого кэша, поскольку нет гарантии, что следующий вызов достигнет того же экземпляра функции.
Холодный старт
Новый экземпляр функции запускается в двух случаях:
- Во время разворачивания вашей функции
- Когда автоматически создается новый экземпляр функции чтобы маштабироваться до нагрузки или для замены существующего экземпляра, что происходит время от времени
Запуск нового экземпляра функции включает загрузку среды (runtime) выполнения и вашего кода. Запросы, включающие запуск экземпляра функции (холодный запуск), могут выполняться медленнее, чем запросы, попадающие в существующие экземпляры функции. Однако, если ваша функция получает постоянную нагрузку, то количество холодных запусков, как правило, незначительно, в противовес функциям, которые часто выходит из строя и требует перезапуска окружения.
Вы можете предположить, что глобальная область была востребована единожды, прежде чем код функции исполнен в новом экземпляре функции (и при каждом последующем создании нового экземпляра функции). Однако вам не стоит рассчитывать на общее количество или время выполнения глобальной области видимости, поскольку они зависят от автоматического масштабирования, управляемого Google.
Хронология выполнения функции
Функция имеет доступ к запрашиваемым ресурсам (ЦП и памяти) только на время ее выполнения. Выполнение кода, запускаемого вне этого периода не гарантируется и может быть остановлено в любое время. Следовательно, вы всегда должны правильно сигнализировать о завершении выполнения вашей функции и избегать запуска какого-либо кода за ее пределами.
Например, код, выполняемый после отправки ответа HTTP, может быть прерван в любое время:
/**
* HTTP-функция исполнение которой может быть прервано до ее
* заверщения в связи с досрочным ответом HTTP
*
* @param {Object} req Контекст запроса.
* @param {Object} res Контекст ответа.
*/
exports.afterResponse = (req, res) => {
res.end();
// Это выражение может быть не достигнуто
console.log('Function complete!');
};
Гарантии исполнения
Ваши функции обычно вызываются единожды для каждого входящего события. Однако Cloud Functions не гарантирует единый вызов во всех случаях из-за различий в сценариях ошибок.
Максимальное или минимальное количество раз, которое ваша функция будет вызываться для одного события, зависит от типа вашей функции:
- HTTP-функции вызываются не более одного раза. Это происходит из-за синхронного характера HTTP-вызовов, и это означает, что любая ошибка при обработке вызова функции будет возвращена без повторных попыток. Ожидается, что вызывающая функция HTTP обработает ошибки и при необходимости повторит попытку.
- Фоновые функции вызываются как минимум один раз. Это связано с асинхронной природой обработки событий, при которой нет вызывающей стороны, ожидающей ответа. В редких случаях система может вызывать фоновую функцию более одного раза, чтобы обеспечить доставку события. Если вызов фоновой функции завершается с ошибкой функция не будет вызываться снова, если для этой функции не включены повторные попытки сбоя.
Чтобы убедиться, что ваша функция ведет себя правильно при повторных попытках выполнения, вы должны сделать ее идемпотентной, реализовав ее так, чтобы событие приводило к желаемым результатам (и побочным эффектам), даже если оно доставлено несколько раз. В случае функций HTTP это также означает возвращение желаемого значения, даже если вызывающий абонент повторяет вызовы HTTP.
Ошибки
Рекомендуемый способ сообщения функции об ошибке зависит от типа функции:
- Функции HTTP должны возвращать соответствующие коды состояния HTTP, которые обозначают ошибку
- Фоновые функции должны журналироваться и возвращать сообщение об ошибке
Если ошибка возвращается рекомендованным способом, то экземпляр функции, который вернул ошибку, помечается как нормальный и может при необходимости обслуживать будущие запросы.
Если ваш код или любой другой код, который вы вызываете, генерирует необработанное исключение или приводит к сбою текущего процесса, то экземпляр функции может быть перезапущен перед обработкой следующего вызова. Это может привести к большему числу холодных запусков, что ведет к увеличению временных задержек, и следовательно, не рекомендуется.
Вы можете отправить ошибку из облачной функции в отчеты об ошибках (Stackdriver Error Reporting), как показано ниже:
// Эти сообщение будут доставленны в Stackdriver Error Reporting
console.error(new Error('I failed you'));
console.error('I failed you', new Error('I failed you too'));
throw new Error('I failed you'); // Will cause a cold start if not caught
// These will NOT be reported to Stackdriver Error Reporting
console.info(new Error('I failed you')); // Logging an Error object at the info level
console.error('I failed you'); // Logging something other than an Error object
throw 1; // Throwing something other than an Error object
callback('I failed you');
res.status(500).send('I failed you');
Необработанные исключения, созданные вашей функцией, появятся в Stackdriver Error Reporting. Обратите внимание, что некоторые не перехваченные исключения, например асинхронные, могут вызвать холодный запуск при следующем вызове функции. Это снижает производительность функции.
Тайм-аут
Время выполнения функции ограничено временем ожидания, которое вы можете указать при развертывании функции. По умолчанию, функция отключается через 1 минуту, но вы можете продлить этот период до 9 минут.
Когда выполнение функции превышает время ожидания, статус ошибки немедленно возвращается вызывающей стороне. Ресурсы ЦП, используемые экземпляром функции, возвращаются системе и обработка запроса может быть немедленно приостановлена. Приостановленная работа может быть продолжена последующими запросами, что может вызвать неожиданные побочные эффекты.
Приведенный ниже фрагмент содержит код, который запланирован на выполнение через 2 минуты после запуска функции. Если время ожидания установлено на 1 минуту, этот код может никогда не выполниться:
/**
* HTTP-функция исполнение которой может не завершиться
* при превышении тайм-аута функции
*
* @param {Object} req Контекст запроса.
* @param {Object} res Контекст ответа.
*/
exports.afterTimeout = (req, res) => {
setTimeout(() => {
// Может не выполниться если тайм-аут < 2 минут
console.log('Function running...');
res.end();
}, 120000); // задержка в 2 минуты
};
В некоторых случаях приведенный выше код может выполняться успешно, но неожиданным образом. Рассмотрим сценарий, когда выполнение функции преодолевает тайм-аут. Экземпляр, обслуживающий запрос, приостанавливается (путем возвращения ресурсов ЦП). Если последующий запрос направляется в тот же экземпляр, работа возобновляется, и Function running... будет выведен.
Типичным проявлением такого поведения является то, что работа и журналы из одного запроса «просачиваются» в последующий запрос. Вы не должны полагаться на это поведение. Вместо этого ваша функция должна избегать тайм-аутов, используя комбинацию следующих методов:
- Установите тайм-аут выше чем ожидаемое время выполнения функции
- Отследите количество времени, оставшегося во время выполнения и сделайте очистку / заблаговременный выход
Чтобы установить максимальное время выполнения функции с помощью инструмента командной строки gcloud, используйте флаг --timeout во время развертывания:
gcloud functions deploy FUNCTION_NAME --timeout=TIMEOUT FLAGS...
В приведенной выше команде FLAGS... относится к другим параметрам, которые вы передаете во время развертывания своей функции.
Окружение функции содержит исполняемый файл функций, а также файлы и каталоги, включенные в развернутый пакет функций, такие как локальные зависимости. Эти файлы доступны в каталоге, доступном только для чтения, который можно определить на основании расположения файла функции. Обратите внимание, что каталог функции может отличаться от текущего рабочего каталога. В следующем примере перечислены файлы, расположенные в каталоге функций:
const fs = require('fs');
/**
* HTTP-функция выводит список файлов в директории функции
*
* @param {Object} req Контекст запроса.
* @param {Object} res Контекст ответа.
*/
exports.listFiles = (req, res) => {
fs.readdir(__dirname, (err, files) => {
if (err) {
console.error(err);
res.sendStatus(500);
} else {
console.log('Files', files);
res.sendStatus(200);
}
});
};
* HTTP-функция выводит список файлов в директории функции
*
* @param {Object} req Контекст запроса.
* @param {Object} res Контекст ответа.
*/
exports.listFiles = (req, res) => {
fs.readdir(__dirname, (err, files) => {
if (err) {
console.error(err);
res.sendStatus(500);
} else {
console.log('Files', files);
res.sendStatus(200);
}
});
};
Вы также можете загрузить код из других файлов, развернутых с помощью функции.
Единственная директория позволяющая запись /tmp , которая может быть использована для хранения временных файлов в экземпляре функции. Это точка монтирования локального диска, известная как том «tmpfs», в котором данные, записанные на том, хранятся в памяти. Обратите внимание, что это будет потреблять ресурсы памяти, выделенные для функции.
Остальная часть файловой системы доступна только для чтения.
Работа с сетевыми подключениями
Ваша функция может получить доступ к всемирной сети с помощью стандартных библиотек, предлагаемых средой выполнения или сторонними поставщиками. Например, вы можете сделать HTTP запрос используя модуль node-fetch для Node.js функций.
Старайтесь переиспользовать сетевые подключения, как описано в разделе Оптимизация сетевых подключений. Однако обратите внимание, что соединение, которое не используется в течение 2 минут, может быть принудительно закрыто системой, и дальнейшие попытки использовать это соединение приведут к ошибке «сброс соединения». Ваш код должен либо использовать библиотеку, умеет правильно обрабатывать закрытые соединения, либо явно управлять ими при использовании низкоуровневых сетевых конструкций.
Множество функций
Каждая развернутая функция изолирована от всех других функций, даже если они развернуты из одного и того же исходного файла. В частности, они не разделяют память, глобальные переменные, файловые системы или другие состояния.
Для обмена данными между развернутыми функциями вы можете использовать службы хранения, такие как Datastore, Firestore или Cloud Storage. Кроме того, вы можете вызывать одну функцию из другой, используя соответствующие триггеры. Например, можно сделать HTTP-запрос к другой функции HTTP или опубликовать сообщение через Pub/Sub, чтобы вызвать другую функцию.
Комментарии
Отправить комментарий