Это разбор архитектуры системы управления транспортом — без сроков, без смет и без сравнения технологий по галочкам. Только устройство: из чего система собрана, как разложена по кластеру, куда идут данные и почему именно так.
Коротко о предмете. Заказы приезжают из SAP, под них подбираются водитель и машина с учётом требований к транспорту и к точкам. Рейс идёт по точкам с временными окнами, на точках чек-листы, при выдаче и возврате машины составляются акты с фиксацией повреждений. Сверху — поток GPS-координат, отслеживание рейсов и мест, мобильное приложение водителя и публичный контур, где клиент видит свою доставку.
Пятнадцать модулей, три ноды за балансировщиком, RabbitMQ и Kafka по три ноды, PostgreSQL, Redis из шести.
Дальше — по частям: чем собрано, как разложено по кластеру и почему потоки разной частоты идут разными дорогами.
Статья самостоятельная — читать предыдущие не нужно. Если интересна не техника, а экономика этого стека, она разобрана в двух других:
Сколько стоит интеграционный слой на WSO2 MI и EF Core — разбор стека «до» по строкам
Переезд с ESB на свой стек: что реально экономит бизнес — сравнение модуля до и после
Здесь — только устройство.
Цикл про redb и redb.Route. Свежие статьи — сверху:
Сколько стоит интеграционный слой на WSO2 MI и EF Core: разбор реальной системы по строкам
redb — типизированное хранилище для .NET поверх Postgres/MSSQL
Полный список — в профиле. Исходники: github.com/redbase-app.
Из чего собран стек
Четыре уровня, и разделены они по одному принципу: свой код только на верхнем, остальное подключается зависимостями.
Модуль — то, что пишется в проекте. Описания маршрутов, бизнес-правила, сущности с атрибутами схемы. Точка входа регистрирует компоненты, фабрики подключений и маршруты. Упаковывается в пакет, кладётся в каталог, подхватывается контейнером на ходу.
redb.Route — интеграционный движок: около сорока шаблонов из каталога корпоративной интеграции, 27 транспортов наружу и четыре канала внутрь процесса. Подключается пакетами; при необходимости можно подключить исходниками и войти отладчиком внутрь.
redb.Tsak — контейнер модулей и рантайм: жизненный цикл, изоляция, горячая замена, кластер с выбором лидера, панель и REST API, планировщик, сторож маршрутов, очередь недоставленного. Разворачивается образом или архивом, настраивается переменными окружения.
redb — типизированное хранилище. Схема выводится из классов, миграций нет. Два входа: объектный для бизнес-сущностей и прямой SQL для того, что в объектную модель сознательно не кладётся.
Провайдер хранилища выбирает хост, код модуля его не называет — PostgreSQL, MS SQL или SQLite при одних и тех же схемах и контрактах.
Единица композиции — контекст, а не сервис
Здесь важно не ошибиться в масштабе. Единица развёртывания в этой системе — не микросервис и не процесс, а модуль со своим маршрутным контекстом.
Модуль — это сборка с точкой входа, которая получает контекст и наполняет его: транспортные компоненты, именованные фабрики подключений в реестр, сервисы, наборы маршрутов. Дальше контейнер поднимает контекст и запускает маршруты.
Контекст даёт три вещи сразу:
-
границу изоляции — у каждого модуля свой контекст загрузки сборок, зависимости соседей не конфликтуют;
-
единицу распределения — контексты раскладываются по нодам кластера и переезжают между ними;
-
единицу управления — контекст можно остановить, запустить и перезапустить отдельно от других.
Отсюда прикладное следствие, ради которого всё и затевалось: выкатка одной интеграции не трогает остальные. Пакет модуля кладётся в каталог, контейнер подхватывает его и перезапускает только его контекст. Соседние продолжают работать, сообщения в них не теряются.
Физическая топология
Три ноды приложения за балансировщиком. Координация нод — через общую базу: выбор лидера, блокировки маршрутов, перераспределение контекстов при отказе ноды или при плановом выводе.
Сразу о границе ответственности, потому что она здесь проходит по живому. Архитектура и прикладной слой — моя зона. Развёртывание и эксплуатация инфраструктуры — зона DevOps-инженеров: ноды, кластеры брокеров, кластер базы, конвейеры сборки, доставка секретов в рантайм.
Это не формальность, а рабочее разделение. Архитектура предъявляет к инфраструктуре требования — три мастера Redis, чтобы был кворум; партиции Kafka, чтобы группа потребителей могла делиться между нодами; реплики базы. А выполняются эти требования не мной. Поэтому дальше я описываю, что нужно системе и почему, но не рассказываю, как это раскатано: там своя работа и свои решения.
Каждый инфраструктурный компонент кластеризован отдельно и по-своему:
RabbitMQ — три ноды. События аудита, интеграция с SAP, обмен запрос-ответ, исходящий обмен. У публичного контура — свой отдельный брокер, о нём ниже.
Kafka — три ноды. Поток GPS-координат и топик незапланированных остановок.
PostgreSQL — три ноды, мастер и реплики. Здесь и объектное хранилище, и партиционированные таблицы, и координация кластера приложения.
Redis — шесть нод. Вот это стоит объяснить, потому что шесть — не «на всякий случай».
Почему Redis именно из шести
Кластерный режим Redis требует минимум трёх мастеров. Причина в кворуме: решение о том, что мастер отказал и надо переключаться на реплику, принимается большинством мастеров. При двух мастерах большинство недостижимо — один узел не может составить большинство из двух, поэтому автоматическое переключение невозможно в принципе.
Три мастера дают большинство два из трёх. К каждому — по реплике, иначе отказ мастера означает потерю его слотов вместе с данными.
Три мастера плюс три реплики — шесть. Меньше — либо нет автоматического переключения, либо нет отказоустойчивости по данным.
Два контура
Система разрезана не только на модули, но и на зоны с разным уровнем доверия.
Внутренний контур — вертикали системы, хранилище, брокеры, кэш. Отсюда работают водитель через мобильное приложение, оператор и администратор через панель управления — каждый со своим фасадом и своими правами.
Публичный контур вынесен за периметр компании в облако. У него своя база, свой брокер, свой кэш — ничего общего с внутренней инфраструктурой. Оттуда работает клиент: смотрит, где сейчас его доставка.
И ключевое свойство: обратных соединений нет. Бэкенд компании ходит в контур и кладёт данные. Изнутри контура наружу — запрещено.
Из этого следует архитектурное требование, которое иначе выглядело бы произволом: проверка доступа в публичном контуре обязана быть локальной. Спросить компанию, валиден ли ключ, нельзя — обратного соединения не существует. Значит проверка самодостаточна и работает на данных, которые уже лежат в зоне.
Отдельно про внешние границы внутреннего контура. Шина SAP — не наш слой, а чужая система: оттуда приходят заказы, водители, места, контрагенты. Внешняя маршрутизация — источник плановой геометрии. Обе остаются на месте при любых изменениях внутри: это границы, а не части.
Транспорты и каналы
Движок различает два принципиально разных вида связи, и на схемах их полезно разводить.
Наружу — 27 транспортов. По каталогу шаблонов это «адаптер канала»: брокеры (RabbitMQ, Kafka, AMQP, IBM MQ, Azure Service Bus, SQS, MQTT), протоколы (HTTP, gRPC, TCP, WebSocket, SignalR), файловые (File, FTP, SFTP, S3), данные (SQL, Redis, Elasticsearch), плюс почта, каталог LDAP, Telegram, планировщик и запуск процессов.
Внутрь процесса — четыре канала, и они решают разные задачи:
|
Канал |
Семантика |
Когда нужен |
|---|---|---|
|
|
синхронно внутри маршрута |
вынести кусок маршрута, не меняя поток управления |
|
|
синхронно между контекстами |
вызвать соседний модуль без сети и без сериализации |
|
|
асинхронно между контекстами |
развязать модули, не поднимая брокер |
|
|
асинхронная очередь в памяти |
разделить стадии обработки с обратным давлением |
Последний заслуживает пояснения. seda — это ограниченная очередь с несколькими конкурирующими потребителями и таймаутом на постановку. Нужно развязать быстрый приём и медленную обработку — ставите между ними такую очередь и задаёте число воркеров. Брокер для этого не нужен, а ограниченность очереди даёт естественное обратное давление вместо бесконечного разбухания в памяти.
По каталогу это «канал точка-точка» вместе с «конкурирующими потребителями» — то есть шаблон закрывается примитивом ядра, а не внешней инфраструктурой.
Высокочастотный поток: где объектная модель заканчивается
Вот место, где архитектура делает сознательное исключение, и его надо проговорить прямо.
Бизнес-сущности — рейсы, точки, заказы, водители, машины — живут в объектном хранилище, загружаются графом одним вызовом, схема выводится из классов. Но координаты и события аудита туда не идут. У них своя дорога: партиционированные таблицы и прямой SQL.
Причина простая: это потоки другого порядка частоты. Объектная модель оптимизирована под граф с типами и связями, а здесь нужна массовая вставка десятков тысяч однотипных строк с последующим удалением целыми месяцами. Разные задачи — разные инструменты, и попытка натянуть одно на другое кончается плохо для обоих.
Один источник, несколько агрегаторов
Координаты приходят в Kafka. И дальше начинается то, что делает эту часть архитектуры интересной: на одном потоке живёт несколько агрегирующих стадий с разными ключами корреляции и разными условиями завершения.
|
Стадия |
Ключ корреляции |
Условие завершения |
Результат |
|---|---|---|---|
|
Сохранение |
рейс |
размер пачки или таймаут |
одна массовая вставка вместо тысячи одиночных |
|
Незапланированные остановки |
рейс и машина |
временное окно простоя |
событие остановки |
|
Группировка остановок |
остановка |
завершение серии |
связная серия вместо россыпи |
|
Дедупликация точек |
точка |
окно |
поток без повторов |
Плюс расчёт метрик точек, определение прибытия и убытия, трансляция позиции водителя.
Это ровно шаблон «агрегатор» из каталога, применённый несколько раз с разной настройкой. И у него есть важное свойство: завершение группы по размеру либо по таймауту. Без таймаута последняя неполная пачка висела бы в памяти до следующего сообщения — а на разреженном потоке это означало бы, что данные не сохраняются часами.
Как поток масштабируется по нодам
Здесь работает механизм самой Kafka, и в коде есть на этот счёт явный комментарий: не указывать партицию в адресе конечной точки, чтобы включился механизм групп потребителей.
Смысл в том, что Kafka сама распределяет партиции топика между потребителями с одинаковым идентификатором группы. Появилась нода — партиции перераспределились в её пользу. Отвалилась — её партиции разъехались по оставшимся. Никакого своего координатора писать не нужно.
По каталогу это «конкурирующие потребители», но реализованные на уровне брокера, а не приложения.
Следствие, из которого растёт всё остальное
И вот теперь ключевое. Партиции переезжают между потребителями. Контексты переезжают между нодами. Значит координаты по одному и тому же рейсу может обрабатывать любая нода кластера, и не факт, что та же, что минуту назад.
Отсюда жёсткое требование: состояние обработки GPS не может жить в памяти ноды. Оно общее — в Redis. Кэш точек рейса, накопленное состояние обработки, плановая и фактическая геометрия. Всё, что нужно для продолжения работы над рейсом, обязано быть доступно любой ноде.
Именно поэтому в системе есть отдельный менеджер общего кэша, а не просто словарь в памяти.
План и факт маршрута
У рейса есть две геометрии, и они получаются совершенно по-разному.
Плановая приходит из внешней маршрутизации — как рейс должен пройти. Строится один раз: по первой точке GPS либо по сохранению оператором.
Фактическая копится из потока координат — как рейс прошёл на самом деле. Точек в ней много, поэтому она упрощается алгоритмом Рамера — Дугласа — Пекера: линия сохраняет форму, число точек падает на порядок.
Обе живут в общем кэше, потому что обе нужны любой ноде.
По брокеру едет ключ, а не геометрия
Геометрия маршрута — крупный объект. Гонять её через брокер значит нагружать очереди, раздувать память потребителей и упираться в ограничение на размер сообщения.
Поэтому сделано иначе: геометрия остаётся в хранилище и в кэше, а в сообщении едет только ключ. Потребитель берёт ключ и читает данные сам — когда они ему действительно понадобятся.
По каталогу шаблонов это «камера хранения»: в сообщении ссылка, тело лежит отдельно. Приём старый и хорошо известный, но в системах его почему-то регулярно изобретают заново, предварительно наевшись проблем с крупными сообщениями.
Чтение с откатом по трём уровням
Когда плановый маршрут запрашивают, поиск идёт по цепочке:
-
Redis — общий кэш кластера. Попадание означает ответ сразу.
-
PostgreSQL — геометрия сохранена и переживает вытеснение из кэша.
-
Внешняя маршрутизация — последний рубеж: платный и медленный вызов.
И обратная цепочка: получили от внешнего сервиса — сохранили в базу, положили в кэш, отдали. Следующий запрос того же маршрута обслуживается из кэша.
Итог: внешний вызов происходит один раз на маршрут, а не один раз на запрос.
Аудит: две дороги у одного события
Аудит здесь не «логи на всякий случай», а полноценный слой со своей архитектурой. И устроен он так, что одно изменение данных порождает два независимых потока.
Дорога первая — в хранилище. Изменение сущности перехватывается, событие аудита попадает в очередь, фоновый сборщик собирает из неё пачку и делает массовую вставку в партиционированную таблицу. Пачка завершается по размеру либо по интервалу — оба параметра в конфигурации, а не в коде.
Обратите внимание, что это тот же шаблон, что и на потоке координат. Разные данные, разная частота, разные ключи — механизм один.
Дорога вторая — подписчикам. Копия события уходит через брокер тем, кому она нужна. Сейчас это как минимум двое: обновление кэшей GPS-контура и исходящий обмен во внешние системы.
Существенно, что основной поток не ждёт подписчиков. Изменение сохраняется своим темпом; рассылка идёт параллельно и её задержка не влияет на запись. По каталогу это «ответвление» плюс «канал публикации-подписки».
Такая конструкция даёт то, ради чего аудит вообще строят: любая система может подписаться на изменения, не трогая ту, что их порождает. Понадобился новый потребитель событий — он появляется как подписчик, а код источника не меняется вовсе.
Обслуживание партиций
Партиционированные таблицы требуют регулярного создания новых секций и отсечения старых. Это делается по расписанию — задачами планировщика, который встроен в контейнер и в кластере работает согласованно, то есть задача не выполнится трижды на трёх нодах.
Мелочь, о которой обычно вспоминают в момент, когда партиция на следующий месяц не создалась.
Наблюдаемость
Раз система разложена по трём нодам и нескольким брокерам, вопрос «где именно оно тормозит» перестаёт решаться чтением логов.
Метрики заведены по шаблонам, а не по движку в целом. Агрегатор отдаёт число собранных групп, групп в работе и групп, развалившихся по таймауту. Разделитель — на сколько частей разделено. Фильтр — сколько отброшено. Идемпотентный приёмник — сколько прошло и сколько отсеяно как дубли. Предохранитель — сколько раз разомкнулся и сколько вызовов отклонил. И так далее по каталогу.
Плюс сквозное: обработано, провалено, в работе прямо сейчас, гистограммы длительности по обмену целиком и по каждому шагу маршрута.
Трассировка — по стандарту OpenTelemetry, с общепринятыми именами атрибутов: система обмена сообщениями, имя назначения, операция, метод HTTP, тип базы. Исключения раскладываются по стандартным полям. Сверх них — свои: идентификатор корреляции, шаблон обмена, маршрут, шаг, конечная точка.
Практическое следствие: сборщики трасс читают это без адаптеров. Цепочка «принял из Kafka → агрегировал → записал пачкой → разослал подписчикам» видна одной трассой со всеми шагами и длительностями.
Панель — плоскость управления, а не витрина
Одиннадцать страниц: обзор, маршруты и просмотр отдельного маршрута, конечные точки, кластер и карточка ноды, планировщик, журналы, аудит, очередь недоставленного, сторож, права доступа.
И с них действуют, а не только смотрят:
-
отдельный маршрут — остановить и запустить, не трогая остальные;
-
контекст целиком — остановить, запустить, перезапустить;
-
зависший маршрут — снять принудительно;
-
ноду — вывести из работы плавно: она доделывает текущее, новых задач не берёт, блокировки отдаёт соседям; потом вернуть;
-
упавший обмен — посмотреть и перезапустить после починки;
-
задачу планировщика — запустить немедленно;
-
эффективную конфигурацию ноды — прочитать с замазанными секретами, не заходя по SSH.
Рядом с каждым маршрутом — его собственные показатели: сколько прошло, сколько сейчас в работе, пропускная способность, история. Не общий график по процессу, а по маршруту: вопрос в эксплуатации всегда звучит «какая именно интеграция встала».
Где проходят границы
Полезнее любого списка возможностей — перечень мест, где архитектура сознательно отступает от собственных правил.
Партиционированные таблицы мимо объектной модели. Координаты и события аудита пишутся прямым SQL. Это осознанный выбор, а не недоделка: у потоков такой частоты другая экономика.
Плановая геометрия хранится как крупный объект, а не как граф точек. Разбирать её на сущности незачем — она читается целиком и отдаётся целиком.
Состояние обработки в Redis, а не в хранилище. Оно горячее, живёт недолго и нужно всем нодам. Класть его в базу означало бы платить за долговечность там, где она не требуется.
Проверка доступа в публичном контуре дублируется локально. Не потому что так удобнее, а потому что обратных соединений нет и спросить некого.
Шина SAP и внешняя маршрутизация остаются внешними. Их не переписывают и не «интегрируют глубже» — с ними работают через адаптеры каналов, и это правильная граница.
Что из этого стоит забрать
Если убрать конкретику домена, остаётся несколько решений, которые переносятся на любую систему сопоставимой формы.
Разделяйте потоки по частоте, а не по слоям. Бизнес-сущности и телеметрия — принципиально разные нагрузки. Один механизм хранения на оба обычно означает, что один из них обслуживается плохо.
Агрегатор — это не «накопить и записать», а шаблон с двумя условиями завершения. По размеру и по таймауту одновременно. Без второго условия разреженный поток замирает.
Если состояние может понадобиться любой ноде — оно не в памяти. Как только брокер или контейнер начинают перераспределять работу, локальное состояние превращается в источник трудноуловимых расхождений.
Крупные объекты не ездят через брокер. Ссылка в сообщении, тело в хранилище — и очереди остаются лёгкими.
Кэш с откатом заканчивается обратным заполнением. Иначе дорогой внешний вызов повторяется на каждый запрос.
Событие должно уметь обрасти подписчиками без правки источника. Это то свойство, ради которого аудит строят слоем, а не набором записей в лог.
Состояние перехода, чтобы не было недосказанности: публичный контур уже работает на описанном стеке, остальные вертикали переезжают. Разбор одного модуля до и после — во второй статье цикла, экономика стека «до» — в первой.
Исходники и релизы: github.com/redbase-app. Про хранилище redb: redb.ru.
Если разбор оказался полезен — ⭐ на GitHub помогает другим его найти.
ссылка на оригинал статьи https://habr.com/ru/articles/1065100/