Модернизация унаследованного кода в производстве — перестройка самолета в середине полета без аварии

Я провел более десяти лет в качестве консультанта, работая с десятками компаний во многих областях и сферах деятельности. Разнообразие кодовой базы каждой из них огромно. В этой статье мы попытаемся определить общие правила модернизации унаследованного кода, которые, надеюсь, будут применимы ко всем. Но она написана с точки зрения разработчика Java.

Когда я писал эту статью, я в первую очередь сосредоточился на обновлении старого J2EE кода в стиле Java 6 до более современного кода Spring Boot/Jakarta EE. Однако я не хочу углубляться в код и пытаюсь сохранить общий характер. Я также обсуждаю COBOL и подобные унаследованные системы. Большинство общих рекомендаций должно работать и при переносе любого другого типа кодовой базы.

Переписывание проекта, по большому счету, не представляет собой большой проблемы — однако сделать это в то время, когда пользователи активно работают с существующей системой без перебоев в обслуживании?

Это требует большого планирования и координации.

Зачем модернизировать?

Я не думаю, что мы должны обновлять проекты ради «новейшего и лучшего». Есть причина, по которой распространенные унаследованные системы, такие как COBOL, все еще используются. Ценный код не теряет своего блеска только из-за возраста. Многое можно сказать о «коде, который работает». Особенно если он был создан сотнями разработчиков несколько десятилетий назад. В нем много скрытых знаний о модели бизнес-логики…

Однако сопровождение часто может стать узким местом. Вам может понадобиться добавлять функции, что делает процесс несостоятельным. Трудно найти что-то в миллионах строк кода. Возможность использовать более новые возможности может стать последним решающим фактором. Возможно, благодаря новым фреймворкам и инструментам можно создать аналогичный проект без тех же сложностей.

Не стоит легкомысленно относиться к решению о переделке существующего кода, который находится в производстве. Необходимо составить план, оценить риски и предусмотреть возможность отступления.

Другие причины включают безопасность, масштабируемость, окончание срока службы систем, на которые мы полагаемся, отсутствие квалифицированных инженеров и т.д.

Обычно не стоит мигрировать ради лучшего инструментария, но лучшая наблюдаемость, оркестровка и т.п. являются огромным преимуществом.

Модернизация дает вам возможность переосмыслить первоначальный дизайн системы. Однако это рискованное предложение, поскольку довольно легко ввести тонкие поведенческие различия.

Вызовы

Прежде чем мы приступим к подготовке, необходимо рассмотреть и устранить несколько глубоких проблем.

Доступ к исходному коду унаследованной системы

Иногда исходный код унаследованной кодовой базы уже не пригоден для работы. Это может означать, что мы не можем добавить даже базовые возможности/функциональность в исходный проект. Это может произойти по многим причинам (юридическим или техническим) и затруднить миграцию. Незнакомый код является огромной проблемой и сделает миграцию сложной, хотя и возможной.

Очень часто для плавной миграции необходимо раскрыть внутренние вызовы в унаследованной системе. Например, мы можем обеспечить возможность отступления, проверяя унаследованную систему. В старом продукте, над которым я работал, была собственная аутентификация. Чтобы сохранить совместимость во время миграции, мы использовали специальный веб-сервис. Если аутентификация пользователя не удавалась на текущем сервере, система проверяла старый сервер, чтобы обеспечить «бесшовный» опыт.

Это важно на этапе миграции, но не всегда может сработать. Если у нас нет доступа к старому коду, такие инструменты, как скраппинг, могут быть единственным средством для обеспечения идеальной обратной совместимости в период миграции.

Иногда источник больше недоступен или был утерян. Это усложняет подготовку.

Невозможность изолировать унаследованную систему

Для анализа унаследованной системы нам нужна возможность запустить ее в изоляции, чтобы мы могли протестировать ее и проверить ее поведение. Это распространенная и важная практика, но она не всегда возможна.

Например, кодовая база COBOL, работающая на специализированном оборудовании или операционной системе. Изолировать такую среду может быть сложно.

Это, вероятно, самая большая проблема/задача, с которой вы можете столкнуться. Иногда здесь может помочь внешний подрядчик с опытом работы в данной области. Если это так, то это стоит каждого пенни!

Другим обходным путем является создание арендатора для тестирования. Например, если система управляет начислением заработной платы, создайте фиктивного сотрудника для тестирования и выполняйте рассмотренные ниже задачи на производстве. Это представляет собой огромную опасность и проблему, поэтому данная ситуация далека от идеала, и принимать ее следует только в том случае, если нет другого выхода.

Нестандартные форматы и пользовательские хранилища

