Привет! На связи Антон Полухин из Техплатформы Городских сервисов Яндекса. После большого релиза ? userver прошло почти два года. За это время мы обзавелись большим количеством внешних пользователей — международных и российских. При этом и количество внутренних пользователей подросло: в Городских сервисах Яндекса появились сотни новых сервисов на userver. Функциональность Такси, Еды, Лавки, Доставки, а также Маркета, Финтеха, Фантеха, Электро и Техплатформы обогатилась новыми возможностями и новыми пользователями. А значит, фреймворк стал ещё надёжнее и оттестированнее.

Мы не сидели сложа руки, и за два года реализовали, оптимизировали и добавили все обещанные в прошлой статье фичи, а также многое другое:

  • популярные запросы пользователей: OpenTelemetry Protocol (OTLP) для отправки метрик, кастомизирование логирования и JSON‑формат логирования, OpenAPI‑парсеры и сериализаторы для JSON Schema и так далее и тому подобное;

  • библиотеку easy для простого прототипирования;

  • потребление CPU и памяти;

  • базовую поддержку HTTP2;

  • рецепты для Conan;

  • множественные улучшения для gRPC;

  • веб‑интерфейс для сервиса динамических конфигов;

  • сотни других упрощений и улучшений.

Ах да, ещё мы решили отбросить поддержку C++17 и начали активно переводить кодовую базу на использование фич C++20.


C++20

На дворе 2026 год, однако разработчики компиляторов C++ живут по своему календарю! Вышедший шесть лет назад стандарт С++ только в следующем месяце перестанет считаться экспериментальным и будет дефолтом для GCC-16. И это не касается C++20 Modules.

Итак, есть только один компилятор, который «зуб даёт», что готов к C++20. Не рано ли переводить фреймворк на C++20?

А давайте посмотрим на результаты прошлогоднего опроса наших пользователей:

77% пользователей уже на C++20, а ещё 15% готовы переключиться! Для сравнения — вот общемировая статистика по использованию C++ от 2025 года:

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

А зачем вообще C++20, почему не хватает C++17?

Часть наших компонент уже требует C++20. Например, YDB SDK существует только под C++20. И наши асинхронные обёртки над ним, соответственно, тоже. Но вот другие компоненты уже долгое время живут на C++17 и бед не знают. Так зачем же C++20?

Всё очень просто. После того как стало понятно, что ничего не мешает нам перейти на C++20, мы стали замечать неприятные шероховатости C++17:

  • гетерогенные поиски для unordered‑контейнеров приходится эмулировать через Boost‑контейнеры;

  • std::void_t и std::enable_if неудобны по сравнению с концептами в C++20;

  • вместо consteval приходится использовать макрос, который раскрывается в consteval или в constexpr (последний не позволяет обнаруживать часть проблем на этапе компиляции);

  • std::span недоступен, приходится писать свой utils::span;

  • самописные ranges трудозатратно поддерживать, а boost::transform тянет тяжёлые заголовочные хедеры в публичные заголовочные файлы userver;

  • очень не хватает std::atomic_ref;

  • operator<=>() = default; — катастрофически не хватает в кодгене.

А ещё приходится тестировать фреймворк сразу в двух режимах — C++17 и C++20.

В общем, со временем эти и многие другие шероховатости стали добавлять всё больше головной боли.

Новинки userver

userver::easy

Если вы собираетесь делать микросервис с большой функциональностью и разрабатывать его отдельной командой разработчиков — userver тут как нельзя кстати. На Хабре есть даже введение в написание подобных сервисов.

Вот только не все задачи требуют столь вдумчивого подхода. Иногда нужно тяп‑ляп — и в продакшен запрототипировать решение. Другими словами, нужен аналог Flask:

from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello_world():
   return "<p>Hello, World!</p>"

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

Именно так и появился userver::easy, где можно писать код в близком к Flask стиле:

#include <userver/easy.hpp>

int main(int argc, char* argv[]) {
   userver::easy::HttpWith<>(argc, argv)
       .DefaultContentType(userver::http::content_type::kTextPlain)
       .Route("/", [](const userver::server::http::HttpRequest& /*req*/) {
           return "<p>Hello, World!</p>";
       });
}

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

#include <userver/easy.hpp>

// schemas::KeyRequest и парсеры+сериализаторы, сгенерированные из JSON Schema
#include "schemas/key_value.hpp"

