Как переписать код с нуля, выжить и не сойти с ума

24 Apr 2013


aka: К черту Джоэла Спольски, мы перепишем наш код с нуля!

Перевод статьи “How To Survive a Ground-Up Rewrite Without Losing Your Sanity”

http://onstartups.com/tabid/3339/bid/97052/Screw-You-Joel-Spolsky-We-re-Rewriting-It-From-Scratch.aspx


...

Статья Дэна Мильшайна (@danmil), сооснователя Hut 8 Labs.
Оригинал: http://onstartups.com/tabid/3339/bid/97052/Screw-You-Joel-Spolsky-We-re-Rewriting-It-From-Scratch.aspx

Конечно, вы читали статью Джоэла “Вещи, которые вы никогда не должны делать, часть I”, не так ли? Ту, в которой он настоятельно советует ни в коем случае, ради всего святого, не переписывать ваш продукт “с нуля”? И приводит кучу примеров ужасных последствий, когда компании пытались это сделать?

Самое первое: он полностью прав. Разработчики склонны фантастически недооценивать усилия, связанные с таким переписыванием (подробнее см. ниже), и фантастически переоценивать выгоды этого предприятия (подробнее тоже см. ниже)

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

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

Привет! Меня зовут Ден, и мне удалось кое-что переписать

Несколько лет назад я присоединился к быстрорастущему стартапу HubSpot, где в конце концов проработал порядочно времени (это был классный опыт, кстати. У каждого когда-нибудь должен быть такой босс, как Иов Шапира).

В мой первый год там я был техлидом небольшой команды, которая c нуля переписывала систему Маркетинговой Аналитики (Marketing Analytics, одна из ключевых функциональностей HubSpot). Мы переписали бекэнд (от хранения сырых данных по хитам в SQLServer к обработке хитов в Hadoop и хранению аггрегированных отчетов в MySQL). Мы переписали фронтенд (с C#/ASP на Java/Tomcat). Мы вникли в детали десятка приложений, которые, оказывается, были завязаны на старое хранилище-вообще-всех-хитов, и нашли способы заставить их работать с данными, которые стали теперь доступными. (Примечание: HubSpot теперь работает преимущественно на MySQL/Hadoop/HBase. См. блог разработки HubSpot.)

И это заняло дооооолгое время. Гораздо, гораздо дольше, чем мы ожидали.

Но это принесло большие выгоды для HubSpot. Важные Шишки в конце концов были очень довольны этим проектом. После своего окончания “Аналитика 2.0”, как ее называли, превратилась из “эпического долгостроя” в “тот большой рефакторинг, который так хорошо сработал”.

Затем, когда переписывание Аналитики закончилось, я, в роли Парня, который спрашивает “Почему?”, проводил “посмертное вскрытие” другого амбициозного проекта по переписыванию, который сложился не так хорошо. Я буду называть его Неудачным Переписыванием.

Из всего я извлек вполне ясные уроки.

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

Готовьтесь к тому, что этот проект никогда нафиг не закончится

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

Конвертация данных тошнотворна сверх всяких ожиданий

Я предполагаю, что в вашей существующей системе “зашиты” куча полезных данных (если нет – поздравляю, но я никогда, слышите, никогда не сталкивался с такой ситуацией). Вы думаете: “мы разработаем новую структуру БД (или перейдем на какое-нибудь NoSQL хранилище и т.п.), и потом, ну не знаю, напишем скрипты для копирования данных. Нет проблем.”

Проблема #1: данные закодированы очень неожиданными способами. Например: “Поле use_conf равно 1, если мы должны использовать авто-сгенеренные кофиги… Но только если spec_version больше, чем 3. Ах да, еще в течение нескольких месяцев у нас был баг, и use_conf всегда оставалось пустым. Почти всегда можно считать, что оно должно быть 1, если оно пустое. За исключением клиентов, которые купили Express-версию, тогда оно должно быть 2.” Вам надо сконвертировать все данные, подсчитать контрольные суммы, показать пользователям и понять, что расходится с их ожиданиями. Заканчивается все тем, что вы пристально изучаете историю коммитов, переписку с разработчиками, которые давно покинули компанию, и строку за строкой разбираете запутанный старый код. (Когда, готовя эту статью, я упоминал эту проблему при разработчиках, они каждый раз перебивали меня, чтобы с жаром описать то особое отвратительное ощущение, которое у них было от этого процесса – это действительно настолько ужасно).

Проблема #2: И подождите, все еще хуже: поскольку у вас много данных, их конвертация часто занимает дни. Так что вы бьетесь над тем, чтобы расследовать все перечисленные щекотливые вопросы с конвертацией данных, а потом ждете несколько дней чтобы понять, сработают ли ваши исправления. А затем находится следующая проблема и процесс повторяетя. У меня перед глазами так и стоит образ Стефена (типичный Подающий Надежды Молодой Разработчик), который был техлидом Неудачного Переписывания, работающего, скажем, 70-й час из 80-часовой недели. Он возился и возился с ползущим со скоростью черепахи экспортом/импортом данных, а тот все падал и падал. Я правда не могу передать, как долго это длилось.

Неимоверно сложно сократить объем работы

В обычном проекте (не переписывании) при приближении к запуску планируемая функциональность всегда (всегда) урезается. Вы начинаете, думая сделать A, B, C и D, а запускаетесь, когда готова часть A. Но как правило, все в восторге. (И честно, они забывают, что когда-либо считали остальные фичи абсолютно необходимыми).

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

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

Оказывается, есть другие системы, которые используют “ваши” данные

Вы думаете: ну, вот четыре страницы, и я понимаю, как реализовать их в новой системе. Но потом оказывается, что пяток кроновых скриптов читают данные прямо из “вашей” БД. И еще есть инициализация для новых клиентов, когда что-то пишется в базу и потом читается. И еще некоторые другие страницы делают запросы, чтобы получить некоторые из ваших данных. И так далее, и так далее. В общем, вы пытаетесь быстренько выключить старую систему, и шквал баг-репортов обрушивается на вас, о фичах, которые были реализованы давным-давно, разработчиками, уволившимися из компании, но от которых по-прежнему зависят клиенты. Пофиксить все это занимает вечность.

Ok, вы меня достаточно напугали. Что делать дальше?

Вы должны полностью управлять бизнес-выгодами.

Прежде, чем вы начнете, вы обязаны определить бизнес-выгоды от этого переписывания. Я имею в виду, вы всегда должны понимать крупномасштабные выгоды того, что вы делаете (см. Тест Рэнда) Но переписывание кода часто продвигают именно техлид или разработчики вообще, и поэтому абсолютно важно, чтобы вы понимали выгоды. Потому что вы встретите неожиданные проблемы, и будете вынуждены идти на компромиссы, и все это будет продолжаться вечно. И если в конце всего этого Важная Шишка, который подписывает ваш чек, не увидит больших выгод – это будет не очень радостный день для вас.

Замечание: будьте очень, очень осторожны, если главная бизнес-выгода оказывается (возможно, замаскированным) вариантом “Разработчикам будет гораздо проще работать с новой системой”. Я не отрицаю, что это приятная выгода, но если она единственная или главная… то через полгода вы будете пытаться объяснить вашему CEO, почему ничего не было сделано в разработке за последние полгода.

Ключ к исправлению соображения “разработчики будут меньше страдать” – в точности определить, что именно вы не можете сделать из-за текущего ужасного состояния системы. Например, вы не можете пройти аудит безопасности? Вебсайт регулярно падает, и клиенты это замечают? Есть какие-то классные новые фичи, которые вы не можете реализовать из-за того, что система слишком сложна, чтобы с нею работать? Если вы определили такие конкретные проблемы, это одновременно означает, что вы говорите о чем-то наблюдаемом для остального бизнеса, а также что вы сможете делать умные компромиссы, когда дела пойдут плохо (а они пойдут).

Например, для нашего большого переписывания Аналитики разработчики собрались с Дэном Данном, (абсолютно замечательным) продакт-менеджером в нашей команде, и составили список заметных для бизнеса побед, которые мы надеялись одержать. В примерном порядке приоритетов, это были:

  • на порядок сократить стоимость хранения каждого хита;
  • разработать новые отчеты, которые были невозможны в старой системе;
  • предоставлять все отчеты быстрее;
  • предоставлять отчеты практически-в-реалтайме (вместо закешированных подневно).

И заметьте: первая цель была очень, очень важной. HubSpot рос очень быстро, и хранение всех хитов как отдельных записей в SQLServer создавало нам всевозможные дополнительные расходы. Windows-админы пытались ввести в эксплуатацию новые кластеры SQLServer (что было рискованно, и сложно, и заканчивалось большими изменениями кода). Сейлам говорили, чтобы они не работали с потенциальными клиентами, если у тех был слишком большой трафик, потому что если бы они установили наш счетчик, это могло “положить” наши БД (это ограничение вносило неразбериху в продажи). И т.д. и т.д.

“Больше никаких хитов в SQLServer” – это Тяжелая Задача для переписывания: вы получаете выгоду только тогда, когда от старой системы не останется и следа. Для других задач, ниже по списку, вы получите частичные выгоды с каждым отчетом, переписанным по-новому. Это очень важное различие, которое необходимо понимать. Если возможно, постарайтесь сделать так, чтобы вы решали не только Тяжелые Задачи. Найдите промежуточные победы по пути.

Для Неудачного Переписывания бизнес-выгоды были недостаточно ясны. И поэтому, как часто случается в таких случаях, каждый предполагал, что в сияющем мире Новой Системы все их персональные любимые мозоли будут вылечены. Новая система будет быстрее! Она будет лучше масштабироваться! Фронтенд будет прекрасным, умным и новым! Он будет приносить нашим клиентам кофе в постель и читать им газеты.

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

Итак, как же не попасться в эту адову ловушку?

У алтаря инкрементализма

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

Кент Бек называет это Преемственностью (Succession), и описывает так:

“Как правило, изменения архитектуры эффектвнее всего реализуются как последовательность безопасных шагов. Преемственность – это искусство превратить одно концептуальное изменение в череду безопасных шагов и затем упорядочить эти шаги, чтобы оптимизировать безопасность, обратную связь, эффективность.”

Мне нравится, что он называет это “искусством” – я ощущаю точно так же. Это не получается само по себе. Вы должны постоянно работать над этим, проговаривать возможности с вашей командой, привлекать кого-то вроде Product owner-а или менеджера, чтобы убедиться, что ранние выгоды, которых вы надеетесь получить, имеют значение для клиентов. Это творческий процесс.

А сейчас с вашего позволения я скажу страшным голосом ветхозаветного пророка: Опасайтесь ложного инкрементализма!

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

К счастью, есть очень простой тест, чтобы определить, не свалились ли вы в Ложный Инкрементализм: если после каждого этапа Важная Шишка сказал бы вашей команде бросить проект, увидит ли бизнес какие-то преимущества? Это – золотой стандарт.

Возвращаясь к моему примеру: наша существующая аналитическая система поддерживала несколько тысяч клиентов, и предоставляла полдесятка ключевых отчетов. В начале мы приняли решения:
a) переписать все существующие отчеты прежде, чем начинать какие-либо новые; и
b) переписывать каждый отчет полностью, выкатывать его в продакшен, конвертировать все существующие данные для этого отчета, и переключать всех клиентов на него.
И только затем переходить к следующему отчету.

