Борьба с холодным стартом serverless-функций: «подогрев» среды и оптимизация запуска контейнеров

b990e9e7ef5bf2076d8f3b16a06c19c8.jpeg

Основная претензия при работе с serverless — время холодного старта, которым нельзя управлять «из коробки». Если функция стартует впервые за последние 5–25 минут, скорее всего запуск будет долгим — сотни миллисекунд. Причём статически типизированные языки имеют в разы большее время холодного запуска, которое может достигать нескольких секунд. Разработчики решают это на этапе загрузки своего кода, и им в помощь есть целые библиотеки. Например, они позволяют вызвать функцию заранее. Эти способы действительно сокращают время, но не устраняют проблему полностью и могут работать нестабильно. Параллельно этот вопрос пытаются решить и сами облачные провайдеры. Сегодня поговорим о том, как с холодным стартом справляются и те и другие.

Откуда берётся время холодного запуска

Задержка запуска бессерверных вычислений «на холодную» появляется из-за того, что после получения запроса или срабатывания любого другого триггера облачный serverless-сервис, как правило, сначала загружает код и готовит среду выполнения с нужными параметрами и только потом запускает функцию. Суть проблемы можно понять по этой схеме:

image-loader.svg

Голубым обозначено время холодного запуска. Всё это время запрос в Amazon API Gateway ожидает в очереди. В зависимости от политики платформы это время до непосредственного выполнения может ещё и оплачиваться. У Yandex.Cloud в старой версии рантаймов — без специальных нововведений для уменьшения времени холодного запуска — оплачивается время старта интерпретатора и загрузка кода.

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

К сожалению, временем жизни среды разработчик управлять никак не может. Платформа сохраняет выделенные ресурсы, пока они не потребуются кому-то другому (или пока не освободятся по таймауту). В итоге среда может и полчаса просуществовать, а может исчезнуть уже через несколько минут.

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

image-loader.svg

Сравнение времени холодного запуска serverless-функций для разных рантаймов на 2019 год. Источник.

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

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

Приёмы оптимизации на уровне своего кода

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

Начнём с уровня разработки.

image-loader.svg

Здесь видно, чья оптимизация работает на каждом из этапов.

«Подогрев»

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

Чтобы оптимизировать нагрузку и снизить стоимость «прогрева» (всё-таки вы используете ресурсы), можно различать обычные и «прогревочные» вызовы. Последние не должны выполнять весь код функции. Зачастую достаточно просто проверить связь со средой обычным пингом — для этого даже существуют специальные библиотеки, например lambda-warmer или serverless-plugin-warmup.

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

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

Фокусы с языками и кодом

Раз уж время холодного запуска в определённой степени зависит от кода, можно заняться его оптимизацией, в частности сокращением размера пакета.

Надо учитывать, что для некоторых языков время холодного запуска меньше (например, у Node.js, Python или Go оно минимально). Пищу для размышлений на эту тему можно найти в соответствующих рейтингах.

Для кода на Java есть ещё, может быть, не самый оптимальный, но элегантный вариант ускорить «прогрев» виртуальной машины, используя универсальную GraalVM, способную компилировать функцию заранее в двоичные файлы (инструмент Native Image). Так из общего времени холодного запуска можно вычесть время «прогрева» виртуальной машины. К сожалению, этот способ не поддерживает динамическую загрузку классов и имеет некоторые другие ограничения.

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

Выделение ресурсов

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

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

Оптимизация на уровне запуска контейнера

Подход платформ к решению проблемы холодного запуска со своей стороны примерно одинаковый: предварительный прогрев инфраструктуры «на всякий случай».

У AWS эта опция называется Provisioned Concurrency. Она поддерживает среду и функцию в состоянии «боевой готовности», заранее развёртывая среду и запуская код инициализации. Документация AWS обещает ответ за двузначное число миллисекунд. Главное преимущество Provisioned Concurrency — гарантии со стороны платформы. Правда, за это придётся заплатить. Опция особенно полезна для языков, время холодного запуска на которых максимально (например, Java).

У Yandex Cloud Functions оптимизация встроена в последний релиз (летом этого года он вышел в статусе Preview для двух рантаймов). Здесь предусмотрен пул виртуальных машин, которые при появлении запроса поступают в распоряжение соответствующей функции. В отличие от AWS, пользовательский код в среде ещё не загружен. Но для некоторых языков (Python и Node.js) запущены интерпретаторы. В итоге время холодного старта удалось сократить в 100 раз — до нескольких миллисекунд.

Плюс в том, что Yandex.Cloud всегда использует последнюю версию рантайма — его не надо отдельно обновлять. Правда, здесь тоже есть свои ограничения, связанные с тем, что интерпретатор загружается до пользовательского кода: некоторые механизмы, например LD_PRELOAD, здесь не работают. Кроме того, нет возможности управлять интерпретатором с помощью переменных окружения. Вероятно, механизм будет ещё как-то дорабатываться, поскольку превью-версии были запущены только летом 2021 года.

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

Разрабатываем вместе с сообществом

В Yandex.Cloud есть живое и растущее serverless-комьюнити Yandex Serverless Ecosystem и официальный чат Yandex.Cloud в Telegram, где можно задавать вопросы и оперативно получать ответы от единомышленников и коллег. Присоединяйтесь!

И ещё в Yandex.Cloud действует программа free tier. Это позволяет реализовать массу проектов бесплатно, если не выходить за лимиты.

© Habrahabr.ru