int main(int argc, char* argv[]) {
   using namespace userver;

   easy::HttpWith<easy::PgDep>(argc, argv)
       // Конкурентно обрабатывает множественные HTTP-запросы к /kv
       .Get("/kv", [](schemas::KeyRequest&& request, const easy::PgDep& dep) {
           // Асинхронное выполнение SQL-запроса в транзакции. Текущий поток
           //  выполнения обрабатывает другие запросы, пока ответ от сервера
           // базы данных получаем по сети:
           auto res = dep.pg().Execute(
               storages::postgres::ClusterHostType::kSlave,
               // Запрос преобразуется в prepared statement. Последующие
               // запросы пошлют только параметр запроса в бинарном формате.
               "SELECT value FROM key_value_table WHERE key=$1", request.key
           );

           return schemas::KeyValue{
               .key=std::move(request.key),
               .value=res[0][0].As<std::string>(),
           };
       });
}

Заметьте, все оптимизации userver присутствуют. Код выглядит как синхронный, а на самом деле это высокоэффективный асинхронный код с целой прорвой оптимизаций, в том числе для баз данных (некоторые из оптимизаций мы описывали в статье к прошлому релизу, другие — в отдельной статье, а ещё говорили о них на конференциях C++ Russia и C++ Zero Cost Conf).

В документации к userver::easy есть несколько разделов по расширению и постепенному приведению к непрототипному состоянию (если вдруг это нужно). Так что вы не окажетесь в ситуации «прототип хороший, но надо всё переделать с нуля».

Кстати, о переделывании...

Chaotic

Где чаще всего косячат программисты? В повторяющемся однотипном коде! Типичным таким «унылым местом» является парсинг JSON/YAML/BSON/... в структуру — взяли поле, сконвертировали в нужный тип, провалидировали, повторили с другим полем...

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

Например, в прошлом примере часть JSON‑схемы выглядит так:

components:
   schemas:
       KeyValue:
           type: object
           additionalProperties: false
           properties:
               key:
                   type: integer
               value:
                   type: string

Программа для генерации кода на C++ из JSON Schema в userver называется Chaotic. Она сгенерирует приблизительно следующий заголовочный файл:

#pragma once

#include <cstdint>
#include <optional>
#include <string>
#include <userver/chaotic/object.hpp>
#include <userver/chaotic/type_bundle_hpp.hpp>

#include "key_value_fwd.hpp"

namespace schemas {

struct KeyValue {
 static constexpr userver::utils::StringLiteral kFieldNamekey = "key";
 static constexpr userver::utils::StringLiteral kFieldNamevalue = "value";
 std::optional<int> key{};
 std::optional<std::string> value{};
};

bool operator==(const KeyValue& lhs, const KeyValue& rhs);

userver::logging::LogHelper& operator<<(
   userver::logging::LogHelper& lh, const KeyValue& value);

KeyValue Parse(
   userver::formats::json::Value json, userver::formats::parse::To<KeyValue>);

KeyValue FromJsonString(
   std::string_view json, userver::formats::parse::To<KeyValue>);

userver::formats::json::Value Serialize(
   const KeyValue& value,
   userver::formats::serialize::To<userver::formats::json::Value>);

}  // namespace schemas

Тут есть парсеры из DOM‑представления, чтобы можно было делать json_value.As<schemas::KeyRequest>(). И SAX‑парсеры из JSON‑строки, чтобы парсить в ту же структуру быстрее, минуя DOM‑представление. И сериализаторы.

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

Сборка и Conan

Для быстрого прототипирования недостаточно удобной библиотеки и генератора из JSON Schema. Фреймворку надо ещё уметь быстро собираться и легко устанавливаться.

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

  • сделали новые Docker‑контейнеры с предустановленными библиотеками и userver;

  • стали собирать DEB‑пакеты с каждым релизом уже под пару версий Ubuntu;

  • провели работу с CMake, чтобы некоторые зависимости можно было подтягивать через CPM;

  • стали тестироваться на намного большем количестве ОС;

  • запустили программу комьюнити мейнтейнерства и обзавелись крутым мейнтейнером для платформы Gentoo;

  • переработали и упростили Conan‑скрипты, реализовали автоматическое тестирование Cоnan на разных ОС и в разных окружениях.

Более того, мы находимся в процессе добавления userver в Conan Center, чтобы можно было начать использовать userver для своего проекта, просто написав в conanfile.txt...

[requires]
userver/2.*

.. и запустив conan install.

Оптимизации

Бытует мнение, что самые лакомые задачи для разработчиков на С++ — это написание своего фреймворка и задачи на оптимизацию производительности.

