Преамбула
Некоторое время назад передо мной встала задача спроектировать высокопроизводительный балансировщик нагрузки для протокола Diameter. Через три месяца проект на стороне заказчика свернули — решение показалось слишком долгим и дорогим в разработке. Тем не менее, я довел дело до конца и собрал MVP, готовый к развертыванию как на «голом железе», так и в облачной инфраструктуре.
Продукт получился отличным: он с лихвой выигрывал у имевшегося в компании решения, превосходя его по производительности примерно в 6 раз, а по эффективности использования ресурсов — почти в 100 раз.
Но речь сейчас не о самом балансировщике, а об одной из его ключевых частей. Балансировщик был кластерным и хранил сессии в Redis. Система работала, функциональность была подтверждена, но меня категорически не устраивала производительность. Я столкнулся с удручающим барьером: по непонятным на первый взгляд причинам система упиралась в 5 000–7 000 Diameter Transactions Per Second (TPS).
Профилирование показало, что узким местом был именно Redis. Потратив некоторое время на оптимизацию взаимодействия, я сумел поднять производительность балансировщика до 20 000 TPS, но это решение заставило пойти на компромиссы и принесло новые проблемы.
Причины создания HurriCache
Проведя детальный анализ, я выяснил: без пайплайнинга Redis физически не способен отдавать данные быстрее 5 000–6 000 RPS на одно соединение/поток. Мой балансировщик аккуратно подтвердил эту цифру, упершись ровно в данный потолок.
Чтобы преодолеть ограничение, я перепробовал разные подходы: от нативного пайплайнинга до группировки запросов в батчи. Желанные 20 000 TPS для балансировщика в итоге были достигнуты, и формальная цель проекта была выполнена. Однако меня зацепил исследовательский вопрос: какова реальная (а не маркетинговая) пропускная способность Redis на одной ноде?
Серия синтетических тестов дала следующие результаты:
Тип операции |
Без пайплайна (Single Request) |
С пайплайном (Pipelined) |
Чтение |
5К RPS |
60К RPS |
Запись |
3К RPS |
20К RPS |
Эта таблица наглядно объясняет, почему пробить потолок в 20 000 TPS на реальной бизнес-логике балансировщика оказалось настолько сложно.
В этот момент и родилась идея: а что если спроектировать собственное in-memory хранилище — похожее на Redis по возможностям, но избавленное от его фундаментальных сетевых и архитектурных узких мест?
Так началась разработка HurriCache.
Архитектурный фундамент HurriCache
Одна из главных проблем Redis — невозможность переиспользовать сетевое соединение до тех пор, пока из него не будут полностью вычитаны данные предыдущего ответа (отсутствие честного мультиплексирования). Из-за этого использовать Redis без пулов соединений (Connection Pools) в высоконагруженных системах практически бессмысленно. Пулы частично решают проблему, но создают оверхед на менеджмент соединений и контекст-свичи.
Я решил сразу строить транспортный слой на протоколе с нативной поддержкой мультиплексирования.
В качестве фундамента был выбран gRPC + Protocol Buffers. Это дало возможность гонять тысячи параллельных запросов через одно TCP-соединение без блокировок и оверхеда на парсинг текстового протокола RESP.
Далее я спроектировал Protobuf-интерфейс, который закрывает большинство привычных примитивов Redis и добавляет то, чего в нем исторически не хватало:
Basic Key-Value (строки, бинарные данные);
Lists (массивы и связные списки);
Queues (очереди с гарантией порядка);
HashSet / HashMap;
OrderedSet / OrderedMap;
Atomics (полный набор атомарных операций, включая Compare-And-Swap / CAS);
Locks (поддержка READ/WRITE/GLOBAL locks в зависимости от клиента)
Нативная кластеризация «из коробки».
Архитектурные делемы и интересные проблемы
В процессе разработки выяснилось множество интересных деталей о поведении стандартных C++ контейнеров под экстремальной нагрузкой.
Например, популярные решения std::unordered_map и absl::flat_hashmap на моих синтетических профилях вызовов показывали практически одинаковую производительность, упираясь в промахи по кэшу процессора.
После долгой борьбы с L1/L2/L3 cache misses и тонкой настройки процессорных инструкций префетчинга (prefetch), мне пришлось написать собственную реализацию flat_hashmap, оптимизированную под специфику аллокаций HurriCache.
(Детальный разбор реализации этой хэш-таблицы, борьбы с cache miss и работы с memory arenas я планирую вынести в отдельную техническую статью, иначе этот материал получится бесконечным).
Что получилось в итоге
На текущий момент HurriCache уверенно работает в кластерном режиме. Вот реальные показатели производительности на одну ноду без ухищрений с пайплайнингом:
Тип операции |
HurriCache (Без пайплайнинга) |
Redis (Без пайплайнинга) |
Redis (С пайплайном) |
Чтение (Read) |
85 000+ RPS |
~5 000 RPS |
~60 000 RPS |
Запись (Write) |
77 000+ RPS |
~3 000 RPS |
~20 000 RPS |
Примечание: Концепция классического пайплайнинга в HurriCache просто не требуется — мультиплексирование gRPC позволяет насыщать канал асинхронными запросами без искусственной задержки на сбор батча.
Заключение
Как первое приближение и MVP, HurriCache показал себя крайне интересным и жизнеспособным решением. Нам удалось не просто догнать Redis с пайплайнингом, а превзойти его показатели (особенно на операциях записи: 77K RPS против 20K RPS), сохранив при этом прозрачность синхронного кода без необходимости собирать ручные батчи на стороне клиента.
Сейчас проект активно развивается: впереди создание клиентов на разных языках программирования. На данный момент уже реализован клиент на Java. Благодаря выбору стек-технологий gRPC + Protocol Buffers, поддержка которых есть практически везде, реализация клиентов под другие языки не должна вызывать особых сложностей.
Буду рад ответить на вопросы по архитектуре в комментариях!
Комментарии (7)

