Комментарии 7
Интересно сравнить TPS ещё с Tarantool. На данный момент мне кажется он прямой конкурент redis.
Интересный разбор, но стоит отметить: проблему 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) был не оптимально настроен.
Сравнение абсолютно валидное, но здесь важно разграничить две разные проблемы: производительность самого 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 — но в данном случае она оказалась полностью оправданной.
Приветствую.
Нагрузка на 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 предлагает заменить на свой бинарный протокол, но тогда и мультиплексир самому придется делать, хотя может быть есть уже готовые инструменты для упаковки-распаковки пакетов (но вам надо не просто их рядом друг за другом, а с прореживанием).

Как я придумывал замену Redis и что из этого получилось