Andersen

Оптимальное решение для Scheduled Lambda

Jun 22, 2020
Blog

Полагаю, тема запланированного запуска Lambda-функций, когда в системе нет инициатора, очень актуальна для Serverless-решений на AWS. Практически на каждом проекте я вижу только один подход, который мне очень не нравится. Это и сподвигло меня немного поделиться своим опытом.

Допустим, у нас есть следующая Serverless-архитектура приложения: точка входа — API GW, вычислительный сервис — Lambda-функции.

Мы разрабатываем мобильное приложение для бронирования номеров в гостинице с бесключевым доступом к номерам. Для открытия/закрытия двери в номер клиенты используют мобильное приложение с Bluetooth.

Перед Backend-разработчиком стоят следующие задачи:

1. Добавить API для создания и управления бронью. Клиент может выбрать даты и время начала и завершения брони. Будем считать, что это уже есть.

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

3. Система должна активировать виртуальный ключ клиента точно в момент начала брони.

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

Если мы начнем искать решения, в основном мы вспоминаем про CloudWatch Rules, SQS, SNS, кто-то задумается о Kinesis Stream.

Почему именно эти сервисы? Да просто потому, что они могут триггерить Lambda-функции.

Когда мы начинаем разбирать эти сервисы, получаем следующее:

  1. Kinesis Stream триггерит функцию, но не позволяет делать задержки перед вызовом функции.
  2. SNS не имеет встроенной поддержки задержек.
  3. SQS уже куда более подходящий сервис, и предоставляет возможность установить задержку (параметр DelaySeconds). Но как только мы залазим в документацию, мы видим, что DelaySeconds может быть максимум 15 минут. Для нашей задачи это то же самое, что SNS или Kinesis.

Итак, у нас остается CloudWatch Rules, и этот подход к решению подобных задач я вижу практически на всех проектах.

Как решается данная задача с использованием CloudWatch Rules


Мы создаем правило, которое, используя планировщик Cron, будет запускать нашу Lambda-функцию, например, каждые 5 минут.

Это рабочее решение, но на этом единственный его плюс. 

А минусы следующие:

В сутках 24 часа, 24 * 60 / 5 = 288, в итоге мы 288 раз в сутки запускаем нашу функцию. При этом мы не знаем, есть ли у нас вообще бронирования в системе.

288 * 31 = 8 928 запусков функции в месяц. Многие скажут, что для Lambda — это понты. И действительно, в месяц бесплатно предоставляется 1 млн запросов и 400 000 ГБ-секунд

Если мы не выходим за рамки лимита, это быстрое и бесплатное решение. Но давайте предположим, что клиент забронировал номер с 18:01. Наша функция запускается каждые 5 минут. Допустим, она запустилась в 18:00 — соответственно, бронь с 18:01 мы не обработаем, а следующий запуск будет только в 18:05.

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

Для клиента это будет просто трагедия.

Конечно же, в голову сразу приходит решение: запуск CloudWatch Rule каждую минуту (кстати, это минимальное значение, меньше минуты указать нельзя). В этом случае клиент максимум может увидеть задержку в 59 секунд.

Тогда у нас получается 24 * 60 * 31 = 44 640 запусков нашей Lambda-функции в месяц, и опять же — это решение. Вот только наше приложение не ограничивается только этим функционалом, поэтому стоит рассмотреть вариант, когда мы все же выходим за лимиты бесплатного использования.

Если вы выделили 128 МБ памяти для своей функции, запускали ее 44 640 раз в течение месяца, и она исполнялась каждый раз в течение 500 мс, расходы рассчитываются следующим образом:

Плата за вычисления за месяц:

  • Стоимость вычислений составляет 0,00001667 USD за ГБ‑с.
  • Суммарные вычисления (в секундах) = 44 640 * (0,5 с) = 22 320 с.
  • Суммарные вычисления (в ГБ‑с) = 22 320 * 128 МБ/1024 = 2 790 ГБ‑с.
  • Плата за вычисления за месяц = 2 790 * 0,00001667 USD = 0,0465093 USD.

Плата за запросы за месяц:

  • Стоимость запросов составляет 0,20 USD за 1 млн.
  • Плата за запросы за месяц = 0 млн * 0,2 USD/млн = 0 USD.

Суммарные расходы за месяц = Плата за вычисления + Плата за запросы = 0,0465093 USD в месяц

Как результат мы имеем следующую структуру:

Давайте честно: я не думаю, что кому-то это решение нравится. К тому же у нас могут возникнуть достаточно неприятные проблемы. Но с учетом того, что даже в самом худшем случае, с огромной нагрузкой, это, на первый взгляд, выливается в копейки, решение остается приемлемым.