Некоторые унаследованные системы могут опираться на глубоко исторические подходы к кодированию. Отличным примером является COBOL. В нем числа хранились в зависимости от их формы и ближе к BCD (ближайший пример — BigDecimal из Java). Это не плохо. Для финансовых систем это действительно правильный путь. Но это может привести к несовместимости при обработке числовых данных, что может помешать параллельной работе систем.

Хуже того, COBOL имеет сложное решение для хранения файлов, которое не является стандартной базой данных SQL. Переход от чего-то подобного (или даже от некоторых новых нишевых систем) может оказаться сложной задачей. К счастью, существуют решения, но они могут ограничить практичность параллельной работы как старого, так и нового продукта.

Подготовка

Прежде чем даже рассматривать подобные начинания, необходимо оценить и подготовиться к миграции. Миграция будет болезненной независимо от того, что вы делаете, но этот этап позволяет вам уменьшить размер пластыря, который необходимо снять.

Существует множество общих правил и настроек, которым необходимо следовать перед миграцией кода. С каждым из них вы должны быть хорошо знакомы.

Извлечение функций

Когда у нас есть давно существующая система, практически невозможно отследить каждую функцию, которая в ней есть, и роль, которую она играет в конечном продукте. Существуют документы, но их трудно читать и просматривать. Трекеры проблем отлично подходят для отслеживания, но они не являются отличными картами.

Обнаружить функции в системе и те, которые «реально используются», проблематично. Особенно когда мы хотим сосредоточиться на мелочах. Нам нужна каждая мелкая деталь. Это не всегда возможно, но если вы можете использовать инструменты наблюдаемости, чтобы указать, что используется, это очень поможет. Перенос того, что не используется, вызывает разочарование, и мы хотели бы избежать этого, если возможно.

Это не всегда практично, поскольку большинство инструментов наблюдаемости, предоставляющих очень тонкие детали, разработаны для более новых платформ (например, Java, Python, Node и т.д.). Но если у вас есть такая платформа, например, старый проект J2EE. Использование такого инструмента, как Lightrun, и размещение счетчика на определенной строке может сказать вам, что используется, а что, возможно, нет. Я расскажу об этом ниже.

Я часто использую электронную таблицу, в которой мы перечисляем каждую функцию и незначительное поведение. Эти таблицы могут быть огромными, и мы можем разделить их на подмодули. Это процесс, который может занять несколько недель. Изучение кода, документации и использования. Затем итерации с пользователями приложения, чтобы убедиться, что мы не упустили важную функцию.

На этом этапе легко срезать углы. Вы можете заплатить за них позже. Бывали случаи, когда я поручал выполнение этого требования младшему разработчику программного обеспечения, не проанализировав должным образом результат. В итоге я пожалел об этом, поскольку были случаи, когда мы упускали нюансы в документации или коде.

Тесты на соответствие

Это самый важный аспект для процесса миграции. Хотя модульные тесты — это хорошо, тесты на соответствие и интеграционные тесты имеют решающее значение для процесса миграции.

Нам необходимо извлечение функций для проверки соответствия. Нам нужно просмотреть каждую функцию и поведение унаследованной системы и написать общий тест, который проверяет это поведение. Это важно как для проверки нашего понимания кода, так и для подтверждения того, что документация действительно верна.

Как только у нас будут тесты соответствия, проверяющие существующую унаследованную систему, мы сможем использовать их для проверки совместимости новой кодовой базы.

Основная проблема заключается в написании кода, который можно использовать в двух совершенно разных системах. Например, если вы собираетесь изменить пользовательский интерфейс, адаптировать эти тесты будет непросто.

Я бы рекомендовал писать тесты с помощью внешнего инструмента, возможно даже с использованием разных языков программирования. Это побуждает вас думать о внешних интерфейсах, а не о проблемах, связанных с языком/платформой. Это также помогает обнаружить «странные» проблемы. Например, у нас были некоторые несовместимости из-за незначительных различий в реализации протокола HTTP между новой и старой системой.

Я также предлагаю использовать совершенно отдельный «тонкий» адаптер для различий в пользовательском интерфейсе. Сами тесты должны быть идентичными при выполнении на старой и текущей кодовой базе.

Процесс, который мы используем для составления тестов, заключается в открытии проблемы в трекере проблем для каждой функции/поведения в электронной таблице из предыдущего шага. Как только это сделано, мы окрашиваем строку таблицы в желтый цвет.

Как только мы интегрируем тест и проблема будет закрыта, мы окрасим строку в зеленый цвет.

Обратите внимание, что нам по-прежнему необходимо тестировать элементы изолированно с помощью модульных тестов. Тесты на соответствие помогают проверить совместимость. Юнит-тесты проверяют качество, а также выполняются гораздо быстрее, что важно для производительности.

Покрытие кода

Инструменты покрытия кода могут быть недоступны для вашей унаследованной системы. Однако если они есть, их необходимо использовать.

Один из лучших способов проверить, что ваши тесты на соответствие требованиям достаточно обширны, — это использование этих инструментов. Вам необходимо проводить проверку кода по каждому отчету о покрытии. Нужно проверять каждую строку или утверждение, которые не покрыты, чтобы убедиться, что нет скрытой функциональности, которую мы пропустили.

Запись и резервное копирование

Если это возможно, запишите сетевые запросы к текущему серверу для тестирования. Вы можете использовать резервную копию текущей базы данных и записанные запросы для создания интеграционного теста «реального использования» новой версии. Используйте живые данные как можно чаще во время разработки, чтобы избежать неожиданностей в производстве.

Это может оказаться невозможным. Ваша живая база данных может быть ограничена в доступе или слишком велика для использования во время разработки. Очевидно, что существуют проблемы конфиденциальности и безопасности, связанные с записью сетевого трафика, поэтому это применимо только тогда, когда это действительно можно сделать.

Масштабирование

Одним из преимуществ переноса существующего проекта является то, что у нас есть отличное чувство масштаба. Мы знаем трафик. Мы знаем объем данных и понимаем бизнес-ограничения.

Но мы не знаем, сможет ли новая система справиться с пиковой нагрузкой, которая нам требуется. Нам нужно извлечь эти детали и создать стресс-тесты для критических частей системы. Нам нужно проверить производительность, в идеале сравнить ее с традиционной системой, чтобы убедиться, что мы не вернемся назад в плане производительности.

Цели

Какие части мы должны мигрировать и каким образом?

Что мы должны сделать в первую очередь и как расставить приоритеты?

Аутентификация и авторизация

Во многих старых системах модули авторизации являются частью монолитного процесса. Это сделает миграцию сложной независимо от выбранной стратегии. Миграция также является отличной возможностью освежить эти старые концепции и внедрить более безопасный/масштабируемый подход к авторизации.

Обычная стратегия в подобных случаях — отправить пользователя «зарегистрироваться снова» или «перенести свои учетные записи», когда ему нужно использовать новую систему. Это утомительный процесс для пользователей, который вызовет множество проблем с поддержкой, например, «Я пытался сбросить пароль, но ничего не получилось». Такие сбои могут произойти, когда пользователь старой системы не выполнил миграцию и пытается сбросить пароль в новой системе. Существуют обходные пути, например, явное обнаружение такого конкретного случая и беспрепятственное перенаправление на «процесс миграции». Но на данном этапе следует ожидать трения.

Однако преимущество разделения аутентификации и авторизации поможет в будущих миграциях и модульности. Пользовательские данные в общей базе данных обычно являются одной из самых сложных вещей для миграции.

База данных

Когда мы имеем дело с унаследованной системой, мы можем реализовать новую версию поверх существующей базы данных. Это распространенный подход, который имеет ряд преимуществ:

  • Мгновенная миграция — это, пожалуй, самое большое преимущество. Все данные уже находятся в новой системе с нулевым временем простоя.
  • Простота — это, вероятно, один из самых простых подходов к миграции, и вы можете использовать существующие данные «реального мира» для тестирования новой системы перед запуском.

Есть и несколько серьезных недостатков:

  • Загрязнение данных — в новую систему могут быть вставлены проблемные данные, что приведет к поломке старой системы и сделает возврат невозможным. Если вы планируете поэтапную миграцию, когда старая и новая системы работают параллельно, это может стать проблемой.
  • Проблемы с кэшем — если обе системы работают параллельно, кэширование может привести к их несогласованному поведению.
  • Сохраняющиеся ограничения — при этом ограничения старой системы переносятся на новую систему.

Если система хранения данных достаточно современная и мощная, то такой подход к миграции данных имеет смысл. Он устраняет или, по крайней мере, откладывает проблемную часть процесса миграции.

Кэширование

Следующие три совета лежат в основе производительности приложений. Если вы правильно их выполните, ваши приложения будут работать быстро:

  1. Кэширование
  2. Кэширование
  3. Кэширование

Вот и все. Однако очень немногие разработчики используют кэширование в достаточном объеме. Это связано с тем, что правильное кэширование может быть очень сложным и нарушать принцип единого источника знаний. Оно также затрудняет миграцию, о чем говорилось в предыдущем разделе.

