Ежедневно на Netlify выполняется около миллиарда Edge Functions — Sunweb персонализирует страницы, Loto-Québec маршрутизирует трафик на основе проверки cookie, а сотни тысяч других сайтов делают всё: от персонализации и маршрутизации до аутентификации. Всё это работает на полноценной среде выполнения JavaScript, которая масштабируется вместе с трафиком наших клиентов.
Это создает серьезную техническую проблему, поскольку мы стремимся максимально снизить задержки. Чтобы выполнять десятки, а иногда и сотни тысяч edge-функций в секунду, нам нужно обработать каждый запрос, правильно его маршрутизировать, выделить вычислительные мощности и запустить как код нашей платформы, так и код клиента. Всё это должно происходить за миллисекунды.
За последние несколько месяцев наша команда перестроила инфраструктуру, лежащую в основе Edge Functions, тесно сотрудничая с командой Unikraft, которая написала об этом опыте со своей стороны. Раньше запросы отправлялись в сторонний сервис выполнения. Сегодня они работают на MicroVM внутри нашей собственной edge-сети — примерно в 5 раз быстрее в медианном значении. Этот переход также повышает безопасность и надежность, а также открывает больше возможностей для выполнения сложных вычислений на границе сети.
Это не меняет того, как пишутся или используются Edge Functions — импорты URL, пакеты npm, встроенные модули Node, декларации netlify.toml, локальная разработка — всё работает точно так же, как и раньше. Теперь это быстрее и надежнее. В этой статье мы хотели бы подробнее рассказать о новой архитектуре и наших выводах при создании новой вычислительной платформы, способной обслуживать большие объемы с низкими накладными расходами на производительность.
Сначала цифры
Edge-функция запускается перед сайтом при каждом соответствующем запросе. Время, которое она занимает, — это время ожидания клиента, поэтому здесь миллисекунды значат больше, чем почти где-либо еще.
Теплый вызов — маршрутизация к вычислительному узлу, вход в MicroVM, выполнение функции, формирование заголовков ответа — теперь стоит:
- ~5–6 мс в медиане (p50) по сравнению с 25–40 мс в нашей предыдущей инфраструктуре
- на 47,4% более быстрые вызовы p99
- доступность 99,998%
- доставка логов edge-функций в 5 раз быстрее
Стоит также упомянуть холодный вызов. Когда запрос поступает в регион, который еще не видел вычислительный узел, ему необходимо получить соответствующие образы, прежде чем он сможет что-либо запустить. Это происходит примерно в 1,2% вызовов и занимает в среднем около 9 мс.
Что происходит во время запроса
Далее описан путь, который проходит один запрос: он поступает на edge-узел, превращается в спецификацию, маршрутизируется на вычислительный узел, а затем передается в MicroVM, которая может существовать или не существовать в зависимости от того, является ли вызов холодным или теплым.
Запрос поступает на edge-узел
Каждый запрос попадает на ближайший к клиенту edge-узел Netlify. Узел завершает TLS-соединение и проверяет путь запроса на соответствие маршрутам Edge Functions для этого развертывания.
Если совпадений нет, запрос переходит в кэш и далее к источнику, как обычно. Если маршрут совпадает, это тот момент, где запрос раньше покидал нашу сеть. В нашей старой инфраструктуре он уходил через интернет, выполнял edge-функцию и возвращался к нам для передачи дальше. С новой вычислительной платформой запрос перенаправляется на вычислительный узел внутри нашей сети.
Создание сервиса Edge Function
Когда вычислительный узел получает запрос со спецификацией машины и ID сервиса, он сначала проверяет, существует ли уже сервис с таким ID. Если да, он перенаправляет запрос в сервис для отправки в MicroVM. Сервис позволяет нам иметь несколько MicroVM, связанных с Edge Functions одного и того же сайта, и позволяет настраивать параметры масштабирования MicroVM. Например, мы настраиваем каждый сервис так, чтобы он разрешал обработку только фиксированного количества запросов через MicroVM, прежде чем мы его завершим, чтобы избежать бесконечной работы MicroVM. Мы используем те же параметры, чтобы знать, когда заранее запустить другую MicroVM в ожидании завершения текущей.
Если сервис для edge-функций сайта еще не существует на вычислительном узле, он создается, и мы проверяем, есть ли у нас на диске все образы из спецификации машины. Если какие-то отсутствуют, они извлекаются с edge-узла и записываются на диск. Такой подход означает, что мы извлекаем только те образы edge-функций, которые получают трафик в этом регионе.
Edge-узел записывает спецификацию
Прежде чем запрос куда-либо отправится, edge-узел записывает спецификацию для машины, которая будет выполнять функцию. Спецификация называет три образа: среду выполнения, образ нашей платформы и образ edge-функции. Она также устанавливает лимиты на CPU, память и соединения.
Спецификация перемещается вместе с запросом при каждом обращении. Ее хеш и специфичная для сайта информация вычисляются, чтобы стать ID сервиса. Это обеспечивает изоляцию, поскольку два развертывания с разным кодом или разными переменными окружения являются разными сервисами, и они никогда не используют одну и ту же MicroVM.
Эта изоляция наиболее важна для сбоев, которые мы не хотим допускать. Потенциально скомпрометированное развертывание работает в отдельной MicroVM, и даже если оно выйдет за пределы среды выполнения, оно не сможет навредить другим клиентам или самому вычислительному уровню. Изоляты V8, независимо от их названия, не обеспечивают такого уровня изоляции.
Выбор вычислительного узла для запуска edge-функции
В каждом регионе есть группа вычислительных узлов. Edge-узел выбирает один из них для сервиса, используя хеширование rendezvous: один и тот же сервис каждый раз попадает на один и тот же узел, что позволяет поддерживать MicroVM «теплой», а код — уже находящимся на диске и в кэше после того, как он был прочитан. Эта «липкость» дает нам стратегию кэширования. Если бы мы распределяли запросы равномерно по всему кластеру, мы бы получили более высокий уровень холодных запусков.
Важно помнить, что хотя отправка каждого запроса для функции на один и тот же вычислительный узел — это быстрый путь, это также способ формирования «горячей точки», где одна загруженная функция конкурирует за ресурсы со всем остальным на этом узле. Сервис, занимающий большую долю трафика региона и привязанный к одному узлу, перегрузит его в ущерб другим сервисам.
Мы балансируем это, ослабляя «липкость». Превысив определенный порог, мы распределяем сервис по части узлов. Это позволяет нам поглощать внезапные скачки трафика от одного клиента, не затрагивая другие сервисы, которые были хешированы на тот же узел.
Наконец, как только узел выбран, он подтягивает код функции. Вычислительный узел, который уже обслуживал эту функцию, уже имеет ее. Узел, видящий ее впервые, извлекает ее один раз и кэширует, поэтому только первый запрос оплачивает эту стоимость.
Запуск MicroVM
Каждая функция работает в своей собственной MicroVM Firecracker. Они создаются менее чем за миллисекунду и запускаются примерно за 2 мс на p99, потому что VM запускает облегченную среду Linux, а не полноценную операционную систему. Файлы edge-функции монтируются как несжатый образ EROFS, а затем отображаются в память, поэтому VM считывает только те части пакета, которые она действительно использует, вместо того чтобы загружать всё целиком.
Когда MicroVM загружается и JavaScript-сервер начинает прослушивать порт, мы делаем снимок (snapshot) MicroVM. Когда edge-функция не вызывается, работающие под ней MicroVM масштабируются до нуля, вместо того чтобы простаивать. При следующем вызове мы запускаем новую MicroVM из этого снимка. Снимок отображается в память (memory-mapped), поэтому виртуальная машина может начать выполнение, не дожидаясь считывания всего снимка в память.
Жизненный цикл виртуальной машины — загрузка, создание снимка, восстановление и масштабирование до нуля — это работа продукта Unikraft. Мы тесно сотрудничали с ними на протяжении всего процесса миграции, чтобы убедиться, что решение выдерживает наш объем запросов и характер трафика.
Запуск и обработка ответов
После эксплуатации этого проекта в течение нескольких лет мы уже накопили знания, которые использовали для максимизации производительности и возможностей отладки. При масштабировании мы сталкивались с самыми разными проблемами: от нехватки портов на виртуальных коммутаторах до DNS (мы были удивлены, но это не всегда был DNS).
В этой итерации мы обеспечили работу локальных DNS-резолверов на вычислительных узлах. Мы также расширили набор собираемых метрик, записывая такие показатели, как время загрузки, время до открытия первого порта и время запуска пользовательского кода. Кроме того, внедрено несколько механизмов защиты (circuit breakers) для обеспечения оперативной перемаршрутизации и вывода из эксплуатации вычислительных узлов.
Это весь путь, и на «разогретом» экземпляре он добавляет около 6 мс. Ничто из этого не покидает нашу сеть, и мы контролируем весь цикл запроса. Все вышеперечисленное происходит в промежутке между поступлением запроса и отправкой ответа.
Проектирование отказоустойчивой вычислительной инфраструктуры
Когда вы создаете систему, подобную описанной выше, вы оптимизируете две вещи одновременно: пользовательский опыт и устойчивость развертывания. Нам нужно иметь возможность быстро внедрять изменения, но при этом балансировать это с возможностью столь же быстрого отката.
Вычислительные узлы создаются на основе базового образа, публикуемого Unikraft, и устанавливают набор пакетов. Эти узлы создаются отдельно от наших edge-узлов по нескольким причинам: это позволяет поддерживать легкость и быстроту edge-узлов, использовать разные типы экземпляров для вычислительных узлов и масштабировать эти узлы независимо.
Control plane отслеживает, какие вычислительные узлы существуют и исправны ли они, а edge-узлы опрашивают его для получения этого списка. Он также управляет развертыванием — новый парк узлов поднимается параллельно с работающим, масштабируется до его уровня и принимает трафик только после того, как становится полностью работоспособным.
Создание вычислительной инфраструктуры потребовало тесного сотрудничества с командой Unikraft. На протяжении всей миграции мы работали с ними над проверкой корректности, обработкой больших объемов запросов и созданием возможностей, специфичных для нашей платформы.
Это работает
Работа по перестройке нашей архитектуры edge-вычислений — это больше, чем просто прирост скорости. Это более быстрый фундамент, на котором мы можем продолжать строиться и иметь больше контроля. Самое приятное то, что он уже обслуживает ваш продакшн-трафик сегодня, по той же цене, без этапов миграции и необходимости что-либо менять в проектах.
Самостоятельное управление вычислениями означает, что мы сами можем повышать планку для Edge Functions. Это делает возможными три вещи, которые раньше были недоступны:
- Поддержка npm-пакетов вне бета-версии. npm-пакеты работают в edge-функциях уже сегодня, в бета-режиме, с оговорками относительно нативных бинарных файлов и импорта файлов во время выполнения. Настоящая виртуальная машина с настоящей файловой системой устраняет большинство причин, по которым существуют эти оговорки.
- Возможность пересмотреть эксплуатационные ограничения. Документированные лимиты в 50 мс CPU на запрос, 512 МБ памяти и 20 МБ сжатого кода были обусловлены моделью выполнения на основе изолятов (isolate-based).
- Вычисления внутри нашей собственной сети. Все, что зависит от контроля сетевого пути, вместо обращения к сторонним сервисам через интернет, теперь является тем, что мы можем реализовать.
Мы еще не закончили. Ограничения и шероховатости, которые мы не могли затронуть раньше, — это то, над чем мы работаем сейчас, так что следите за обновлениями.









