Публикация Школы траблшутеров

Как опечатка в коде и ручные деплои тормозят разработку

Время чтения: 6 мин 50 сек
20 мая 2026 г. Просмотров: 214

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

Путь к масштабной архитектуре редко начинается с распределённых протоколов или мультирегиональных кластеров. На практике всё стартует с куда более приземлённых вещей – идентификаторов, очередей событий и, как ни странно, мелких ошибок в коде, которые годами живут незамеченными. В нашем случае именно такие мелочи и оказались главным тормозом.

Нужно было навести порядок в идентификаторах. Система представляла собой типичный набор разнородных решений: где-то автоинкрементные 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, неявные зависимости, опечатки в протоколах, ручные шаги в деплое.

Когда эти вещи устранены, проект начинает собираться. Компоненты перестают конфликтовать, система становится прозрачной, поведение – объяснимым.

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

И, пожалуй, самое важное – исчезла неопределённость. Теперь можно точно предсказать, как система поведёт себя в любой ситуации. Именно так выглядит уровень зрелости, без которого невозможно построить по-настоящему глобальный продукт.