Это решение нас спасло: спустя 3 месяца переписывания, которое мы изначально оценили в 3-5 месяцев, мы полностью сконвертировали один единственный отчет. Поскольку мы концентрировались на том, чтобы пройти весь путь до продакшена, а так же на конвертации старых данных, нам пришлось осознать, насколько сложным будет весь процесс. Мы собрались, и выдвинули новую оценку: закончить все и избавиться от SQLServer займет скорее что-то около 8 месяцев.

В этот момент Дэн Данн (Абсолютно Отличный Продакт-менеджер, потому что он не боится идти на сложные компромиссы), сказал: “Я бы поменял наши приоритеты. Я хочу выпустить Классные Новые Отчеты прямо сейчас, не дожидаясь, пока мы полностью избавимся от SQLServer.” Мы сказали: “Даже если это сделает переписывание в целом дольше, и мы не избавимся от SQLServer в этом году, и нам все-таки придется запустить тот новый кластер, без которого мы надеялись обойтись?” И он сказал: “Да.” И мы сказали: “Договорились.”

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

Примечание: Дэн сделал стопроцентно правильный выбор (см.: Оличный). Классные Новые Отчеты стали огромным, выдающимся достижением. То, что мы сделали их раньше, а не позже, имело большое экономическое влияние на наш бизнес. И это было хорошо, потому что проект задержался еще на год, прежде чем мы смогли наконец отключить SQLServer и полностью выкинуть старую систему.

Одна интересная выгода, которую мы создали для менеджеров разработки, была в улучшении понимания, сколько времени займет весь проект. Я думаю, это именно то, что Бек имеет в виду под “обратной связью”. Это действительно выгода для бизнеса. Если бы мы не протащили тот единственный отчет через все стадии до продакшена, то спустя 3-4 месяца мы бы имели все данные (для всех отчетов) в каком-то промежуточном состоянии в частично сделанной новой системе, и никакого точного понимания всех сложностей, связанных с окончательным релизом хотя бы одного отчета. Вы видели, какую выгоду дала нам эта обратная связь – она позволила Дэну принять хорошее экономическое решение. Снова и снова напомню, что вы должны пойти и прочитать книгу Дональда Райнертзена Principles of Product Development Flow, чтобы понять, как уменьшение неопределенности создает выгоды для бизнеса.

В Неудачном Переписывании не был разработан тщательный план такой инкрементальной поставки. Разные Очень Крутые Штуки должны были случиться/стали бы возможными, когда проект закончился бы. Но он не заканчивался, и не заканчивался, и команда обнаруживала новые и новые точки, в которых части, которые они делали, не стыковались. На “посмертном вскрытии” кто-то подвел итог: “Мы не хотели, но как-то превратили все это в Водопадную разработку.”

