Вступление
АСУДД автоматизированная система управления дорожным движением это отнюдь не новое в мире, но всё ещё недостаточно широко используемое в России программно-аппаратное решение. Кто-то может подумать, что система управления, да ещё и автоматизированная это что-то запредельно сложное и невообразимое, однако, на практике, данная система довольно проста. Если всё обобщить, то это некоторое количество датчиков, выдающих разнообразную информацию о том, что происходит на разных участках дороги, некоторый набор сценариев, запускающих то или иное оборудование, и набор правил вида если-то (если падает метеорит, то мы бегаем по офису кругами с криком а-а-а-а), запускающих соответствующие сценарии.
На словах всё очень просто, на деле это невообразимая куча разнообразного железа, соединённого оптоволоконной сетью с повышенными условиями отказоустойчивости. И, конечно, огромный ворох проблем, связанный со всем этим. Возможно, однажды я расскажу про всю эту внутреннюю машинерию, если у достопочтенного общества будет на то запрос. Но сегодня хотел бы остановиться на той части, за которую отвечал я.
Про управление
Немного сбивает с толку слов управление. Лично у меня, когда я только об этом узнал, в голове был образ роботизированной тележки, которая ездит вдоль трассы и переставляет блоки разграничительной линии между полосами, расширяя или сужая проезжую часть.
На деле всё намного проще: по всей дороге расставлены датчики, определяющие текущее состояние дороги и оптимальных условий для вождения, а также развешаны табло, на которых появляется информация, зачастую генерируемая, на основании показаний с датчиков. При помощи камер и датчиков в автоматическом или полуавтоматическом режиме распознаётся опасная ситуация и на табло выводится соответствующая информация. Вы, наверное, видели что-то вроде Впереди, в 10 км, ДТП или значок жёлтой стрелочки, указывающий на необходимость съезда с полосы. Иногда ещё бывают светофоры и шагбаумы, но это в современном управлении дорогами редкость.
Интерфейс
Мне досталась самая ответственная часть интерфейс. Кто-то может возразить, мол, а что же ядро приложения? Без него ничего работать не будет. Несомненно, однако АСУДД создаются для управления дорогой, а управляет ей диспетчер. Именно поэтому особенно важно, чтобы информацию, которую он получает была бы легко доступна, содержала всё необходимое, но и ничего лишнего. Самая ли важная часть всей системы АСУДД интерфейс безусловно нет, но самая ответственная.
Начало реализации
Мы остановились на клиент-серверном подходе (технологии столь же старой, как сама вычислительная техника). В качестве клиента был браузер (мы использовали Chrome). В качестве сервера и вот тут определилась первая сложность. Дело в том, что ядро приложения это просто ядро, которое обрабатывает текущие состояния, предоставляет API, но совершенно не беспокоится о том, какой клиент будет обрабатывать всё это. Иными словами, очень много данных, которые предоставляло ядро, были, по факту, сырыми. Их можно было использовать для генерации важной для диспетчера информации, но сами по себе они этой информацией не являлись.
На самом деле, это довольно распространённый подход: имеется некоторый сервер, который выдаёт сырые данные; они принимаются другим сервером, который, в свою очередь, преобразовывает их и предоставляет клиенту. Иногда бывает и вовсе конвейер, когда проходят несколько ступеней обработки, на каждом из которых может быть заинтересованный клиент. Но это так, лирическое отступление.
Конечно, тут следовало бы написать: и мы сделали всё по уму, всё хорошо продумав. Но нет, случилось всё с точностью до наоборот. Поджимали сроки, заказчик нетерпеливо бил копытом и требовал каких-то рабочих прототипов и мы мы пошли на сделку с совестью и сделали временный толстый клиент. Надо ли говорить, что этот подход так и остался с нами навсегда?
Не могу сказать, что идея толстого клиента безвозвратно плоха в нём есть свои преимущества: например, перенос вычислительных затрат на пользователя (читай, удешевление серверных мощностей), более простое монолитное исполнение приложения (монотехнология, либо набор родственных технологий), повышенная простота поддержки и тому подобное. Короче говоря, расценив, что зло мы совершаем малое, толстого клиента мы оставили.
Общая структура и её взаимодействие
Сама структура АСУДД была довольно простой, но, как мы знаем, сложно сделать просто и она стоило какого-то количество нервных клеток нашему архитектору и мне (в части касающейся).
Всё АСУДД состоит из следующих блоков:
- Ядро приложения
- Универсальный драйвер оборудования(обеспечивает взаимодействие ядра с оборудованием)
- База данных (мы использовали postgresql)
- Сервера дорожных станций (если коротко это промышленные компьютеры, работающие на местах оборудования; обеспечивают повышенную отказоустойчивость и ещё много чего)
- Очередь сообщений (у нас RabbitMQ), обеспечивающая взаимодействие всех блоков приложения в ЦОДе
- Интерфейс (о нём я подробней расскажу дальше).
Конечно, был ещё дополнительный софт для наблюдение за оборудованием и специальный софт для работы с некоторыми видами оборудования (вроде Zabbix), однако я этого практически не касался, поэтому данный момент рассматривать не стану.
Как нетрудно догадаться, все блоки (за исключением серверов дорожных станций) были вполне себе автономными процессами и требовали особо пристального внимания к себе в процессе работы для обеспечения стабильной работы приложения. Напрашивалась контейнеризация, и она-таки наступила. Мы используем docker.
Преимущества наступили сразу же: разработка более не требовала держать в голове все существующие зависимости, а развёртывание стало предсказуемым и контролируемым. В повышении удобства этого процесса огромную роль сыграл на разработчик ядра, написавший сборщик дистрибутива и внедривший Ansible для развёртывания приложения. Сборка новой версии продукта и её развёртывание превратились в рутинную операцию, в то время как в самом начале этот процесс вполне мог занять целый день.
Но я забежал вперёд. Ansible и сборщик пакетов появится только спустя 3 года с начала разработки. А мы, пока что, только готовились к своей первой установке.
Что же из себя представлял интерфейс
Думаю, следует немного отвлечься от технической части. Но только в этом разделе.
По сути, все системы АСУДД являются SCADA (аббр. от англ. Supervisory Control And Data Acquisition диспетчерское управление и сбор данных) системами. Это некоторая условная схема (мы её называем мнемосхемой) размещения объектов и оборудования, а также возможность взаимодействовать с ней через пользовательский интерфейс.
Вот так вот выглядела наша мнемосхема:
И это, на первый взгляд совершенно незамысловатое решение, стоило нам многих часов обсуждений. Остановлюсь на том, что же мы использовали.
Сам интерфейс отрисовывался на SVG: решение выглядело наиболее простым и быстрым (конечно, мы смотрели и в сторону WebGL, и разных надстроек, вроде three.js, однако получалось сложно и долго, нам же нужна была быстрая победа). Кстати, именно быстрота работы с SVG и определила выбор Chrome: он обыграл Firefox по производительности более чем в 2 раза.
Все иконки оборудования интерактивны; клик по каждой приводит к открытию диалога управления, содержащего как справочную информацию, так и возможность задать новое состояние. Например, чёрные прямоугольники это табло (знаете, такие, над дорогой устанавливаются и на них отображается различный текст). Так вот, через этот интерфейс была возможность зайти и вывести свой собственный текст. Это, разумеется, не приветствовалась, т.к. матерные фразы с грамматическими ошибками несколько дискредитируют оператора дороги в глазах автомобилистов, но, хоть и с контролем доступа, такая возможность была.
Важной частью системы является сигнализация: дорожная и техническая. Если просто, то дорожные сигналы это то, что произошло на дороге (выпал снег, случилось ДТП итп), а вот технические это, обычно, какой-то отказ оборудования (и эти сигналы были, есть и будут всегда, потому что, как показывает наблюдение, 100% исправленного оборудования не бывает никогда).
Карточки событий это инструмент фиксирования происшествий. Упала корова на дороге и мешает движению заводится карточка событий. Суицидник решил прыгнуть с моста и был замечен диспетчером на камере карточка событий. Проще говоря, это журнал всех значимых происшествий. В нашем приложении мы уделили мало внимания данному разделу, в то время как существуют целый стандарты (вернее стандартищи, включащие в своей структуре справочников даже падения метеоритов) описания дорожных событий. Наша же карточка события была существенно скромнее, однако была довольно гибкой в настройке: мы используем JSON файлы для описания полей и их взаимодействие. Получается не так, чтобы очень просто, но вполне человекопостигаемо. Вот пример карточки события:
Несколько слов хочется сказать о камерах. Благодаря нехитрым манипуляциям с настройкой выходного потока с камеры, нам удалось без шума и пыли вывести видеопоток прямо в браузер. Не мудрствуя лукаво, мы использовали MJPEG (motion jpeg), так что нам хватило стандартного элемента IMG для отображения текущего состояния. Там, правда, в какой-то момент вылезли некоторые трудности, связанные с отображением видео, но это было не фатально. Например, вот так выглядит диалог камеры, установленной на одном из участков трассы М3:
Хочу заметить, что это довольно умная камера со множеством различных возможностей (например, мытьё и очистка стекла объектива). Всё это мы, разумеется, поддержали.
Конечно, было много справочников и редакторов: управление всем, начиная от сценариев и правил, заканчивая списком и правами пользователей. По сути, ничего сверхестественного, хотя, конечно, везде были свои особенности.
Опорные данные приложения
Но вернёмся к технической части. Для работы приложения требовался большой объём данных. По сути, требовались почти все данные, которыми располагало ядро, т.к. многие решения принимались на основании именно их. Очень распространённым был случай, когда приходили какие-то данные, отреагировать на которые следовало лишь через 5-10-30 минут. При этом была вероятность, что за указанный интервал времени эти данные утратят актуальность.
Так мы пришли к концепции словарей. Все данные, необходимые для принятия решения, загружались на клиент после входа пользователя в систему и поддерживались в актуальном состоянии.
То есть нам требовалась двусторонняя связь клиента с сервером. Изначально, очень просились на эту роль сокеты, однако в силу природной лени, мы остановились на уже готовом решении SSE. Да, формат был не идеальным, но покрывал функциональность на 100%.
Безусловно, была введена система контроля версии данных и периодическая пересинхронизация. Особое опасение вызывал объём словарных данных, хранимый на клиенте. Однако, проведя исследование и подсчитав предельный объём используемых данных, то даже со стократным запасом он не превышал бы 50-100 мегабайт. Кроме того, изначально закладывалась возможность оптимизации. Так что на этот риск мы пошли смело.
Вторым типом словарей была информация об оборудовании. Тут каждый объект (считай, каждый элемент оборудования) имел свою собственную структуру данных, меняющуюся по мере того, как изменялось состояние объекта. Они подгружались по тому же каналу связи, но отдельно.
Вообще, довольно скоро выяснилось, что у нас будут разные каналы данных: часть каналов будет востребована, часть, возможно, нет. Поэтому мы пошли по пути подписок: в определённый момент (например, при инициализации приложения или открытия диалога) приложение подписывалось на какой-то набор данных, а, когда в них пропадала необходимость, отписывалось. Более того, сами входящие данные получили числовые типы, каждый из которых обрабатывался своей собственной структурой данных.
Тут надо рассказать и о ложке дёгтя в данной бочке мёда. Дело в том, что существующие решения для отслеживания sse сообщений (по крайней мере внутри devtools), не позволяют показать всё сообщение целиком (а у нас ходили длинные json строки), что значительно усложняло отладку, так как нельзя было проверить, что за данные пришли. По этой причине, пришлось сделать дополнительную консоль отладки, отображающую все приходящие пакеты, но это не было какой-то тяжёлой работой.
Всё прочее
Вообще, интерфейс приложения обладал довольно большим функционалом, но весь он являлся уже надстройкой над имеющейся системой сообщения. По сути, разные классы приложения решали ряд задач, для чего они подписывались на интерсующие их словари и асинхронно (т.е. по мере изменения данных) обновляли систему: отображали окна сигнализации, зажигали сигнализацию на элементах полевого оборудования, если с ними случался какой-то сбой.
В качестве основного фреймворка, на котором работает приложения, был выбран angularjs. Кто-то может счесть данную технологию несколько устаревшей, но на момент старта проекта он очень хорошо удовлетворял поставленной задаче, а angular2 был ещё весьма сырым и рисковать не хотелось. Помимо основного фреймворка, позже, как я ни сопротивлялся (старался, по возможности, минимизировать количество используемых технологий для простоты поддержки), появились другие библиотеки: d3, lodash, printjs. Не то, чтобы они были необходимы, но с ними жизнь немного упростилась. В частности, d3 мы использовали для построения графиков (у нас была несколько нестандартная система визуализации параметров, так что ничего доступного не подошло).
Примерно с этим багажом мы вышли на приёмосдаточные испытания и даже прошли их. В последствии были ещё доработки, но они были уже из разряда косметики.
Сборка
Когда всё только начиналось, до конца не было понятно, что же из всего этого получится. В то время grunt был отличным и популярным средством сборки и он был отличной альтернативой, нежели ничего.
Не могу сказать, что это безнадёжно устаревший подход. Он долгое время исправно выполнял свою роль, до тех пор, пока не появилось достаточно много файлов, размещение, порядок подключение и взаимодействие которых требовало некоторой последовательности. Так, спустя почти 4 года использования, мы переехали на webpack.
Вообще, фраза переехали не совсем достоверная. В какой-то момент он начался использоваться для сборки отдельных компонент приложения: так уж вышло, что по мере усложнения проекта и появления всё большего количества нетривиальных задач, собирать компоненты в отдельные модули требовалось всё чаще и когда количество обособленных компонент достигло 6, а сборка приложения начала занимать около 2 минут, стало ясно, что пора разрубать этот гордиев узел и использовать один сборщик, вместо двух. Не могу сказать, что всё сразу и прошло гладко; миграция заняла около 2 недель.
Немаловажным фактором был процесс настройки CI/CD. Лично я к этому вопросу подходил, как минимум, два раза. Пробовал jenkins и bamboo. Первый мне больше знаком, так как с ним доводилось работать ранее, второй очень хорошо интегрировался в нашу экосистему (у нас использовалась вся линейка продуктов от Atlassian), однако с ним постоянно возникали сложности (в 9 случаях из 10 они упирались в права доступа и отсутствие ресурсов), которые невозможно было решать достаточно оперативно, что ставило жирный крест на использование технологии. В конечном итоге мы-таки сформировали процесс, однако до сих пор так его и не внедрили, но это уже совершенно бюрократическая история.
Компоненты как средство выживания
Кто имел дело с angularjs, тот знает, что у него есть немало своих собственных сущностей для обозначения и интеграции какой-то функциональности. Например, есть сущность service, которая, по сути, является описанием класса и возвращает объект (обычно одиночку / singleton). И когда разрабатываешь в парадигме фреймворка, поначалу охотно используешь предлагаемую функциональность.
Однако, в какой-то момент, появляется желание поменять фреймворк и внезапно обнаруживаешь, что сделать это не то, что непросто невозможно. Большая часть приложения не просто использует возможности фреймворка, она построена на нём и если его удалить, то от приложения может вообще ничего не остаться, а то, что останется, работать не будет.
У angularjs есть два больших недостатка. Во-первых, он уже морально устарел и найти разработчика, способного (и желающего) с ним работать проблематично. А во-вторых, он не очень любит большое количество управляемых элементов на странице, нещадно зависая и превращая работу с приложением в пытку.
Короче говоря, мы-таки столкнулись сразу с обеими проблемами angularjs и невозможностью переезда и тогда было принято решение немножечко отстраниться от него, насколько позволяли возможности. Для этого вся новая функциональность разрабатывалась уже в качестве отдельных компонент (в том числе и с использованием WebComponents, хотя это и были разовые случаи), а также готовился плацдарм для постепенного отказа (в случае необходимости) от используемого фреймворка.
Разумеется, компоненты не всегда получались простыми, а длинные и очень длинные связи потребовали отдельный сборщик так в проекте появился webpack. Кстати, от angularjs мы так и не избавились, однако смогли побороть технологическую отсталость, но об этом позже.
Сервер отладки
Когда работаешь со сложным приложением, которое, вдобавок, требует от сервера некоторой интерактивности, то требуется, помимо статичного сервера, что-то вроде сервера разработки: специального сервера, который поставляет данные (правильные или неправильные) для эмулирования ситуаций и отладки.
В идеале использовать урезанный вариант используемого сервера, где уже есть все необходимые данные. Однако это был явно не наш путь и на то было немало причин. Во-первых, ядро работало в среде GNU/Linux внутри контейнера (а мы все сидели на винде), так что приходилось разворачивать виртуальную машину, на ней ставить докер и через такую длинную цепочку производить отладку. Во-вторых, не было решительно никакой возможности легко и просто изменить какие-то данные. Ну и в-третьих, разработка интерфейса, зачастую, опережала разработку ядра, так что нужного функционала там просто не было.
Решение было найдено простое, как апельсин: мы взяли node.js, который запускался через nodemon (надстройка, автоматически перезапускающая скрипт при любом изменении входных файлов), подключили к нему express.js и так у нас появился свой собственный сервер отладки. Oauth авторизацию и sse реализовать было относительно просто, так что в какой-то момент у нас появился ничем не уступающий ядру аналог, снабжающий разработку управляемыми данным.
За него, разумеется, пришлось побороться с архитектором и разработчиками ядра, свято веривших в то, что отлаживаться на чём-то отличном от ядра святотатство, однако, в том раунде победа осталась за нами.
Проект vs продукт
После того, как мы сдали наш первый проект, на горизонте замаячило два новых проекта, а после ещё один. В принципе, они хотя и сильно отличались один от другого, однако большая часть была неизменна, а меньшая, являлась, по сути, настраиваемой.
Так, в результате продолжительных дебатов, интерфейс и ядро нашей АСУДД раскололись на две части: продуктовую и конфигурационную. Хотя, наверное, правильней сказать, подобная возможность оговаривалась изначально, вот только в виду сжатых сроков была отложена.
Продуктовая часть развивалась отдельно и поддерживала весь имеемый функционал: все специальные диалоги и ядерные методы. Конфигурационная (или проектная) часть содержала в себе набор файлов, на основании которых создавался файл настройки, указывающий откуда какие данные брать, как отображать то или иное оборудование.
Изначально, проектная часть для интерфейса была крайне проста и представляла из себя простой json файл, в котором по секциям были разбиты аспекты приложения и располагались ссылки на подгружаемые ресурсы (например, из какого файла будет браться мнемосхема, как будет выглядеть диалог и тому подобное). А учитывая, что наше приложение сильно усложнилось: появилось несколько уровней приближения для мнемосхемы с управляемым слоями отображения, картографической подложкой с интерактивным отображением на нём полевого оборудования (кстати, мы использовали OpenStreetMaps + OpenLayers) стало ясно, что просто json файла недостаточно: требовались скрипты управления. Последней каплей стало заключение, что от проекта к проекту оборудование не просто меняется, но меняется его внешний вид, иконки, принципы работы и использовать дважды одно и то же попросту не получится. Так стало ясно, что теперь частью настройки является ещё и принцип работы полевого оборудования, несмотря на то, что изначально не планировалось выносить в конфигурационную часть какую-то интерактивность.
Так мы постепенно пришли к тому, что существующая модель настройки ни на что не годится и была произведена перестройка, благодаря которой система конфигурации изменилась.
Для начала, мы отказались от написание длинного json файла (кто правил такие знает, насколько это неудобно, да и лишняя запятая может, внезапно, поломать обратный разбор). Вместо этого стали использовать js файлы в схеме, используемой node.js (require.js). Теперь все настройки были разнесены по отдельным секциям и правились, в случае необходимости, относительно просто.
Затем, мы пересмотрели способ доставки ресурсов. Изначально, они подгружались при инициализации с сервера. Это не занимало много времени, но и нужды в этом особой не было. Теперь они начали упаковываться внутрь конфигурационного файла, разворачиваясь в память уже на сервере в момент загрузки. А так как у нас был довольно толстый канал связи и предполагалось кэширование, распухший файл конфигурации не пугал.
Потом мы занялись элементами полевого оборудования: их было решено делать автономными модулями, каждый из которых имел angularjs-обрабатываемую вставку в диалог, настройки для диалога, скрипт управления иконками и ещё много всяких мелочей. Подумав, я принял решение расширить список используемых технологий и добавил возможность применения сторонних фреймворков, например, react.js.
Всё великолепие, по прежнему, собиралось в json файл, только теперь это выполнял специальный скрипт, не требующий человеческого вмешательство. Однако, каждое изменение файлов настройки приводило к необходимости пересборки, что хорошо было бы автоматизировать. Для этого мы использовали lua-пристроку к nginx, которая запускала скрипт проверки и, если какие-то файлы успели измениться с последнего запроса конфигурации, создавала файл с новой, в противном случае отдавала старую. Всё работает довольно быстро, хотя есть планы по дальнейшему улучшению данной технологии.
Что-то вроде выводов
На данный момент, наша АСУДД установлена как минимум на 3 объектах и, возможно, в обозримом будущем появится на четвёртом. Не так давно, мы подключили к проектам sonarqube и собрали некоторую статистику. Выяснилось, что код интерфейса примерно в 3 раза больше, чем код ядра и почти в 2 раза больше, чем код всего остального приложения. Так, незаметно для себя, я стал управляющим самого большого актива нашего направления. И это позволяет мне сделать кое-какие выводы, которые я сделал в рамках данного проекта (и вообще, и в частности):
- Самый главный ресурс любой команды это человек; если есть рядом человек он может научиться тому, чего не знает и сделать то, что требуется сделать, даже если на это уйдёт больше времени; если же человека нет то работа не будет сделана в принципе.
- Нет ничего хуже технического долга, потому что именно он заставляет переписывать проекты. Вчера воткнули костыль, сегодня по просьбе менеджера сделали по-быстренькому, завтра что-то сделали на скорую руку, а спустя полгода кодовая база превращается в месиво, в котором сходу не разобраться и для даже мелких изменений может потребоваться очень много времени.
- Технологии постоянно развиваются и невозможно постоянно быть на волне, однако хорошо продуманная архитектура позволит сравнительно безболезненно не только поменять технологии, но и сделать это в короткие сроки. Отсутствие данных разработок, зачастую, приводит к смерти продукта или финансовой нецелесообразности его дальнейшей поддержки. Кстати, это актуально и для предыдущего пункта.
- Разработка программного обеспечения это не только написания кода. Это, зачастую, путь от задумки до рабочего стола клиента. И этот путь следует продумать ещё до того, как будет написана первая строчка кода. Недаром в последнее время професси devops становится всё более и более востребованной.
Но это, как можно заметить, относится не столько к разработке АСУДД, а к разработке в принципе.
Что дальше
Конечно мы планируем дальнейшее развитие нашей АСУДД. И планы, как водится, наполеновские. Есть желание улучшить картографическую подложку, оптимизировать загрузку приложения, рассмотреть использование webGL технологий вместо SVG (и пяти лет не прошло, как мы вновь о них вспомнили) ну и, конечно, сделать автогенерируемую мнемосхему (сейчас она рисуется дизайнером). Так что планов много.