Обновить

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

Уровень сложностиСредний
Время на прочтение4 мин
Охват и читатели11K
Всего голосов 8: ↑8 и ↓0+10
Комментарии7

Комментарии 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 системы.

  1. По памяти: Идеальная «пила» или ровная полка благодаря аренам. Никакого фрагментирования RAM, минимум работы с кучей (Heap).

  2. По процессору: Жуткий оверхед на «сериализацию/демультиплексирование» gRPC, который база может себе позволить только благодаря тому, что сама хэш-таблица за счет prefetch практически не тратит процессорное время на ожидание RAM.

Если бы мы убрали gRPC и заменили его на простой бинарный протокол поверх TCP без мультиплексирования (но с пайплайном), производительность этой же базы на чтение легко улетела бы за 200 000–300 000+ RPS на одном ядре, так как освободилось бы больше половины CPU.

gRPC предлагает заменить на свой бинарный протокол, но тогда и мультиплексир самому придется делать, хотя может быть есть уже готовые инструменты для упаковки-распаковки пакетов (но вам надо не просто их рядом друг за другом, а с прореживанием).

Приветствую.

При нагрузке 85К+ HurriCache потребляет около 20-30% CPU. Я не буду углубляться в архитектуру HurriCache. То что вы описали при помощи гугл ИИ не соответствует действительности и очень далеко от нее.

ну дай бог. лень самому проверять. а кстати, ссылки не вижу на ваш проект, или код не хотите открывать?

Пока не хочу публиковать серверный код. Клиентским кодом поделиться могу + могу собрать демо докер если заинтересовало

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации