Разбираем систему управления транспортом — TMS. Не игрушечную и не с конференции: заказы приезжают из SAP, под них подбираются водитель и машина с учётом требований к транспорту и к точкам, рейс идёт по точкам с временными окнами, на точках — чек-листы, при выдаче и возврате машины составляются акты с фиксацией повреждений. Сверху — поток GPS-координат в партиционированные таблицы, отслеживание рейсов, мест и контрагентов, определение присутствия по WiFi. Плюс мобильное приложение водителя и публичный контур, где клиент видит, где его доставка.
Пятнадцать модулей, три ноды за балансировщиком, RabbitMQ и Kafka по три ноды, Redis, PostgreSQL, отдельный сервер идентичности.
Зачем её вообще стали писать. TMS покупали как услугу у внешних поставщиков, и риски выросли.
Это стоит развернуть, потому что решение принималось не по техническим мотивам. Когда ключевой процесс компании работает на внешнем сервисе, риск считается деньгами: простой, на который вы не влияете; сроки исправлений, которые определяет не ваш приоритет; зависимость от одного поставщика при смене условий. Пока риск невелик, покупать дешевле, чем строить. Когда он вырастает — арифметика переворачивается, и собственная разработка становится способом вернуть управляемость, а не способом сэкономить.
Систему забрали себе.
И вот главное, что стоит сказать сразу, потому что оно ломает привычную рамку разговора про техдолг. Этой системе год и четыре месяца. Первый коммит — апрель 2025-го, сейчас июль 2026-го, 931 коммит.
Это не легаси из нулевых, доставшееся по наследству вместе с автором, который давно уволился. Это молодой проект, написанный с нуля. Сто семьдесят пять тысяч строк бэкенда за шестнадцать месяцев — и в них уже накопился инфраструктурный слой, который приходится разбирать.
Про команду, потому что это влияет на чтение цифр. Бэкенд писался не в одиночку и не «как получится». В команде были:
-
системный аналитик — требования приходили разобранными, а не пересказом в переписке;
-
QA — сделанное проверял не автор кода;
-
проектный менеджер — полноценная классическая роль: бюджет, сроки, приоритеты, очерёдность работ, оценка рисков.
Веб-интерфейс делала отдельная команда, мобильное приложение — отдельный разработчик. Их работы в этих цифрах нет: считается бэкенд.
Это важно для дальнейшего в двух отношениях.
Первое: накладные расходы, которые я считаю ниже, — не следствие бардака в процессе. Проще всего объяснить сто пять тысяч строк инфраструктуры тем, что не было аналитики, тестирования и внятного порядка работ. Здесь это объяснение не работает: всё перечисленное было. Инфраструктурный слой набежал не вместо процесса, а вместе с ним.
Второе: все часы в таблицах — только разработка. Работа аналитика, QA и менеджера в них не входит ни одним часом, хотя без неё проект бы не выехал. То есть полная стоимость владения выше моих цифр, а не ниже.
Про такие системы не пишут статей. Не стартап, не хайлоад, не красивая архитектура — обычный крупный корпоративный бэкенд, содержание которого стало отдельной статьёй бюджета, и её мало кто раскладывает.
Я разобрал этот бэкенд по строкам. Не по рассказам, а по коду: посчитал артефакты, замерил объёмы, посмотрел, где что дублируется и что оказалось мёртвым.
Дальше — цифры и модель затрат на содержание такого стека. В конце — раздел про то, чего эти цифры не учитывают, потому что честная оценка без него не бывает.
Дисклеймер. Я архитектор и разработчик разбираемого бэкенда, а также автор экосистемы, на которую он опирается. Поэтому дальше — не разбор чужих решений, а собственная смета. И поэтому же измерения отделены от выводов, а цифры разложены по строкам: спорить нужно со строками, а не с моей добросовестностью.
Что обезличено, а что нет. Класс системы называю прямо — TMS с компонентой отслеживания в реальном времени: таких систем сотни, и по классу опознать нельзя. А вот названия системы, компании и конкретной ERP не будет нигде. Стек — WSO2 Micro Integrator, Entity Framework Core, PostgreSQL, .NET — тоже встречается у сотен организаций, так что речь о классе решений, а не о конкретном заказчике.
Цикл про redb и redb.Route. Свежие статьи — сверху:
Платёжная платформа на .NET: во что она обходится и что из этого можно не писать
redb 3.4.0: переигрываем упавшее, патчим фреймворк без пересборки и раздаём права
redb.Route — уходим от MassTransit, идём к Apache Camel: Kafka, Scatter-Gather и транзакции
Полный список — в профиле. Исходники: github.com/redbase-app.
Что за система
Коротко о масштабе, чтобы цифры дальше были к чему привязать. Считал по коду, вендорские библиотеки не включал.
|
Что |
Файлов |
Строк |
|---|---|---|
|
C# — прикладной слой, свой движок, контейнер |
851 |
121 466 |
|
SQL — модель, миграции, отчётность, справочники |
28 |
17 341 |
|
synapse-XML — артефакты WSO2 MI |
206 |
14 564 |
|
OpenAPI — контракты |
20 |
5 235 |
|
Razor — операторский дашборд контейнера и шаблонная обвязка хостов |
54 |
5 224 |
|
Java — коннектор к шине и классовые медиаторы |
18 |
4 099 |
|
Свои JS и CSS |
26 |
3 999 |
|
Maven — описания сборки модулей шины |
7 |
3 235 |
|
Итого написанного людьми |
1 210 |
≈ 175 200 |
|
Сверху — сгенерированные артефакты миграций |
4 |
17 407 |
|
Документация в репозиториях |
165 |
28 000 |
Две строки внизу требуют пояснения, потому что обычно их молча сваливают в общий котёл, а разница между ними принципиальная.
Сгенерированные артефакты миграций — 17 407 строк в четырёх файлах. Их не писал человек и не читает человек: это снимки модели данных, которые инструмент ORM создаёт сам. Но они лежат в системе контроля версий, попадают в запросы на слияние и конфликтуют при слиянии ветвей. В счёт написанного кода я их не включаю — честнее держать отдельной строкой. К ним ещё вернёмся, когда дойдём до цены миграций.
Описания сборки для шины — 3 235 строк на семь модулей, в среднем по 460 строк на модуль. Это не артефакты маршрутизации, а Maven-конфигурация: как упаковать модуль в устанавливаемый пакет. Для сравнения: описание сборки .NET-проекта в этой же системе — 30 строк. Пятнадцатикратная разница в файле, который не делает ничего, кроме объяснения, как собрать остальное.
Шесть языков описания одной системы, и почти у каждого свой конвейер сборки, свой способ отладки и свой цикл выкатки.
Важная оговорка о границах счёта: здесь только бэкенд. Ни веб-интерфейса, ни мобильных приложений в этих цифрах нет — их делают другие команды, это отдельные и совсем не маленькие системы. Единственный интерфейс, попавший в таблицу, — операторский дашборд контейнера модулей, то есть админка для своих.
Это сужение важно в обе стороны. С одной — оценки ниже, чем «стоимость системы» целиком: клиентские приложения тут не посчитаны ни одной строкой. С другой — всё, что дальше в статье называется инфраструктурными накладными расходами, набежало внутри одного бэкенда, без участия интерфейсов. Сто семьдесят пять тысяч строк — это цена серверной части.
Инфраструктура: три ноды за балансировщиком, RabbitMQ и Kafka по три ноды, Redis, PostgreSQL, отдельный сервер идентичности со своим набором баз.
Внутри — два интеграционных слоя одновременно: WSO2 Micro Integrator и собственный движок маршрутизации с контейнером модулей на 38 600 строк. К тому, зачем системе с WSO2 MI понадобился второй, вернёмся отдельно — это самое интересное место в разборе.
Сто семьдесят пять тысяч строк — это не «средняя корпоративная система». Это большой проект, причём только его серверная часть, и содержание такого объёма стоит соответственно.
Слой первый: WSO2 MI — интеграции на synapse-XML
Начнём со слоя, который в оценку обычно не попадает — его по привычке относят к конфигурации.
Отношение неверное. Synapse-XML — это код. Декларативный, узкоспециализированный, с ограниченной выразительностью — но код: в нём ветвления, обработка ошибок, трансформации, вызовы внешних сервисов и управление потоком сообщения. Разница с C# не в том, код это или не код, а в длине цикла обратной связи: связи между артефактами держатся на именах, выражения над сообщением вычисляются при выполнении, и заметная часть ошибок проявляется не на сборке, а в рантайме.
Отсюда и берётся недооценка: то, что выглядит как настройки, ведёт себя как программа — и ломается как программа, только позже и молча.
Интеграционный слой состоит из 195 XML-артефактов:
|
Тип артефакта |
Штук |
|---|---|
|
Endpoint |
102 |
|
Sequence |
54 |
|
Proxy-service |
10 |
|
Local-entry |
10 |
|
API |
6 |
|
Задача по расписанию |
4 |
|
Шаблон |
2 |
И Java, куда XML не дотянулся
Артефактами дело не заканчивается. Рядом лежит 4 099 строк Java в 18 файлах, и делятся они на две части.
Форк официального коннектора к Kafka — 2 429 строк в 12 файлах. И вот это отдельный сюжет, который стоит рассказать целиком, потому что он типичнее, чем кажется.
Коннектор к Kafka у вендора есть. Он лежит в его же репозитории, ставится штатно и в комплекте присутствует. Проблема в том, что он не заработал как нужно: в нём обнаружились ошибки.
Дальше — обычная развилка, знакомая всем, кто живёт на чужой платформе: ждать исправления от вендора или чинить самому. Ждать было нельзя, поэтому проект форкнул коннектор и починил его у себя.
Цена такого решения выглядит низкой в моменте: разобраться в чужом коде и внести правки дешевле, чем писать транспорт с нуля. Но она приходит потом и уже не уходит.
Форк снимает вас с траектории обновлений. Версия в проекте зафиксирована на снапшоте — то есть на неопубликованном состоянии вендорской ветки. Каждое обновление платформы теперь означает: взять новую версию коннектора, заново перенести свои правки, проверить, что ничего не разъехалось. Либо не обновляться и копить расхождение.
И вы теперь сопровождаете Java-код, который не проектировали. Почти две с половиной тысячи строк чужой архитектуры, в которой надо разбираться заново каждый раз, когда что-то ломается.
Это не про то, что вендор плохой. Это про свойство модели: когда платформа чужая, любой её дефект превращается в вашу постоянную статью расходов — либо вы ждёте, либо форкаете и платите за форк вечно.
Классовые медиаторы — 1 670 строк. Это Java-классы, которые подключаются прямо в XML-последовательности там, где выразительности XML не хватило. И вот их список говорит о границах языка лучше любого рассуждения:
|
Медиатор |
Строк |
Что делает |
|---|---|---|
|
Масштабирование изображений |
433 |
Ресайз и управление превью — обработка бинарных данных |
|
Кэш OAuth-токенов |
402 |
Состояние, переживающее отдельный запрос |
|
Пул уникальных ключей |
401 |
Потокобезопасная выдача идентификаторов блоками с автопополнением |
|
Диагностика пула потоков |
250 |
Интроспекция рантайма |
|
Валидация токена |
183 |
Собственная логика безопасности |
Каждая строка — место, где разработчик упёрся в «XML так не умеет» и переключился на другой язык. Работа с байтами, состояние между запросами, конкурентность, интроспекция рантайма, нетривиальная безопасность — всё это за пределами того, для чего декларативный формат маршрутизации задумывался.
Отдельно любопытен третий пункт. Пул идентификаторов с блочной выдачей и автопополнением — это классический приём, когда обращаться к базе за каждым ключом слишком дорого. Здесь его пришлось реализовать Java-медиатором внутри интеграционной шины. Тот же самый механизм потом появится в другом месте этой истории — но об этом во второй статье.
Мелкая деталь, показательная сама по себе: масштабирование изображений присутствует в двух модулях — рабочая реализация на 433 строки и однострочная заглушка с тем же именем в другом. Тот же сюжет, что с мёртвыми SQL-view ниже: попробовали, перенесли, старое не убрали, потому что доказать безопасность удаления в системе с логикой в конфигах — отдельное расследование.
Итого интеграционный слой описан на трёх языках сразу: XML для маршрутов, Java для того, что XML не выражает, и конфигурационные файлы для настроек. Плюс .NET на прикладной стороне.
Выборка из базы — отдельная история
Ещё одно ограничение, о котором не пишут в обзорах, а узнают в проекте.
Штатный способ обратиться к базе прямо из последовательности — медиатор DBLookup. И у него есть ограничение, которое документация вендора называет прямым текстом: он не возвращает несколько строк. Одна запись — и всё.
Официальная рекомендация на этот случай — завести отдельный сервис данных: свой тип артефакта, со своими SQL-запросами, описанием входных параметров и маппингом выходных полей, — и вызывать его из последовательности отдельным медиатором. То есть ещё один вид артефактов, ещё один слой описания и ещё одна сущность в развёртывании.
Либо — второй путь, который на практике выбирают чаще: заставить базу отдавать не строки, а готовый JSON или XML. Запрос сам сворачивает выборку в документ, а шина получает то, с чем умеет работать.
В этой системе видно оба следа. В SQL-скриптах 17 мест с агрегацией результата в JSON, и одно из представлений так и называется — «джейсон-вью для водителей».
Обратите внимание, что здесь произошло: логика сборки ответа уехала в базу данных. Не потому что там ей место, а потому что ни один из потребителей выше не готов принять обычную выборку в нужном виде.
И самое любопытное — давление идёт с обеих сторон. По внутренней оценке той же системы, пять SQL-представлений на стороне приложения были заведены как «костыли для сборки графов в JSON», уже под ORM. То есть и шина, и ORM независимо друг от друга вытолкнули одну и ту же работу в базу.
Цена такого решения приходит позже. Логика в SQL не покрывается тестами приложения, не видна в поиске по коду, не рефакторится вместе с моделью и требует человека, который одинаково хорошо читает и предметную область, и диалект СУБД. А когда представление окажется ненужным — про четыре из пяти это уже выяснилось — удалить его будет страшно, потому что непонятно, кто на него ссылается.
Сразу оговорка: WSO2 MI бесплатный
Чтобы не было недопонимания. Сама платформа распространяется под открытой лицензией, скачивается свободно и в лицензионных платежах не участвует. Вендор зарабатывает на подписке поддержки, а не на праве использования.
Поэтому дальше речь не про лицензии. Ноль рублей в этой строке.
Речь про то, что бесплатное скачивание и дешёвое владение — разные вещи. Реальная цена здесь складывается из другого: из чужого стека внутри вашей организации, из языка описания без типов и отладчика, из собственного коннектора, когда нужного нет в комплекте, и из того, что мажорные обновления платформы приходится проверять на всех артефактах.
Это и есть предмет разбора: бесплатная не значит дешёвая, и разница видна только когда её посчитаешь.
Почему это дороже, чем выглядит
Разберём подробнее, где именно копится цена.
Нет проверки типов. Опечатка в имени свойства, несовпадение типа, обращение к несуществующему полю — всё это выясняется при выполнении, на стенде, а иногда и в проде.
Связи держатся на именах. Общая логика выносится в отдельные последовательности и подключается по имени — то есть по строке. Переименовал — сломал вызывающих, и узнаешь об этом в рантайме. Автоматического переименования, как в IDE для обычного кода, здесь нет: переименование — это поиск по всем артефактам глазами.
Отдельно оговорюсь, потому что это частое заблуждение — отладчик у WSO2 MI есть. В Integration Studio можно ставить точки останова на медиаторах и смотреть контекст сообщения. Так что «отлаживается только логами» — неправда, и я в первой редакции этой статьи написал именно так, пока меня не поправили. Инструменты есть; дороже здесь другое.
Дороже — то, что заметная часть ошибок ловится не на сборке, а при выполнении. Компилятор в этом языке не встанет между вами и опечаткой, а рефакторинг не поддержан на уровне инструментов. Поэтому основные расходы приходятся не на написание артефакта, а на его последующие изменения.
Плюс единица поставки здесь — не файл, а весь модуль: проекты собираются Maven’ом в Carbon Application, .car-архив, и в шину едет он целиком. В репозитории это видно по <packaging>car</packaging> в шести сборочных файлах.
Оценка
Модель с явными допущениями. Правьте ставки под свою реальность — таблица разложена так, чтобы менять построчно.
|
Что |
Штук |
Часов за штуку |
Итого |
|---|---|---|---|
|
Endpoint (простые, обёртка над адресом) |
102 |
0,5–1 |
51–102 |
|
Sequence (логика, трансформации, условия) |
54 |
4–8 |
216–432 |
|
Proxy-service (полноценный сервис) |
10 |
8–16 |
80–160 |
|
API (маршрутизация + описание) |
6 |
8–16 |
48–96 |
|
Задача по расписанию |
4 |
3–6 |
12–24 |
|
Local-entry, шаблоны |
12 |
1–2 |
12–24 |
|
Форк коннектора Kafka: разбор чужого кода, починка, доработка |
1 |
60–120 |
60–120 |
|
Классовые медиаторы на Java (1 670 строк) |
5 |
25–50 |
125–250 |
|
Развёртывание и настройка шины, кластер, обучение команды |
— |
300–500 |
300–500 |
|
Итого разово |
|
|
904–1 708 |
Оговорка к таблице: часы за штуку — это написание плюс отладка, а не только набор текста. Для sequence разброс 4–8 часов взят потому, что простая трансформация делается за час, а sequence с ветвлением, обработкой ошибок и вызовом внешнего сервиса легко съедает день.
Слой второй: EF Core — данные через ORM
Здесь картина, знакомая всем, кто вёл проект на Entity Framework. С поправкой, о которой стоит помнить всю эту главу: всё описанное ниже накопилось за шестнадцать месяцев, а не за годы.
|
Что |
Объём |
|---|---|
|
|
~1 500 строк |
|
|
51 |
|
Модели с navigation properties |
54 файла |
|
Файлов с |
40 |
|
Файлов с |
30 |
|
Файлов с |
40 |
|
SQL-view для сборки графов |
5, из них 4 — мёртвый код |
|
Generic Repository + Specifications |
отдельный слой |
Две вещи здесь стоит разглядеть отдельно.
Пять view, из которых четыре мертвы
Это не небрежность. Это диагноз.
View появились потому, что собирать граф объектов через Include оказалось дорого, и логику сборки вынесли в базу — пусть PostgreSQL сам соберёт JSON. Каждая view — это обходной манёвр вокруг ограничения ORM.
А четыре из пяти оказались мёртвыми, потому что подход перепробовали несколько раз, каждый раз чуть иначе, и старые попытки никто не удалил. Их не удалили не из лени: чтобы уверенно удалить view, надо доказать, что на неё никто не ссылается, а в системе, где часть логики живёт в XML-артефактах, это отдельное расследование.
Процессоры, где большая часть кода — не бизнес-логика
По внутренней оценке той же системы, наиболее тяжёлые обработчики выглядят так:
|
Обработчик |
Объём |
Из них на загрузку и ручную сборку графа |
|---|---|---|
|
Получение основной сущности по идентификатору |
810 строк |
~60% |
|
Получение связанного списка |
495 строк |
6 отдельных запросов + сборка |
|
Сервис фильтрации |
163 строки |
|
Полторы тысячи строк в трёх файлах, и больше половины из них — не бизнес-правила, а транспортировка данных из базы в память и обратно.
Оценка
|
Что |
Часов |
|---|---|
|
|
150–250 |
|
54 модели с navigation properties |
110–220 |
|
Generic Repository + Specifications |
80–150 |
|
Пять SQL-view с моделями (включая четыре, которые окажутся мёртвыми) |
40–80 |
|
Настройка миграций, скрипты по контурам |
60–120 |
|
Итого разово |
440–820 |
Слой третий: то, что написали сами
А теперь самое интересное в этой системе, и то, ради чего её вообще стоит разбирать.
Параллельно с WSO2 MI в ней работал собственный интеграционный движок. Не обвязка и не хелперы — полноценный фреймворк маршрутизации в духе Apache Camel, написанный под .NET.
|
Компонент |
Объём |
|---|---|
|
Движок маршрутизации |
173 файла, 27 529 строк |
|
Контейнер модулей с дашбордом и REST API |
54 файла, 11 117 строк |
|
Итого |
≈ 38 600 строк инфраструктурного кода |
Что внутри движка:
-
Восемь транспортов — Kafka, RabbitMQ, Redis, Cron, Timer, SEDA, Direct, Validator, каждый со своими потребителем, производителем и точкой подключения;
-
43 файла абстракций —
IComponent,IConsumer,IProducer,IEndpoint,IProducerTemplate: дословный словарь Camel; -
41 файл языка выражений — выражения и предикаты, компилируемые, а не интерпретируемые;
-
19 файлов определений маршрутов — то есть свой DSL;
-
16 процессоров и 4 файла транзакционности.
А контейнер — это не «хост, который запускает модули». По инвентарю в нём:
-
загрузка модулей через единую точку входа, реестр модулей и фабрику контекстов;
-
жизненный цикл с горячей перезаменой: остановить контекст, удалить, пересоздать, зарегистрировать маршруты заново, запустить — без остановки процесса;
-
состояния модуля — обнаружен, загружен, инициализирован, ошибка;
-
фильтр состава с шаблонами включения и исключения: можно поднять ноду только с частью модулей;
-
кластеризация и планировщик с общим хранилищем;
-
веб-панель на 14 экранов — обзор, модули, контексты, эндпоинты, API, мониторинг, планировщик, логи, база, хранилища, процессоры, система, пользователи, роли;
-
сбор метрик памяти и рантайма.
Это не заготовка и не эксперимент. Это работающий продукт внутри проекта.
Зачем системе с WSO2 MI понадобился второй движок
Ответ прозаичен и знаком всем, кто работал с корпоративными интеграционными платформами.
Готовая шина хорошо делает то, что задумано её авторами, и плохо — всё остальное. Нужен транспорт, который в комплекте сломан, — форкаете и чините сами. Нужна логика сложнее ветвления — уносите её в Java-классы. Нужна выборка из базы в несколько строк — заводите отдельный сервис данных или заставляете базу отдавать JSON. Нужен обмен запрос-ответ под нагрузкой — разбираетесь, почему зависает общий канал.
Каждый обход по отдельности выглядит приемлемо. Сложите их вместе — и окажется, что вы уже написали половину своего движка, только размазанного по чужим точкам расширения.
В какой-то момент это становится очевидным, и вопрос переворачивается: не «зачем писать своё», а «сколько ещё мы будем доплачивать за чужое».
Оценка
Фреймворк пишется медленнее прикладного кода: абстракции, крайние случаи, конкурентность, поведение под отказами. Ставки ниже это учитывают.
|
Что |
Часов |
|---|---|
|
Восемь транспортов (потребитель, производитель, точка подключения, повторы, отказы) |
640–1 200 |
|
Движок выражений и предикатов, компилируемый |
250–400 |
|
DSL определений маршрутов и процессоры |
300–500 |
|
Ядро и абстракции |
250–400 |
|
Транзакционность поверх транспортов |
100–200 |
|
Контейнер модулей: загрузка, жизненный цикл, веб-панель, REST |
400–700 |
|
Итого разово |
1 940–3 400 |
Это 1,2–2 человеко-года, и это самая дорогая строка во всей смете.
Что контейнер модулей отменил
Есть в этой системе четыре проекта с суффиксом .App, и они наглядно показывают, зачем нужен контейнер.
Изначально каждый модуль был отдельным приложением: свой процесс, свой хост, своя выкатка. Мобильный API — приложение. Слой доступа к данным — приложение. Интеграция — приложение. GPS-контур — приложение.
Что внутри такого хоста, на примере одного из них:
|
Файл |
Строк |
Что делает |
|---|---|---|
|
|
158 |
Точка входа, сборка контейнера зависимостей |
|
|
88 |
Фабрика контекста БД |
|
|
54 |
Фоновая служба |
|
|
31 |
Поднять контекст маршрутов |
|
|
54 |
Страницы из шаблона проекта |
Ни одной строки бизнес-логики. Это церемония хостинга — код, который существует исключительно потому, что модулю нужен собственный процесс.
Четыре хоста, 741 строка на всех. И заметьте деталь: в каждом лежит страница «Политика конфиденциальности» из стандартного шаблона ASP.NET, которую никто никогда не открывал. Она приехала при создании проекта и осталась — потому что удалять её никто не считал своей задачей.
Когда появился контейнер модулей, все четыре стали не нужны: модули загружаются в один процесс, их маршрутные контексты поднимает контейнер, управление одно на всех вместо четырёх приложений.
Четыре единицы развёртывания схлопнулись в одну, и цена этого схлопывания — удалить 741 строку обвязки. Сами модули остались как были: мобильный API — 2 855 строк, слой данных — 2 074, GPS — 556. Изменилось не то, что они делают, а то, где они живут.
Вот это и есть аргумент за контейнер, который в презентациях звучит абстрактно, а в цифрах выглядит просто: четыре процесса вместо одного стоили 741 строку и четыре отдельные выкатки.
Но именно она себя оправдала
Разница между этой строкой и всеми предыдущими принципиальная, и её стоит проговорить прямо.
Покупная шина и ORM обошлись дешевле на входе — и оба слоя пришлось убрать. Собственный движок обошёлся дороже всех — и он единственный, что пережил переход. Больше того, он лёг в основу продукта, который сейчас развивается отдельно от системы, для которой писался.
Это не аргумент «пишите всё сами» — так можно и разориться. Это аргумент про то, какие именно решения окупаются: те, что закрывают вашу конкретную боль и остаются под вашим контролем.
Слой четвёртый: WSO2 IS — 227 таблиц до первой строки бизнес-логики
Отдельная строка, которую в сметах не видно никогда. И здесь самая наглядная цифра всего разбора.
За аутентификацию в системе отвечает WSO2 Identity Server. Он приносит с собой четыре собственные базы данных, которые надо создать, наполнить, версионировать, бэкапить и обновлять вместе с продуктом.
Я посчитал таблицы в развёрточных скриптах:
|
База |
Таблиц |
Размер скрипта |
|---|---|---|
|
|
139 |
92 КБ |
|
|
60 |
44 КБ |
|
|
23 |
16 КБ |
|
|
5 |
4 КБ |
|
Итого служебных |
227 |
156 КБ |
Теперь сравните с бизнес-моделью самой системы — той, ради которой всё затевалось:
|
|
Таблиц |
|---|---|
|
Служебные таблицы сервера идентичности |
227 |
|
Бизнес-модель: рейсы, точки, заказы, транспорт, справочники |
53 |
Соотношение четыре к одному. На каждую таблицу с предметными данными приходится четыре таблицы чужой инфраструктуры — и всё это разворачивается, обслуживается и бэкапится до того, как появится первая бизнес-сущность.
Оговорюсь сразу: эти 227 таблиц — штатная схема продукта, её никто не писал. Она приезжает при установке. Но развернуть, версионировать, бэкапить и обновлять её приходится вам, на всех контурах.
Из чего система реально пользуется
Прошёлся по артефактам и посмотрел, какие эндпоинты сервера дёргаются:
|
Эндпоинт |
Вызовов |
|---|---|
|
|
7 |
|
|
2 |
|
|
2 |
|
|
1 |
Весь используемый контракт — четыре эндпоинта: выдать токен, отозвать токен, управлять пользователями, управлять группами.
Это не претензия к продукту. WSO2 IS умеет несопоставимо больше — федерацию, SAML, адаптивную аутентификацию, десятки протоколов. Он рассчитан на организацию, которой всё это нужно. Если вам нужны четыре эндпоинта, вы всё равно разворачиваете и обслуживаете продукт целиком: вы платите за его полноту, а не за свою потребность.
Но главное не в этом
Сервер идентичности выдаёт и проверяет токены. Проблема начинается там, где это упирается в нагрузку.
Смотрите, какой поток идёт через систему. Координаты водителей приезжают каждую секунду — и поштучно, и пачками. Одновременно на линии около пятисот водителей и двести-триста поездок. Каждое такое сообщение приходит с токеном, и каждое по-хорошему надо авторизовать.
Штатная схема — спросить сервер идентичности — на таком потоке не работает: он превращается в узкое место всего конвейера.
Решение, которое пришлось построить: кэш токенов Java-медиатором прямо внутри шины. Первую проверку делает сервер идентичности — он же токен и выдал. Если токен признан годным, он кладётся в кэш, и дальше проверка идёт на стороне WSO2 MI, без обращения к серверу.
Четыреста строк Java, которых не было бы, если бы поток был меньше.
И вот здесь важное для оценки: это не задача интеграционной шины и не задача сервера идентичности. Ни один из них такого не предусматривает. Это то, что достраивают своими руками, когда реальная нагрузка встречается с архитектурой из документации.
|
Что |
Часов |
|---|---|
|
Развёртывание сервера идентичности, четыре базы, интеграция |
200–350 |
|
Настройка кластера: три ноды, балансировщик, тройки брокеров, Redis |
150–300 |
|
Конвейеры сборки для двух стеков — .NET и Java/Maven |
100–200 |
|
Итого разово |
450–850 |
Ежегодное содержание
Разовые затраты — половина истории. Вторая половина приходит каждый год.
Два стека вместо одного
Прикладной слой на .NET, интеграционный на Java с Maven. Это два конвейера сборки, два набора зависимостей, два цикла обновлений и две области компетенции внутри одной команды.
Практически это означает, что задача «поправить поведение интеграции» требует человека, который умеет и то, и другое, — либо передачи между двумя людьми с потерями на стыке.
Отправитель и потребитель на одном соединении — взаимная блокировка
Вот настоящая боль, а не гипотетическая.
Механика такая. Обмен по схеме запрос-ответ означает, что отправитель, послав сообщение, ждёт ответа. Если в этот момент на том же соединении RabbitMQ висит ещё и потребитель, ожидание занимает общий канал — и всё встаёт. Обмен не идёт, соединение зависает.
Внешне это выглядит иначе и хуже: потребители просто молча перестают читать очередь. Не падают, не пишут в лог ошибку, не поднимают алерт. Обнаруживается по последствиям — в очереди копятся сообщения.
Лечится разделением соединений: отдельные фабрики на отправку и на приём, с одинаковыми параметрами, но объявленные раздельно. В конфигурации системы это видно прямо — для каждой фабрики заведены отдельные блоки отправителя и слушателя с идентичными настройками. Дублирование, существующее исключительно чтобы обойти поведение платформы.
Стоит осознать класс проблемы. Упавший сервис виден сразу: ошибка, алерт, перезапуск. Зависший на общем канале обмен не виден никак, пока кто-то не заметит растущую очередь или не пожалуется пользователь. И диагностика — это не чтение стектрейса, а раскопки в поведении чужого рантайма: почему потребитель жив, соединение открыто, а сообщения не разбираются.
Цена не в часах на исправление — оно в итоге уместилось в конфиг. Цена во времени на обнаружение и локализацию, и в том, что до этого момента система молча работала неправильно.
И второе: RPC-очереди не переживают перестроение кластера
Тот же класс проблем, но злее.
Часть обмена между шиной и прикладными нодами идёт по схеме запрос-ответ через RabbitMQ. Такая схема использует временные очереди для ответов — они создаются под конкретного клиента и живут, пока живёт соединение.
Когда прикладные ноды перестраиваются — обновление, перезапуск, переназначение модулей — обмен начинает сыпаться. Под большой нагрузкой особенно.
Причём нельзя сказать, что платформа этого не предусматривает. Механизмы восстановления там есть, и они включены: автоматическое восстановление соединения, восстановление топологии, интервал переподключения, счётчик повторов на фабрике. Всё это настроено.
Но работает через раз. И лечение в итоге — перезапуск ноды шины.
Конфиг как дневник борьбы
Лучшее свидетельство того, чего это стоило, — сам конфигурационный файл. Он не выглядит как настройки из документации, он выглядит как протокол расследования. Там есть:
-
пометка, что проверку соединения при возврате в пул пришлось отключить, чтобы не ловить
NullPointerException— с комментарием «критично»; -
три параметра времени жизни соединений и каналов, закомментированных с пометкой «слишком дорого»;
-
интервал восстановления, уменьшенный вдвое, с записью прежнего значения рядом;
-
таймауты запрос-ответа, поднятые с 90 до 120 секунд «для времени восстановления» — тоже с сохранённым прежним значением;
-
отдельные фабрики на отправку и на приём с идентичными параметрами — то самое лечение тихо умирающих потребителей.
Каждая строка с комментарием — это чей-то потраченный день. Обычно несколько.
Вдумайтесь в связку: штатное событие на одной стороне (прикладные ноды переехали) требует ручного вмешательства на другой (перезапустить интеграционную платформу). Два продукта, которые ничего не знают друг о друге, и стык между ними не выдерживает того, что для распределённой системы является нормой жизни, а не аварией.
Это и есть настоящая стоимость интеграции разнородных платформ — не строки кода, а такие стыки: они обнаруживаются только под нагрузкой и только в проде, и о них нет ни строчки в документации ни одного из продуктов.
У схемы данных два источника правды
Схема живёт одновременно в двух местах: миграции ORM и отдельные SQL-скрипты, которые применяются руками. Оба источника подтверждены, а канонический порядок их применения во внутренней документации помечен как «требует уточнения у владельцев базы».
Это классическая развилка, в которую приезжает любой проект, где ORM не покрывает всё: партиции, представления, индексы под конкретный план запроса, права — всё это делается скриптом мимо миграций. И дальше два потока изменений идут параллельно, а знание об их правильной последовательности живёт в головах.
Следствие видно невооружённым глазом. Миграционные скрипты лежат отдельно для трёх окружений; я сверил их хеши: test и prod идентичны, dev разошёлся.
Это не чья-то ошибка, это свойство подхода. Как только применение схемы становится ручной операцией, расхождение контуров — вопрос времени, а не аккуратности.
Оценка
|
Что |
Часов в год |
|---|---|
|
Поддержка и правка 195 XML-артефактов (без типизации и отладчика) |
250–450 |
|
Обновления интеграционной платформы и проверка совместимости |
100–200 |
|
Миграции базы, три контура, устранение расхождений |
150–300 |
|
Поддержка форка коннектора: перенос правок при обновлениях вендора |
60–120 |
|
Обслуживание четырёх служебных баз |
60–120 |
|
Двойные компетенции: онбординг и передача задач между стеками |
150–250 |
|
Итого в год |
750–1 400 |
Сводка
Разовые затраты на построение такого слоя:
|
Слой |
Часов |
|---|---|
|
Интеграции на WSO2 MI (synapse-XML + Java) |
904–1 708 |
|
Данные через ORM |
440–820 |
|
Собственный интеграционный движок и контейнер |
1 940–3 400 |
|
Инфраструктура и вход |
450–850 |
|
Итого |
3 734–6 778 |
Это 2,2–4 человеко-года — до единой строки бизнес-логики.
Ежегодно: 750–1 400 часов, то есть 0,45–0,85 постоянной ставки, только на то, чтобы слой продолжал работать.
По ставке 4 000 ₽/час это 14,9–27,1 млн ₽ разово и 3–5,6 млн ₽ ежегодно.
И обратите внимание на распределение внутри разовой суммы. Больше половины приходится на слой, который написали сами — и это единственный слой, который пережил последующий переход. Всё, что было взято готовым, обошлось дешевле на входе и было демонтировано.
Половина кодовой базы — не бизнес-логика
Ещё один срез, который стоит посчитать отдельно. Из ста семидесяти пяти тысяч строк, написанных людьми:
|
Что это |
Строк |
|---|---|
|
Собственный движок маршрутизации и контейнер модулей |
38 646 |
|
SQL: модель, миграции, отчётность, справочники |
17 341 |
|
Артефакты WSO2 MI на XML плюс описания сборки |
17 799 |
|
Контракты, дашборд, свои JS и CSS |
14 458 |
|
Конфигурация ORM, модели, репозитории, обходные представления |
12 672 |
|
Java: форк коннектора и классовые медиаторы |
4 099 |
|
Инфраструктура, итого |
≈ 105 000 |
|
Прикладной код: правила, расчёты, обработка, маршруты модулей |
≈ 70 000 |
Правила, расчёты, валидация, обработка — то, за что системе платят, — составляют около сорока процентов написанного. Остальное это транспорт данных, конфигурация, обвязка и обходные манёвры вокруг ограничений инструментов.
Соотношение примерно три к двум не в пользу прикладного кода: на каждые две строки, делающие дело, приходится три строки, обеспечивающие возможность его делать. Мой опыт говорит, что это не аномалия, а норма для корпоративных систем на готовой интеграционной платформе, — и именно эти три и есть предмет разговора.
И напомню про границы счёта: это соотношение внутри одного бэкенда. Веб-интерфейс и мобильные приложения делают другие команды, в цифрах их нет.
Ставка здесь — полная стоимость часа для компании: оклад, налоги, взносы, рабочее место, управление, отпуска и простой. Не сумма в оффере — её обычно и подставляют по ошибке, занижая результат в два-три раза. Четыре тысячи — осознанно нижняя граница; подставьте свою, часы от неё не зависят.
Чего эти цифры не учитывают
Раздел, без которого оценка была бы нечестной.
Это модель, а не замер. Никто не вёл параллельный учёт по каждому артефакту. Часы за штуку — экспертная оценка, и каждую строку можно оспорить отдельно. Именно поэтому таблицы разложены построчно, а не даны итогом.
Часть работы всё равно пришлось бы сделать. Бизнес-логика — правила, расчёты, валидация — не зависит от того, на каком стеке она написана. В цифрах выше её нет и не должно быть: считается только инфраструктурный слой.
Здесь только часы разработки. Аналитика, тестирование и управление проектом в таблицы не входят, хотя в команде были и системный аналитик, и QA, и менеджер. Полная стоимость владения выше моих цифр — насколько, зависит от того, как у вас распределены роли, поэтому множитель я не выдумываю.
У ESB есть свои сильные стороны. Готовая шина даёт коробочные адаптеры к тому, что вы иначе писали бы руками, единообразную точку администрирования и понятную модель для команды, которая на ней уже работала. Если у вас в штате есть эта компетенция, часть моих оценок для вас завышена.
Организация могла бы часть закупить. Тогда экономия конвертируется в лицензии и в отсутствие собственного развития, а не в человеко-часы.
И главное: система работает. Она держит прод, обслуживает интеграцию с SAP и мобильные клиенты — и, напомню, ей год и четыре месяца. Команда за шестнадцать месяцев подняла с нуля то, что раньше покупали как услугу у внешних поставщиков. Это не разбор провала, а разбор цены, которую платят за конкретный набор архитектурных решений. Цена может быть оправданной; вопрос в том, знаете ли вы её.
Что дальше
Всё, что описано выше, — состояние «до». Система из него уходит: интеграционная шина, сервер идентичности и ORM убираются целиком, а собственный движок и контейнер модулей остаются и становятся основой.
Три слоя из четырёх демонтируются. Остаётся тот, что обошёлся дороже всех.
Вторая часть — про этот переход. Там будет разбор конкретной пары модулей один к одному: тот же контур, та же задача, слева артефакты шины с Java-медиатором, справа переписанный модуль. Начали, кстати, с изолированной зоны — публичного контура, у которого своя база, свой брокер и нет обратных соединений внутрь компании. Граница там уже была проведена архитектурно, поэтому цена ошибки при замене оказалась минимальной.
Главный вывод из сравнения получился неожиданным: кода стало почти вдвое больше. Почему это правильный результат — в следующей статье.
Если у вас похожий стек и другие цифры — напишите в комментариях свои. Мои оценки я готов пересматривать: интереснее сойтись на реалистичной модели, чем защищать первую версию.
Исходники и релизы: github.com/redbase-app. Про хранилище redb: redb.ru. Прошлые статьи цикла — в профиле.
If this was useful — a ⭐ on GitHub helps others find it.
ссылка на оригинал статьи https://habr.com/ru/articles/1064688/