Платформа контакт-центра с нагрузкой более 80 тыс. busy hour call completions, 10 тыс. одновременных агентов и свыше 5 млн транзакций в день стала для автора InfoQ не витриной успеха, а полигоном, где событийная архитектура начала показывать зубы. Для русскоязычных команд, которые строят real-time сервисы на Java, вывод неприятный, но полезный: там, где продукт обещает реакцию за миллисекунды, событийная архитектура легко превращает «асинхронно и масштабируемо» в «формально работает, а пользователь уже злится».
Об этом по данным InfoQ пишет Sagar Deepak Joshi в статье от 30 июня 2026 года. Речь идет о Java-платформе контакт-центра на Apache Kafka, где под боевой нагрузкой вскрылись не абстрактные «компромиссы распределенных систем», а вполне земные сбои: интерфейс агента отставал на 2-3 секунды, события ответа на входящий звонок не успевали дойти вовремя, а зависшие карточки чатов и звонков могли оставаться в UI больше суток. Главная мысль автора звучит без архитектурного романтизма: для сигнальных путей звонка, переходов статуса агента и presence eventual consistency эквивалентна отказу.
Самый показательный фрагмент статьи связан с эволюцией хранения состояния. Сначала команда использовала Kafka Streams global state stores, чтобы каждый pod видел полную копию общего состояния. На диаграмме это выглядит красиво, но репликация через changelog topics оказалась асинхронной, и под нагрузкой разные pod'ы держали разные версии одного и того же состояния звонка. Затем архитектуру переделали на локальные in-memory кэши с восстановлением через replay Kafka-событий. Проблема синхронизации между pod'ами исчезла, зато появилась другая: холодный старт занимал около пяти минут, потому что новый экземпляр должен был проиграть весь backlog. Для Kubernetes HPA это почти насмешка: autoscaler честно поднимает pod, а тот еще несколько минут занят археологией по Kafka и в пике нагрузки бесполезен.
Третья итерация оказалась куда более прозаичной и потому рабочей: Redis сделали авторитетным shared state store. Kafka по-прежнему поставляла события, но все pod'ы читали актуальное состояние напрямую из Redis. Это убрало расхождение кэшей между инстансами и сократило задержку старта на 60%. Дальше команда добавила страхующий механизм: если Redis на старте недоступен или заполнен не полностью, отдельный фоновый поток молча восстанавливает кэш из Kafka, не блокируя прием трафика. Практический вывод здесь важнее конкретного стека: события хороши для доставки изменений, но когда речь идет о real-time маршрутизации, состояние лучше держать в общем быстром источнике, а не надеяться, что каждый pod соберет свою «истину» достаточно быстро и одинаково.
Отдельный пласт проблем связан с тем, что Kafka масштабируется не по презентациям с конференций, а по числу partitions. В описанной системе крупные топики имели 12 partitions, средние 6, малые 3. Это жестко ограничивало число реально работающих consumer'ов: лишние pod'ы просто простаивали, съедая ресурсы. Повысить число partitions в проде тоже не подарок, потому что live-repartitioning вызывает rebalancing и паузы обработки, а для контакт-центра даже короткая остановка уже операционный инцидент. В итоге архитектура как будто обещала горизонтальное масштабирование, но на деле ставила потолок, зашитый в ранних решениях по топологии топиков. Для команд, которые проектируют event-driven системы сегодня, урок предельно конкретный: partitions надо считать заранее и не раздавать общие топики нескольким сервисам без очень веской причины.
Еще один болезненный кейс касается дедупликации и межкластерного взаимодействия. Платформа была разнесена между двумя кластерами Azure Kubernetes Service, а voice switch жил отдельно в Google Cloud. Из-за этого одни и те же gRPC-сообщения получали сразу несколько backend-pod'ов, и дубликаты надо было гасить. Первая версия делала это через Kafka: сообщения писались в raw topic с partition key по call_id, затем один consumer выбирал первое событие и отбрасывал остальные. Работало корректно, но дорого по времени: стандартный polling interval давал минимум 100 миллисекунд на каждый Kafka-hop, а дедупликация добавляла два hop'а, то есть не меньше 200 миллисекунд задержки еще до реальной бизнес-логики. Потом механизм заменили на Redis first-write-wins, где первый pod, успевший захватить ключ по call_id, становился обработчиком, а остальные сразу отступали. Если нужен real-time, делать дедупликацию через Kafka, как показывает этот кейс, идея примерно из той же серии, что тушить пожар бензином, но очень дисциплинированно.
Java-специфика тоже внесла свой вклад. Инициализация Spring Boot-контекста добавляла 30-45 секунд к старту pod'а еще до подключения consumer'ов Kafka. В худшем случае вместе с replay событий подъем сервиса занимал почти шесть минут. Под пиковой нагрузкой garbage collection давал stop-the-world паузы по 200-400 миллисекунд, из-за чего consumer мог отстать от своей partition и потом догонять ее уже минутами. Самым заметным улучшением стал переход на JDK 17, затем помогли настройка G1GC, tiered compilation и object pooling. Автор также отдельно упоминает JDK 21 virtual threads как способ не блокировать carrier thread при синхронных REST-вызовах из consumer'а, хотя в описанных инцидентах эта возможность еще не использовалась. Здесь хорошо видно старую истину, которую любят забывать на уровне «архитектуры»: JVM-тюнинг в таких системах не послесловие, а часть дизайна.
Самый дорогой сбой произошел при массовом provisioning до 10 тыс. агентов. Админский интерфейс публиковал события в топик всего с тремя partitions, consumer'ы делали синхронные REST-вызовы во внешний сервис, а общий pipeline еще и требовал склеивать по три события на одного агента. Когда downstream API замедлился, потоки consumer'ов просто встали. Lag перевалил за 30 минут, UI дождался своего таймаута, часть агентов успела создаться, часть нет, а инженерам потом понадобилось около трех часов ручной сверки и переповторов с idempotency-проверками. После переделки три provisioning-события схлопнули в одно, а синхронный REST убрали из consumer thread в отдельную Redis-очередь с асинхронными worker'ами. Результат был не философский, а измеримый: lag сократился примерно на 50%, а процесс стал restart-safe.
Для рынка разработки это хороший антидот против веры в то, что событийная архитектура автоматически решает вопросы масштаба и отказоустойчивости. Она отлично подходит для аналитики, аудита, CRM-синхронизации и прочих durable-потоков, где лишние сотни миллисекунд не ломают продукт. Но если сервис живет на маршрутизации вызовов, presence и мгновенной обратной связи в UI, то архитектуру придется смешивать: где-то оставлять Kafka, а где-то выбирать Redis, gRPC-streams, WebSocket или другие почти синхронные пути. Главный вопрос здесь уже не в любви к конкретному паттерну, а в зрелости команды: умеет ли она заранее отделить зоны, где асинхронность помогает, от зон, где она просто красиво маскирует будущий инцидент.