Вот некоторые из оптимизаций:

  • Мы стали чаще использовать аналоги std::string::resize_and_overwrite(). Казалось бы, компиляторы должны быть уже достаточно умными, чтобы самим оптимизировать dead store, но нет. Использование этой функции на «горячем пути» помогло нам увеличить максимальный RPS на 1–3%.

  • В популярных типах данных userver мы перешли на использование view в std::initializer_list. Это позволило избежать лишнего копирования данных в ряде краевых случаев.

  • Старый добрый std::FILE* на самом деле обладает одной не очень приятной особенностью: он захватывает мьютекс для защиты внутреннего буфера. Отключение этого мьютекса даёт до 0,2% ускорения логирования и ~40% ускорения при дампе кешей из чисел.

  • Из C++29 мы тоже взяли некоторые идеи, в частности zstring_view. Теперь большинство интерфейсов не требуют const std::string&, а значит, будет создаваться меньше временных строк.

  • Креативно перетрясли низкоуровневую работу с таймерами и балансировку нагрузки на потоки и обработчики событий. Сэкономили CPU и упростили настройку.

  • Инициализацию запросов сделали полностью compile time, убрав возможность получить static initialization order fiasco.

  • Kafka‑драйвер стал матёрым и полностью асинхронным и не блокирующим потоки ОС. Кажется, что мы единственные в мире, кто дошёл до этого (пишите в комментах, если ситуация изменилась).

Упрощение конфигурирования

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

Размеры конфигов в userver‑туториалах сократились ещё сильнее благодаря разумным дефолтам. А userver::easy позволяет не только завести сервис без явного написания конфига, но и дампнуть конфиг вашего прототипа, чтобы переопределить его новыми значениями при необходимости.

Контрибьюторы

Отдельное спасибо всем тем, кто работал над проектом! За два года было сделано невероятное количество улучшений от сторонних пользователей — как маленьких исправлений, так и огромных добавлений новой функциональности. На userver было сделано множество дипломных проектов, больших и крутых оптимизаций.

Из самого запоминающегося:

  • переиспользование SSL_CTX, что значительно ускоряет работу с шифрованными соединениями и существенно сокращает потребление памяти;

  • множество улучшений для Kafka, включая поддержку SSL, headers и seek;

  • SQLite‑драйвер;

  • S3 API клиент;

  • работа Valkey/Redis в режиме standalone;

  • асинхронный UDP Multicast;

  • множественные улучшения в документации, сервисах‑примерах и сервисе динамических конфигов;

  • безумное количество фикстов для сборки под используемые контрибьюторами платформы;

  • много других улучшений.

Ещё раз огромное спасибо всем контрибьюторам! Вы лучшие!

Дальнейшие планы

Разумеется, работа над фреймворком продолжится. Мы видим, что фреймворк людям интересен, а это мотивирует продолжать работу. Да и в Яндексе фреймворк крайне популярен.

В ближайших планах — сделать всё что можно для удобной и надёжной работы с gRPC.

На этом пока всё, до новых встреч! Заходите на наш github, мы теперь делаем минорные релизы каждые пару месяцев!

А ещё рекомендую заглянуть на следующие мероприятия, где можно встретиться с людьми, использующими или разрабатывающими userver:

Кстати, на конференциях мы иногда раздаём прикольных осьминожек. Это случается редко и спонтанно (даже для нас самих).

