Когда находишься на критическом пути API-аутентификации, важна каждая миллисекунда. Спустя два года борьбы с ограничениями serverless мы пересобрали весь наш стек API, добившись таким образом существенного снижения сквозных задержек.

Когда мы запускали наш API на Cloudflare Workers, они казались идеальным выбором для сервиса API-аутентификации. Глобальная периферийная инфраструктура, автоматическое масштабирование и оплата только за использование. Разве это не замечательно?
Перенесёмся в будущее: мы полностью пересобрали эту систему на основе Go-серверов с хранением состояния, в результате получив шестикратный рост производительности и существенное упрощение архитектуры, позволившее реализовать самохостинг и платформонезависимость.
TL;DR:
Мы перешли с Cloudflare Workers на Go-серверы
Снизили задержки в шесть раз
Устранили сложные механизмы обхода кэшей и оверхед конвейеров данных
Упростили архитектуру, перейдя от распределённой системы к простому приложению
Обеспечили возможность самохостинга и платформонезависимость
В статье мы расскажем о том, почему совершили этот переход, о проблемах, вынудивших нас на это пойти, и о том, чему мы научились в процессе.
Предел производительности, в который мы упёрлись
Когда разработчики интегрируют Unkey в путь запросов, наши задержки напрямую влияют на удобство пользования сервисом. Мы знали, что нам нужно быть быстрыми, но serverless ставил нам препятствия на каждом этапе.
Проблема кэширования
Фундаментальной проблемой стало кэширование. В serverless отсутствует гарантия постоянства памяти между вызовами функции. Каждое чтение кэша требует сетевого запроса к внешнему хранилищу, здесь-то всё и становится болезненным. Мы продолжали использовать трюк с глобальной областью видимости, чтобы кэшировать часть данных между вызовами, но частота попадания была очень низкой.

И хотя кэш Cloudflare имеет очень хорошую частоту попадания, задержки оказались неприемлемыми. В p99 для чтения кэша стабильно требовалось от 30 мс и больше. По сравнению с другими сетевыми кэшами это не так уж плохо, но когда пытаешься создать API с временем отклика меньше 10 мс, этот результат совершенно не подходит.

В нашей стратегии кэширования использовался SWR для многоуровневых кэшей, но имелась одна тонкость: ноль сетевых запросов всегда быстрее, чем один сетевой запрос. Никакое количество уровней внешних кэшей не позволило бы нам обойти это фундаментальное ограничение.
Проблема клея SaaS
Serverless обещают, что нам не нужно будет беспокоиться об операциях, всё просто работает. И в случае выполнения конкретной функции у нас всё именно так и было. Сами по себе Cloudflare Workers очень стабильны. Однако оказалось, что необходимо множество других продуктов, решающих искусственные проблемы, созданные самим serverless.
Нужно кэширование? Добавьте Redis. Нужен батчинг? Добавьте очередь и обработчик вниз по потоку. Необходимы фичи реального времени? Добавьте ещё что-нибудь. Каждый сервис добавляет задержки, сложность и ещё одну точку отказа, не говоря уже об оплате этих сервисов.
Раздражает то, что это не какие-то неустранимые технические сложности, а ограничения, накладываемые самой моделью serverless. В традиционном сервере все эти возможности встроены или легко достижимы без сетевых переходов.
В результате нам пришлось использовать Cloudflare Durable Objects, Cloudflare Logstreams, Cloudflare Queues, Cloudflare Workflows, а затем и самодельные серверы с хранением состояния поверх всего этого.
Оказалось, что мы постоянно выбираем и интегрируем новые продукты SaaS, но не для повышения ценности продукта, а просто для того, чтобы обойти ограничения выбранной архитектуры. Многие «простые» фичи требовали сравнения поставщиков и цен, обработки аутентификации для ещё одного сервиса и отладки сетевых проблем между системами, которые находятся вне нашего контроля.
Кошмар конвейеров данных
Производительность не была единственной нашей проблемой. Столь же сложно было извлекать данные из serverless-функций.
Проблема батчинга
Наш API генерирует события для каждой верификации ключа, ограничения частоты и вызова API. В традиционных серверах эти события группируются в памяти и периодически выполняется их очистка. В serverless необходимо выполнять очистку при каждом вызове функции, потому что после обработки запроса функция может пропасть.
Из-за этого мы создали изощрённый и чрезмерно сложный конвейер:
Для событий аналитики:
Мы собрали конкретно chproxy, потому что ClickHouse не любит тысячи крошечных insert. Это Go-сервис, буферизирующий события и отправляющий их большими пакетами (батчами). Каждый Cloudflare Worker отправляет отдельные события аналитики chproxy, который агрегирует их и отправляет в ClickHouse.
Для метрик и логов:
При извлечении метрик и логов из Cloudflare Workers и передаче их в Axiom мы не могли отправлять их напрямую, потому что Axiom иногда отклонял их из-за ограничения частоты запросов. Нам пришлось создать сервис буферизации, агрегирующий логи и метрики перед отправкой их в Axiom. Поначалу мы думали. что можно просто использовать Cloudflare Queues, но это бы оказалось слишком дорого. Одним только добавлением очередей мы бы увеличили свои текущие затраты приблизительно в три раза.
Наши метрики превратились в сложные JSON-логи, записываемые Cloudflare, после чего ещё один воркер выполнял роль потребителя logdrain. Воркер-потребитель парсил полезную нагрузку от Cloudflare, отделяя события метрик от событий логов, а затем отправлял их в Axiom.
По сути, лишь для обхода ограничений serverless мы создали распределённую систему обработки событий со множеством точек отказа.
Решение: stateful-простота
Когда для версии 2 мы решили пересобрать наш API на Go, разница стала заметной мгновенно. Вместо сложного конвейера мы могли просто помещать пакеты событий в память, а затем выполнять очистку каждые несколько секунд или при достижении буфером определённого размера.
Вот и всё. Никаких вспомогательных сервисов, никаких сложных конвейеров логов, никакой координации. Простой батчинг, с которым справится любое серверное приложение.
Влияние на производительность
Одно дело — удобство эксплуатации и анализа системы, но наших пользователей волнует совсем другое. Им важна производительность. Насколько всё стало быстрее?

