Про асинхронность в Cloud Functions

Nikolay Matrosov
5 min readJun 26, 2022

Сегодня я хотел бы разобрать модель работы облачных функций в Яндекс Облаке.

Функции в облаке запускаются синхронно и в один момент времени обрабатывают ровно один запрос.

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

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

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

Теперь подробнее разберем, что значит синхронность запуска функции.

Для этого возьмем простую функцию на Python, которая будет делать только одно: ждать 1 секунду и возвращать ответ Ok.

Теперь, если мы вызовем ее при помощи curl, то через 1 секунду получим ответ Ok.

curl 'https://functions.yandexcloud.net/d4eic18eqks97gmifd3l'

Но, что будет, если мы будем ждать ответа не 1 секунду, 500мс?

curl -m .5 'https://functions.yandexcloud.net/d4eic18eqks97gmifd3l'

В ответ мы увидим вот что:

curl: (28) Operation timed out after 504 milliseconds with 0 bytes received

А в логах функции:

Что же произошло?

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

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

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

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

Каскадный таймаут

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

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

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

Асинхронность в функциях.

Первое, о чем вы могли подумать: «Точно просто вместо обычной функции-обработчика сделаем её асинхронной!»

Но это не поможет. Да обработчик стал асинхронным, теперь мы можем использовать в нём синтаксический сахар async/await, но вызов функции так и останется синхронным. То есть, если вы вызывая функцию по http, не дождетесь окончания ее исполнения, то она точно так же как и в случае с синхронным обработчиком вернёт 499 код ошибки.

Так что же делать, если вы хотите выполнить в облачной функции какую-то длительную работу и не хотите ждать ее завершения, и вас бы устроило, чтобы например результат был записан в базу данных?

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

Как положить сообщение в очередь?

У нас есть как минимум два похода:

  1. Мы можем напрямую взаимодействовать с очередью через AWS SDK (т.к. YMQ поддерживает протокол AWS SQS);
Пример отправки сообщения в YMQ

2. Воспользоваться интеграцией API Gateway.

После этого нужно будет лишь настроить триггер, который будет обрабатывать сообщения из очереди и готово.

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

Ограничения YMQ.

У вас может возникнуть желание применить описанный выше подход и для FIFO-очередей. К сожалению в текущей реализации триггеров вы не сможете создать триггер для FIFO-очереди YMQ. В таком случае у вас есть опции использовать для разбора очереди приложение развернутое на ВМ, в нём вы сможете читать любые YMQ очереди. Или же, если вы хотите оставаться serverless, вы можете использовать Yandex Data Streams.

Это сервис, который реализует API совместимое с AWS Kinesis. Вы точно так же, как и в случае с YMQ, сможете писать в него при помощи либо AWS SDK, либо интеграции с API Gateway. Yandex Data Streams предоставит вам гарантии порядка обработки сообщений записанных в стрим. Вам лишь нужно будет определить ключ, по которому будут шардироваться сообщения и помнить, что порядок гарантируется только в рамках сегмента, который задается ключом.

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

--

--