Кровавое легаси: как в одиночку раздробить монолитный сервис и не сойти с ума
Было раннее утро понедельника. Я проснулся раньше времени из-за грохота грома за окном. Подойдя к окну, я увидел, как по небу плыли свинцовые тучи, безжалостно заливая дождем все вокруг. Казалось, что еще чуть-чуть — и наступит второй всемирный потоп. Как будто Вселенная всеми способами пыталась мне намекнуть, что надвигается что-то ужасное, но я даже не представлял, что меня ждет.
Натянув на себя черный плащ, я отправился на работу, прорываясь через ветер и дождь, словно через поле боя. Казалось, что весь мир был против меня. И вот я стоял перед восьмиэтажным, потрепанным жизнью зданием, увешанным ржавыми кондиционерами, словно бородавками. Там находился офис конторы, где я работал.
Лифта рано утром, как всегда, было не дождаться, и я отправился на четвертый этаж по лестнице, любуясь местными красотами и оставляя за собой мокрый шлейф на полу. Я оказался в офисе первым и чувствовал себя королем мира. Сделав себе чай и расположившись в кресле, я принялся доделывать очередную фичу. Казалось, что настали покой и умиротворение.
Команда постепенно стала подтягиваться в офис, и в 10 часов начался ежедневный митап, где мы обменивались статусом о том, что сделали вчера и чем будем заниматься сегодня. После наших отчетов настала пора говорить руководителю, и за окном вдруг отчетливо послышалась стройка, звуки которой пробивались в офис даже сквозь стеклопакеты. Создавалось ощущение, будто дятел сидел у тебя на плече и пытался пробить череп до самого мозга.
В воздухе чувствовалось нарастающее напряжение. Руководитель заговорил, и я почувствовал себя, словно на гильотине.
Произошло действительно страшное: мне предстояло путешествие по удивительному миру legacy-кода в старом корпоративном сервисе.
Часть первая: заглядываем врагу в лицо
Придя в себя после услышанного, я, словно оккультист, принялся изучать исчадие ада, с которым мне предстояло сразиться.
Это было древнее корпоративное зло, которое пришло и отравило наш мир где-то в 2011 году. Оно представляло собой монолитный Windows-сервис, использующий технологию WCF. MS SQL Server 2008 R2 использовался в качестве СУБД, а NHibernate — в качестве ORM. Для взаимодействия с конечным пользователем он использовал несколько Silverlight-сайтов с разным функционалом, который обращался к этому Windows-сервису через WCF-клиент.
Использование Silverlight доставляло боль конечному пользователю из-за того, что все браузеры давно открестились от NPAPI-плагинов, и запускать сайты сервиса можно было только через страшную демоническую сущность — Internet Explorer, которая так и норовила зависнуть, как бы насмехаясь и открыто показывая, что ей безразлична проделанная тобой работа. Все это работало под .Net Framework 4.0, а писалось, скорее всего, на более раннем фреймворке: в коде встречались треды вместо тасков и прочие устаревшие конструкции.
Компания занималась оцифровкой документов, и за индексацию данных операторы, которые работали по всей России, получали деньги. По своей концепции это напоминало Яндекс.Толоку, но со своими фишками, которые я не буду раскрывать.
Сервис носил статистическую функцию и считал зарплаты операторам, строил различные отчеты руководству, к нему стучались Silverlight-приложения. Например, одно из приложений позволяло посмотреть, сколько сотрудник заработал денег за выполненную работу, сколько было ошибок ввода и т. д. и т. п.
Сервис решал ряд задач:
строил отчеты — как правило, в формате Excel: как по требованию, так и по расписанию; затем они рассылались по почте или скачивались напрямую;
собирал статистику по выполненным на проектах работам путем загрузки в БД файлов статистики, которые выкладывались другим сервисом;
позволял настроить цены и коэффициенты для тех или иных проектных работ, которые затем использовались при построении отчетов;
позволял гибко настроить правила обработки статистических файлов.
Как видите, он брал на себя целых четыре задачи, по которым в конечном итоге и было решено разбить его на отдельные сервисы.
Зачем это было нужно? Во-первых, чтобы перенести все на новые рельсы и избавиться от «мертвых» на текущий момент технологий. Во-вторых, чтобы разнести код по отдельным сервисам для более удобной поддержки.
Забегая вперед, скажу, что, на мой взгляд, по итогу у нас так и не получилось «чистых» микросервисов, так как некоторые вещи были слишком дорогими в переписывании, и с ними пришлось мириться. Например, некоторые отчеты строились монструозными процедурами со стороны SQL Server, которые от греха подальше решено было не трогать, и в них могло идти обращение к таблицам с настройками и всем остальным.
Также скажу, что претензий к разработчикам я никаких не имею, так как неизвестно, в каких условиях это все писалось: как обычно бывает, начальству чаще важен скорейший результат, что приводит ко всяким неудачным решениям. Вдобавок, мне неизвестно, какие практики были в моде при написании данного приложения, так что я смотрю на все через призму современных подходов к разработке, и, возможно, что-то из того, что мне кажется сейчас варварством, было в порядке вещей.
Я расскажу вам о процессе переноса отчетов в отдельный сервис и о боли, которую я испытал при выполнении этой задачи, ведь большую часть времени я занимался именно ей.
Часть вторая: укрощение
Итак, было решено все это поставить на рельсы:
ASP.NET Core 2.0 WEB API, что давало возможность без проблем взаимодействовать с любыми технологиями;
Entity Framework Core, поскольку он доступен из коробки, и за многие годы Microsoft его напильником довели до ума;
Angular + TypeScript, так как наш frontend-разработчик работал с этими технологиями, да и в целом Angular — это стильно, модно, молодежно.
Концепция нового проекта была довольно простой:
Каждый отчет должен был строиться в рамках своего билдера, конструктор которого, как правило, требовал:
перечень DataSource, так как данные для отчета могли браться из нескольких источников,
вспомогательные service-классы.
В метод Build, когда нужно было построить отчет, передавался объект, который содержал необходимый набор параметров, нужных для построения, — например, на какую дату строить отчет. Сам метод возвращал объект byte[], который либо отсылался назад пользователю, и он его скачивал браузером, либо выполнялась рассылка на почту.
Так как на тот момент в Entity Framework Core было проблематично использовать SQL-процедуры и вызывать «грязные запросы», которые передались по наследству от старого сервиса (а их приходилось использовать в местах, которые трудно было переписать или сложно быть выразить через EF), была заведена сущность DataSource, которая скрывала внутри себя использование тех или иных запросов;
Результаты отчетов кэшировались в отдельной БД;
Контроллеры должны быть очень простыми, и в этом пришла на помощь библиотека MediatR, которая помогла перенести код в отдельные классы, при этом не создавая сильной связанности;
Разумеется, все зависимости должны были внедряться через конструктора, и для этого стала применяться мощная библиотека autofac;
Для маппинга между Domain, Dal и Api использовалась проверенная библиотека automapper.
С основной концепцией разобрались, и надеюсь, что у вас сложилось понимание, к чему я шел.
«Legacy как коробка шоколадных конфет: никогда не знаешь, какая начинка тебе попадется»
Маршрут был намечен, и я, затаив дыхание, начал открывать проект, психологически готовя себя к самому худшему.
Первое, что предстало моему взору, — монструозный класс, который реализовывал контракт, где, помимо контрактных методов, присутствовали еще и всякие приватные методы, а по названию контрактных методов не всегда можно было догадаться о том, за какой именно отчет отвечает каждый из них, поскольку отсутствовали элементарные комментарии.
Люди, которые разрабатывали данный сервис, уже не работали в компании. Точнее, один работал, и это был мой руководитель, но многие моменты он уже не помнил так отчетливо, как раньше, да и какая-либо документация отсутствовала. Как следствие, мне пришлось заглянуть в Silverlight-проект для того, чтобы сопоставить отчеты.
Открыть старый Silverlight-проект в новенькой Visual Studio 2019 было делом не таким простым, как я думал, скажу я вам, так как поддержка этого типа проектов когда-то была просто выпилена, и он корректно не открывался. Пришлось выйти в Интернет с этим вопросом, ведь не ставить же мне Studio 2013 для того, чтобы зайти и просто посмотреть, что там в документе. Можно было, конечно, воспользоваться Notepad++, но без удобной навигации IDE это было бы не так комфортно. К счастью, решение было найдено в виде отдельного плагина от сообщества, который позволил открыть проект и сделать то, что я планировал: сопоставить контракт с UI и понять, за что тот или иной параметр при построении отвечает.
В первом же отчете я увидел то, что количество статики зашкаливает. Дело в том, что нельзя сразу понять, что требуется тому или иному классу для работы, и смотря на класс, которому в конструкторе передается два каких-то базовых типа, создается обманчивое впечатление, что внутри класса ничего сверхъестественного не происходит. Но потом, когда заглядываешь в сам класс, приходит понимание, что там творится вакханалия — например, работа с БД или запрос к AD. Временами даже доходило до абсурда, когда в конструкторе какого-то класса шло прямое обращение к БД за данными, которому, как мне кажется, там не самое лучшее место.
Подобного рода зависимости нужно было выносить за интерфейсы и передавать через конструктор в новой реализации отчета. К счастью, между отчетами использовался примерно одинаковый набор статики, и, уже перенеся несколько отчетов, я имел почти весь пул необходимых сервисов.
В целом статика оказалась не такой уж и большой проблемой, ведь там выполнялся вполне конкретный и ограниченный набор действий: контекст доступа к БД, доступ к AD и т, д. В остальном коде было много участков, которые так и порождали в голове вопросы в духе: «Зачем это нужно именно в этом классе?», — что приходилось также выносить во вспомогательные классы-сервисы.
Вишенкой на торте были LINQ-выражения, которые занимали несколько экранов моего монитора. Их пришлось разбирать по кусочкам, присваивая промежуточные операции переменным, чтобы было понимание происходящего.
Помимо этого, в коде временами встречались пугающие комментарии, которые предостерегали: «Если кто-то захочет поменять <<ClassName>>, то лучше его отговорить от этого».
Но я не мог предполагать, что меня ждут еще более страшные вещи: большое количество бизнес-логики в хранимых процедурах, Views и прочих объектах SQL Server. Вы когда-нибудь видели, как из MS SQL Server напрямую обращаются к Active Directory через OpenQuery, чтобы получить список пользователей для фильтрации людей, которые должны или не должны попасть в отчет, или что делается через xp_cmdshell? Так вот, я увидел эту воочию. Кроме того, было много самописных CLR-функций на C#, которые выполняли тоже какую-то особую логику — например, алгоритм, который вычисляет, насколько строка А соответствует строке Б. Но и это еще не все. Были монструозные SQL-процедуры — строк на 500, если не больше, — с множеством вложенных подзапросов с различными типа соединений, также попадались обращения ко вьюхам и прочим вещам.
Справиться с бизнес-логикой было крайне тяжело на стороне SQL Server, поэтому пришлось пойти на компромиссы: что понятно и может быть выражено на стороне новой службы в виде какого-то объекта (например, доступ к АД) или понятным EF, рефакторилось, а с остальным в той или иной мере пришлось мириться и оставлять как есть.
Финальным штрихом было уйти от использования платной библиотеки, которая предоставлялась devexpress, в сторону использования нашей внутренней библиотеки на основе OpenXML. А больно было из-за того, что нужно было добиться аналогичного результата, который давался оригинальным сервисом как визуально, так и по цифрам.
С визуалом пришлось повозиться: из-за разности библиотек нужно было выразить старый рендер через методы новой либы. Тут цвета не такие — подгоняем, тут отступы и выравнивание хромают — подгоняем снова. С цифрами тоже пришлось несладко, так как они временами ощутимо не бились. Лезешь сверять код формирования Excel, и если там обнаруживается ошибка, выполняешь правку в надежде, что этого будет достаточно. Все равно данные не бьются? Тогда запускай две визуалки (одна со старым сервисом, а другая — с новым) и выполняй дебаг по шагам, пытаясь найти причину расхождения.
И так раз за разом: предполагаешь, что расхождение происходит в этом месте и ставишь брейк, смотришь на цифры. Предполагаемое место не выстрелило? Значит проблема где-то в другом месте. Снова ставишь брейк где-то выше или ниже относительно последнего, и так до тех пор, пока не найдешь причину расхождения.
Доходило до того, что новый сервис, когда были расхождения, считал все правильнее, чем старый. Например, раньше была багулина, которая неправильно рассчитывала премию, и кто-то получал больше, чем положено, а кто-то меньше.
Стоит ли мне говорить, что это все сильно выматывало?
Каждый день встаешь, как на каторгу. Завершаешь работу с одним отчетом, начинаешь переносить другой, при изучении кода со словами: «Матерь божья…»
Хватаешься за голову и начинаешь ходить по комнате, пытаясь взять себя в руки, образно говоря.
Как не сойти с ума после такого? Да просто не засиживаться допоздна, а накопившийся негатив в течении дня понижать посредством занятия любимым делом — хобби.
В заключение
«Все отчеты были переписаны. Последний тап поставил жирную точку в этой истории. Я убрал пальцы с клавиатуры — все было кончено».
По итогу получилось сделать так, что проект стал чище и современнее — но, тем не менее, что-то решено было оставить в первоначальном виде. Схематично это выглядело как-то так с учетом других сервисов, которые были вынесены из монолита:
На схеме не обозначено взаимодействие сервисов, так как я его, к сожалению, не помню. Но видно самое главное: монолит был раздроблен на независимые блоки.
Что я усвоил, работая над этим проектом?
Корпоративный проект любой сложности можно переписать, как бы страшен он ни был;
Иногда приходится идти на компромиссы в силу сжатых сроков и нехватки ресурсов — и что-то оставлять в неизменном, пусть и слегка переработанном виде;
Правило «работает — не трогай» далеко не универсально: то, что работало годами, могло делать это неправильно;
Старые проекты могут быть хорошими учителями в том, почему те или иные практики сейчас уже не используются, даже если по вашим воспоминаниям кажется, что раньше было лучше;
Важно не закапываться в работу и в свободное время переключаться на что-то другое — соблюдать work-life balance.
Я приобрел ценный опыт работы по переносу старых проектов на новые рельсы, так как до этого если и прикасался к legacy, это были лишь небольшие доработки, и теперь знаю, чего можно ожидать от таких проектов.