Модернизация устаревшего кода в рабочей среде
12 мая 2022 г.Я провел более десяти лет в качестве консультанта, работая в десятках компаний во многих областях и областях. Разнообразие каждой кодовой базы огромно. В этой статье мы попытаемся определить общие правила модернизации устаревшего кода, которые, как мы надеемся, применимы ко всем. Но это происходит с точки зрения разработчика Java.
При написании этого я основное внимание уделяю обновлению старого кода J2EE в стиле эпохи Java 6 до более современного кода Spring Boot/Jakarta EE. Однако я не хочу углубляться в код и пытаться сохранить его универсальным. Я также обсуждаю COBOL и подобные устаревшие системы. Большинство всеобъемлющих рекомендаций должны работать и при миграции кодовой базы любого другого типа.
В большинстве случаев переписать проект не так уж сложно, однако делать это, пока пользователи активно работают с существующей системой без прерывания обслуживания? Это требует тщательного планирования и координации.
Зачем модернизировать?
Я не думаю, что мы должны обновлять проекты ради «самых последних и лучших». Есть причина, по которой распространенные устаревшие системы, такие как COBOL, все еще используются. Ценный код не теряет своего блеска только из-за возраста. Многое можно сказать о «коде, который работает». Особенно, если он был построен сотнями разработчиков несколько десятков лет назад. Там много скрытых знаний о модели бизнес-логики…
Однако техническое обслуживание часто может стать узким местом. Возможно, вам потребуется добавить функции, делающие процесс несостоятельным. Трудно найти что-то в миллионах строк кода. Способность использовать новые возможности может стать решающим фактором. Возможно, удастся создать аналогичный проект без таких же сложностей благодаря более новым фреймворкам и инструментам.
Мы не должны легкомысленно пересматривать существующий код, находящийся в производстве. Вам нужно создать план, оценить риски и найти способ отступить.
Другие причины включают безопасность, масштабируемость, окончание срока службы систем, на которые мы полагаемся, отсутствие квалифицированных инженеров и т. д.
Обычно вы не должны мигрировать для улучшения инструментов, но для лучшей наблюдаемости, оркестровки и т. д. Это огромное преимущество.
Модернизация дает вам возможность переосмыслить первоначальный дизайн системы. Тем не менее, это рискованное предложение, поскольку оно позволяет довольно легко ввести тонкие поведенческие различия.
Вызовы
Прежде чем мы начнем подготовку, нам нужно рассмотреть и смягчить несколько серьезных проблем.
Доступ к устаревшему исходному коду
Иногда исходный код устаревшей кодовой базы больше не работает. Это может означать, что мы не можем добавить даже базовые функции/функции в исходный проект. Это может произойти по многим причинам (юридическим или техническим) и усложнит миграцию. Незнакомый код — огромная проблема, и миграция может быть сложной, хотя и возможной.
В устаревшей системе очень часто выставляются внутренние вызовы, чтобы обеспечить плавную миграцию. Для например. мы можем предоставить резервные возможности, сверившись с устаревшей системой. В старом продукте, над которым я работал, была собственная внутренняя аутентификация. Чтобы сохранить совместимость во время миграции, мы использовали специальный веб-сервис. Если аутентификация пользователя на текущем сервере не удалась, система сверилась со старым сервером, чтобы обеспечить «бесшовную» работу.
Это важно на этапе миграции, но не всегда работает. Если у нас нет доступа к унаследованному коду, такие инструменты, как парсинг, могут оказаться единственным средством обеспечения идеальной обратной совместимости в период миграции.
Иногда источник больше недоступен или был утерян. Это усложняет подготовку.
Невозможно изолировать устаревшую систему
Чтобы проанализировать унаследованную систему, нам нужна возможность запустить ее изолированно, чтобы мы могли протестировать ее и проверить ее поведение. Это распространенная и важная практика, но не всегда возможная.
Например. кодовая база COBOL, работающая на специальном оборудовании или в операционной системе. Изолировать такую среду может быть сложно.
Это, вероятно, самая большая проблема / вызов, с которым вы можете столкнуться. Иногда здесь может помочь внешний подрядчик с опытом работы в предметной области. Если это так, то это стоит каждой копейки!
Другой обходной путь — настроить клиент для тестирования. Например. если система управляет расчетом заработной платы, настройте фальшивого сотрудника для тестирования и выполните описанные ниже задачи против производства. Это огромная опасность и проблема, поэтому эта ситуация далека от идеальной, и мы должны идти на это, только если не существует другого выхода.
Нестандартные форматы и пользовательские хранилища
Некоторые устаревшие системы могут опираться на глубоко исторические подходы к кодированию. Отличным примером является COBOL. В нем они хранили числа в зависимости от их формы и ближе к двоично-десятичному (BigDecimal в Java — самый близкий пример). Это неплохо. Для финансовых систем это на самом деле правильный путь. Но это может привести к несовместимости при обработке числовых данных, что может помешать параллельной работе систем.
Хуже того, у COBOL есть сложное решение для хранения файлов, которое не является стандартной базой данных SQL. Отказ от чего-то подобного (или даже от некоторых более новых нишевых систем) может оказаться сложной задачей. К счастью, есть решения, но они могут ограничить практичность параллельной работы как старого, так и нового продукта.
Подготовка
Прежде чем нам нужно будет даже рассмотреть попытку такого типа, нам нужно оценить и подготовиться к миграции. Миграция будет болезненной независимо от того, что вы делаете, но этот этап позволяет вам уменьшить размер пластыря, который вам нужен.
Существует множество общих правил и настроек, которым необходимо следовать перед переносом кода. С каждым из них вам нужно хорошо познакомиться.
Извлечение признаков
Когда у нас есть давно работающая устаревшая система, почти невозможно отслеживать каждую ее функцию и роль, которую она играет в конечном продукте. Документы есть, но их тяжело читать и просматривать при рецензировании. Трекеры проблем отлично подходят для отслеживания, но они не являются отличными картами.
Обнаружение функций в системе и тех, которые «действительно используются», проблематично. Особенно, когда мы хотим сосредоточиться на мелочах. Нам нужна каждая маленькая деталь. Это не всегда возможно, но если вы можете использовать инструменты наблюдаемости, чтобы указать, что используется, это очень поможет. Миграция того, что не используется, разочаровывает, и мы хотели бы избежать этого, если это возможно.
Это не всегда практично, так как большинство инструментов наблюдения, предоставляющих очень мелкие детали, предназначены для более новых платформ (например, Java, Python, Node и т. д.). Но если у вас есть такая платформа, как старый проект J2EE. Использование такого инструмента, как Lightrun, и размещение счетчика в определенной строке может сказать вам, что используется, а что, вероятно, нет. Я обсуждаю это ниже.
Я часто использую электронную таблицу, в которой мы перечисляем каждую функцию и второстепенное поведение. Эти электронные таблицы могут быть огромными, и мы можем разделить их на основе подмодулей. Это процесс, который может занять недели. Просматриваем код, документацию и использование. Затем повторяем с пользователями приложения, чтобы убедиться, что мы не пропустили важную функцию.
На этом этапе легко срезать углы. Вы можете заплатить за них позже. Были времена, когда я назначал это требование младшему разработчику программного обеспечения, не просматривая результат должным образом. В итоге я пожалел об этом, так как были случаи, когда мы упускали нюансы в документации или коде.
Тесты на соответствие
Это наиболее важный аспект процесса миграции. Хотя модульные тесты хороши, тесты соответствия и интеграции имеют решающее значение для процесса миграции.
Нам нужно извлечение признаков для соответствия. Нам нужно просмотреть каждую функцию и поведение устаревшей системы и написать общий тест, который проверяет это поведение. Это важно как для проверки нашего понимания кода, так и для проверки правильности документации.
Как только у нас будут тесты соответствия, которые проверяют существующую устаревшую систему, мы можем использовать их для проверки совместимости новой кодовой базы.
Фундаментальной задачей является написание кода, который можно запускать в двух совершенно разных системах. Например. Если вы намерены изменить пользовательский интерфейс, адаптировать эти тесты будет сложно.
Я бы рекомендовал писать тесты с помощью внешнего инструмента, возможно даже с использованием разных языков программирования. Это побуждает вас думать о внешних интерфейсах, а не о проблемах, связанных с языком/платформой. Это также помогает в обнаружении «странных» проблем. Например. У нас были некоторые несовместимости из-за незначительных различий в реализации протокола HTTP между новой и устаревшей системами.
Я также предлагаю использовать совершенно отдельный «тонкий» адаптер для различий пользовательского интерфейса. Сами тесты должны быть идентичны при работе с устаревшей и текущей кодовой базой.
Процесс, который мы используем для создания тестов, заключается в том, чтобы открыть проблему в средстве отслеживания проблем для каждой функции/поведения в электронной таблице из предыдущего шага. Как только это будет сделано, мы окрашиваем строку электронной таблицы в желтый цвет.
Как только мы интегрируем тест и проблема закрыта, мы окрашиваем строку в зеленый цвет.
Обратите внимание, что нам по-прежнему нужно тестировать элементы изолированно с помощью модульных тестов. Тесты соответствия помогают проверить совместимость. Модульные тесты проверяют качество, а также выполняются намного быстрее, что важно для производительности.
Покрытие кода
Инструменты покрытия кода могут быть недоступны для вашей устаревшей системы. Однако, если они есть, вам нужно их использовать.
Один из лучших способов убедиться, что ваши тесты на соответствие требованиям достаточно обширны, — использовать эти инструменты. Вы должны делать обзоры кода для каждого отчета о покрытии. Мы должны проверить каждую строку или оператор, которые не покрыты, чтобы убедиться, что нет скрытых функций, которые мы пропустили.
Запись и резервное копирование
Если есть возможность, записывайте сетевые запросы к текущему серверу для тестирования. Вы можете использовать резервную копию текущей базы данных и записанных запросов для создания интеграционного теста «реального использования» для новой версии. Максимально используйте оперативные данные во время разработки, чтобы избежать неожиданностей в рабочей среде.
Это может быть несостоятельно. Доступ к вашей действующей базе данных может быть ограничен или она может быть слишком большой для использования во время разработки. Очевидно, что есть проблемы с конфиденциальностью и безопасностью, связанные с записью сетевого трафика, поэтому это применимо только тогда, когда это действительно можно сделать.
Шкала
Одна из замечательных особенностей переноса существующего проекта заключается в том, что у нас есть прекрасное чувство масштаба. Мы знаем трафик. Мы знаем объем данных и понимаем ограничения бизнеса.
Чего мы не знаем, так это того, сможет ли новая система справиться с необходимой нам пиковой нагрузкой. Нам нужно извлечь эти детали и создать стресс-тесты для критических частей системы. Нам нужно проверить производительность, в идеале сравнить ее с устаревшей версией, чтобы убедиться, что мы не вернемся назад с точки зрения производительности.
Цели
Какие части мы должны мигрировать и каким образом?
На что мы должны ориентироваться в первую очередь и как нам расставить приоритеты в этой работе?
Аутентификация и авторизация
Многие старые системы встраивают модули авторизации как часть монолитного процесса. Это сделает вашу миграцию сложной, независимо от стратегии, которую вы выберете. Миграция также является отличной возможностью освежить эти старые концепции и внедрить более безопасный/масштабируемый подход к авторизации.
Распространенной стратегией в подобных случаях является отправка пользователю «зарегистрироваться снова» или «перенести свои учетные записи», когда ему нужно использовать новую систему. Это утомительный процесс для пользователей, который вызовет множество проблем со службой поддержки, например. «Я пытался сбросить пароль, и это не сработало». Такого рода сбои могут произойти, когда пользователь в старой системе не выполнил миграцию и пытается сбросить пароль в новой системе. Существуют обходные пути, такие как явное обнаружение конкретного случая, подобного этому, и беспрепятственное перенаправление в «процесс миграции». Но в этот момент следует ожидать трения.
Однако преимущество разделения аутентификации и авторизации поможет в будущих миграциях и модульности. Сведения о пользователе в общей базе данных обычно представляют собой одну из самых сложных задач для переноса.
База данных
Имея дело с устаревшей системой, мы можем внедрить новую версию поверх существующей базы данных. Это распространенный подход, который имеет ряд преимуществ:
- Мгновенная миграция — это, пожалуй, самое большое преимущество. Все данные уже в новой системе с нулевым временем простоя
- Простой — это, вероятно, один из самых простых подходов к миграции, и вы можете использовать существующие данные «реального мира», чтобы протестировать новую систему перед запуском.
Есть и несколько серьезных недостатков:
- Загрязнение данных — новая система может вводить проблемные данные и ломать устаревшую систему, делая невозможным откат. Если вы намерены обеспечить поэтапную миграцию, когда и старая, и новая системы работают параллельно, это может быть проблемой.
- Проблемы с кэшем — если обе системы работают параллельно, кэширование может привести к их непоследовательному поведению.
- Сохраняющиеся ограничения — это переносит ограничения старой системы в новую систему.
Если система хранения достаточно современная и мощная, такой подход к переносу данных имеет смысл. Он удаляет или, по крайней мере, откладывает проблемную часть процесса миграции.
Кэширование
Следующие три совета лежат в основе производительности приложения. Если вы сделаете это правильно, ваши приложения будут работать быстро:
- Кэширование
- Кэширование
- Кэширование
Вот и все. Тем не менее, очень немногие разработчики используют кэширование в достаточной степени. Это потому, что правильное кэширование может быть очень сложным и может нарушить принцип единого источника знаний. Это также усложняет миграцию, как упоминалось в предыдущем разделе.
Отключение кэширования во время миграции может быть нереалистичным вариантом, но сокращение срока хранения может смягчить некоторые проблемы.
Стратегия
Есть несколько способов решить проблему крупномасштабной миграции. Мы можем посмотреть на «общую картину» миграции, например. От монолита к микросервисам. Но чаще всего в процессе возникают более тонкие различия.
Я пропущу очевидную «полную переработку», когда мы мгновенно заменяем старый продукт новым. Я думаю, что это довольно очевидно, и мы все понимаем риски/последствия.
Модуль за модулем
Если вы можете выбрать эту стратегию и медленно заменять отдельные части устаревшего кода новыми модулями, то это идеальный путь. Это также одно из главных преимуществ микросервисов.
Этот подход может работать хорошо, если есть команда, которая управляет и обновляет устаревший код. Если его нет, у вас могут возникнуть серьезные проблемы с этим подходом.
Параллельное развертывание
Это может работать для развертывания общей базы данных. Мы можем развернуть новый продукт на отдельном сервере, при этом оба продукта будут использовать одну и ту же базу данных, как указано выше. С этим подходом связано много проблем, но я часто выбирал его, так как он, вероятно, самый простой для начала.
Поскольку старый продукт все еще доступен, существует обходной путь для существующих пользователей. Часто рекомендуется запланировать время простоя устаревших серверов, чтобы заставить существующих пользователей мигрировать.
В противном случае в этом сценарии вы можете получить пользователей, которые откажутся переходить на новый продукт.
Скрытое развертывание
В этой стратегии мы скрываем существующий продукт от общественности и устанавливаем на его место новую систему. Чтобы упростить миграцию, новый продукт запрашивает у старого продукта недостающую информацию.
Например. если пользователь попытался войти в систему и не зарегистрировался в системе, код может запросить устаревшую систему для беспрепятственной миграции пользователя. Это сложно и в идеале требует некоторых изменений в устаревшем коде.
Огромным преимуществом является то, что мы можем перенести базу данных, сохраняя при этом совместимость и не перенося все данные одним махом.
Основным недостатком является то, что это может увековечить существование устаревшего кода. В результате это может работать против наших целей в области развития.
Реализация
Вы закончили писать код. Мы готовы нажать на курок и выполнить миграцию… Теперь нам нужно сообщить пользователям, что миграция будет выполнена. Вы не хотите, чтобы разгневанный клиент жаловался, что что-то внезапно перестало работать.
Репетиция
Если возможно, выполните пробный прогон и подготовьте сценарий для процесса миграции. Когда я говорю сценарий, я не имею в виду код. Я имею в виду сценарий обязанностей и задач, которые необходимо выполнить.
Нам нужно убедиться, что все работает после завершения миграции. Если что-то сломалось, должен быть сценарий, чтобы все отменить. Вам лучше отступить, чтобы передислоцироваться в другой день. Я бы предпочел иметь миграцию, которая завершается сбоем на ранней стадии, от которой мы можем «вернуться», чем иметь что-то «недоделанное» в продакшене.
Кто?
На мой взгляд, вам следует использовать меньшую команду для фактического развертывания перенесенного программного обеспечения. Слишком много людей может создать путаницу. На борту вам потребуется следующий персонал:
- IT/OPS — для обработки развертывания и возврата при необходимости.
- Поддержка — для решения вопросов и проблем пользователей. Поднимать флаги, если пользователь сообщает о критической ошибке
- Разработчики — чтобы выяснить, есть ли проблемы с развертыванием, связанные с кодом.
- Менеджер — нам нужен кто-то с полномочиями мгновенного принятия решений. Никто не хочет тянуть развертывание. Нам нужен кто-то, кто понимает, что поставлено на карту для компании
Существует тенденция исправлять код, чтобы выполнить миграцию. Это нормально работает для небольших стартапов, и я сам виноват в этом. Но если вы работаете в масштабе, нет никакого способа сделать это. Изменение кода, сделанное «на месте», не может пройти тесты и может привести к ужасным проблемам. Наверное, это плохая идея.
Когда?
Аксиома «не развертывать в пятницу» может быть ошибкой в данном случае. Я считаю, что пятницы — отличный период миграции, когда я готов пожертвовать выходными. Очевидно, я не выступаю за то, чтобы заставлять людей работать по выходным. Но если есть интерес к этому (в обмен на отпуск), то дни с низким трафиком — идеальное время для серьезных изменений.
Если вы работаете в нескольких часовых поясах, разработчикам из наименее активного часового пояса лучше всего выполнять миграцию. Я бы предложил иметь команды во всех часовых поясах, чтобы отслеживать любые возможные последствия.
Ловкость в таких ситуациях имеет решающее значение. Быстрое реагирование на изменения может иметь значение между отменой развертывания и пайкой.
Поэтапное внедрение
С небольшими обновлениями мы можем поэтапно выпускать наши выпуски и распространять обновления для подмножества пользователей. К сожалению, когда мы вносим серьезные изменения, я нахожу это скорее помехой. Источник ошибок становится труднее различить, если у вас работают обе системы. Обе системы должны работать одновременно, и это может вызвать дополнительные трения.
После миграции
Прошло пару недель, все успокоилось, и миграция заработала. В конце концов.
Что теперь?
Пенсионный план
В рамках миграции мы принесли с собой большой набор устаревших функций. Некоторые из них, вероятно, нам нужны, а некоторые другие могут не понадобиться. После завершения развертывания нам нужно определиться с планом выхода на пенсию. Какие устаревшие функции следует удалить и как?
Мы можем легко увидеть, используем ли мы конкретный метод или он не используется в коде. Но используют ли пользователи определенную строку кода? Конкретная особенность?
Для этого у нас есть наблюдаемость.
Мы можем вернуться к электронной таблице извлечения функций и просмотреть каждую потенциальную функцию. Затем используйте системы наблюдения, чтобы увидеть, сколько пользователей вызывают функцию. Мы можем легко сделать это с помощью таких инструментов, как Lightrun, поместив в код метрику счетчика (вы можете скачать бесплатно здесь). В соответствии с этой информацией мы можем начать сужать набор функций, используемых продуктом. Я обсуждал это раньше, так что это могло бы быть не так применимо, если бы эта функциональность работала в устаревшей системе.
Еще более важным является выход на пенсию бегущего наследия. Если вы выбрали путь миграции, в котором все еще работает устаревшая реализация, самое время решить, когда отключаться. Помимо затрат, проблемы с безопасностью и обслуживанием делают это нецелесообразным в долгосрочной перспективе. Распространенная стратегия заключается в периодическом отключении устаревшей системы на час для обнаружения зависимостей/использования, о которых мы можем не знать.
Такие инструменты, как сетевые мониторы, также могут помочь оценить уровень использования. Если у вас есть возможность изменить устаревшее или прокси-сервер на устаревшее, самое время собрать данные об использовании. Определите пользователей, которые все еще зависят от этого, и спланируйте кампанию/процесс по электронной почте для их перемещения.
Используйте инструменты, чтобы избежать наследства в будущем
Современная система может пользоваться многими новыми возможностями, имеющимися в нашем распоряжении. Процессы CI/CD включают в себя сложные линтеры, которые обнаруживают проблемы безопасности, ошибки и выполняют проверки, которые намного лучше, чем их человеческие аналоги. Инструмент качества кода может иметь огромное значение для ремонтопригодности проекта.
Ваш продукт должен использовать эти новые инструменты, чтобы он не ухудшился до статуса устаревшего кода. Патчи безопасности доставляются «бесшовно» в виде запросов на вытягивание. Изменения получают неявные проверки для устранения распространенных ошибок. Это облегчает долгосрочное обслуживание.
Поддержание тестирования на соответствие
После процесса миграции люди часто отказываются от тестов на соответствие. Имеет смысл преобразовать их в интеграционные тесты, если это возможно/необходимо, но если у вас уже есть интеграционные тесты, они могут быть избыточными и более сложными в обслуживании, чем ваше стандартное тестирование.
То же самое верно и для электронной таблицы извлечения признаков. Это не то, что можно обслуживать, это всего лишь инструмент на период миграции. Как только мы закончим с этим, мы должны отказаться от него, и мы не должны считать его авторитетным.
Окончательно
Миграция старого кода всегда представляет собой проблему, так как agile-практики имеют решающее значение при выполнении этой задачи. В этом процессе так много подводных камней и так много точек отказа. Это особенно верно, когда эта система находится в эксплуатации и миграция значительна. Я надеюсь, что этот список советов и подходов поможет вам в разработке.
Я думаю, что боль в этом процессе неизбежна. Так же и некоторые неудачи. Наши инженерные команды должны быть гибкими и быстро реагировать на такие случаи. Выявляйте потенциальные проблемы и быстро устраняйте их в процессе. Я мог бы сказать об этом гораздо больше, но я хочу, чтобы это было достаточно общим, чтобы его можно было применить к более широким случаям. Если у вас есть мысли по этому поводу, свяжитесь со мной в Твиттере — [@debugagent] (https://twitter.com/debugagent) — чтобы сообщить мне об этом.
Также опубликовано [Здесь] (https://talktotheduck.dev//modernize-legacy-code-in-production-rebuild-your-airplane-midflight-without-crashing)
Оригинал