Отключение кэширования на время миграции может оказаться нереальным вариантом, но уменьшение объема хранения может смягчить некоторые проблемы.

Стратегия

Существует несколько способов решения проблемы крупномасштабной миграции. Мы можем рассмотреть «общую картину» миграции, например, переход от монолита к микросервисам. Но чаще всего в процессе возникают более тонкие различия.

Я пропущу очевидный «полный рерайт», когда мы мгновенно заменяем старый продукт на новый. Я думаю, это довольно очевидно, и мы все понимаем риски/последствия.

Модуль за модулем

Если вы можете выбрать эту стратегию и медленно заменять отдельные части старого кода новыми модулями, то это идеальный путь. Это также один из самых больших аргументов в пользу микросервисов.

Этот подход может хорошо работать, если все еще существует команда, которая управляет и обновляет унаследованный код. Если такой команды нет, у вас могут возникнуть серьезные проблемы с этим подходом.

Одновременное развертывание

Это может сработать при развертывании общей базы данных. Мы можем развернуть новый продукт на отдельном сервере, при этом оба продукта будут использовать одну и ту же базу данных, как упоминалось выше. С этим подходом связано много проблем, но я часто выбираю его, поскольку он, вероятно, самый простой для начала.

Поскольку старый продукт все еще доступен, для существующих пользователей существует обходной путь смягчения последствий. Часто рекомендуется планировать время простоя для старых серверов, чтобы заставить существующих пользователей мигрировать. В противном случае, в этом сценарии вы можете столкнуться с пользователями, которые откажутся переходить на новый продукт.

Скрытое развертывание

В этой стратегии мы скрываем существующий продукт от общественности и устанавливаем на его место новую систему. Чтобы облегчить миграцию, новый продукт запрашивает старый продукт на предмет недостающей информации.

Например, если пользователь попытался войти в систему, но не зарегистрировался в ней, код может запросить старую систему для беспрепятственной миграции пользователя. Это непросто и в идеале требует внесения некоторых изменений в старый код.

Огромное преимущество заключается в том, что мы можем перенести базу данных, сохранив совместимость и не перенося все данные одним махом.

Основной недостаток заключается в том, что это может увековечить существование унаследованного кода. В результате это может пойти вразрез с нашими целями развития.

Реализация

Вы закончили писать код. Мы готовы нажать на курок и осуществить миграцию… Теперь нам нужно сообщить пользователям, что миграция состоится. Вы же не хотите, чтобы разгневанный клиент жаловался, что что-то внезапно перестало работать.

Репетиция

Если возможно, проведите пробный запуск и подготовьте сценарий для процесса миграции. Когда я говорю «сценарий», я не имею в виду код. Я имею в виду сценарий обязанностей и задач, которые необходимо выполнить.

Нам нужно проверить, что все работает, когда миграция завершается. Если что-то сломалось, должен быть сценарий, чтобы все отменить. Лучше отступить, чтобы переделать все в другой день. Я бы предпочел иметь миграцию, которая не удалась на ранней стадии и которую мы можем «отменить», чем иметь что-то «наполовину готовое» в производстве.

Кто?

По моему мнению, для фактического развертывания перенесенного программного обеспечения следует использовать небольшую команду. Слишком большое количество людей может привести к путанице. Вам нужен следующий персонал:

  • IT/OPS — для выполнения развертывания и, при необходимости, возврата к исходному состоянию.
  • Служба поддержки — для решения вопросов и проблем пользователей. Поднимать флаги в случае, если пользователь сообщает о критической ошибке.
  • Разработчики — чтобы выяснить, есть ли проблемы с развертыванием, связанные с кодом.
  • Менеджер — нам нужен человек с правом мгновенного принятия решений. Никто не хочет тянуть с развертыванием. Нам нужен человек, который понимает, что стоит на кону для компании.

Существует тенденция исправлять код, чтобы провести миграцию. Это хорошо подходит для небольших стартапов, и я сам в этом виноват. Но если вы работаете в масштабах компании, то так поступать нельзя. Изменение кода, сделанное «на месте», не сможет пройти тесты и может принести ужасные проблемы. Скорее всего, это плохая идея.

Когда?

Аксиома «не внедряйте в пятницу» может быть ошибочной в данном случае. Я считаю, что пятница — это отличный период миграции, когда я готов пожертвовать выходными. Разумеется, я не выступаю за то, чтобы заставлять людей работать в выходные. Но если есть заинтересованность в этом (в обмен на отпуск), то дни с низким трафиком — идеальное время для внесения серьезных изменений.

Если вы работаете в нескольких часовых поясах, то для переноса лучше всего подойдут разработчики в наименее активном часовом поясе. Я бы посоветовал иметь команды во всех часовых поясах, чтобы отслеживать возможные последствия.