Обычно мы не обходимся одним таким CloudWatch-правилом — их может быть 10-20 и проблемы возникнут именно тогда, когда функция будет работать дольше минуты. А происходит это очень часто, потому что мы начинаем обрабатывать все возможные бронирования в Lambda-функции. Соответственно одна функция читает список броней, не успевает выполниться за минуту и CloudWatch запускает еще одну функцию параллельно. Новая функция может получить из базы брони, попавшие в предыдущую, еще работающую. Это приводит к появлению коллизий, плюс съедаются слоты параллельных функций.

Да, количество параллельно вызываемых функций — это софтовое ограничение, но не бесконечное. У AWS есть клиенты, у которых этот лимит на максимуме, и им этого не хватает.

Попробуем найти более оптимальное решение 

В свое время AWS Support предлагали 2 подхода:

  1. Программно создавать CloudWatch Rule с конкретным указанием времени запуска через cron. Но этот вариант можем сразу же отбросить, так как существует ограничение в 100 правил для CloudWatch.
  2. В качестве альтернативы также можно использовать сторонний инструмент под названием Rundeck, который является планировщиком рабочей нагрузки и может использоваться на той же схеме, но без ограничения.

При этом мы выходит за рамки AWS, они уже не несут никакой ответственности и никак не помогают со сторонними сервисами. Следовательно, и этот подход я тоже не стал рассматривать.

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

Что было важно для меня:

1. Я могу запустить Lambda-функцию

2. Я могу указать точное время, когда это нужно сделать.

У меня получился следующий State Machine Definition:

Дальше я могу создавать execution в который могу передать delayTimestamp — точное время, когда нужно запустить функцию и body — данные, которые будут переданы в функцию.

Какие есть ограничения? Задание может выполняться не дольше 1 года. Для конкретных задач можно считать, что ограничений нет.

Что по стоимости? Давайте посчитаем. Рабочий процесс приложения с двумя шагами содержит три перехода между состояниями, обозначенные стрелками на схеме:

1. От состояния Start (Начало) до состояния Delay (Ожидание точного времени).

2. От состояния Delay до состояния Invoke Lambda (Запуск lambda функции).

3. От состояния Invoke Lambda до состояния End (Конец). 

Цена за переход между состояниями в регионе Восток США (Северная Вирджиния) – 0,000025 USD, а лимит бесплатного пользования составляет 4000 переходов между состояниями в месяц. Если при выполнении этого процесса 100 000 раз в течение месяца не возникало ошибок, расчет стоимости будет следующим:

Количество переходов между состояниями в рабочем процессе *  количество выполнений рабочего процесса = общее количество переходов между состояниями.

3 * 100 00 = 300 000

Общее количество переходов между состояниями — количество переходов между состояниями в рамках уровня бесплатного пользования = количество оплачиваемых переходов между состояниями.

300 000 — 4000 = 296 000

Стоимость в месяц = 296 000 х 0,000025 USD = 7,40 USD

Сюда еще нужно добавить 100 000 вызовов Lambda-функции.

128 МБ памяти, 100 000 вызовов в месяц, 500 мс = 0,041675 USD

Суммарные расходы за месяц: 7,441675 USD

Можно сказать “нифига себе, + 7 USD накрутили”, но не тут то было! Со Step Functions мы создаем задачу каждый раз после бронирования. Это означает, что у нас было 100 000 броней в месяц, когда в случае с CloudWatch Rules мы можем не иметь ни одной брони и запускать огромное количество функций. Пускай мы не заплатим за Lambda, но мы заплатим очень много за CloudWatch Logs (которые далеко не дешевы).

В итоге мы получаем:

1. Куда более чистую, красивую и наглядную архитектуру.

2. Чистую и понятную схему расходов.

3. Так же как и всегда легко расширяемую.

Добавим SQS с триггером Lambda-функции, чтобы не потерять какие-то важные эвенты с бронью на случай ошибок, и получим следующую схему:

State Machine:

В Руководстве разработчика по Step Functions описан проект Task Timer, который использует Lambda-функцию. Это отличный пример того, как управлять AWS Lambda с помощью Step Functions, хотя он и не совсем актуален так как сейчас Step Functions поддерживает прямую интеграцию с SNS, как в примере выше с SQS.

Иван Сорокин, JavaScript Software Engineer in Andersen

HR Manager

Join the team of skilled and great specialists! We are always looking for talent, so don't hesitate to contact us.

Wanted
Previous articleNext article