Комментарии (19)


  1. domix32
    22.04.2026 10:09

    А поддержку FreeBSD никто не запрашивал?


    1. antoshkka Автор
      22.04.2026 10:09

      Мы когда-то проверяли на одной из BSD, и даже апстримили патчи в смежные проекты (кажется gRPC), чтобы сборка работала

      Но без настроенного CI и энтузиаста, желающего поддержать платформу, скорее всего сборка немного сломалась.

      Кстати, мы ищем добровольцев https://userver.tech/d1/d00/md_en_2userver_2distro__maintainers.html. Не хотите им стать?


      1. domix32
        22.04.2026 10:09

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


  1. SilverTrouse
    22.04.2026 10:09

    Когда import userver?


    1. antoshkka Автор
      22.04.2026 10:09

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


  1. SilverTrouse
    22.04.2026 10:09

    Будет ли внедрена работа с С++26 рефлексией или пока слишком рано ее использовать в userver (через два три года только стоит ожидать) ?


    1. stasrodin98
      22.04.2026 10:09

      Рефлексию, насколько я знаю, даже сейчас нельзя попробовать. Учитывая скорость перехода с одной версии на другую (напоминаю, мы в статье 2026 года, рассказывающей о переходе на 20 стандарт), в лучшем случае это произойдет году этак к 30-ому.


    1. antoshkka Автор
      22.04.2026 10:09

      Мы сейчас во многих местах фреймворка используем Boost.PFR для рефлексии. Как раз пару дней назад был релиз Boost 1.91 и там я добавил экспериментальную поддержку C++26 рефлексии в PFR. Через пол годика можно снимать эксперимент и использовать по умолчанию

      Но вот использовать рефлексию C++26 не под ifdef макросами мы не сможем ещё лет 6 - наши пользователи используют C++20


      1. domix32
        22.04.2026 10:09

        Кстати, а по какому алгоритму вы обновляете версии компиляторов? Полная поддержка С++20 есть начиная с gcc 14 и clang 19, но у вас в ci gcc 13 и clang 18. Вас интересует только базовая поддержка языковых фич, а стандартная библиотека уже не интересно?


        1. SilverTrouse
          22.04.2026 10:09

          в gcc 16 все еще не полная поддержка с++20 как и в clang 19. Модули еще допиливаются


  1. simplepersonru
    22.04.2026 10:09

    Немножко может не по теме, но

    Есть ли у вас интеграционные авто-тесты, которые внутри себя поднимают какой-нибудь in-memory postgresql, который целиком со всем своим клиент-сервером живет в RAM во время исполнения бинаря теста?

    Иногда искал что-то готовое, находил только подобные упоминания для node-js инфраструктуры


    1. antoshkka Автор
      22.04.2026 10:09

      Мы используем yandex-taxi-testsuite для подобных вещей. Это отдельный Python пакет, и он доступен в pip. Он умеет поднимать не только Postgres, но и Redis, Valkey, Mongo... а ещё умеет поднимать ваш сервис, патчить ваши конфиги, делать запросы в ваш сервис и много-много всего другого

      Полноценно интегрировать своё приложение с yandex-taxi-testsuite может быть не просто. В качестве примера можно глянуть в userver/testsuite, там как раз лежит всё для бесшовной интеграции приложений на userver. Если нужен только подъем базы от yandex-taxi-testsuite, то всё попроще, и можно посмотеть на userver/postgres тесты в качестве примера


  1. acDev
    22.04.2026 10:09

    Я заметил, что userver ссылается на тесты TechEmpower. Но совсем недавно этот проект был закрыт.

    Сегодня я решил немного вспомнить плюсы и добавил userver в новый проект, который стремится превзойти TechEmpower по качеству… вот в этот проект https://www.http-arena.com/leaderboard/

    Собственно говоря вот мой PR: https://github.com/MDA2AV/HttpArena/pull/609

    Делал я этот PR на основе кода, который был 2 года назад добавлен в TechEmpower. А значит там используется v2. Т.к. я уже растерял скилы в плюсах (да и лень локально всё поднимать), то я не решился всё переписывать на v3. Сразу скажу, что в этом PR много мусора (я не знаю для чего первоначально его добавили).

    Очень было бы прекрасно, если бы кто-то из мейнтейнеров userver занялся бы созданием нормального PR на основе v3.

    Я всего то добавил поддержку 2-х тестов: pipeline и baseline11.

    Вот результаты тестирования:

    | Test      | Conn | RPS       | CPU     | Mem    
    |:-----     |:-----|:----      |:----    |:----   
    | baseline  | 512  |   461,734 | 5712.9% | 228MiB 
    | baseline  | 4096 |   890,041 | 6211.6% | 391MiB 
    | pipelined | 512  | 1,591,150 | 6681.4% | 236MiB 
    | pipelined | 4096 | 1,481,967 | 6734.2% | 760MiB 
    

    Для плюсов это слабенькие результаты. Может в v3 с этим делом уже лучше?


    1. Botanegg
      22.04.2026 10:09

      Приходите в https://t.me/userver_ru пообсуждаем :)


      1. acDev
        22.04.2026 10:09

        Я только сейчас заметил, что вы сделали новый PR и провели тестирование. Причём никакие настройки и код обработчиков не поменяли. Изменили только тег для скачивания исходников (на v3.0).

        И вот какие результаты:

        | Test      | Conn | RPS       | CPU     | Mem    | Δ RPS  
        |------     |------|-----      |-----    |-----   |------- 
        | baseline  | 512  |   626,174 | 6056.2% | 211MiB | +66.8% 
        | baseline  | 4096 | 1,025,643 | 6435.7% | 428MiB | +21.5% 
        | pipelined | 512  | 1,556,085 | 6707.8% | 255MiB | -3.7%  
        | pipelined | 4096 | 1,436,273 | 6688.3% | 875MiB | -3.9%  
        

        Т.е. v3.0 прям стала быстрее (в сравнении с v2.x) без изменения кода и настроек. И это прекрасно!

        Теперь буду от вас ждать PR, который добавит поддержку и всех остальных тестов с HttpArena (CRUD тоже). И что бы именно prodaction реализация была (т.е. без всяких недокументированных фич).


        1. Botanegg
          22.04.2026 10:09

          В свободное время допинаю :)


    1. antoshkka Автор
      22.04.2026 10:09

      о, спасибо большое, будем смотреть и разбираться


  1. Arenoros
    22.04.2026 10:09

    а что с поддержкой для win? я вижу в main ветке не мало ifdef win32, но так же ещё не весь код переносим. Есть ли вообще желание чтоб под win можно было использовать userver?


    1. antoshkka Автор
      22.04.2026 10:09

      Разные энтузиасты пробовали доьавить поддержку Win, но на этапе сборки как правило застревали :(

      Если хотите попробовать - присылайте PR, с радлстью поревьюим и вмержим