Но я должен выкатить все за раз, потому что данные постоянно меняются!

Одна из причин, по которой разработчики возражают против инкрементализма: разработчики понимают, что для того, чтобы этот подход сработал, должен быть продолжительный период, когда каждое обновление данных должно попадать в обе системы (старую и новую). Разработчикам это не нравится.
Разработчики подумают (и даже скажут вслух): “Мы не можем этого делать, это добавит месяц на разработку прослойки с двойной записью. Это слишком замедлит нас.”

И вот что я вам скажу: всегда добавляйте эту прослойку с параллельной записью. Всегда. За эту небольшую, практически фиксированную цену вы покупаете невероятное количество гарантий. Это позволит вам, как я рассказывал ранее, постепенно переключаться с одной системы на другую. Это позволит вам дать задний ход в любой момент, если вы обнаружите крупную проблему с конвертацией данных (а вы будете обнаруживать такие проблемы снова и снова). Это означает, что конвертация ваших данных может занимать неделю, и это не составляет проблемы, потому что вам не надо замораживать запись в обе системы на это время. И вдобавок, это позволит обнаружить кучу странных ситуаций, когда “другие” системы пишут непосредственно в вашу старую БД.

Я снова процитирую, как Кент Бек пишет о Facebook: “Мы часто перевозим большие объемы данных из одного хранилища в другое, чтобы улучшить производительность или надежность. Эти переезды – часть Преемственности, потому что вы не можете взмахнуть волшебной палочкой и мгновенно перенести данные. Преемственность, которую мы используем, такова:

Перевести получение и изменение данных на DataType, абстракцию, которая прячет особенности хранения данных.

Модифицировать DataType, чтобы он писал данные как в старое, так и в новое хранилище.

Массово перевезти существующие данные.

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

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

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

Отказ от проекта всегда должен быть в списке возможностей

Если 3-месячное переписывание экономически оправдано, а 13-месячное – сплошной расход, вы создадите много выгоды, если поймете, с каким из них вы в действительности имеете дело. К несчастью, чем дольше вы продолжаете проект, тем сложнее избежать ловушки Невозвратных Издержек. Решение есть: если вы в какой-либо степени не уверены, сколько продлится ваш проект, разделите вашу работу на части, чтобы убрать эту неопределенность, и предоставьте людям что-то “законченное”, что позволило бы им двигаться дальше. После месяца работы, вы все еще можете сказать: мы решили переписать только фронтенд. Или: сейчас мы добавим только API-прослойку. Или даже: это оказалось плохой идеей, мы отказываемся от нее. После полугода работы, кода конца проекта не видно даже на горизонте, сделать то же самое будет невероятно сложно
(даже если экономически это по-прежнему правильный выбор).

Некоторые конкретные тактики

Сужающийся луч

Отличная идея, спасибо Келлану Эллиот-Маккрею, CTO из Etsy. Он описывает эту технику так:

“У нас есть методика, которую мы называем сужение луча. Это диаграмма: сколько еще старой системы осталось. Как правило, показатель считается кроновскими скриптами, грепающими код на наличите особых меток. Иногда это результат мониторинга компонент. Иногда – специальные таблицы. А когда показатель приходит к нулю, мы всегда устраиваем вечеринку. Большую вечеринку.

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

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

Уничтожьте бардак в ваших скриптах-миграциях

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

Но см. выше: вы будете запускать вашу конвертацию снова и снова, чтобы получить правильный результат. Плюс, вы конвертируете, аггрегируете и копируете данные, так что вам действительно стоит иметь юнит-тесты, чтобы как можно раньше обнаруживать ошибки (потому что “данные” – это “куча бессмысленных для вас цифр, но если они будут неправильными, то клиенты будут разгневаны”). А еще случится вот что: кто-то случайно нажмет ctrl-c и прибьет вашу 36-часовую конвертацию на 34-м часе. Так что стоит потратить чуть больше времени и сделать конвертацию строго идемпотентной
(под строгой идемпотентностью я подразумеваю, что вы можете заново запустить ваш процесс после частичного или неудачного запуска, и большая часть уже проделанной работы будет сохранена).

Так что относитесь к коду для конвертации данных очень уважительно. Это сбережет вам много времени в будущем.

Если ваши данные не выглядят странными, значит, вы не смотрели достаточно внимательно

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

Эмпирическое правиль конвертации и контроля данных: пока вы не нашли полдесятка дурацких несоответствий в старых данных, вы еще не закончили. Когда мы переписывали Аналитику, мы завели страничку в нашей внутренней wiki, которая называлась “Погрешности в данных”. Эта страница стала действительно длинной.

С большим инкрементализмом приходит большая сила

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

Это потрясающее ощущение.


А вы что думаете? Хотите поделиться уроками, извлеченными из эпических переписываний?