Основатель «Школы траблшутеров» Олег Брагинский и ученица Марина Строева разберут, как мелочи становятся системным тормозом, почему опечатка переживает архитектуру, как ручные деплои мешают росту и чем оборачивается наведение порядка в работающей системе.

Путь к масштабной архитектуре редко начинается с распределённых протоколов или мультирегиональных кластеров. На практике всё стартует с куда более приземлённых вещей – идентификаторов, очередей событий и, как ни странно, мелких ошибок в коде, которые годами живут незамеченными. В нашем случае именно такие мелочи и оказались главным тормозом.
Нужно было навести порядок в идентификаторах. Система представляла собой типичный набор разнородных решений: где-то автоинкрементные integer ID, где-то UUIDv4, где-то вообще составные ключи. Пока система живёт в рамках одного инстанса, это не критично.
Но как только появляется федерация – всё ломается. Невозможно гарантировать уникальность, корректно синхронизировать данные между регионами и строить устойчивые протоколы. Решение было радикальным: повсеместный переход на UUIDv7 решил две задачи:
- во-первых, уникальность всех сущностей – сообщений, звонков, комнат, пользователей
- во-вторых, упорядоченность по времени, что критично для систем реального времени.
Следующий слой – реальное время. Изначально система работала по классической схеме: клиент подключается к серверу, сервер отправляет события. Но при росте нагрузки это превращается в узкое место. Любой сбой – и тысячи пользователей остаются без обновлений.
Мы вынесли realtime-логику в отдельный слой pollnode. Это специализированные узлы, которые занимаются только одним: держат long-poll-соединения с клиентами и читают события из Redis. Core-серверы больше не занимаются доставкой событий напрямую, а публикуют их в поток.
Разделение оказалось ключевым и позволило масштабировать систему горизонтально: добавление новых узлов pollnode не требует изменения бизнес-логики. Более того, система стала устойчивее к кратковременным сбоям – если один узел падает, клиент переподключается к другому, а события доставляются из очереди backlog.
Но именно здесь вскрылась одна из самых неприятных проблем – исторические артефакты. В коде существовала опечатка: recive вместо receive. На первый взгляд – ерунда. Но она была зашита в API, в события, в клиентский код, в десктопные обёртки. Прямая правка ломала совместимость между версиями клиентов и серверов.
Для решения потребовался отдельный слой совместимости. Мы не исправляли опечатку напрямую, а ввели поддержку двух вариантов, постепенно перевели все компоненты на корректное написание и только после этого убрали legacy.
Процесс занял больше времени, чем внедрение отдельных архитектурных решений, и стал хорошим уроком: мелкие ошибки в протоколах – технический долг, который со временем становится системной проблемой.
Дальше – медиа. Классический подход в распределённых системах – каскадирование медиа-серверов (SFU-to-SFU), что усложняет структуру, увеличивает задержки, создаёт точки отказа. Мы пошли другим путём: мульти-SFU-модель, при которой клиент сам подключается к нужному серверу.
Решение сильно упростило серверную часть, но усложнило клиентскую. Если раньше устройство держало одно соединение с одним SFU, теперь должно управлять множеством параллельных соединений. Причём у каждого соединения – свой lifecycle, свои транспорты и параметры.
Фактически, пришлось переписать ядро mediasoup-клиента. Появилась модель Map<sfu_id, SfuSession>, где каждая сессия – отдельный набор устройств, транспортов и консьюмеров. Это позволило добиться главного: одинакового поведения для локальных и кросс-инстансных звонков. Клиенту всё равно, где находится собеседник – в соседнем дата-центре или в другой стране.
Федерация стала следующим логичным шагом. Мы отказались от любых «магических» решений вроде shared Redis или прямых соединений между сервисами. Всё взаимодействие между инстансами происходит через HTTP с криптографическими подписями Ed25519.
Теперь каждый запрос можно проверить, каждый ответ – подтвердить. Никаких скрытых каналов и неявных зависимостей. При этом выбрали синхронную модель доставки сообщений (Z3 sync): пока принимающая сторона не подтвердила получение, сообщение не считается доставленным.
Да, это увеличивает задержку в некоторых сценариях. Взамен получаем чёткую модель состояний: pending, sent, queued. Никаких потерянных сообщений и иллюзий доставки.
Отдельного внимания заслуживает деплой. До определённого момента система существовала в режиме ручного управления: настроить сервер, прописать ключи, скопировать конфиги. Такая архитектура не масштабируется и становится источником постоянных ошибок.
Пришлось вынести весь деплой в параметризованные скрипты. Запуск нового инстанса – одна команда. Генерируются ключи, настраиваются сервисы, прописываются конфиги, регистрируются узлы. То же самое касается узлов pollnode и SFU.
Это не просто упростило работу, но и заложило фундамент для масштабирования. Когда в системе десятки инстансов в разных регионах, ручная настройка становится невозможной.
Но даже после всех этих изменений главный вопрос оставался прежним: работает ли система как единое целое? Не отдельные компоненты, а именно end-to-end сценарии. Пришлось проверить всё:
- как синхронизируются события между инстансами
- как ведёт себя система при обрыве соединения
- что происходит при перезапуске SFU
- не ломается ли порядок
Каждый такой тест вскрывал слабые места. Где-то не хватало таймаута, где-то – ретрая, где-то – явного состояния. Постепенно система обрастала защитными механизмами, пока не стала предсказуемой.
Вывод простой: разработку сложных проектов тормозят не большие архитектурные проблемы, а мелкие несогласованности: разные форматы ID, неявные зависимости, опечатки в протоколах, ручные шаги в деплое.
Когда эти вещи устранены, проект начинает собираться. Компоненты перестают конфликтовать, система становится прозрачной, поведение – объяснимым.
В итоге мы получили не просто набор сервисов, а цельную инфраструктуру. Каждый инстанс полностью автономен, но при этом встроен в глобальную сеть. Сообщения доставляются предсказуемо, звонки работают стабильно, деплой занимает минуты, не дни.
И, пожалуй, самое важное – исчезла неопределённость. Теперь можно точно предсказать, как система поведёт себя в любой ситуации. Именно так выглядит уровень зрелости, без которого невозможно построить по-настоящему глобальный продукт.