Как стать автором
Обновить

Комментарии 18

Было бы хорошо посравнивать предложенное решение с реализацией на Kafka, NATS, классическими брокерами. И аргументировать почему Вы берете именно редис здесь.

Спасибо за хороший вопрос! Да, Kafka, NATS, RabbitMQ — это отличныеи и мощные брокеры. Но у нас немного другой кейс.

Нам нужно просто отправить уведомления в UI: пользователь должен сразу увидеть изменение (например, что вагон разгрузили). Эти события не критичны, мы специально не храним их историю. Если что-то «улетело» — не страшно, фронт всегда может получить актуальное состояние вручную.

Kafka и ей подобные — отличны для сложных сценариев, но вот с какими моментами придётся столкнуться в нашем случае:

  • offset'ы: если сервис уведомлений был недоступен какое-то время, Kafka при восстановлении высыпит всю очередь сообщений — что нам тут вообще не нужно. В итоге на UI приходят старые и ненужные события, и фронт начёт спамить по бэку запросами.

  • consumer-группы: нам важно, чтобы все поды получали события. А с Kafka надо городить схемы с разными groupId, чтобы не получилось, что один получил — а второй не узнал.

  • история: повторюсь — нам не понадобится она для этой задачи, события "одноразовые" и живут ровно до момента отображения.

А Redis даёт простой и быстрый Pub/Sub. Все поды получают события одновременно, нет лишнего слоя абстракции и инфраструктуры. Плюс бонусом — это ещё и распределённый кэш, который пригодится не только для real-time.

Поэтому в таких задачах Redis выглядит как самое простое и при этом эффективное решение.

А при развертывании новой версии не потеряются уведомления?

Да, Redis не хранит события, но у нас обычно настроено бесшовное обновление с двумя или более работающими подами. Пока один обновляется, другие продолжают слушать Redis. Уведомления не теряются, всё работает в реальном времени

Ну и на всякий случай: если уведомление потеряется — всё равно можно нажать F5 ;)

Спасибо, что делитесь опытом. А почему нельзя было сделать разные группы для реплик, Тогда бы каждая считывала одинаковые сообщения и не было бы разницы, где подключен WebSocket.

Интересный комментарий, такой подход действительно возможен.

Да, при использовании уникального group.id на каждую реплику Kafka действительно рассылает одно и то же сообщение в каждую consumer group — технически это реализуемо и будет работать.

Но для нашей задачи это оказалось избыточным по нескольким причинам:

1. Kafka будет дублировать доставку сообщения в каждую группу отдельно. При увеличении числа реплик такая схема начинает нагружать брокер.

2. В динамическом окружении, вроде Kubernetes, сложно гарантировать уникальность group.id для каждой поды. Это требует дополнительной обвязки или авто-генерации конфигурации.

3. Если пода была перезапущена (например, во время деплоя), Kafka сохраняет для неё накопившиеся сообщения, и после восстановления она начнёт их вычитывать. Это приведёт к тому, что в UI будут отправлены устаревшие события, которых по факту уже не должно быть, и фронт может сделать лишние запросы к бэкенду.

В нашем случае push-уведомления нужны "на сейчас", история и ретраи не требуются. Если пользователь не получил событие — он просто увидит актуальное состояние при следующем действии.

Поэтому Redis Pub/Sub оказался проще и лучше подходящим для такой задачи — все поды получают событие сразу, без лишней обработки и хранения.

А ещё можно липкие сессии на балансировщике и тогда для такой простой задачи даже инфраструктура с редисом не нужна и горизонтальное масштабирование есть из коробки

Да, sticky-сессии могут помочь в простых случаях, но здесь они не решают основную проблему. Событие может произойти в любом поде сервиса и не связано с конкретной сессией пользователя. Если клиент подключён ко второй поде, а событие произошло в первой — он его просто не получит.

В таких сценариях нужен общий канал, через который все поды узнают о событии и смогут передать его клиентам. Для этого используется Redis Pub/Sub, а сама рассылка уведомлений вынесена в отдельный сервис — чтобы не дублировать эту логику в каждом микросервисе.

Наверное, стоило добавить схему, чтобы это было видно наглядно. В этот раз сосредоточился на коде и примерах, но в следующих статьях обязательно добавлю визуализацию.

Благодарю за статью. Обязательно попробую в песочнице.

Согласно документации Redis, система с 3 подами в Kubernetes может обрабатывать до 500 000 сообщений в секунду через Redis Pub/Sub
...
Проблема: Недостатки обычного Redis в Kubernetes

Обычно используют Redis Pub/Sub, но у него есть минусы:
1) Один Redis без Sentinel — один instance (точка отказа, нет High Availability (Высокой Доступности))
2) Если Redis упадёт, то все поды потеряют соединение с уведомлениями

В связи с использованием Redis Sentinel пара вопросов:
1. А как часто на вашей практике падал Redis?
2. А какое количество сообщений в секунду в пиках нагрузки логировалось в вашей системе?