lazarus_net
05.08.2026 15:46Интересный разбор, но стоит отметить: проблему single-threaded Redis + RESP-протокола индустрия уже решала несколькими путями, и ни один не потребовал ухода на gRPC.
Dragonfly — полный C++ рерайт (shared-nothing, файберы, SIMD), держит RESP как есть, масштабируется через многопоточность сервера: заявляют ~3.8М ops/sec на 8 ядрах против ~150K у Redis.
KeyDB — форк Redis с многопоточным исполнением команд, MVCC для неблокирующих чтений и per-key локами вместо глобального — свыше 1М ops/sec без переписывания движка с нуля.
Garnet (Microsoft) — тоже RESP-совместим, показывает лучшую масштабируемость по числу клиентских сессий, чем Redis/KeyDB.
Все они атакуют именно однопоточность сервера, а не транспортный протокол. Переход на gRPC решает другую грань — клиентский connection management (не нужен пул), но добавляет свою цену: сериализация protobuf, HTTP/2 framing, per-stream overhead, потенциальный head-of-line blocking на TCP. Было бы честнее сравнить HurriCache не с голым Redis, а с Dragonfly/KeyDB — иначе неясно, выигрыш от архитектуры или просто от того, что baseline (Redis без io-threads) был не оптимально настроен.

AlexB2012 Автор
05.08.2026 15:46Сравнение абсолютно валидное, но здесь важно разграничить две разные проблемы: производительность самого in-memory движка и масштабирование сетевого транспорта на клиенте и сервере.
Зачем именно gRPC / HTTP/2?
Переход на gRPC был продиктован не попыткой обогнать RESP на одном сокете, а необходимостью иметь мультиплексирование. В микросервисных архитектурах при росте числа клиентов (когда тысячам подов/потоков нужно параллельно ходить в кэш) традиционная модель RESP заставляет создавать либо огромные пулы соединений, либо упираться в блокировки. Оверхед от поддержания и переключения десятков тысяч TCP-соединений в Redis/Dragonfly на практике может оказывается трагичным для общей latency системы. HTTP/2 закрывает эту проблему: мы гоняем тысячи параллельных RPC-стримов через один-два TCP-сокета без необходимости держать пулы.
Бенчмарки в реальных условиях (Non-pipelined workload)
Заявления в миллионы ops/sec у Dragonfly или KeyDB почти всегда строятся на идеальном pipelining (когда сотни команд пакуются в один TCP-пакет) и огромном количестве сокетов. Однако в реальных сервисах приложения часто делают одиночные неблокирующие запросы (GET/SET) из разных потоков.
Я ради интереса замерил Dragonfly в честном режиме без пайплайнинга на типичных операциях (32 потока, 100K пар ключей размером около 100 байт - стандартный тест производительности HurriCache). Результат — ~13K RPS. Он действительно ощутимо быстрее классического Redis, но на таком типе нагрузки оказался медленнее HurriCache.
Архитектура исполнения
Что касается внутрисерверного масштабирования (shared-nothing, lock-free/per-key locks), HurriCache использует подходы, во многом схожие с теми же KeyDB/Dragonfly (шардирование, локальность данных по ядрам) но как всегда есть свои нюансы.
gRPC действительно добавляет свою цену на Protobuf и HTTP/2 framing — но в данном случае она оказалась полностью оправданной.

