Проектирование для бессерверной модели

Что это такое

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

Почему это важно

Отказы бессерверной системы сосредоточены в швах. Функция, выполнившаяся дважды из-за повторной доставки сообщения, спишет деньги дважды и может отправить два письма. Шаг, у которого тайм-аут истёк после половины работы, оставляет несогласованное состояние. Веер вызовов на тысячу штук упирается в предел параллелизма, и излишек отбрасывается. Ничего экзотического тут нет: это обычные следствия системы, у которой нет одной машины, чтобы о ней рассуждать. Заложить защиту заранее гораздо дешевле, чем обнаружить эти проблемы под нагрузкой.

Ключевые идеи

Повторные попытки делают каждый обработчик потенциальным дублем

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

id события -> запись идемпотентности -> если видели, остановиться
                                    -> иначе сделать работу и записать id

Лучше оркестрованные шаги, чем одна длинная функция

Разбиение работы на шаги, каждый из которых быстро завершается, даёт повторным попыткам разумную гранулярность, делает частичный прогресс видимым и позволяет продолжить с места отказа, а не начать заново. Оркестровка на стороне платформы ведёт учёт шагов сама; цепочка «очередь плюс обработчики» делает это вручную. Правило проектирования в обоих случаях одно: результат каждого шага долговечен до начала следующего.

Пределы параллелизма — реальный потолок

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

Холодные старты — строка в бюджете задержки

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

Наблюдаемость обязательна, когда нет хоста

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

Модель — это спектр, а не два полюса

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

Практика

Сделайте обработчик безопасным для повторного запуска

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

Готово, когда: вторая доставка не делает записей и не отправляет письма, и вы можете указать запись, которая это доказала.

Поглотите всплеск очередью

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

Готово, когда: вы можете назвать предельное число одновременных вызовов и сказать, что происходит с сообщением, которое падает снова и снова.

Частые ошибки

  • Считать, что обработчик выполняется ровно один раз — повторные попытки дублируют побочные эффекты; исправить: сделайте обработчик идемпотентным по id события.
  • Помещать весь процесс в один вызов — при отказе он начинается с начала, а тайм-аут уже близко; исправить: разбейте на долговечные шаги.
  • Пускать веер без очереди — предел параллелизма срабатывает ровно на пике нагрузки; исправить: буферизуйте работу и рассасывайте её с безопасной скоростью.
  • Игнорировать цену холодного старта — хвостовая задержка растёт вместе с размером зависимостей; исправить: держите обработчик небольшим и измеряйте холодный путь.
  • Отлаживать по временным печатям в лог — хоста для осмотра нет; исправить: испускайте структурные логи и передавайте идентификатор корреляции.

Что читать дальше