Спрашиваю не из праздного любопытства, а для понимания достаточности той или иной конфигурации. В моем случае я склоняюсь в сторону простого Redis, так как сторонник минимально достаточного подхода.

Спасибо за отличный вопрос — по существу.

Если коротко: в нашей практике Redis сам по себе не падал. Прямых инцидентов с его отказами не было.

Использование Redis с Sentinel у нас — это архитектурно утверждённый стандарт (насколько мне известно). Одиночный Redis считается потенциальной точкой отказа, поэтому отказоустойчивость через Sentinel закладывается на этапе проектирования — не как реакция на сбои, а как часть принятой инженерной политики.

Примеры в статье — рабочие, конфигурация проверена, всё запускается. Специального нагрузочного тестирования Pub/Sub мы не проводили, но на текущей нагрузке система работает более чем стабильно.

Ваш подход с упором на минимально достаточную конфигурацию вполне понятен. Redis даже в одиночном экземпляре обладает хорошим запасом прочности и в большинстве случаев работает надёжно. Если push-уведомления не критичны и система может обойтись без автоматической отказоустойчивости — обычный Redis справляется с задачей. Всё зависит от контекста, объёма и требований конкретной системы.

Можно посмотреть Garnet от майкрософт, его можно в кластер собирать, по производительности обгоняет radis, dragonfly и keydb

Спасибо, интересный вариант для роли шины данных! Garnet действительно выглядит интересно — особенно с учётом заявленной производительности и кластера "из коробки". Судя по их тестам, он обгоняет Redis. Посмотрим, как покажет себя в реальных условиях.

В принципе, для таких целей можно использовать и Postgres через LISTEN/NOTIFY — как простую, встроенную шину. Но... давайте честно: PostgreSQL и без того не всегда знает, куда деваться от нагрузки, а тут мы ещё и события хотим через него гонять :)

У нас Redis и так используется как кеш во многих сервисах, поэтому он оказался естественным выбором и для Pub/Sub — ничего нового подключать, конфигурировать или поддерживать не нужно. Проверено, стабильно, понятно всем в команде.

Но в целом круто, что появляются альтернативы вроде Garnet. Особенно если они в будущем дорастут до продакшен-уровня и будут поддерживать всё то, что уже стабильно работает в Redis. Буду с интересом следить за развитием.

У posgresql есть такая штука, что обычно его прячут за pgbouncer там обычно используются 2 режима, транзакционный и сессионный, чтобы не было проблем лучше успользовать транзакционный, но NOTIFY работает только в сессионном режиме, но в нём есть свои особенности. Так же тут нужно учитывать что каждый под будет держать открытый коннект к бд, а значит съедать его из пула. Гораздо проще если нет распределённого кэша, использовать funout очереди

Да, вы правы — с PgBouncer и LISTEN/NOTIFY всё не так просто. Я упомянул это скорее как опцию, но без погружения в особенности сессионного режима.

Спасибо за отличный технический комментарий!

Благодарю за разъяснение и также хочу отметить особенность вашего общения: У вас расширенные ответы, полностью отвечающие на вопрос. Читаются легко.
Приятно читать вашу статью и ваши ответы в комментариях.

Спасибо! Да, уже пару человек сказали, что обсуждение получилось даже "полезнее" статьи — в любом случае приятно, что люди нашли время прочитать и поучаствовать в беседе :)

Имеются вредные советы с таймаутом на Ingress, решать проблемы через инфру является костылём. Напомню что данная аннотация распространяется только на HTTP соединение, WebSocket же использует HTTP только на этапе хендшейка, далее апгрейд до ws. По итогу: аннотация бесполезна для WS, но имеет смысл для SSE, ценой 10 минутного таймаута для всех HTTP соединений, дурной пример подаётся в статье.

Лучше оставить: Flux.interval(Duration.ofSeconds(2)), так будет логично и более надёжно, чем вымещать недостатки прикладного уровня на инфраструктурной части.

Спасибо за комментарий — круто, что вчитались и обратили внимание на такие детали.

Согласен: аннотация на Ingress действительно касается только HTTP и работает только в момент установки WebSocket-соединения. Дальше — уже нет, вы правы. Я добавил её скорее исходя из экспериментов в процессе настройки — посчитал, что кому-то это всё-таки может оказаться полезным, особенно в контексте SSE. Для них эта аннотация реально помогает избежать обрывов по таймауту со стороны ingress-контроллера.

По поводу heartbeat — полностью согласен. Я и сам скорее за то, чтобы такие вещи решать на прикладном уровне. Поэтому в примере и используется Flux.interval — он стабилизирует соединение вполне надёжно, особенно если клиент "висит" в подписке долго.

Спасибо ещё раз за развернутый отклик. Такие комментарии под статьёй — это вообще лучшее, что может быть :) Обсуждение явно получилось не зря.

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