Операция на сердце: как мы переписывали основной компонент DLP-системы
Переписывание legacy-кода как поход к стоматологу — вроде, все понимают, что надо бы пойти, но все равно прокрастинируют и стараются оттянуть неизбежное, потому что знают: будет больно. В нашем случае дела обстояли еще хуже: нам надо было переписать ключевую часть системы, и в силу внешних обстоятельств мы не могли заменять старые куски кода на новые по частям, только все сразу и целиком. И все это в условиях нехватки времени, ресурсов и документации, но с требованием руководства, что в результате «операции» ни один заказчик не должен пострадать.
Под катом история о том, как мы переписали основной компонент продукта с 17-летней историей (!) со Scheme на Clojure, и все сразу заработало как надо (ну, почти :)).
17 лет в «Дозоре»
Продукт Solar Dozor — DLP-система с очень долгой историей. Первая версия появилась еще в далеком 2001 году как относительно небольшой сервис фильтрации почтового трафика. За 17 лет продукт вырос до большого программного комплекса, который выполняет сбор, фильтрацию и анализ разнородной информации, курсирующей внутри организации, и защищает бизнес клиентов от внутренних угроз.
При разработке 6-й версии Solar Dozor мы решительным образом встряхнули продукт, выкинули из кода старые костыли и заменили их новыми, обновили интерфейс, пересмотрели функционал в сторону современных реалий — в общем, сделали продукт архитектурно и концептуально более целостным.
На тот момент под капотом обновленного Solar Dozor существовал огромный пласт монолитного legacy-кода — того самого сервиса фильтрации, который все эти 17 лет постепенно обрастал новым функционалом, воплощая как долгосрочные решения, так и сиюминутные бизнес-задачи, но сумел остаться в рамках изначальной архитектурной парадигмы.
Сервис фильтрации
Надо ли говорить, что внесение любых изменений в такой древний код требовало особой деликатности. Разработчики должны были проявлять крайнюю внимательность, чтобы случайно не испортить функционал, созданный десятилетие назад. К тому же, довольно новые интересные решения вынуждены были втискиваться в прокрустово ложе архитектуры, придуманной на заре эпохи.
Понимание того, что назрела необходимость в обновлении системы, появилось уже довольно давно. Но вот духу тронуть огромный и древний системный сервис явно недоставало.
Не пытаемся оттянуть неизбежное
Продукты с долгой историей развития имеют интересную особенность. Каким бы странным ни казался какой-либо кусок функционала, если он успешно дожил до наших дней, это значит, что он создавался не из теоретических представлений разработчиков, а в ответ на конкретные нужды клиентов.
При таком раскладе ни о какой поэтапной замене речи идти не могло. Нельзя было выпиливать и переделывать функционал по частям, потому что все эти части востребованы заказчиками, и мы не могли «закрыть их на реконструкцию». Надо было аккуратно извлечь старый сервис и предоставить ему полнофункциональную замену. Только целиком, только сразу.
Улучшение процесса разработки продукта, скорость внесения изменений и повышение качества в целом было условием необходимым, но недостаточным. Руководство интересовало, какие преимущества принесут изменения нашим клиентам. Ответом стало расширение набора интерфейсов для взаимодействия с новыми системами перехвата, что обеспечило бы быструю обратную связь и позволило перехватчикам оперативнее реагировать на инциденты.
Также предстояло побороться за то, чтобы уменьшить потребление ресурсов, сохранив (а в идеале — увеличив) текущий темп обработки.
Немного о начинке
На всем пути развития продукта команда Solar Dozor тяготела к функциональному подходу. Отсюда следует довольно нестандартный для зрелой индустрии выбор языков программирования. В разные этапы жизни системы это были Scheme, OCaml, Scala, Clojure, помимо традиционных С (++) и Java.
Основной сервис фильтрации и другие сервисы, помогающие приему и передаче сообщений, были написаны и развивались на языке Scheme в различных его реализациях (последней использовалась Racket). Как бы ни хотелось петь дифирамбы простоте и элегантности этого языка, нельзя не признать, что его развитие отвечает больше академическим интересам, нежели промышленным. Особенно заметно отставание в сравнении с другими, более современными сервисами Solar Dozor, которые разрабатываются в основном на Scala и Clojure. Новый сервис также решено было реализовать на языке Clojure.
Clojure?!
Тут, конечно, надо сказать пару слов о том, почему мы выбрали Clojure в качестве основного языка реализации.
Во-первых, не хотелось терять уникальный опыт команды, разрабатывающей на Scheme. Clojure также является современным представителем семейства Lisp-языков, и перейти с одного Lisp на другой обычно довольно просто.
Во-вторых, благодаря приверженности функциональным принципам и ряду уникальных архитектурных решений, Clojure обеспечивает беспрецедентную легкость манипулирования потоками данных. Важно и то, что Clojure функционирует на платформе JVM, а значит, можно использовать совместную базу с другими сервисам на Java и Scala, а также пользоваться многочисленным инструментарием для профилирования и отладки.
В-третьих, Clojure — краткий и выразительный язык. Это обеспечивает легкость чтения чужого кода и облегчает передачу кода коллеге по команде.
Наконец, мы ценим Clojure за легкость прототипирования и так называемую REPL-ориентированную разработку. Практически в любой ситуации, когда есть сомнения, можно просто создать прототип и продолжить дискуссию уже более предметно, с новыми данными. REPL-ориентированная разработка дает быструю отдачу, ведь для проверки работоспособности функции не надо не то что перекомпилировать программу, но даже перезапускать ее (даже если программа — сервис, расположенный на удаленном сервере).
Забегая вперед, могу сказать: считаю, что мы не прогадали с выбором.
Собираем функционал по крупицам
Когда мы говорим о полнофункциональной замене, в первую очередь встает вопрос сбора сведений о существующем функционале.
Это стало довольно интересной задачей. Казалось бы, вот работающая система, вот документация к ней, вот люди — эксперты, тесно работающие с системой и обучающие этому других. Но получить из всего многообразия цельную картину, а тем паче требования для разработки оказалось не так-то просто.
Сбор требований не зря считается отдельной инженерной дисциплиной. Существующая реализация парадоксальным образом оказывается в роли некоторого «испорченного эталона». Она показывает, что и как должно работать, но при этом от разработчиков ожидают, что новая версия получится лучше оригинала. Приходится отделять обязательные для реализации моменты (обычно связанные с внешними интерфейсами) от тех, которые можно улучшить в соответствии с ожиданиями пользователей.
Процесс фильтрации сообщения
Документации недостаточно
Каков на самом деле функционал системы? Ответ на этот вопрос дают различные описания, такие как пользовательская документация, руководства и архитектурные документы, отражающие структуру сервиса в различных аспектах. Но когда доходит до дела, прекрасно понимаешь, насколько расходятся представления и реальность, сколько нюансов и неучтенных возможностей содержит старый код.
Хочу обратиться ко всем разработчикам. Берегите свой код! Это самое главное ваше достояние. Не полагайтесь на документацию. Доверяйте лишь исходным текстам.
К счастью для нас, код на Scheme, благодаря самой природе языка, созданного для обучения программированию, довольно легко читать даже неподготовленному человеку. Главное — привыкнуть к некоторым отдельным формам, несущим в себе легкий налет Lisp-архаики.
Выстраиваем процесс
Объем работы был колоссальный, а команда весьма небольшая. Так что не обошлось без организационных трудностей. Рабочий поток багов и запросов на исправление (и небольшие доработки) старого сервиса фильтрации и не думал останавливаться. Разработчикам регулярно приходилось отвлекаться на эти задачи.
К счастью, удавалось отбиться от запросов на встраивание в старый фильтр новых кусков большого функционала. Правда, под обещание встроить этот функционал в новый сервис. Тем не менее, набор задач релиза медленно, но верно рос.
Другим фактором, который добавил немало хлопот, стали внешние зависимости сервиса. Будучи центральным компонентом, сервис фильтрации использует многочисленные сервисы распаковки и анализа содержимого (текстов, изображений, цифровых отпечатков и т.п.). Работа с ними частично ориентировалась на старые архитектурные решения. В процессе разработки пришлось также переписать некоторые компоненты на современный лад (а некоторые и на современный язык).
В таких условиях была построена система этапного тестирования функционала. Мы как бы выращивали сервис до определенного состояния, которое закреплялось активным тестированием, а затем переходили к реализации нового.
Начинаем разработку
В первую очередь были реализованы основной каркас сервиса, базовые механизмы приема сообщений и распаковки файлов. Это был абсолютный минимум, необходимый для того, чтобы можно было начать тестирование на скорость и корректность работы будущего сервиса.
Тут надо пояснить, что под распаковкой понимается рекурсивный процесс получения из файла частей и извлечение из них полезной информации. Так, например, документ Word может содержать в себе не только текст, но и изображения, встроенный документ Excel, OLE-объекты и еще много чего интересного.
Механизм распаковки не делает различий между использованием внутренних библиотек, внешних программ или сторонних сервисов, предоставляя единый интерфейс для организации конвейеров распаковки.
Еще комплимент в сторону Clojure: мы получили рабочий прототип, в котором обозначили контуры будущего функционала, в кратчайшие сроки.
DSL для политики
Вторым этапом стало добавление проверки сообщения с помощью политик фильтрации.
Для описания политик был создан специальный DSL — простой язык без излишеств, который позволил в более-менее человекочитаемом виде представить правила и условия политики. Он получил название MFLang.
Скрипт на MFLang «на лету» интерпретируется в Clojure-код, кэширует результаты проверок над сообщением, ведет подробный лог работы (и, откровенно говоря, заслуживает отдельной статьи).
Использование DSL пришлось по душе тестировщикам. Долой копания в БД или в экспортном формате! Теперь можно было просто прислать сгенерированное правило для проверки, и сразу становилось ясно, какие условия проверялись. Также появилась возможность получить детализированный лог проверки сообщения, из которого понятно, какие данные брались для проверки и какие результаты вернули функции сравнения.
Можно с уверенностью сказать, что MFLang оказался совершенно неоценимым подспорьем для отладки функционала.
В полную силу
На третьем этапе был добавлен механизм применения к сообщению действий, определенных политикой безопасности, а также сервисные обвязки, позволяющие включить новые компоненты в состав комплекса Solar Dozor. Наконец-то мы смогли запускать сервис и наблюдать результат работы во всем многообразии.
Главный вопрос, конечно же, состоял в том, насколько реализованный функционал соответствует ожидаемому и насколько полно он его реализует.
Замечу, что если необходимость модульного тестирования уже давно не подвергается сомнению (хотя сами практики TDD до сих пор вызывают оживленные споры), то внедрение автоматизированного тестирования системного функционала зачастую наталкивается на открытое сопротивление.
Разработка автотестов помогает всем членам команды лучше понять процесс работы продукта, экономит силы на регресс, вселяет определенную уверенность в работоспособности продукта. Но сам процесс их создания сопряжен с рядом трудностей — сбором необходимых данных, определением интересующих показателей и вариантов тестирования. Программисты неизбежно воспринимают создание автотестов как необязательную, побочную работу, от которой по возможности лучше увильнуть.
Но если удается преодолеть сопротивление, создается довольно прочный фундамент, позволяющий выстраивать представление о работоспособности системы.
Проводим замену
И вот наступил важный момент: мы включили сервис в комплект поставки. Пока вместе со старым. Таким образом можно было одной командой провести смену версии и сравнить поведение сервисов.
В таком параллельном режиме новый сервис фильтрации просуществовал в течение одного релиза. За это время мы успели собрать дополнительную статистику по работе, наметить и реализовать необходимые доработки.
Наконец, собравшись с силами, мы удалили из продукта старый сервис фильтрации. Пошел финальный этап внутренней приемки, правились баги, разработчики начали постепенно переключаться на другие задачи. Как-то незаметно, без фанфар и аплодисментов, произошел релиз продукта с новым сервисом.
И только когда начали поступать вопросы от команды внедрения, наступило понимание — сервис, над которым мы так долго трудились, уже стоит на площадках и… работает!
Конечно, были и баги, и небольшие доработки, тем не менее через месяц активного использования у заказчиков был вынесен вердикт: внедрение продукта с новой версией сервиса фильтрации вызвало меньше проблем, чем внедрение предыдущих версий. Хей! Похоже, мы справились!
В итоге
Разработка нового сервиса фильтрации заняла примерно полтора года. Дольше, чем предполагалось изначально, но не критично, тем более, что фактическая трудоемкость работ совпала с первоначальной оценкой. Гораздо важнее, что нам удалось оправдать ожидания руководства и заказчиков и заложить основы для будущих доработок продукта. Уже в текущем состоянии видно значительное снижение потребления ресурсов — при том, что в продукте еще есть широкие возможности для оптимизации.
Могу добавить немного личных впечатлений.
Замена центрального компонента с долгой историей — глоток свежего воздуха для разработки. Впервые за долгое время появляется уверенность в том, что контроль над продуктом возвращается в наши руки.
Сложно переоценить пользу от правильно организованного процесса коммуникации и разработки. В данном случае важно было наладить работу не столько внутри команды, сколько с многочисленными потребителями продукта, у которых были как давно сформированные четкие предпочтения и ожидания от работы системы, так и довольно расплывчатые пожелания.
Для нас это был первый опыт разработки такого масштабного проекта на Clojure. Вначале были опасения, связанные с динамической природой языка, скоростью и устойчивостью к ошибкам. К счастью, они не оправдались.
Остается лишь пожелать, чтобы новый компонент проработал так же долго и успешно, как его предшественник.