Оперативность в таких ситуациях имеет решающее значение. Быстрое реагирование на изменения может сделать разницу между отменой развертывания и продолжением работы.

Поэтапное развертывание

При небольших обновлениях мы можем поэтапно развертывать наши релизы и распространять обновления на подмножество пользователей. К сожалению, когда мы делаем крупные изменения, я считаю это скорее помехой. Источник ошибок становится сложнее определить, если у вас работают обе системы. Обе системы должны работать одновременно, и это может вызвать дополнительные трудности.

После миграции

Прошла пара недель, все успокоилось, и миграция сработала. В конце концов.

Что теперь?

Пенсионный план

В процессе миграции мы взяли с собой большой набор функций из наследия. Некоторые из них нам, вероятно, нужны, а другие могут оказаться ненужными. После завершения развертывания нам нужно определиться с планом выхода на пенсию. Какие функции, пришедшие из наследия, должны быть выведены из эксплуатации и каким образом?

Мы можем легко определить, используем ли мы определенный метод или он не используется в коде. Но используют ли пользователи конкретную строку кода? Конкретную функцию?

Для этого у нас есть наблюдаемость.

Мы можем вернуться к электронной таблице извлечения функций и просмотреть каждую потенциальную функцию. Затем используйте системы наблюдаемости, чтобы узнать, сколько пользователей задействуют ту или иную функцию. Мы можем легко сделать это с помощью таких инструментов, как Lightrun, поместив метрику счетчика в код (вы можете бесплатно скачать его здесь). Согласно этой информации, мы можем начать сужать круг функций, используемых продуктом. Я уже обсуждал это ранее, поэтому, возможно, это будет не так применимо, если эта функциональность работала в унаследованной системе.

Еще более важным является выход на пенсию работающего наследия. Если вы выбрали путь миграции, при котором унаследованная реализация все еще работает, то сейчас самое время решить, когда ее отключить. Помимо затрат, проблемы с безопасностью и обслуживанием делают это нецелесообразным в долгосрочной перспективе. Обычная стратегия заключается в периодическом отключении старой системы на час для обнаружения зависимостей/использования, о которых мы могли не знать.

Такие инструменты, как сетевые мониторы, также могут помочь определить уровень использования. Если у вас есть возможность редактировать унаследованную систему или прокси-сервер для нее, это время для сбора данных об использовании. Определите пользователей, которые все еще зависят от этого, и спланируйте кампанию/процесс электронной почты для их перевода.

Используйте инструментарий, чтобы избежать будущего наследия

Современная система может использовать многие из новых возможностей, имеющихся в нашем распоряжении. Процессы CI/CD включают в себя сложные лайнеры, которые обнаруживают проблемы безопасности, ошибки и выполняют проверку, значительно превосходящую человеческие аналоги. Инструмент качества кода может существенно повлиять на сопровождаемость проекта.

Ваш продукт должен использовать эти новые инструменты, чтобы он не вернулся к статусу устаревшего кода. Патчи безопасности доставляются «без проблем» в виде запросов на исправление. Изменения проходят неявные проверки для устранения распространенных ошибок. Это облегчает долгосрочное сопровождение.

Поддержание тестирования на соответствие

После процесса миграции люди часто выбрасывают тесты на соответствие. Имеет смысл преобразовать их в интеграционные тесты, если это возможно/необходимо, но если у вас уже есть интеграционные тесты, они могут быть избыточными и более сложными в сопровождении, чем стандартное тестирование.

То же самое верно и для электронной таблицы извлечения функций. Это не то, что можно поддерживать, и это только инструмент на период миграции. Как только мы закончим с этим, мы должны отбросить ее и не считать ее авторитетной.

Наконец,

Миграция старого кода — это всегда сложная задача, поэтому в этом деле очень важна гибкая практика. В этом процессе так много подводных камней и так много точек отказа. Это особенно актуально, когда система находится в производстве и миграция является значительной. Я надеюсь, что этот список советов и подходов поможет направить ваши усилия по разработке.

Я думаю, что боль в этом процессе неизбежна. Как и некоторые неудачи. Наши инженерные команды должны быть гибкими и быстро реагировать на такие случаи. Обнаруживать потенциальные проблемы и быстро решать их в ходе процесса. Я мог бы еще многое сказать по этому поводу, но я хочу, чтобы это было достаточно общим и применимо к более широкому кругу случаев. Если у вас есть мысли по этому поводу, свяжитесь со мной в twitter и дайте мне знать.

Оцените статью
Procodings.ru
Добавить комментарий