Мы протестировали вызов нашей конечной точки /v1/keys.verifyKey из нескольких регионов, а затем перешли на /v2/keys.verifyKey. Думаю, из графиков выше всё достаточно понятно.
Возможно, вы скажете, что довольно несправедливо измерять задержки от того же поставщика облачных услуг, от которого работает API. Однако там находится и большинство наших клиентов. Так что, может быть, сравнение и несправедливо, однако оно точно отражает реальность для пользователей. Стоит отметить, что первая версия API работает в более чем трёхстах точек присутствия по миру, а также имеет дата-центры во всех этих регионах.
Стратегические преимущества
Переход к серверам с хранением состояния открыло и другие возможности.
Самохостинг
Из-за привязки к среде исполнения Cloudflare наши клиенты не могли реализовать самохостинг Unkey. Теоретически, среда исполнения Workers опенсорсная, но настроить её локально (даже в режиме разработки) невероятно сложно.
При работе со стандартными Go-серверами самохостинг реализуется тривиальным образом:
# Вот и всё. Никакой специальной среды исполнения, никакой сложной настройки.
docker run -p 8080:8080 unkey/api
Но дело не только в выборе клиентов: это существенно повысило и удобство разработки для нас. Теперь разработчики могут за секунды запустить весь стек Unkey локально, что бесконечно упрощает отладку и тестирование.
Трансформация процесса разработки
Налог на сложность serverless негативно влиял на всю нашу команду. Для добавления каждой новой фичи нужно было задумываться о следующем:
Как обходить ограничения функций
Как обеспечивать хранение данных между вызовами функций
Как отлаживать проблемы в распределённых конвейерах логов
Как выполнять локальное тестирование с API, рассчитанными на Cloudflare
Платформонезависимость
Что ещё важнее, мы больше не были привязаны к экосистеме Cloudflare. Мы могли выполнять развёртывание, где угодно, использовать любую базу данных и интегрировать любой сторонний сервис, не беспокоясь о совместимости сред исполнения.
Стратегия миграции и уроки
Мы использовали эту миграцию как возможность устранения накопившихся со временем проблем архитектуры API. Новая версия v2 API работает параллельно со старой v1, и в течение периода вывода из эксплуатации старой версии клиенты могут пользоваться обеими.
Преимущество сохранения serverless заключается в том, что работа v1 API стоит не так много, потому что её использование постепенно сходит к нулю. По сути, мы получили бесплатный период миграции.
Что мы сохранили
Мы не выбросили всё, что использовали в serverless:
Глобальная периферийная инфраструктура: для обеспечения низких задержек по всему миру мы пользуемся AWS Global Accelerator.
Автоматическое масштабирование: Fargate занимается масштабированием без ограничений serverless.
Скачок производительности ограничителя частоты
Особенно заметным улучшение стало в ограничителе частоты запросов. В нашей serverless-модели необходимо было идти на серьёзные компромиссы между скоростью, точностью и затратами. Из-за распределённости системы достичь всех трёх параметров было практически невозможно.
При работе с stateful-серверами и хранении состояния в памяти мы смогли создать более быстрый и точный ограничитель частоты запросов, к тому же снизивший наши эксплуатационные затраты. Подробнее мы расскажем об этом в ещё одном посте.
Когда serverless использовать разумно (а когда нет)
Этот пост — не антиреклама serverless. Serverless замечательно подходит для множества сценариев использования:
Нечастые рабочие нагрузки: когда задачи выполняются только время от времени, экономику масштабирования до нуля не победит никто.
Простые паттерны запросов-ответов: когда вам не нужно постоянное хранение состояния или сложные конвейеры данных.
Архитектуры на основе событий: serverless превосходно справляется с реагированием на события без управляющей инфраструктуры.
Но у serverless возникают сложности в следующих ситуациях:
Когда нужны стабильно низкие задержки: внешние сетевые зависимости убивают производительность
Вам требуется постоянно хранимое состояние: обход отсутствия хранения состояния создаёт сложность
Наличие высокочастотных нагрузок: модель с оплатой за каждый вызов становится слишком дорогой
Необходимость точного контроля: абстракции платформ могут стать ограничениями
Расплата сложностью
Самым важным уроком нашей миграции стало понимание расплаты сложностью при попытках обхода ограничений платформ.
В serverless мы создали:
Сложную библиотеку кэширования, чтобы обойти отсутствие хранения состояния
Множество вспомогательных сервисов для батчинга данных
Сложные конвейеры логов для сбора метрик
Изощрённые обходные пути для локальной разработки
При работе с серверами с хранением состояния от всего этого мы избавились. Мы перешли от распределённой системы со множеством подвижных частей к простой архитектуре приложения.
Иногда лучшее решение — не обходить ограничения, а выбрать другой фундамент.
Комментарии (6)

