Поэтому мы вышли из беты и сделали релиз!
Что нового в релизе? Зачем вообще нужен userver и какие существуют технологии для обеспечения надёжной работы серверных приложений? Можно ли воспользоваться крутыми C++ классами из userver, не используя при этом корутины? Какие дальнейшие планы? Ответы на все эти вопросы ждут вас под катом.
Новинки
После выхода в бету мы получили много фидбэка и постарались реализовать все запрошенные улучшения:
- Реализовали WebSockets сервер.
- Добавили драйвер MySQL.
- Сделали драйвер RabbitMQ.
- Реализовали TLS/HTTPS сервер.
- Проинтегрировали Deadline Propagation.
- Реализовали pipelining запросов для PostgreSQL. Теперь начало транзакции, выставление опций и, собственно, query идут одним запросом, значительно экономя RTT.
- Переработали механизм работы с метриками. Добавились форматы Prometheus и Graphite. Значительно упростили запись метрик и их тестирование, а получение метрик значительно ускорили.
- Добавили в документацию множество примеров и новых страниц. А ещё сменили оформление и улучшили поиск.
- Переработали и упростили систему сборки. Выкинули всю внутреннюю специфику и вычистили скрипты сборки от упоминания внутренних пакетов. Отбросили зависимость на spdlog и заапстримили больше правок в смежные проекты, а ещё поддержали Docker.
- Нам принесли (а мы вмёржили) множество PR на поддержку пакетного менеджера Conan и других различных дистрибутивов.
- Ну и, разумеется, наше любимое: реализовали множество различных оптимизаций, чтобы экономить CPU и отвечать на запросы быстрее.
Подробный список всех изменений можно найти в changelog.
Мы развиваем фреймворк по модели green trunk, а это значит, что каждый комит в userver проходит тесты и оказывается в нашем проде. Так что просто берите последнюю версию из develop-ветки Гитхаба и пользуйтесь, если вам не требуется повышенная стабильность интерфейсов.
Разумеется, все остальные функции никуда не делись: HTTP- и HTTPS-клиенты, PostgreSQL, Mongo, Redis, HTTP-сервер, поддержка различных форматов данных, gRPC, congestion control и всё остальное. Простота написания кода тоже сохранилась:
А зачем вообще нужен userver?
Один из самых частых вопросов после выхода в опенсорс: «А вот есть библиотека X или технология Y. Стоит мне использовать что-то из них или userver?»
Зависит от ваших потребностей. Фреймворк userver хорош тем, что он производителен, прост в использовании, а все его части проинтегрированы друг с другом. Например, если вы пользуетесь базой данных PostgreSQL, то сразу получаете её метрики, быстрый бинарный протокол отправки (недоступный из libpq), статистику по времени выполнения запросов, pipelining, возможность менять конфигурацию без рестарта сервиса, автоматическое создание prepared statements, Deadline Propagation, указание таймаутов на выполнение запросов и многое другое.
Вот несколько ситуаций, в которых удалось избежать больших неприятностей благодаря функциям, доступным «из коробки».
Сервису поплохело
🐙 userver пишет множество метрик. На них у нас настроена автоматика, которая предупреждает разработчика в случае отклонения от допустимых значений. И вот разработчику как раз прилетело такое предупреждение: «Ваш сервис начал подозрительно часто отвечать не 200 HTTP-кодом».
Идём читать метрики. На общем графике запросов сервиса ничего подозрительного нет, но вот график одного из REST API URL настораживает:
Что-то не так именно с GET запросами на /v1/cached-value
! Теперь мы знаем, какой части сервиса поплохело, и можем целенаправленно поискать логи именно этого URL с кодом ответа 404 (например, с помощью Kibana):
... level=INFO meta_type=/v1/cached-value method=GET uri=/v1/cached-value?id=experiment_42 meta_code=404 ...
Смотрим на графики или в логи и понимаем, что на некоторых инстансах нашего сервиса почему-то нет данных для experiment_42
. Дальше идём к поставщику данных и выясняем, как так получилось.
В этой ситуации помогли следующие технологии userver:
- Метрики. Они помогли быстро обнаружить и диагностировать проблему. При этом их невозможно забыть реализовать, потому что они автоматически пишутся для всех обработчиков запроса.
- Логи. Автоматическое логирование параметров запроса значительно упростило диагностику проблемы.
Эксперимент не удался
Команда разработчиков сделала крутую фишку, благодаря которой заказы и такси приезжают к пользователям на минуту быстрее. Обложили тестами, проверили на тестовых окружениях, выкатили в прод… а потом обнаружили опасную вещь: есть шанс с вероятностью 1 к 100 000, что запрос уронит сервис из-за ошибки в коде.
Ребята порадовались, что в userver встроен механизм перезапросов. Когда сервис падает, запрос приходит уже на другой инстанс, и он благополучно отвечает. Пользователи получают свой заказ и уезжают на такси — для них всё хорошо.
Ну а потом разработчики мгновенно отключили новую функциональность через динамический конфиг и для этого не потребовалась перевыкатка.
if (dynamic_config[kFasterDeliveryV5]) {
return do_fast_delivery_v5(params);
}
return do_fast_delivery_v4(params);
В спокойной обстановке разработчики стали смотреть по логам параметры запросов. Через встроенный трейсинг запросов они выяснили, что один из вышестоящих сервисов иногда не проставляет параметр — в этом случае с шансом 1 к 10 всё может упасть. Зарепортили багу в вышестоящий сервис.
Потом ребята написали тест, в котором поднимают свой сервис и задали пару сотен запросов, роняющих его:
async def test_bad_param(service_client):
for _ in range(200):
response = await service_client.get('/v1/prod', params={'important': ''})
assert response.status == 200
assert response.content == b'Some answer'
Исправили проблему, спокойно перевыкатились — всё заработало как часы.
В этой ситуации помогли следующие технологии userver:
- Встроенные перезапросы с экспоненциальным backoff. Они помогли обработать пользовательские запросы и не уронить серверы.
- Динамические конфиги. По внутренним правилам вся новая функциональность выкатывается с возможностью экстренного отключения через динамический конфиг. То есть сломанный кусочек кода можно мгновенно выключить без перевыкаток сервиса.
- Встроенный трейсинг. В userver встроен механизм OpenTracing. Можно отследить запрос через весь граф микросервисов, посмотреть через логи смотреть параметры запросов и найти ответы сервисов для заданного изначального запроса.
- Функциональные тесты testsuite. В userver встроены механизмы для тестирования. Автоматически поднимаются необходимые базы данных, накатываются миграции, заливаются данные, запускается сервис с заданными параметрами динамических конфигов. Остаётся только написать сам запрос к сервису в виде python-теста и проверить ответ.
Спонтанный рост нагрузки
Закончился футбольный матч, все стали вызывать такси и заказывать домой еду. И притом как-то очень не вовремя для нас: всё случилось ночью, когда в дата-центре что-то экстренно меняли. Да ещё один из сторонних сервисов затупил и разом отправил на один инстанс нашего сервиса десяток миллионов запросов…
… и ничего плохого не случилось!
В этой ситуации помогли следующие технологии userver:
- Deadline Propagation. Таймаут запроса прокидывается по всей цепочке микросервисов. Первый микросервис, обнаруживший, что запрос больше не актуален, сразу прерывает его обработку и не отправляет запрос в последующие микросервисы.
- Congestion Control для Mongo. Если временные показатели ответов базы данных значительно и резко увеличиваются, то автоматика уменьшает количество активных запросов к базе, чтобы та пришла в норму и могла дальше обрабатывать запросы.
- Congestion Control по CPU. Прошлые способы не помогли, и вы упёрлись в ресурсы машины? Тогда встроенный механизм откинет часть запросов с кодом 429, чтобы остальные запросы могли завершиться за отведённый таймаут.
Приложение падает в тестовой сборке
И это хорошо, ведь userver нашёл багу в вашем коде! Фреймворк прилагает множество усилий, чтобы упростить разработку и сделать код более надёжным:
- Корутины. Код на userver выглядит как простой линейный синхронный код, но при этом под капотом всё максимально эффективно и асинхронно. Огромное количество проблем с lifetime просто не возникает, ведь в большинстве случаев нет необходимости использовать колбэки.
- Проверки времени компиляции. На некоторые проблемы даже тесты писать не надо, потому что компилятор C++ не даст вам собрать сломанный код. Например, на некоторые проблемы многопоточности userver выдаст понятную диагностику по их исправлению на этапе компиляции вашего проекта.
- Защищённое программирование. Различные ассерты выдадут вам диагностику, если вы вдруг пользуетесь фреймворком неправильно. Например, передаёте неправильные параметры или как-то не так используете примитивы синхронизации.
- Провоцирование редких проблем. Вам всё же понадобился колбэк, а в тесте приложение на нём падает? Отлично, userver специально постарался и сэмулировал в тесте редкую ситуацию, например, гонку между пользовательскими запросами и завершением сервиса.
- Инструменты для тестирования. Можно форсировать обновления кэшей, срабатывание периодических тасок, слать тестовые нотификации вашему сервису из python-тестов и самостоятельно провоцировать гонки.
Этот фреймворк только для микросервисов?
Не обязательно. У нас в компании на userver написано как множество микросервисов, так и огромные серверные приложения с кэшами на сотни гигабайт, которые ближе к монолитным приложениям.
Если вы не собираетесь писать серверы, у нас для вас хорошие новости. 🐙 userver можно использовать для написания консольных приложений, использующих корутины (например, netcat или dns_resolver).
Но что делать, если вы уже давно работаете над своим проектом и вам не нужен целый фреймворк? Вам лишь не хватает небольших вспомогательных C++ классов и функций, о которых мы так часто рассказываем на конференциях. Для вас у нас тоже есть хорошие новости: с недавнего времени userver предоставляет CMake-цель userver-universal
, которая не содержит корутин и драйверов. Только всякие полезные C++ мелочи:
- utils::FastPimpl — возможность пользоваться идиомой PImpl без динамических аллокаций.
- cache::LruMap и cache::LruSet — LRU-контейнеры, оптимизированные на переиспользование нод и отсутствие динамических аллокаций после прогрева.
- utils::TrivialBiMap — невероятно быстрая bidirectional-мапа, обгоняющая по производительности и размерам std::unordered_map и std::flat_map. Ей ещё и в compile time можно пользоваться!
- utils::FastScopeGuard — неаллоцирующий scope guard.
- formats::yaml::Value, formats::json::Value — возможность работать с разными форматами, используя один и тот же интерфейс.
- decimal64::Decimal — decimal-тип для точных вычислений (чтобы при работе с деньгами не терялись копейки).
- utils::FixedArray — динамический массив заданного на рантайме размера с возможностью поэлементной инициализацией.
- crypto::Certificate, crypto::PrivateKey, crypto::PublicKey — RAII-обёртки над криптографией, чтобы ключи держались в памяти, а не читались с диска.
- utils::datetime::SteadyCoarseClock — steady-часы пониженной точности и повышенной производительности с интерфейсом
std::chrono::steady_clock
. - и сотни других функций и классов.
Полный список функций здесь — 🐙 userver universal.
Планы
После релиза работа не останавливается. Мы продолжим улучшать производительность и внедрять новую функциональность и постараемся к следующему релизу реализовать драйверы для YDB и Кафки, а ещё упростить конфигурирование и заняться пакетированием.
Заглядывайте к нам в Гитхаб. Надеемся, что фреймворк вам пригодится и понравится!
P.S
Спасибо всем, кто контрибьютил и багрепортил в userver. Вы лучшие!
Автор: Antony Polukhin