Как мы строили системы городского масштаба: архитектура и нагрузка

Когда платформой одновременно пользуются десятки тысяч жителей города, цена ошибки в архитектуре измеряется не в строчках кода, а в очередях на остановках, недоступных госуслугах и потерянной выручке. Системы городского масштаба — это транспортные приложения, платёжные шлюзы, порталы госуслуг, биллинг коммунальных платежей, агрегаторы доставки. Их объединяет одно: нагрузка приходит не равномерно, а волнами, и в пиковый момент система обязана работать так же стабильно, как в три часа ночи. В этой статье мы делимся подходом, который OneDev применяет при проектировании таких решений в Узбекистане.
Почему «городской масштаб» — это отдельный класс задач
Главная иллюзия заказчика звучит так: «у нас просто будет много пользователей, добавим серверов помощнее». На практике высоконагруженная система отличается от обычной не количеством, а характером нагрузки и требованиями к отказоустойчивости. Приложение, которое отлично работает на тысяче пользователей, может полностью лечь на пятидесяти тысячах — не из-за нехватки мощности, а из-за архитектурных решений, которые не масштабируются линейно.
Городской масштаб приносит три специфические сложности. Первая — пиковая неравномерность: оплата проезда утром и вечером, подача деклараций в последний день срока, открытие записи к врачу в 8:00. Вторая — недопустимость простоя: если транспортный валидатор не принимает оплату, страдает реальная физическая инфраструктура, а не абстрактный «UX». Третья — связанность данных: один платёж порождает запись в биллинге, фискальный документ, push-уведомление, обновление баланса и строку в отчётности, и всё это должно остаться согласованным даже при сбое посредине цепочки.
Архитектурный фундамент: где принимаются ключевые решения
Большинство фатальных проблем высоконагруженных систем закладывается в первые недели проектирования, а проявляется через год под нагрузкой. Поэтому мы начинаем не с фреймворка, а с модели нагрузки: сколько запросов в секунду ожидается в пике, какое соотношение чтения и записи, какие операции критичны к задержке, а какие можно выполнять асинхронно.
Базовый принцип — разделять путь чтения и путь записи. В городских системах чтений почти всегда на порядок больше: расписания, балансы, статусы смотрят постоянно, а изменяют редко. Это позволяет выносить чтение на реплики и кэш, разгружая основную базу, которая остаётся узким местом любой системы.
Как мы держим нагрузку: практические слои
Устойчивость к нагрузке — это не одно решение, а несколько слоёв защиты, каждый из которых снимает часть давления с следующего.
- Кэширование. Часто запрашиваемые и редко меняющиеся данные (справочники, расписания, тарифы) отдаются из кэша в памяти. Это снижает обращения к базе в разы. Главное правило — заранее продумать инвалидацию: устаревший кэш баланса опаснее, чем его отсутствие.
- Очереди и асинхронная обработка. Всё, что не обязано выполняться в момент запроса пользователя — отправка уведомлений, генерация документов, выгрузка в смежные системы — уходит в очередь. Пользователь получает быстрый ответ, а тяжёлая работа выполняется в фоне с возможностью повтора при сбое.
- Горизонтальное масштабирование stateless-сервисов. Сервисы приложения не хранят состояние в себе, поэтому под пик можно добавить экземпляры за балансировщиком, а после пика убрать. Состояние живёт в базе и кэше, а не в памяти конкретного процесса.
- Пул соединений к базе. База данных переживает ограниченное число одновременных подключений. Между приложением и СУБД ставится пул (например, пулер соединений), иначе тысяча параллельных запросов исчерпает лимит коннектов и положит базу при живых серверах приложения.
- Деградация вместо падения. При перегрузке система должна отключать второстепенное (аналитику, рекомендации), но сохранять ядро — приём платежа, выдачу билета. Полный отказ всегда хуже частичного.
Данные и согласованность под нагрузкой
В городских системах с деньгами и юридически значимыми действиями согласованность данных важнее скорости. Здесь мы придерживаемся строгого подхода: критичные операции (списание баланса, регистрация платежа) выполняются в транзакциях с понятными гарантиями, а не «отложенно ради скорости».
Отдельная дисциплина — идемпотентность. Сеть в реальном городе ненадёжна: мобильный клиент теряет связь и повторяет запрос. Если повторная отправка оплаты спишет деньги дважды, доверие к платформе рушится мгновенно. Поэтому каждая платёжная операция получает уникальный ключ, и повтор с тем же ключом не создаёт дубль, а возвращает результат первой операции.
Не менее важна устойчивость интеграций. Городская система почти никогда не существует изолированно — она общается с банками, фискальными операторами, госреестрами. Эти внешние сервисы падают и тормозят независимо от вас. Поэтому каждый внешний вызов оборачивается таймаутами, повторами с задержкой и «предохранителем» (circuit breaker), который временно отключает обращения к упавшему сервису, чтобы он не утянул за собой всю платформу.
Наблюдаемость: нельзя удержать то, что не видишь
Высоконагруженную систему невозможно эксплуатировать вслепую. До запуска под реальную аудиторию мы закладываем три вещи: метрики (нагрузка, задержки, ошибки по каждому критичному пути), структурированные логи и трассировку запросов через все компоненты. Это превращает разбор инцидента из многочасового гадания в точечную диагностику.
Отдельно настраивается мониторинг бизнес-показателей, а не только технических. Падение числа успешных платежей в минуту — сигнал раньше и важнее, чем рост загрузки процессора. Хорошая система оповещает о проблеме до того, как о ней сообщат пользователи и СМИ.
Типичные ошибки, которые мы видим в существующих проектах
Когда нас приглашают спасать систему, которая «не держит нагрузку», набор причин почти всегда повторяется:
- Нагрузочное тестирование не проводилось вовсе либо проводилось на нереалистичных данных — пиковая нагрузка впервые встречается в день запуска.
- Состояние хранится в памяти процесса (сессии, кэши), из-за чего систему невозможно масштабировать горизонтально без потери данных пользователей.
- Синхронные вызовы внешних сервисов в основном потоке запроса — один медленный банк подвешивает всю платформу.
- Отсутствие лимитов и защиты от всплесков, из-за чего один сбойный клиент или ботнет кладёт систему.
- Деплой без возможности отката и без «прогрева» — обновление в час пик превращается в инцидент.
Хорошая новость в том, что почти всё это лечится поэтапно, без полной переписки, если за дело берётся команда, которая понимает природу нагрузки.
Вывод
Системы городского масштаба выигрывают не за счёт модных технологий, а за счёт дисциплины: честной модели нагрузки на старте, разделения чтения и записи, асинхронной обработки, идемпотентности платежей, устойчивых интеграций и реальной наблюдаемости. Каждый из этих слоёв снимает часть риска, и вместе они дают систему, которая остаётся стабильной в пиковые часы и предсказуемой в эксплуатации. Если вы запускаете или развиваете платформу, от которой зависит ежедневная жизнь тысяч горожан или работа госсектора, обсудите архитектуру с командой OneDev до того, как нагрузка проверит её за вас — мы поможем спроектировать решение, рассчитанное на рост, и подскажем, где у текущей системы узкие места.