Tyiler
05.08.2026 15:46Приветствую.
Нагрузка на CPU будет у вас высокая из-за мультиплр-я и grpc, на полезную работу может проца не хватать, по памяти тоже с запасом всегда будете потреблять (но на таких объемах rpc может и ладно).
Спросил у гугл-ии.если посмотреть загрузку cpu и выделение памяти, что увидим?
Если мы запустим профилировщик (например, perf для CPU и je_prof / valgrind massif для памяти) под максимальной нагрузкой в 85 000+ RPS, то увидим очень специфическую картину. Она наглядно покажет, какую «цену» HurriCache платит за мультиплексирование gRPC.
Вот детальный расклад того, что отобразится на графиках и в FlameGraph.
1. Профиль загрузки CPU (Куда уходят такты процессора)
В отличие от классического Redis, где 80-90% CPU уходит на логику базы данных и простую обработку TCP-сокетов, в HurriCache картина разделится примерно 50/50 или даже 60/40 в пользу сетевого оверхеда.
Что покажет FlameGraph (топ горячих функций):
-
~45–55% CPU — Экосистема gRPC и HTTP/2:
grpc_core::HeaderTableи функции парсингаHPACK(сжатие/декомпрессия заголовков HTTP/2).grpc_core::chttp2::ParsingState— нарезка потока на фреймы, разбор 9-байтовых заголовков и валидация Stream ID.grpc::protobuf::Parser— десериализация бинарного потока Protobuf в C++ структуры сообщений.Функции управления окнами (
Flow Control) и отправкиWINDOW_UPDATE.
-
~30–35% CPU — Кастомная хэш-таблица (Бизнес-логика):
Вычисление хэш-функций (например, MurmurHash или CityHash).
Сравнение ключей в памяти.
Примечание: Строки, связанные с ожиданием памяти (инструкции
prefetch), будут занимать минимум времени процессора, так как CPU не уходит в Stall (простой), а молотит полезную работу.
-
~10–15% CPU — Системные вызовы ядра Linux:
epoll_wait,recvmsg,sendmsg— чтение и запись в сетевые сокеты.
Общий вывод по CPU: Процессор будет загружен «под полку» (близко к 100% на задействованных ядрах). При этом gRPC будет буквально пожирать такты на обслуживание структуры стримов и демультиплексирование, подтверждая ваши опасения.
2. Выделение и освобождение памяти (Аллокации)
Если бы автор HurriCache использовал стандартный gRPC и стандартный
std::allocator«из коробки», база данных захлебнулась бы в аллокациях (Memory Churn). На каждый из 85 000 запросов в секунду выделялись бы десятки объектов.Но поскольку это MVP высоконагруженной системы, профилировщик памяти покажет работу арен памяти (Memory Arenas), о которых автор вскользь упомянул в статье.
Что покажет профиль аллокаций (
malloc/freeв секунду):Почти нуль (0) реальных системных вызовов
malloc/freeво время работы. Если база делаетmallocна каждый запрос при 85K RPS — она ложится. Вместо этого мы увидим, что память выделяется крупными блоками (например, по 64 МБ) при старте, а затем переиспользуется.Поведение на уровне gRPC: gRPC под капотом использует механизм
grpc_slice. Память под входящие фреймы берется из внутреннего пула буферов и туда же возвращается. Мы увидим интенсивный инкремент/декремент счетчиков ссылок (Reference Counting) на куски памяти, но не реальное выделение у ОС.Поведение на уровне Protobuf: Современный gRPC использует
google::protobuf::Arena. Объект запроса создается внутри заранее выделенного куска памяти (арены). После отправки ответа вся арена сбрасывается одной инструкцией (сдвигом указателя) без вызова деструкторов для каждого поля.Кастомная хэш-таблица: На графике
Live Memoryмы увидим стабильную ровную линию. Так как этоflat_hashmap, данные лежат плотно в больших массивах. Перевыделение памяти (rehash) будет происходить только при скачкообразном росте количества ключей.
Резюме: Что мы увидим в итоге?
Мы увидим классический паттерн CPU-bound системы.
По памяти: Идеальная «пила» или ровная полка благодаря аренам. Никакого фрагментирования RAM, минимум работы с кучей (Heap).
По процессору: Жуткий оверхед на «сериализацию/демультиплексирование» gRPC, который база может себе позволить только благодаря тому, что сама хэш-таблица за счет
prefetchпрактически не тратит процессорное время на ожидание RAM.
Если бы мы убрали gRPC и заменили его на простой бинарный протокол поверх TCP без мультиплексирования (но с пайплайном), производительность этой же базы на чтение легко улетела бы за 200 000–300 000+ RPS на одном ядре, так как освободилось бы больше половины CPU.
gRPC предлагает заменить на свой бинарный протокол, но тогда и мультиплексир самому придется делать, хотя может быть есть уже готовые инструменты для упаковки-распаковки пакетов (но вам надо не просто их рядом друг за другом, а с прореживанием).

AlexB2012 Автор
05.08.2026 15:46Приветствую.
При нагрузке 85К+ HurriCache потребляет около 20-30% CPU. Я не буду углубляться в архитектуру HurriCache. То что вы описали при помощи гугл ИИ не соответствует действительности и очень далеко от нее.
-
Denzeriko
Интересно сравнить TPS ещё с Tarantool. На данный момент мне кажется он прямой конкурент redis.