Разбираем систему управления транспортом — TMS. Заказы приезжают из SAP, под них подбираются водитель и машина с учётом требований, рейс идёт по точкам с временными окнами, на точках чек-листы, при выдаче и возврате машины — акты с фиксацией повреждений. Сверху поток GPS в партиционированные таблицы, отслеживание рейсов и мест, мобильное приложение водителя и публичный контур, где клиент видит свою доставку. Пятнадцать модулей, три ноды за балансировщиком, RabbitMQ и Kafka по три ноды, Redis, PostgreSQL, отдельный сервер идентичности.
Систему стали писать, когда TMS покупали как услугу у внешних поставщиков и риски выросли: собственная разработка оказалась способом вернуть управляемость ключевым процессом. И написали быстро — системе год и четыре месяца, первый коммит в апреле 2025-го. Это не легаси из нулевых, а молодой проект, который уже успел обзавестись инфраструктурным слоем и уже начал его разбирать.
Команда была полной: системный аналитик разбирал требования, QA проверял сделанное, проектный менеджер вёл бюджет, сроки, приоритеты, очерёдность работ и оценку рисков. Это стоит помнить, читая цифры дальше, — накопившийся инфраструктурный слой не объяснить отсутствием аналитики или тестирования. И все часы в сметах — только разработка, работа остальных ролей в них не входит.
В первой части я разобрал эту систему по строкам и посчитал, во что обходится содержать её инфраструктурный слой на WSO2 Micro Integrator и Entity Framework Core. Получилось 3 700–6 800 человеко-часов разово и 750–1 400 в год — то есть 2,2–4 человеко-года разово и 0,45–0,85 постоянной ставки ежегодно, до единой строки бизнес-логики.
Эта часть — про то, что происходит дальше. Система из этого состояния уходит: интеграционная шина, сервер идентичности и ORM убираются целиком.
Начну с того модуля, который уже переехал, — разбор пары один к одному, построчно. И главный результат этого сравнения оказался обратным тому, чего ждёшь от статьи про миграцию: кода стало почти вдвое больше. Почему это правильный исход — первая половина статьи.
А вторая — про остальные три слоя, где выгода считается уже не десятками часов: ORM, у которой вместе с собой исчезают миграции как класс операционных событий; интеграции, которые схлопываются не сами по себе, а именно из-за ухода ORM; и сервер идентичности, из-за чужого стека заставлявший машину спрашивать токен у машины по HTTP через два промежуточных слоя.
Статусы я развожу явно: что сделано, что измерено, что пока план.
Дисклеймер. Я архитектор и разработчик разбираемого бэкенда, а также автор экосистемы, на которую он опирается. Поэтому дальше — не разбор чужих решений, а собственная смета. И поэтому же измерения отделены от выводов, а цифры разложены по строкам: спорить нужно со строками, а не с моей добросовестностью.
Что обезличено, а что нет. Класс системы называю прямо — TMS с компонентой отслеживания в реальном времени: таких сотни, по классу опознать нельзя. Названий системы, компании и конкретной ERP не будет нигде. Стек — WSO2 MI, EF Core, PostgreSQL, .NET — тоже встречается у сотен организаций, речь о классе решений, а не о заказчике.
Границы счёта: здесь только бэкенд. Веб-интерфейс и мобильные приложения делают другие команды — это отдельные и совсем не маленькие системы, и в цифрах статьи их нет ни строкой. Всё, что дальше называется объёмом кода и накладными расходами, набежало внутри одной серверной части.
Цикл про redb и redb.Route. Свежие статьи — сверху:
Сколько стоит интеграционный слой на WSO2 MI и EF Core: разбор реальной системы по строкам
redb — типизированное хранилище для .NET поверх Postgres/MSSQL
Полный список — в профиле. Исходники: github.com/redbase-app.
Хронология, которая объясняет всё остальное
Прежде чем считать, посмотрим на даты. Они говорят больше, чем любая смета, — я вытащил их из истории репозитория.
|
Когда |
Что появилось |
|---|---|
|
Апрель 2025, день 1 |
первый коммит |
|
Апрель 2025, день 2 |
ORM с миграциями |
|
Апрель 2025, день 8 |
собственный движок маршрутизации |
|
Июнь 2025, месяц 3 |
собственный контейнер модулей |
|
Июнь 2026, месяц 15 |
первый модуль на новом стеке, без шины |
|
Июль 2026, месяц 16 |
текущее состояние |
Три наблюдения, и каждое стоит проговорить.
Собственный движок маршрутизации появился на восьмой день проекта. Не на третьем году, когда «уперлись в ограничения платформы» — сразу.
За восемь дней фреймворк маршрутизации на 27 тысяч строк не пишут. Его приносят готовым — и здесь стоит сказать прямо, кто и почему.
Движок не искали на рынке и не выбирали по сравнительным таблицам: архитектура была спроектирована до старта проекта, вместе с инструментом, который её реализует. Отсюда и восьмой день — принесли готовое.
Из этого следует вещь, важная для чтения всего дальнейшего: система не случайна. Ни один из разбираемых слоёв не появился «так вышло» — каждый выбран осознанно, и за каждый выбор есть кому отвечать. Всё, что дальше названо ценой, — собственная смета, а не разбор чужих решений через плечо.
Шина при этом всё равно осталась. Оба интеграционных слоя стояли рядом с первого месяца и стоят до сих пор. Не «сначала шина, потом на смену ей своё» — а два рантайма одновременно, все шестнадцать месяцев.
Разбирать инфраструктурный слой начали на пятнадцатом месяце. Не через пять лет, когда система окостенела, а практически сразу, как только стало ясно, во что обходится содержать выбранное на старте.
Отсюда главный вывод, который я хочу вынести в начало, а не в конец. Инфраструктурный слой в этой системе — не наследство. Он создан за шестнадцать месяцев решениями, принятыми в первую неделю.
И это меняет адресата статьи. Если вы читаете разбор чужого техдолга с мыслью «у меня-то проект новый, у меня такого нет» — вот система, которой год и четыре месяца, и в ней сто пять тысяч строк инфраструктуры. Техдолг набирается не временем. Он набирается выбором стека на старте.
Сначала о том, как мы вообще считаем — потому что считаем нечестно
Прежде чем сравнивать, надо разобраться с перекосом, который сидит в самом способе счёта. Он есть и в первой части, и в подавляющем большинстве статей про миграции. Я его сам допустил.
Смотрите. Когда мы говорим «интеграционная шина» — мы считаем свои артефакты: 213 файлов XML, 4 099 строк Java. Внутренности самой платформы мы не считаем никогда. А это большой Java-продукт: рантайм, движок медиации, десятки транспортов, менеджмент-консоль. Сотни тысяч строк, которые кто-то написал, отлаживает и версионирует. В смету они не попадают ни строкой.
Когда мы говорим «сервер идентичности» — мы считаем 227 таблиц, потому что они видны в базе. Код, который эти таблицы обслуживает, мы не считаем. Хотя он есть, и его тоже кто-то поддерживает.
А когда речь о собственном движке — мы считаем всё: 27 529 строк движка маршрутизации, 11 117 строк контейнера модулей, и выставляем за это 1 940–3 400 часов.
Разница не в экономике. Разница в том, что лежит у нас в репозитории. Чужие внутренности не видны, поэтому кажутся бесплатными. Свои видны — и поэтому кажутся статьёй расходов.
Это перекос против своего стека, а не в его пользу
Проговорю прямо, потому что вывод контринтуитивный: такой способ счёта завышает цену своего решения и занижает цену чужого. Не наоборот.
Если считать симметрично, есть ровно два честных варианта:
Вариант первый — не считать внутренности ни у кого. Тогда «бесплатная» шина — это внешняя зависимость, которую вы подключили. И движок, подключённый пакетом из NuGet, — тоже внешняя зависимость, которую вы подключили. Обе строки в смете нулевые. Сравнивать надо не их, а то, что вы написали сверху: 861 строку XML плюс 4 099 строк Java против 212 строк описания маршрутов.
Вариант второй — считать внутренности у всех. Тогда рядом с 38 646 строками своего движка и контейнера встают сотни тысяч строк платформы шины и сервера идентичности. И собственный движок внезапно оказывается на порядок меньше того, что он заменяет.
Оба варианта дают один и тот же вывод, и он противоположен интуитивному. Асимметричный счёт — тот, что мы применяем по умолчанию, — единственный, при котором своё решение выглядит дороже.
Ключевое: этот код не надо тащить в свой проект
И вот здесь снимается сама постановка вопроса. Потому что внутренности движка в вашем репозитории не появляются вообще.
Он поставляется так же, как любая нормальная платформа: библиотеки — пакетами из NuGet, рантайм-контейнер — готовым образом либо самостоятельным архивом с подписью для проверки, релизы — на GitHub. Вы подключаете зависимость и пишете свой модуль. Ни исходников в дереве проекта, ни их сборки в вашем конвейере, ни их обновления вашими руками.
Именно поэтому 38 646 строк из таблицы выше в новой схеме не строчка расходов, а ноль строк в репозитории. Ровно в том же смысле, в каком нулём всегда были внутренности готовой шины. Просто теперь это симметрично.
А сверху остаётся то, чего у чужой платформы нет: если понадобилось внутрь — можно внутрь. Подключить исходниками вместо пакета и пройти отладчиком до самого низа. Не «форкнуть и жить с форком», а посмотреть, поправить, предложить обратно.
У чужой платформы этого выбора нет: есть бинарник, и есть ровно один способ влезть внутрь — форкнуть. Что в этом проекте и произошло: коннектор транспорта работал не так, как нужно, вендорского исправления ждать было нельзя, и в репозитории появились 2 429 строк чужого Java-кода на постоянном сопровождении, снятые с траектории обновлений.
Вот это и есть настоящая разница, а не количество строк. По умолчанию не тащишь ничего — а если надо, дверь внутрь открыта. Форк — не преимущество открытой платформы, а цена за отсутствие нормального способа влезть внутрь. И платят её один раз, а потом при каждом обновлении.
Что убирают и что остаётся
Напомню расклад из первой части. В системе было четыре инфраструктурных слоя:
|
Слой |
Стоил |
Судьба |
|---|---|---|
|
Интеграционная шина: 195 артефактов на XML + Java |
904–1 708 ч |
убирается |
|
ORM: 1 500 строк конфигурации, 54 модели, репозитории |
440–820 ч |
убирается |
|
Сервер идентичности: 4 базы, 227 служебных таблиц |
часть от 450–850 ч |
убирается |
|
Собственный движок маршрутизации и контейнер модулей |
1 940–3 400 ч |
остаётся, становится основой |
Три слоя из четырёх демонтируются. Остаётся тот, что обошёлся дороже всех остальных вместе взятых.
Это, пожалуй, главный вывод обеих статей, и он не про технологии. Дешёвое на входе ушло. Дорогое на входе осталось и продолжает приносить пользу.
С чего начали: публичный контур
Первым переехал контур клиента — публичная зона, вынесенная за периметр компании в облако.
Устроена она так:
ВНУТРИ КОМПАНИИ ОБЛАКО (публичный контур) ─────────────── ───────────────────────── бэкенд ──── кладёт ───────────► свой RabbitMQ (только внутрь) │ ▼ своя PostgreSQL свой Redis │ ▼ HTTP API → сайт клиента
Ключевое свойство: обратных соединений нет. Бэкенд компании ходит в контур и кладёт данные. Изнутри контура наружу — запрещено. Своя база, свой брокер, свой кэш; зона самодостаточна.
Почему начали именно с неё — понятно, если подумать про риск. Граница здесь уже проведена архитектурно, а не нарисована на схеме. Контур можно заменить целиком, не трогая ничего внутри компании: снаружи он общается только с сайтом клиента, внутрь не звонит вовсе. Цена ошибки ограничена одной зоной.
Если вы планируете похожий переезд — ищите у себя такое же место. Не самое важное, не самое сложное, а самое отрезанное.
Пара один к одному
Теперь собственно сравнение. Слева — модуль на интеграционной шине, справа — он же, переписанный. Одна и та же зона, одна и та же задача.
Было
Десять артефактов на synapse-XML плюс Java-медиатор:
|
Артефакт |
Строк |
|---|---|
|
API клиента |
345 |
|
Прокси-сервис позиций водителя |
312 |
|
Прокси-сервис информации о доставке |
147 |
|
Последовательность обработки ошибок |
57 |
|
Настройка подключения к Redis |
23 |
|
Проверка токена |
18 |
|
Два описания подключения Redis |
23 |
|
Точка подключения к RabbitMQ |
5 |
|
Описание библиотеки медиаторов |
6 |
|
Итого XML |
936 |
|
Java-медиатор валидации токена |
183 |
|
Итого на двух языках |
1 119 |
Стало
Один модуль на C#, восемнадцать файлов:
|
Файл |
Строк |
|---|---|
|
Маршруты API клиента |
407 |
|
Маршрут позиций водителя |
343 |
|
Маршрут информации о доставке |
289 |
|
Валидатор токена |
238 |
|
Загрузчик конфигурации |
172 |
|
Точка входа модуля |
120 |
|
Обработчик исключений |
83 |
|
Модель ошибки API |
46 |
|
Обслуживание партиций по расписанию |
42 |
|
Шесть файлов типизированной конфигурации |
168 |
|
Две модели и результат валидации |
33 |
|
Итого |
1 941 |
Разница
1 119 → 1 941. Рост в 1,7 раза.
Вот тут статья про миграцию обычно и заканчивается — потому что цифра неудобная. Разберём её честно. И начнём с методики, потому что счёт строк на разных языках — вещь, которой легко подыграть в любую сторону.
Как именно я считал
Три способа посчитать одно и то же, все три — на одном и том же коде:
|
Методика |
Было |
Стало |
Рост |
|---|---|---|---|
|
Сырые строки, файлы сборки исключены |
1 119 |
1 941 |
×1,73 |
|
Без пустых строк и комментариев |
921 |
1 330 |
×1,44 |
|
Со файлами сборки включительно |
1 582 |
1 971 |
×1,25 |
Рост подтверждается всеми тремя — то есть это не артефакт способа счёта. Дальше в статье я пользуюсь первой строкой, как самой невыгодной для нового кода: если вывод устоит на ней, на остальных устоит тем более.
А третья строка требует пояснения, потому что в ней спрятан отдельный сюжет. Чтобы упаковать этот модуль, платформа шины требует описания сборки на 463 строки. Новому модулю хватает тридцати. Разница в пятнадцать раз — в файле, который не делает ничего, кроме объяснения, как собрать остальное. Я вынес файлы сборки из основного счёта, чтобы сравнивать код с кодом, но выбрасывать этот факт совсем было бы нечестно: он тоже кем-то поддерживается.
Оговорка, без которой таблица не стоит ничего: строка XML и строка C# — разные единицы, и складывать их в одну метрику можно только с пониманием, что это грубая прикидка объёма, а не измерение сложности. Именно поэтому дальше я разбираю не итог, а из чего он складывается.
Главная поправка: я сравнивал не то с тем
А вот здесь я обязан сдать собственную цифру, потому что в таком виде она вводит в заблуждение.
Складывать «весь XML» с «всем C#» неправильно, потому что в каждом стеке есть два разных слоя: декларативное описание маршрута и императивная логика, которую декларацией не выразить. Считать их надо порознь — иначе множитель ничего не значит.
Почему эти две вещи вообще сравнимы
Сначала обосную саму возможность сравнения, иначе дальше будет спор о вкусах.
Обе стороны — не произвольные поделки, а реализации одного и того же индустриального каталога. Он описан в книге Грегора Хопа и Бобби Вульфа «Enterprise Integration Patterns» (на русском — «Шаблоны интеграции корпоративных приложений»), где формализованы 65 шаблонов обмена сообщениями: маршрутизаторы, разделители, агрегаторы, обогатители, преобразователи и так далее. Это стандартный словарь отрасли: на нём построен Apache Camel, на нём же построена WSO2 MI, на нём построен и движок из этой статьи.
То есть сравниваются не «чужой продукт» и «моя самоделка», а две реализации одной и той же спецификации. Один и тот же шаблон в шине записывается тегом медиатора, в новом стеке — вызовом метода. Именно поэтому построчное сравнение деклараций осмысленно: обе описывают один набор понятий.
Раз уж зашла речь, проверю покрытие по коду, а не по описанию на странице проекта. И сразу оговорюсь про распространённую ошибку счёта, которую сам чуть не допустил: книга не сводится к маршрутизации. Больше половины каталога — это главы про каналы, конечные точки и построение сообщений, и они реализуются не языком маршрутов, а транспортами и ядром.
|
Группа по книге |
Что есть в движке |
|---|---|
|
Маршрутизация |
Content-Based Router ( |
|
Преобразование |
Message Translator ( |
|
Каналы |
Channel Adapter — 27 транспортов, Point-to-Point Channel ( |
|
Конечные точки |
Polling Consumer ( |
|
Построение сообщений |
Request-Reply ( |
|
Управление |
Wire Tap, Message Store ( |
Сложим: около сорока шаблонов каталога — и это без натяжек, каждый подтверждается либо методом языка, либо транспортом, либо примитивом ядра.
Отдельно стоит выделить Channel Adapter, потому что в книге это один из центральных шаблонов, а в жизни — самая трудоёмкая часть любой интеграции. Двадцать семь транспортов: брокеры (RabbitMQ, Kafka, AMQP, IBM MQ, Azure Service Bus, SQS, MQTT), протоколы (HTTP, gRPC, TCP, WebSocket, SignalR), файловые (File, FTP, SFTP, S3), данные (SQL, Redis, Elasticsearch), плюс почта, LDAP, Telegram, планировщик и запуск процессов.
И второе, что заслуживает отдельного слова, — внутрипроцессные каналы, где брокер не нужен вовсе.
Их четыре, и они решают разные задачи. direct — синхронный вызов внутри маршрута. direct-vm — синхронный вызов между разными контекстами одного процесса: именно на нём построен разговор модулей с сервером идентичности, о котором речь дальше в статье. vm — асинхронная передача между контекстами. И seda — асинхронная очередь в памяти с ограниченным размером, N конкурирующими потребителями, таймаутом на постановку и счётчиками длины очереди.
Ценность последнего в том, что классическое разделение стадий обработки достаётся без брокера. Нужно развязать быстрый приём и медленную обработку — ставите между ними очередь в памяти и задаёте число воркеров. Не нужно поднимать RabbitMQ, чтобы получить асинхронную ступень внутри одного процесса; при этом ограниченная очередь даёт естественное обратное давление, а не бесконечное разбухание.
По книге это Point-to-Point Channel вместе с Competing Consumers, и это тот случай, когда шаблон каталога закрывается примитивом ядра, а не внешней инфраструктурой. В стеке «до» аналогичная развязка означала бы очередь в брокере — со всеми сопутствующими: соединение, топология, мониторинг, сетевой участок.
Сверх каталога — ещё девять шаблонов, которых в книге 2003 года нет, но которые стали стандартом де-факто в Camel: Circuit Breaker, Throttle, Debounce, Delay, Loop, Threads, Sample, Validate, TryCatch.
Каждый шаблон видно в телеметрии
И вот что к этому каталогу прилагается, а в разговорах про EIP обычно теряется. Шаблоны не просто реализованы — каждый из них инструментирован отдельно, и это не собственная статистика в своём формате, а стандартная телеметрия OpenTelemetry.
Метрики заведены не «на движок в целом», а по конкретным шаблонам:
|
Шаблон |
Что меряется |
|---|---|
|
Aggregator |
собрано групп, групп в работе, сколько развалилось по таймауту |
|
Splitter |
на сколько частей разделено |
|
Filter |
сколько сообщений отброшено |
|
Idempotent Receiver |
сколько прошло, сколько отсеяно как дубли |
|
Multicast |
число ветвей и сколько из них упало |
|
Recipient List |
сколько получателей выбрано |
|
Circuit Breaker |
сколько раз разомкнулся, сколько вызовов отклонил |
|
Retry |
попытки, успехи после повтора, исчерпания |
|
Saga |
завершено, провалено, компенсировано |
|
Throttle / Timeout / Debounce |
задержано, истекло, отброшено и слито |
|
Wire Tap |
отправлено копий, сколько не доставлено |
|
Dead Letter |
сколько ушло в очередь недоставленного |
Плюс сквозное: обработано, провалено, в работе прямо сейчас, и гистограммы длительности — по обмену целиком и по каждому шагу маршрута.
Трассировка тоже не самодельная. Спаны выпускаются через стандартный источник активности, а атрибуты проставляются по семантическим соглашениям OpenTelemetry — messaging.system, messaging.destination.name, messaging.operation, http.method, db.system, file.system, исключения раскладываются в exception.type / message / stacktrace. Сверх них свои: идентификатор корреляции, шаблон обмена, маршрут, шаг, конечная точка.
Практическое следствие простое: Jaeger, Grafana или Tempo понимают это без единого адаптера — достаточно подписаться на источник. Контейнер модулей знает про телеметрию и подхватывает её сам, так что цепочка «принял из очереди → обогатил → сходил в базу → ответил» видна одной трассой со всеми шагами и длительностями.
Разница со стеком «до» здесь не в наличии логов — логи есть везде. Разница в том, что вопрос «на каком шаге и почему тормозит» решается трассой, а не сопоставлением записей из журналов двух рантаймов по времени.
И проект развивается, причём быстро. Двенадцать релизов за три месяца — с версии 1.0.4 в начале мая до 3.4.0 в конце июля. За это время появились управление параллелизмом отдельным шаблоном .Threads(N), точки сохранения с повторным проигрыванием упавших обменов, коннектор к очередям Amazon, разделяемый слой библиотек, при котором патч фреймворка ставится подменой файла без пересборки. Каталог шаблонов закрывается дальше, транспорты добавляются.
Это важно проговорить именно здесь, в статье про переезд с зрелой платформы. Молодость движка — реальный фактор при выборе, и делать вид, что его нет, было бы нечестно. Обратная сторона у неё тоже реальная: то, что в стеке «до» жило в трекере вендора годами, здесь закрывается за недели, потому что автор рядом и приоритеты определяет эксплуатация, а не чужая дорожная карта.
Теперь, когда понятно, что стороны говорят на одном языке, разделим слои.
Слой первый — декларация маршрута. В шине это synapse-XML. В новом модуле — блок описания маршрута на встроенном языке (DSL). Сравниваем сопоставимое:
|
Декларация маршрута |
XML |
DSL |
Плотность |
|---|---|---|---|
|
API клиента (три ресурса) |
345 |
72 |
×4,8 |
|
Позиции водителя |
312 |
52 |
×6,0 |
|
Информация о доставке |
147 |
53 |
×2,8 |
|
Обработка ошибок |
57 |
35 |
×1,6 |
|
Итого |
861 |
212 |
×4,1 |
Восемьсот шестьдесят одна строка XML против двухсот двенадцати строк DSL. На том слое, где сравнение вообще корректно, новый стек в четыре раза плотнее. Именно этот слой в статьях про ESB обычно и называют главным достоинством готовой шины — «декларативность». Он проиграл вчетверо.
Слой второй — императивная логика. Она нужна обоим стекам: разобрать полезную нагрузку, сравнить статусы, собрать ответ. Разница в том, где она живёт.
В шине для неё нет другого места, кроме Java. И вот сколько Java лежит в проекте шины целиком, не в одном модуле:
|
Java в проекте шины |
Файлов |
Строк |
|---|---|---|
|
Форкнутый коннектор транспорта — чужой код на своём сопровождении |
12 |
2 429 |
|
Свои классовые медиаторы — то, что XML не выразил |
6 |
1 670 |
|
Итого |
18 |
4 099 |
Четыре тысячи строк Java держат на себе «декларативную» платформу. Половина — форк чужого коннектора; половина — собственные медиаторы, написанные там, где выразительности XML не хватило.
В новом модуле этот слой никуда не исчез — он остался в том же файле и на том же языке, что и декларация. Это и есть весь источник роста итоговой цифры: то, что в шине лежало отдельным проектом на отдельном языке с отдельной сборкой, здесь лежит рядом с маршрутом.
Отсюда переформулировка вывода. Не «код вырос в 1,7 раза при уходе с ESB». А так: декларация сжалась вчетверо, а императивная логика переехала из невидимого Java-проекта в тот же файл рядом с маршрутом. Итоговый плюс — цена этого переезда из невидимого в видимое, и она честная.
И вторая поправка: новый код написан не сжато — намеренно
Новый модуль написан классически и разряженно — под отладчик. Вот что это значит на практике, по коду:
-
каждый шаг маршрута вынесен в именованный приватный метод с описанием — извлечь данные, определить сохранённое из кэша, определить из базы, сравнить статусы, собрать ответ. Не потому, что иначе нельзя, а чтобы на каждый шаг можно было поставить точку останова и увидеть состояние;
-
имена свойств обмена вынесены в тринадцать констант вместо строковых литералов по месту;
-
параметры повторных попыток — отдельные именованные константы;
-
у каждого шага — комментарий с его номером и назначением.
И главное: фреймворк умеет то, чем этот код не воспользовался. В нём есть подсистема выражений с JSONPath — компилируемые и типизированные варианты. А извлечение данных в модуле сделано руками: разбор JSON, обращение к полям по одному, try/catch вокруг, раскладка результата по шести свойствам поштучно. Сорок с лишним строк там, где хватило бы нескольких выражений.
Сколько именно можно ужать — не измерял и цифру не назову. Но в разы — реально, и я это утверждаю как автор обеих сторон сравнения.
Теперь сложим обе поправки — и станет видно, насколько несимметрично устроено это сравнение.
У synapse-XML механизм сжатия ровно один: вынести логику в Java-медиатор. Именно так проверка токена и стала восемнадцатью строками — это не плотность, это делегирование. Ужать XML, не выселив логику наружу, нельзя: каждый шаг медиации — это тег.
XML в этом сравнении стоит на своём полу — плотнее он не бывает, а сжать его можно только сокрытием. Новый код стоит выше своего пола по осознанному выбору — ради точек останова, читаемости и построчной сверки с оригиналом.
Значит, ×1,73 — это не цена стека, а цена стиля, выбранного для переноса один к одному. На том же фреймворке тот же результат можно было получить кодом в разы плотнее — и потерять ровно то, ради чего переезжали.
Дальше в статье я цифру не пересматриваю и «в разы плотнее» в таблицы не подставляю: гипотетический компактный код никто не писал, а сравнивать измеренное с воображаемым нельзя. Но трактовать множитель как «вот сколько стоит уход с ESB» — неверно, и я предпочитаю сказать это сам, чем услышать в комментариях.
Куда делся рост
Сопоставим позиции напрямую.
|
Было |
Строк |
Стало |
Строк |
|---|---|---|---|
|
API клиента (XML) |
345 |
Маршруты API клиента |
407 |
|
Позиции водителя (XML) |
312 |
Маршрут позиций водителя |
343 |
|
Информация о доставке (XML) |
147 |
Маршрут информации о доставке |
289 |
|
Обработка ошибок (XML) |
57 |
Обработчик исключений |
83 |
|
Проверка токена (XML + Java) |
201 |
Валидатор токена + результат |
255 |
|
Подключения Redis и RabbitMQ (XML) |
51 |
Типизированная конфигурация |
168 |
|
Описание библиотеки медиаторов (XML) |
6 |
— |
0 |
|
— |
0 |
Модель ошибки API, две модели предметной области |
62 |
|
— |
0 |
Обслуживание партиций |
42 |
|
— |
0 |
Загрузчик конфигурации, точка входа |
292 |
|
Итого |
1 119 |
|
1 941 |
Три источника роста, и ни один из них не про многословие.
Первый: появилось то, чего не было
Обслуживание месячных партиций базы по расписанию — сорок две строки, которых в старой версии не существовало вовсе. Раньше этим занимался кто-то ещё или не занимался никто.
Загрузчик конфигурации и точка входа модуля — почти триста строк. В старой схеме их роль выполняла сама платформа: конфигурация приезжала из её файлов, жизненный цикл держала она. Теперь это явный код, который видно и можно прочитать.
Итого 334 строки — это не переписанное, а дописанное. Вычтите их, и рост из 1,73 превращается в 1,44 — ровно та же цифра, которая выходит при счёте без пустых строк и комментариев. Два независимых способа считать сошлись в одной точке, и это неплохая проверка обоих.
Второй: конфигурация стала типизированной
Пятьдесят одна строка описаний подключений в XML превратилась в сто шестьдесят восемь строк классов настроек.
Звучит как ухудшение, пока не посмотришь, что именно получили. Раньше: имя фабрики строкой, параметры вперемешку, опечатка в имени параметра обнаруживается при старте в лучшем случае и при первом отказе в худшем. Теперь: классы с типами, значения по умолчанию, проверка на этапе компиляции.
Сто семнадцать дополнительных строк — цена того, что ошибка в настройке подключения перестала быть находкой продакшена.
Третий: спрятанное вылезло наружу
И вот это самое главное.
Проверка токена в старой версии — восемнадцать строк XML. Восемнадцать, потому что вся работа происходила не там: XML только вызывал Java-медиатор на сто восемьдесят три строки. Одна логика, два языка, два места, где её искать.
Стало — двести пятьдесят пять строк C# в одном файле, который открывается в отладчике, покрывается тестами и находится поиском по коду.
Та же история с обработкой ошибок и с маршрутом информации о доставке: то, что в XML выглядело коротким, было коротким за счёт вынесенного вовне.
Что на самом деле изменилось
Цифра «в 1,7 раза» описывает объём, но не то, что произошло. Произошло вот что.
Три языка схлопнулись в один
Было: XML для маршрутов, Java для того, что XML не выражает, файлы настроек для конфигурации. Три конвейера сборки, три способа отладки, три места, где искать причину.
Стало: один язык, один конвейер, один отладчик.
Стоит осознать, что означает эта потеря «экономии строк». Восемнадцать строк XML выглядели дешевле двухсот строк C# ровно до того момента, пока кто-то не спросил: а где, собственно, происходит проверка? И ответ требовал знать, что существует Java-медиатор, где он лежит, как он подключается и кто его собирает.
Своим кодом остался один сервис
В новом модуле ровно один класс с собственной логикой — валидатор токена. Комментарий в точке входа объясняет почему:
Только кастомная логика, не покрываемая DSL: валидатор токена. SQL, Redis и RabbitMQ выполняются в маршрутах, отдельных репозиториев нет.
Всё остальное — приём из очередей, запись в базу, работа с кэшем, HTTP-фасад, обслуживание партиций — выражено маршрутами. HTTP-фасад собран без единого контроллера: три ресурса описаны прямо в маршрутах.
Валидатор остался своим не потому, что так удобнее, а потому что иначе нельзя: публичный контур не может спросить у компании, валиден ли ключ, — обратных соединений нет. Проверка обязана быть локальной и самодостаточной.
Две детали, которые говорят больше цифр
Перенос описан в комментарии
В заголовке одного из новых маршрутов написано прямо: «Перенос XML-прокси один к одному» — и дальше по шагам изложен алгоритм оригинала: что извлечь из заголовков, что из тела, где искать сохранённый статус, при каком условии обновлять базу и кэш, что вернуть.
Это не документация ради документации. Это способ доказать, что поведение не поехало: пока алгоритм оригинала записан рядом, любой может сверить.
Приём стоит перенять всем, кто переписывает работающее. Перенос один к одному, поведение зафиксировано в комментарии, улучшения — потом, отдельным шагом. Соблазн «заодно сделать красиво» — главная причина, по которой миграции срываются.
Ключи в кэше до сих пор помнят платформу
Имена ключей Redis в новом модуле начинаются с префикса, содержащего название интеграционной платформы. Той самой, которой в контуре больше нет.
Так вышло, потому что формат ключей менять было нельзя — данные должны были остаться совместимыми во время перехода. Платформу выключили, а её имя осталось в структуре данных.
Мелочь, но показательная: миграция не заканчивается в день, когда старый код удалён. Следы остаются в форматах, в именах, в схемах — и живут ещё долго.
Во что обошёлся переезд
Здесь я собирался дать модель с допущениями, как в первой части. Разложил по этапам — разбор оригинала, перенос маршрутов, валидатор токена, конфигурация, тестирование — и получил 193–335 часов, то есть полторы-две рабочие недели на модуль.
Потом посмотрел в историю репозитория.
|
Когда |
Что |
|---|---|
|
30 июня, 21:22 |
первый коммит модуля |
|
1 июля, 11:21 |
второй |
|
1 июля, 12:25 |
третий |
|
7 и 9 июля |
две правки по итогам проверки |
Перенос занял день. Вечер тридцатого июня и первая половина следующего дня — дальше только доводка по результатам тестирования, двумя коммитами за девять дней.
Моя модель ошиблась примерно в двадцать раз. Оставляю это в статье именно так, потому что расхождение интереснее самой оценки: экспертная смета, построенная по привычным меркам, разошлась с реальностью на порядок с лишним. Если бы я не полез в git, то опубликовал бы правдоподобные и полностью неверные цифры.
Почему день, а не две недели
Причин три, и первая из них — та, которую в статьях про миграции пока не пишут.
Перенос делался в паре с языковой моделью. И сработало это не потому, что «нейросети теперь умеют код», а по конкретной причине: DSL движка следует идиоматике Apache Camel — те же шаблоны из каталога, та же схема from → process → to, тот же способ адресации конечных точек через URI.
А Camel языковые модели знают отлично. Двадцать лет в проде, тысячи проектов, горы примеров, документации и вопросов на форумах — всё это в обучающих данных. То есть следование индустриальному стандарту дало неожиданный побочный эффект: модель уже знает ваш API, хотя никогда его не видела. Она узнаёт идиому.
Вот это стоит осознать тем, кто проектирует библиотеки сегодня. Раньше довод в пользу знакомой идиомы звучал так: разработчику проще войти. Теперь к нему добавился второй, и он весомее: инструмент, которым разработчик пишет код, тоже уже её знает. Своя оригинальная идиома этого преимущества лишает — и цена оригинальности выросла.
Обратное я проверил на той же задаче: synapse-XML с расширениями конкретной платформы и связями по именам артефактов помощь принимает заметно хуже. Формат узкий, примеров в мире мало.
Вторая причина — перенос один к одному. Алгоритм оригинала известен, изобретать нечего. Задача сводится к переводу с одного языка описания на другой, а это ровно то, что модель делает лучше всего.
Третья — автор писал обе стороны. И систему-источник, и движок-приёмник. Это преимущество, которого нет ни у кого другого, и списывать на инструмент то, что объясняется знанием предмета, было бы нечестно.
А развернуть это — час руками, четверть часа с моделью
Вторая цифра из той же серии, и её стоит сопоставить с первой частью напрямую.
В смете стека «до» есть строка «развёртывание и настройка шины, кластер, обучение команды — 300–500 часов». Это не выдумка для красоты: поднять шину, собрать из неё кластер, настроить хранилища конфигураций и научить команду с этим жить — работа на пару месяцев.
Сравнить есть с чем. Возьму развёртывание другой системы на том же стеке — там топология описана одним файлом сборки контейнеров, и вот что в нём поднимается:
-
три рабочих узла в кластере, координация через общую базу, общее имя группы;
-
веб-консоль управления поверх всех трёх, ходит к ним по служебному ключу;
-
поисковый движок с интерфейсом к нему и объектное хранилище;
-
прикладной веб-интерфейс.
Плюс сквозная настройка: строка подключения к базе, параметры фабрики очередей в контексте по умолчанию, который наследуют все модули, авторизация с подписью и ролями, секреты вынесены в переменные окружения.
Этот файл собран в паре с языковой моделью примерно за четверть часа. Руками, без подсказчика, — около часа: файл на десять килобайт, и его надо написать внимательно.
Обе цифры честные, и обе стоит держать в голове: час — если пишете сами, четверть часа — если в паре с моделью. На фоне трёхсот часов разница между ними уже не принципиальна, но заявлять пятнадцать минут как общий результат было бы передёргиванием.
Причина, по которой модель здесь так эффективна, ровно та же, что и с переносом маршрутов: никакого собственного формата описания развёртывания нет. Обычный файл сборки контейнеров плюс настройка через переменные окружения по предсказуемой схеме. Модель видела тысячи таких файлов и пишет их уверенно.
Сопоставьте с альтернативой: у платформы свой формат описания развёртывания, свой сборочный артефакт, свои мастера настройки — и вот это модель не напишет, потому что примеров в мире мало, а специфика вендорская. Там вы остаётесь один на один с документацией вендора, как и десять лет назад.
А выкладка самих модулей — копирование файла. Собранные пакеты кладутся в каталог, примонтированный к контейнеру, контейнер подхватывает их на ходу. Не пересборка образа, не перезапуск процесса, не выкладка всей системы ради одной интеграции.
И уровень при этом не «для разработки». Всё, что обычно и составляет те триста часов, приходит уже собранным:
-
три режима работы — отдельный процесс без базы, одна нода с хранилищем, кластер с выбором лидера и автоматическим перераспределением контекстов между нодами;
-
наблюдаемость — метрики процесса и по каждому маршруту, кольцевой буфер логов, трассировка OpenTelemetry, при желании отдача метрик для сбора;
-
сторож — обнаруживает подвисшие маршруты и при необходимости перезапускает их сам;
-
планировщик — с автоматическим созданием схемы при первом запуске, в кластере безопасный;
-
безопасность — ключи доступа с подписью, роли, сроки жизни, отзыв, привязка к пользователю;
-
изоляция модулей — у каждого свой контекст загрузки, зависимости не конфликтуют;
-
управление — REST API, командная строка и веб-панель, не «напишете сами», а в комплекте.
Ничего из этого не пишется и не настраивается по частям. Модуль кладётся в каталог — контейнер подхватывает его на ходу, без остановки соседних.
И отдельно про панель, потому что именно она обычно и оказывается тем, ради чего платят за коммерческую шину.
Одиннадцать страниц: обзор, маршруты и просмотр отдельного маршрута, конечные точки, кластер и карточка ноды, планировщик, журналы, аудит, очередь недоставленного, сторож, права доступа.
И это не показывалка, а плоскость управления — принципиальная разница, которую стоит проговорить.
Отдельный маршрут можно остановить и запустить прямо из панели, не трогая остальные. Целый контекст — тоже: остановить, запустить, перезапустить. Зависший маршрут — снять принудительно. То есть ответ на вопрос «отключите вон ту интеграцию, она валит смежную систему» — это две кнопки, а не выкладка новой версии процесса и не поход к DevOps.
Рядом с каждым маршрутом — его собственные показатели: сколько прошло, сколько сейчас в работе, пропускная способность, история. Не общий график по процессу, а по каждому маршруту в отдельности, потому что вопрос всегда звучит «какая именно интеграция встала», а не «как чувствует себя сервис».
Сторож — отдельная страница: показывает подозрительные и подвисшие маршруты, умеет перезапускать их сам, и это поведение включается и выключается там же.
Работа с кластером — не просто список нод. Видно, кто сейчас лидер и когда каждая нода последний раз отчиталась. Ноду можно вывести из работы плавно: она доделывает текущее, новых задач не берёт и отдаёт свои блокировки соседям — а потом вернуть обратно. Можно перераспределить нагрузку целиком или выселить ноду жёстко. В карточке ноды — процессор, память, потоки, сборка мусора с историей, её контексты и её журналы.
Тут же, в панели: упавшие обмены можно посмотреть и перезапустить после починки, задачу планировщика — запустить немедленно, эффективную конфигурацию ноды — прочитать с замазанными секретами, не заходя на машину по SSH.
Сопоставьте это со списком реальных болей из первой части: потребители, которые молча перестали читать очередь; зависший обмен запрос-ответ; ручные перезапуски после перестроения кластера. Каждая из них — это ровно тот случай, когда нужна такая панель, а её приходилось заменять походом на ноду.
Вот где на самом деле лежит экономия человеко-часов DevOps, о которой шла речь выше. Не в том, что маршруты короче, а в том, что управляющий слой — кластер, панель, метрики, планировщик, права — не разворачивается неделями и не пишется руками в третий раз за десятилетие.
Границы этой цифры
День — это перенос, а не всё вместе. За скобками остались: разбор оригинала, который делался раньше и отдельно; тестирование и сверка поведения со старым контуром; две правки по итогам проверки. Полный срок от первого коммита до последнего — девять дней.
И повторю про третью причину, потому что она решающая: сокращение с двух недель до дня — не свойство инструмента, а сумма трёх обстоятельств, из которых воспроизводимы два. Если у вас перенос один к одному и фреймворк со стандартной идиомой — считайте, что вам доступны первые два. Знания обеих сторон в одних руках у вас, скорее всего, не будет.
Что это даёт в год
Экономия здесь не в строках, а в том, чего больше не происходит:
|
Что уходит |
Часов в год |
|---|---|
|
Обслуживание артефактов на XML для этого контура |
15–25 |
|
Диагностика зависаний обмена запрос-ответ на общем канале |
20–40 |
|
Ручные перезапуски после перестроения кластера |
10–20 |
|
Поддержка Java-медиатора: отдельный конвейер сборки, отдельная компетенция |
15–30 |
|
Итого |
60–115 |
Окупаемость считаем от факта, а не от моей несостоявшейся сметы: день переноса против 60–115 часов ежегодной экономии — то есть вложение возвращается в первый же месяц.
Но в расчёт не входит то, что посчитать нельзя: тихие отказы. Потребитель, который перестал читать очередь и не сказал об этом. Обмен, зависший на общем канале. Ночной перезапуск после штатного обновления. Первая часть разбирала эти истории подробно — каждая из них стоит дороже, чем строка в таблице, и ни одна не поддаётся оценке заранее.
Настоящая причина переезда не в часах. Она в том, что отказы стали видимыми и диагностируемыми.
Больше, чем один модуль: два рантайма на одной ноде
Пока переехал один модуль из контура клиента. Но кое-что важное видно уже сейчас — если посмотреть не на код, а на то, где всё это физически работает.
Топология кластера подтверждена архитектурной документацией системы: три ноды, и на каждой из них рядом стоят собственный контейнер модулей и WSO2 MI. Не «шина где-то в отдельном контуре» — два разных рантайма бок о бок, на одном и том же железе, всё время существования проекта.
Это и есть настоящая цена разнородного стека — не абстрактная строчка «два конвейера сборки» из первой части, а буквально: каждая из трёх нод требует одновременного обслуживания двух рантаймов с нуля разными инструментами.
-
WSO2 MI пакуется в Carbon Application (
.car) через Maven — свой конвейер сборки, своя система версионирования артефактов, свой процесс выкладки на каждую ноду. -
Модуль на redb.Tsak — обычная .NET-сборка, разложенная в volume контейнера. Состав загруженных модулей на конкретной ноде управляется одной переменной окружения —
STATIC_MODULES_FILTER, с wildcard-паттернами (Api.*,!Dal.*, список через запятую) — без переупаковки образа и без отдельного релиза под каждый модуль.
Для DevOps-инженера, который обслуживает три ноды, это означает не «плюс немного работы», а вторую полноценную область экспертизы рядом с первой: Java, Maven, Carbon-паковка, свой набор логов и метрик — и всё это нужно пока в кластере остаётся хоть одна нода с WSO2 MI, независимо от того, сколько модулей уже переехало.
В этом и главная нелинейность экономики такого перехода. Экономия на DevOps-компетенции не растёт пропорционально числу мигрировавших модулей — она реализуется одним скачком, в момент, когда последний Carbon Application снимается с последней ноды и кластер перестаёт быть кластером двух рантаймов. До этого момента держать вторую компетенцию в команде приходится целиком, а не «наполовину, раз половина модулей уже переехала».
Честная оговорка, а не подсчёт: сколько именно стоит держать в команде компетенцию на два рантайма вместо одного — величина управленческая, а не инженерная. Она зависит от размера команды, рынка труда на Java/Carbon-специалистов в конкретном регионе и от того, распределена ли экспертиза между разными людьми или совмещена в одних руках. Я это не измерял и не буду выдавать оценку за факт. Что подтверждено — топология: оба рантайма делят одну ноду, и до полного переезда от этого совмещения нельзя отказаться частично.
Дальше по слоям: где выгода на порядок больше
До этого места был один модуль и один слой — интеграционная шина. Дальше начинается то, где деньги считаются уже не десятками часов.
Сразу разделю статусы, чтобы не выдавать план за сделанное:
|
Слой |
Статус |
|---|---|
|
Интеграционная шина, публичный контур |
сделано — разобрано выше построчно |
|
Остальные HTTP-фасады (7 модулей) |
сделано раньше — ушли на движок-предшественник, не на шину |
|
ORM с миграциями |
разобрано по коду, есть план пилота — цифры ниже измеренные, решение о сносе принято, работа впереди |
|
Сервер идентичности |
целевое решение выбрано — своё, вместо внешнего |
Дальше — измерения по коду и разбор того, что из них следует. Где оценка — помечено.
Слой второй: ORM, у которой исчезают миграции
Разбор кодовой базы по прикладному слою дал такую картину. Всё измерено, не оценено:
|
Что |
Сколько |
|---|---|
|
|
51 |
|
Строк конфигурации модели в одном методе |
~1 500 |
|
Файлов с цепочками жадной загрузки |
70 (40 + 30 вложенных) |
|
Файлов, вызывающих сохранение изменений |
40 |
|
Моделей ORM с навигационными свойствами |
54 |
|
SQL-представлений — сборка графов объектов в JSON |
5, из них используется 1, четыре — мёртвый код |
|
Обобщённый репозиторий + спецификации |
есть |
|
Файлов миграций |
3 |
А теперь вопрос, ради которого всё это считалось: что из перечисленного действительно требует ORM?
Ответ по коду: две партиционированные таблицы, пять представлений и три вызова сырого SQL. Всё остальное — обслуживание самой ORM.
Полторы тысячи строк описания модели и система миграций существуют, чтобы обслужить два партиционированных объекта и три запроса. Партиции и сырой SQL никакой ORM не требуют — они работают через прямой доступ к соединению.
Пять представлений, которые были костылём
Отдельно стоит история пяти SQL-представлений. Они собирают графы объектов в JSON — то есть делают на стороне базы работу, которую ORM не смогла сделать приемлемо. Четыре из пяти уже никем не используются: мёртвый код, который никто не решился удалить, потому что непонятно, кто на него смотрит.
Объектное хранилище собирает граф одним вызовом нативно. Представления удаляются вместе с моделями — не «переписываются», а перестают быть нужны.
Что означает исчезновение миграций — не для разработчика, для бизнеса
Вот это тот пункт, который в технических статьях проговаривают вскользь, а он самый дорогой.
Миграция — не артефакт в репозитории. Это операционное событие. Каждая миграция на живой системе означает:
-
окно релиза, согласованное с бизнесом;
-
план откатa и ответ на вопрос, что делать, если откат не сработал;
-
участие человека с правами на продовую базу;
-
три ноды, на которых порядок применения имеет значение;
-
запрет на параллельные изменения схемы в других ветках, пока эта не доехала.
В объектном хранилище схема выводится из типов: добавил свойство в класс — при следующей инициализации оно появилось. Ни файла миграции, ни окна, ни отката, ни человека с правами.
Это не «удобнее разработчику». Это исчезновение целого класса операционных событий из календаря компании — вместе с согласованиями, ночными окнами и переносами релизов из-за того, что миграция не прошла на второй ноде.
А теперь измеренный размер того, что уезжает на свалку
Я посчитал слой данных целиком, разделив написанное людьми и сгенерированное инструментом — смешивать их нельзя:
|
Что уходит |
Файлов |
Строк |
|---|---|---|
|
Слой данных: 55 моделей, справочники, перехватчики, сервисы справочников |
88 |
7 547 |
|
Провайдер базы: контекст на 2 049 строк, генератор ключей, тела миграций |
8 |
5 125 |
|
Написано людьми |
96 |
12 672 |
|
Сгенерировано инструментом: снимки модели данных |
4 |
17 407 |
|
Всего уезжает |
100 |
30 079 |
Тридцать тысяч строк. Сто файлов. Но самое интересное — в предпоследней строке.
Сгенерированные снимки модели — 17 407 строк в четырёх файлах, по четыре с лишним тысячи в каждом. Это описание схемы на момент каждой миграции плюс актуальный снимок. Инструмент создаёт их сам.
Эти файлы лежат в системе контроля версий. Они попадают в запросы на слияние. Они конфликтуют при слиянии ветвей — и разрешать конфликт в сгенерированном снимке на четыре тысячи строк не хочет никто. Их никто не читает, но все обязаны их коммитить.
Семнадцать тысяч строк, которых не писал человек и не читает человек, но которые версионируются, ревьюятся и конфликтуют как обычный код. Вот что физически означает «система миграций» в репозитории, помимо ночных окон на продакшене.
Плюс к ним 2 877 строк рукописных тел миграций — вот эти как раз писали руками, из них 2 748 в одном файле создания схемы.
При выводе схемы из типов не существует ни того, ни другого. Не «их становится меньше» — их нет.
Слой третий: интеграции схлопываются — но не сами по себе
И вот здесь причинно-следственная связь, которую важно не потерять. Интеграции упрощаются не потому, что их переписали лучше. Они упрощаются потому, что из них ушла ORM.
Интеграционных модулей четыре: преобразование данных из SAP, сохранение результата в базу, исходящий обмен по схеме outbox, хост-приложение.
Первый — чистая трансформация, к базе не обращается. Его ORM никогда не касалась, и он не изменится вовсе.
Вся масса кода сидит во втором. Что он делает сейчас, дословно по коду: поиск сущности по внешнему идентификатору, создание или обновление графа связанных объектов, ручная синхронизация коллекций, сохранение изменений. И так для каждого типа сущностей, приходящих из ERP, — а каждая из них тянет за собой от двух до четырёх связанных объектов.
Что от этого остаётся при объектном хранилище: сохранение объекта одним вызовом. Граф уезжает целиком.
|
Что делала интеграция |
Во что превращается |
|---|---|
|
Поиск по внешнему ключу → создание/обновление графа → сохранение изменений |
Запрос по внешнему ключу → сохранение объекта |
|
Ручная синхронизация вложенных коллекций |
Часть графа, отдельного кода нет |
|
Свой кэш справочников на словаре |
Встроенный поставщик списков с временем жизни |
|
Чтение внешнего идентификатора отдельным контекстом без отслеживания |
Обычный запрос |
Почему здесь код сокращается, а в модуле шины — вырос
Это важный разворот, и он объясняет обе цифры сразу.
В случае с шиной код вырос в 1,7 раза — потому что логика, спрятанная в Java-медиаторе, вылезла наружу и стала видимой. Объём вырос, скрытого не осталось.
В случае с ORM код сокращается — потому что здесь наружу ничего не вылезает. Здесь исчезает церемония: цепочки жадной загрузки, ручная сборка объектов из отдельных запросов, обобщённый репозиторий со спецификациями. Это не бизнес-логика, которую можно потерять. Это накладные расходы на общение с ORM.
Оценка автора разбора по трём самым нагруженным местам — помечаю как оценку, не измерение: процессор на 810 строк, где около 60% кода уходит на загрузку и ручную сборку графа, сжимается примерно до 300; другой на 495 строк с шестью отдельными запросами — примерно до 150; сервис фильтрации на 163 строки — примерно до 60.
Меньше строк здесь — не цель, а следствие. Цель — чтобы шесть запросов к базе на сборку одного объекта стали одним. Вот это уже про железо: меньше запросов на тот же ответ означает меньше нагрузки на те же ноды.
Слой четвёртый: идентичность на чужом стеке, и HTTP там, где нужен вызов в процессе
Теперь самое интересное с точки зрения архитектуры — и, пожалуй, самое недооценённое по деньгам.
Что есть сейчас: внешний сервер идентичности. Четыре отдельные базы данных, 227 служебных таблиц — против 53 таблиц, в которых лежит собственно бизнес системы. Соотношение примерно четыре к одному в пользу инфраструктуры аутентификации.
Точек взаимодействия шины с этим сервером в коде подтверждено девять: выдача токена, обновление токена, отзыв, разбор и проверка JWT, восстановление и сброс пароля, привязка пользователя, синхронизация групп, добавление в группу.
И вот деталь, ради которой стоит остановиться. Среди подтверждённых сценариев есть получение токена служебной учётной записи для двух каналов — мобильного и панели управления.
Служебная учётная запись — это машина-машине. Никакого пользователя, никакого браузера, никакого редиректа. Одна часть системы доказывает другой, что она — это она.
А путь этого запроса, подтверждённый архитектурной документацией, проходит через несколько слоёв: клиент → шина → сервер идентичности → бэкенд.
То есть за токеном, который нужен машине для разговора с машиной внутри одного контура, система ходит по HTTP через два промежуточных слоя. С сериализацией, с установлением TLS, с таймаутами, с ретраями, с диагностикой «а на каком из четырёх участков оно сейчас отвалилось».
Что меняется при своём сервере идентичности
В своём решении каждая точка входа зарегистрирована на внутреннем транспорте, работающем в процессе, без сети — синхронно, без копирования данных, между модулями одного рантайма.
Модуль, которому нужен токен служебной учётной записи, получает его вызовом метода. Ни HTTP-слушателя, ни рукопожатия TLS, ни JSON через петлевой интерфейс. Обмен идёт из маршрута прямо в обработчик и обратно, в том же потоке.
И принципиальное разделение, которое из этого следует:
|
Сценарий |
Нужен браузер |
Работает любым транспортом |
|---|---|---|
|
Служебная учётная запись (машина-машине) |
— |
✅ |
|
Переход на страницу входа |
✅ |
— |
|
Обмен кода на токен |
— |
✅ |
|
Обновление, отзыв, проверка токена, профиль |
— |
✅ |
|
Управление, каталог пользователей, аудит |
— |
✅ |
Браузер фундаментально нужен ровно двум взаимодействиям во всём стандарте: редирект на страницу авторизации и страница подтверждения для устройств без клавиатуры. Всё остальное транспортно-нейтрально.
Отсюда и главный вывод по слою. Сеть между своими сервисами появилась не потому, что она нужна протоколу, а потому что сервер идентичности был чужим, и разговаривать с ним можно было только по HTTP. Стек перестаёт быть чужим — сеть на этом участке исчезает как класс.
Плюс 227 служебных таблиц чужой схемы заменяются двумя дюжинами типизированных схем, которые выводятся из классов. Без миграций — по той же причине, что и в предыдущем разделе.
Система нарезана не только слоями — и это множитель ко всему сказанному
До сих пор я говорил про горизонтальные слои: фасад, интеграции, данные, идентичность. Но так система устроена только на схеме в презентации.
В реальности поверх горизонтальных слоёв идёт вертикальная нарезка: панель управления, мобильный канал, GPS-контур, геометрия, интеграция с SAP, исходящий обмен, справочники, расписание, выдача ключей. Пятнадцать точек входа модулей в кодовой базе.
И вот что важно: каждая вертикаль тащит через себя все горизонтальные слои. Свой фасад, свой слой доступа к данным, свои модели, своё подключение к контексту базы. Абстрактный контекст, провайдерный контекст, фабрика контекстов, сервис выбора провайдера, фабрика для инструментов проектирования — вся эта обвязка существует по одному разу на систему, но каждая вертикаль в неё вплетена.
Поэтому тридцать тысяч строк слоя данных — это не «один слой из четырёх». Это то, что режется поперёк всех пятнадцати вертикалей одновременно. Убирая ORM, вы не выпиливаете модуль — вы вынимаете прошивку, которой прошиты все модули насквозь.
Сложим то, что измерено, в одну картину — что уходит из репозитория:
|
Что |
Файлов |
Строк |
|---|---|---|
|
Артефакты шины на XML плюс описания сборки |
213 |
17 799 |
|
Java: форк коннектора и классовые медиаторы |
18 |
4 099 |
|
Слой данных и провайдер базы — написанное людьми |
96 |
12 672 |
|
Движок маршрутизации и контейнер модулей — не удаляются, а выносятся в зависимость |
227 |
38 646 |
|
Итого написанного кода покидает репозиторий |
554 |
≈ 73 200 |
|
Сверх того: сгенерированные артефакты миграций |
4 |
17 407 |
Против примерно 175 200 строк написанного людьми. Порядка сорока двух процентов кодовой базы перестаёт быть предметом сопровождения — а если считать вместе со сгенерированными артефактами, около сорока семи.
Оговорюсь про эти проценты, потому что легко подтасовать в любую сторону. Я специально держу написанное людьми и сгенерированное в разных строках: смешивать базы нельзя, иначе получается двойной счёт. И «сорок два процента» — не «мы удалили сорок два процента системы». Это доля кода, за который больше не отвечает эта команда в этом репозитории: часть удаляется по-настоящему, часть превращается во внешнюю зависимость.
Последнюю строку подчеркну отдельно, чтобы не выглядело подтасовкой: движок и контейнер никуда не исчезают как функциональность — они перестают быть вашим кодом и становятся зависимостью, которую вы подключаете пакетом или исходниками, как решите. Это ровно тот переход из «считаем» в «не считаем», о котором шла речь в начале статьи, — только теперь он виден внутри одного проекта, на до и после.
Что это даёт на скорости разработки
И вот здесь та статья расходов, которую в сметах не пишут, а платят всегда.
Когда у вас одна вертикаль требует правки, сколько компетенций надо позвать? В стеке «до»: XML маршрутизации, Java для того, что XML не выразил, Maven и упаковка артефактов, ORM с миграциями, схема чужого сервера идентичности. Даже если всё это умеет один человек — он переключается между пятью контекстами, а если не умеет, задача едет между людьми с потерями на каждом стыке.
В стеке «после» — C#. Один. Маршруты, логика, доступ к данным, конфигурация, тесты, отладка. Один язык, один конвейер, один отладчик, одна точка останова через всю цепочку от HTTP до базы.
Это меняет не только цену часа, но и скорость входа в задачу. Новую вертикаль можно писать с колёс: взял соседнюю как образец, повторил структуру, поменял содержание. Аналитики перед началом требуется меньше, потому что меньше решений «а на каком языке эту часть» и «а где это будет жить».
И отдельно — про то, о чём в статьях про корпоративные миграции пока не принято говорить, хотя это уже реальность рабочего дня. Однородный код на массовом языке с внятными паттернами сегодня пишется в паре с языковой моделью в разы быстрее, чем разнородный. Причины прозаические: массовый язык и типовые паттерны модель знает; повторяющаяся структура модулей означает, что образец лежит рядом; типизация и компилятор ловят ошибку сразу, а не в рантайме на третьей ноде. Каждое из этих свойств сокращает и время, и объём контекста, который надо держать в голове или скармливать инструменту.
Обратное тоже верно и проверяется на первой же попытке: synapse-XML с расширениями конкретной платформы и связями по именам артефактов — материал, на котором такая помощь работает заметно хуже. Не потому, что формат плохой, а потому, что он узкий и его в мире мало.
Цифру я здесь не назову — измерений у меня нет, а называть на глаз «в три раза быстрее» было бы именно тем, за что я критикую чужие статьи. Но направление считаю очевидным: один массовый язык с однородными паттернами дешевле в разработке, чем пять узких — и разрыв со временем растёт, а не сокращается.
Контейнер повзрослел: то, что раньше делали руками
Отдельно — про рантайм, в котором всё это живёт. За последние версии он добрал именно тот класс возможностей, который прямо конвертируется в человеко-часы эксплуатации.
Пробегусь по тому, что появилось, и сразу — какую ручную работу это отменяет.
Общий слой библиотек. Фреймворк и провайдеры больше не лежат в каталоге приложения — они в отдельном общем слое и загружаются оттуда при старте. Следствие: патч любой библиотеки — это подмена одного файла, без пересборки контейнера и без перевыпуска архивов. Обновление с патча на патч перестало быть релизом всего сразу. Если сборка испорчена или её нет — старт падает сразу с точным сообщением, какая именно и где, а не через сутки под нагрузкой в виде невнятной ошибки типов.
Плавный вывод ноды из работы. Ноду можно перевести в состояние, когда она доделывает текущее и не берёт новое, отдавая свои блокировки соседям, — и потом вернуть обратно. Раньше выбор был бинарным: либо перераспределить всё, либо жёстко выселить ноду. Промежуточного состояния для планового обновления по одной ноде не существовало.
Стоит вспомнить, что в первой части значилось среди реальных болей: ручные перезапуски после перестроения кластера. Вот это ровно про тот класс проблем.
Очередь недоставленного с повторным запуском. Обмен, упавший на отмеченной точке маршрута, теперь захватывается, просматривается и перезапускается — из панели или командой. Тот самый сценарий «покажи, что упало ночью, и перезапусти после починки», который до этого делался руками в брокере, если делался вообще.
Тут же честная оговорка из документации, а не из рекламы: семантика — не менее одного раза и вручную. Перезапущенный хвост маршрута может выполниться повторно, поэтому побочные эффекты обязаны быть идемпотентными. Это не система долговременного исполнения, и она таковой не притворяется. Захват при этом устроен как подписка: точку сохранения оставляют только маршруты, явно помеченные разработчиком, — механизм не влезает туда, где повторной доставкой уже управляет брокер или транзакция.
Ответы на вопросы «а что сейчас на этой ноде» без SSH. Появились точки, отдающие эффективную конфигурацию, с которой нода реально работает, — с замаскированными секретами; список фактически загруженных сборок с версиями и origin; запуск задачи по расписанию немедленно.
Каждый из этих пунктов раньше был походом инженера на ноду. Не катастрофа по отдельности — но именно из таких походов набегают человеко-часы эксплуатации, которые в смете никогда не видны, потому что их никто не заводит как задачу.
Так в чём выгода бизнесу
Соберу все четыре слоя в один ответ — по осям, которыми это меряет бизнес, а не разработчик.
Что не надо писать и что не надо носить. Движок маршрутизации, контейнер модулей с дашбордом и кластером, сервер идентичности, слой доступа к данным — всё это в проекте не пишется и в репозиторий не попадает. Библиотеки приходят пакетами, рантайм — образом или подписанным архивом. Своим кодом остаётся описание маршрутов, бизнес-правила и сущности. В разобранном модуле это выглядело так: на весь модуль один класс с собственной логикой, всё остальное — маршруты и конфигурация.
Железо. Один рантайм на ноду вместо двух, стоящих рядом. Один запрос к базе на сборку объекта вместо шести. Сеть между своими сервисами на участке идентичности исчезает целиком — вместо четырёх сетевых участков вызов метода.
Компетенции. Было: XML маршрутов, Java для того, что XML не выражает, Maven и паковка артефактов шины, ORM с миграциями, схема чужого сервера идентичности из 227 таблиц. Стало: один язык, один конвейер, один отладчик. И — что важнее — эта экономия наступает скачком, когда последний артефакт снят с последней ноды, а не пропорционально числу переехавших модулей.
Человеко-часы разработки. Там, где уходит церемония общения с ORM, код сокращается кратно. Там, где логика вылезает из спрятанных медиаторов, — растёт, и это правильный рост. Суммарно объём — не тот показатель, за которым тут стоит следить. Следить стоит за другим: около сорока двух процентов кодовой базы перестаёт быть предметом сопровождения — примерно 73 200 строк из 175 200, написанных людьми. Плюс отдельно 17 407 строк сгенерированных снимков модели, которые никто не читает, но все коммитят.
Скорость входа в задачу. Одна компетенция вместо пяти означает, что новую вертикаль пишут с колёс по образцу соседней, без предварительного решения «на каком языке эта часть» и «где она будет жить». Однородный код на массовом языке к тому же заметно лучше поддаётся разработке в паре с языковой моделью, чем узкий декларативный формат со связями по именам, — а это уже не про будущее, а про текущий рабочий день.
Человеко-часы эксплуатации. Исчезает класс операционных событий: миграции с окнами и откатами; ручные перезапуски после перестроения кластера; походы на ноду за ответом «на каких она настройках»; разбор упавшего ночью обмена вручную в брокере.
Деньги на том, что не поддаётся смете. Тихие отказы. Потребитель, молча перестающий читать очередь. Обмен, зависший на общем канале. Четыре мёртвых представления, которые никто не удаляет, потому что непонятно, кто на них смотрит. Ни одна из этих строк не попадает в оценку заранее — и каждая дороже той, что попадает.
Единственную цифру, которую я здесь не назову, — сколько это в рублях. Она зависит от ставки, размера команды, рынка труда на конкретные компетенции в конкретном регионе и от того, совмещены ли эти компетенции в одних руках. Модель с явными допущениями я строил в первой части для слоя шины; распространять её на все четыре слоя, пока три из них не прошли, значило бы продавать оценку как измерение. Измеренное здесь — объёмы кода, число таблиц, число точек взаимодействия и топология. Выводы из них — мои.
Честные границы
Три слоя из четырёх ещё не пройдены. Это главная оговорка, и я вынес её первой. Сделан один модуль на шине. Снос ORM — измеренный разбор кодовой базы плюс план пилота из трёх фаз, а не прошедшее время: сначала простые изолированные сущности, потом иерархия, потом сложный граф, и только по результатам — решение о полном удалении. Переход на свой сервер идентичности — выбранное целевое решение. Всё, что в этих разделах названо цифрой, — измерено в текущем коде; всё, что названо следствием, — вывод, который ещё предстоит проверить работой.
Один модуль — не вся система. Переехал контур, у которого граница была проведена изначально. Модули, завязанные на внутренние контуры, SAP и сервер идентичности, переезжают тяжелее, и экстраполировать эти цифры на них линейно нельзя.
Автор — архитектор и разработчик этого бэкенда, и он же автор стека, на который тот переезжает. Сказано во вступлении, повторю здесь: хранилище, движок маршрутизации, контейнер и сервер идентичности — мои. Это одновременно источник цифр, которых больше ни у кого нет, и причина читать выводы критически.
Сравнение с ORM неполное по одной оси. Часть преимуществ объектного хранилища на запросах — компиляция выражений в SQL, обход деревьев рекурсивным CTE, отслеживание изменений — относится к платной редакции. Свободная работает через JSON-факеты и медленнее. Если вы считаете этот переход для себя, редакцию надо выбрать до подсчёта, а не после.
Смена парадигмы хранения бьёт по тому, что вокруг базы. Данные лежат не в отдельных таблицах на сущность. Всё, что смотрит в схему снаружи приложения — отчёты, выгрузки, ETL, руки аналитика в pgAdmin, — потребует адаптации. Данные остаются в обычной реляционной базе и доступны сырым SQL, но привычные запросы «select из таблицы поездок» переписываются. Это реальная работа, и её нет в сметах выше.
Рост объёма кода — реальный. В 1,7 раза по сырым строкам, в 1,44 по строкам без пустот и комментариев. Часть объясняется дописанным и типизацией, но не вся: явный код в принципе многословнее декларативного описания. Если для вас критичен объём — это цена, и её надо знать.
У ESB остаются сильные стороны. Единая точка администрирования, коробочные адаптеры, понятная модель для команды, которая уже на ней работает. Если эта компетенция у вас есть и боли из первой части вас не касаются — переезд может не окупиться.
Собственный движок — не бесплатное решение. В первой части он стоил 1 940–3 400 часов. Эти цифры были потрачены раньше и в другом месте; команда, у которой такого движка нет, начинает не с того места, с которого начали здесь. Но именно к этой оценке относится оговорка про асимметрию счёта из начала статьи: она посчитана по правилам, по которым внутренности чужой платформы не считаются вовсе.
Про скорость разработки у меня нет измерений. Ни по часам на вертикаль до и после, ни по разработке в паре с языковой моделью. Направление я считаю очевидным и аргументирую его свойствами языков, а не замером. Если у вас есть цифры — они интереснее моих рассуждений.
Итог
Сравнение один к одному дало результат, обратный ожидаемому: кода стало в 1,7 раза больше (1 119 → 1 941 строка; без пустых строк и комментариев — в 1,44).
Но разложенный по слоям этот множитель говорит противоположное тому, что кажется. Декларация маршрута сжалась вчетверо — 861 строка XML против 212 строк на встроенном языке. Выросло другое: императивная логика, которая в шине жила отдельным Java-проектом (4 099 строк на весь проект, из них 2 429 — форк чужого коннектора), переехала в тот же файл рядом с маршрутом. Плюс новый код намеренно написан разряженно, под отладчик, и не воспользовался встроенными выражениями фреймворка — ужать его в разы было можно, XML же стоял на своём полу.
При разборе выяснилось, что рост складывается из трёх честных причин: примерно треть — дописанное, чего раньше не было; часть — типизированная конфигурация вместо строковой; остальное — логика, которая раньше пряталась в Java-медиаторах и выглядела короткой только потому, что жила в другом месте.
Что получили взамен: три языка схлопнулись в один, вся логика стала видна поиском по коду, отлаживается обычным отладчиком и покрывается обычными тестами. Своим кодом остался ровно один класс — тот, который не мог не остаться.
Меньше строк — не цель. Цель — чтобы поведение системы было в одном месте и на одном языке, а отказы были видны в момент отказа, а не по растущей очереди через сутки.
А по остальным трём слоям картина обратная
И вот что важно свести вместе, потому что цифры разошлись в разные стороны, и это не противоречие.
Там, где логика была спрятана — в Java-медиаторах за восемнадцатью строками XML, — код при переносе вырос. Скрытое стало видимым, объём это честно отразил.
Там, где логики не было вовсе, а была церемония — полторы тысячи строк описания модели ради двух партиционированных таблиц, семьдесят файлов с цепочками жадной загрузки, обобщённый репозиторий со спецификациями, пять представлений для сборки графов, из которых четыре мёртвых, — код сокращается. Терять там нечего.
Разница между этими двумя случаями и есть главный практический вывод обеих статей. Прежде чем считать экономию на строках, разберитесь, что перед вами: спрятанная логика или накладные расходы. В первом случае объём вырастет, и это нормально. Во втором — сократится, и это тоже не заслуга переписывания, а признак того, что раньше вы платили за общение с инструментом.
А самая крупная статья экономии не измеряется в строках ни в одном из четырёх слоёв. Это исчезновение операционных событий: миграций с окнами и откатами, ручных перезапусков после перестроения кластера, четырёх сетевых участков там, где машина спрашивает токен у машины внутри одного процесса, и второй области экспертизы, которую приходится держать в команде, пока в кластере остаётся хоть одна нода с чужим рантаймом.
Если вы проходили похожий путь — расскажите в комментариях, каким у вас получилось соотношение. Мне интересно ровно две вещи: действительно ли рост объёма при уходе с декларативного описания на явный код — закономерность, и сходятся ли ваши цифры по сокращению кода после снятия ORM с той оценкой, которую я дал выше. Свои оценки я готов пересматривать.
Исходники и релизы: github.com/redbase-app. Про хранилище redb: redb.ru. Первая часть — разбор стека «до» по строкам.
If this was useful — a ⭐ on GitHub helps others find it.
ссылка на оригинал статьи https://habr.com/ru/articles/1064692/