savostin
22.10.2025 06:09Так как теперь масштабируется? Или тупо один гигантский сервер, который все держит в памяти?
Я так понимаю, привязали себя к другому провайдеру, раз aws…

elsaqq
22.10.2025 06:09server это свобода, serverless это игла провайдера
если нужно быстро, то serverless
если нужно владеть своим продуктом и иметь возможность реализовать вообще всё что только возможно программно (ведь к вашим услугам почти все языки программирования, базы данных и потенциально полезные примочки; а не 2-3 инструмента, которые провайдер решил одобрить) то server
а если нужны простые типичные функции, в простых условиях, то возможно serverless будет быстрее и выгоднее

belonesox
22.10.2025 06:09Мы собрали конкретно chproxy, потому что ClickHouse не любит тысячи крошечных insert. Это Go-сервис, буферизирующий события и отправляющий их большими пакетами (батчами)
Это странно. Ибо chproxy не склеивает мелкие инсерты, он только диспетчеризует на разные кластеры, склеиванием обычно другие занимаются, типа clickhouse-bulk
SWATOPLUS
Serveless это про плавающую нагрузку. Сегодня у вас 1.5 запроса, а завтра миллионы оно все скейлится, работает и потребляет столько денег, сколько запросов. А есть нужен минимальное время отклика, то должен быть поднятый сервер, со всеми кэшами, вероятно даже в одном процессе, а не Redis, и хитро перенаправлять клиентов от сервера к северу.
Про время холодного старта serveless не знает только ленивый. Статья лишь описывает азбучные основны и говорит, что мы натянули сову на глобус, а потом стянули.
Это могло произойти либо, потому что не подумали, либо потому что бизнес вырос и надо адаптироваться к росту нагрузки. И вот я ожидал про это почитать.
До этого был тоже был код на Go или может быть на Python? А он напрямую платформой запускался или был обернут в докер? Какое было время холодной и теплой обработки запроса? Может быть было достаточно делать прогрев воркеров (да дороже, но насколько?) Может быть эффект из-за того, что переписали код на другой язык или по другому запускали? На сколько сэкономили на инфраструктуре? Не выросли ли издержки в человеко-часаз на деплой?
Надеюсь в нем будет больше конкретики и цифр.
yrub
а еще это дешево на определенном интервале запросов. у нас лямбды меньше бакса в месяц кушают судя по скрипту подсчета затрат.