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

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

Об авторе
Мод Лемер (Maude Lemaire) — инженер компании Slack Technologies, Inc. Отвечает за масштабирование продукта и поддержку крупных клиентов компании. На работе в основном занимается поиском нужных специалистов, переговорами, рефакторингом громоздких фрагментов кода, консолидацией избыточных схем баз данных и созданием инструментов для других разработчиков. Мод обеспокоена вопросом удобства процесса разработки, поэтому активно ищет более простые и эффективные способы структурирования кода на всех уровнях.
С отличием окончила Университет Макгилла. Имеет степень бакалавра по специальности «программная инженерия».

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

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

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

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

Структура издания
Книга разделена на четыре части и разбита на главы в соответствии с хронологическим порядком этапов выполнения крупномасштабного рефакторинга.

Часть I знакомит с важными концепциями рефакторинга.
-В главе 1 обсуждаются основы и отличие масштабного рефакторинга от более мелких переделок кода.
-Глава 2 описывает причины деградации кода и ее влияние на эффективность рефакторинга.

Часть II посвящена всему, что нужно знать о планировании рефакторинга.
-В главе 3 представлен обзор метрик для количественной оценки проблем, которые пытается решить рефакторинг. Вы узнаете, как в цифрах описать исходное состояние системы.
-Глава 4 объясняет, из каких компонентов состоит план выполнения рефакторинга и как его правильно составить.
-В главе 5 обсуждаются способы получения поддержки рефакторинга от руководства всех уровней и коллег.
-Глава 6 описывает, как определить лучших кандидатов для работы над рефакторингом, и дает советы, как привлечь их к участию в проекте.

В части III вы узнаете, как убедиться в корректном протекании рефакторинга на каждом этапе его выполнения.
-Глава 7 рассказывает, как общаться и взаимодействовать с членами команды и любыми внешними заинтересованными сторонами.
-В главе 8 рассматриваются способы сохранения темпов выполнения рефакторинга.
-Глава 9 советует, как лучше сохранить изменения после рефакторинга.

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

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

Переход к новой базе данных


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

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

Тем не менее рефакторинг прошел успешно. Все четко понимали поставленную задачу и то, что продукт просто перерос нынешнее архитектурное решение в процессе развития (глава 2). Мы тщательно планировали проект и ввели в рассмотрение еще несколько переменных, зная, что это увеличит пользу от рефакторинга (глава 4). Была спланирована осторожная стратегия развертывания и разработаны инструменты для ее максимально надежной реализации (глава 8).

Хотя рефакторинг в конечном итоге позволил нам расширять наш продукт новыми и интересными способами, он занял почти вдвое больше времени, чем мы изначально планировали. Мы оказались слишком оптимистичными в оценках (глава 4). Потребовалось больше года для завершения проекта, изначально рассчитанного на шесть месяцев. Мы недооценили влияние рефакторинга на продукт и привлекли к сотрудничеству инженеров по продукту только после того, как несколько месяцев почти не двигались вперед (глава 6).

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

Распределение по рабочим пространствам


Чтобы понять суть проблем, которые мы хотели решить рефакторингом, нужно описать способ распределения данных по базам MySQL. Изначально большинство данных было распределено по рабочим пространствам. Каждое из них было выделено для одного клиента компании Slack. Я немного рассказывала об этом ранее в разделе «Архитектура Slack 101» главы 10. Распределение данных клиентов по разным шардам было показано на рис. 10.3.

Несколько лет это работало нормально, но постепенно такая схема становилась более неудобной. Это происходило по двум причинам.

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

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

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

Чтобы разобраться с операционными проблемами с MySQL и сложностями масштабирования, мы начали оценивать другие варианты хранения. Выбор пал на Vitess (https://vitess.io/) — систему кластеризации баз данных, созданную YouTube, которая обеспечивает горизонтальное масштабирование MySQL. Миграция на Vitess позволит нам делить данные на шарды не только по рабочим пространствам. Так мы сможем освободить место в самых загруженных шардах и распределить данные так, чтобы упростить запросы!

Миграция таблицы channels_members на Vitess


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

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

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

Мы создали канал #feat-vitess-channels для обмена идеями и координации рабочих потоков. Разослали приглашения присоединиться к нему и сразу приступили к решению первой задачи.

Схема шардирования


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

  • по каналу (channel_id), чтобы запрос к одному шарду давал всю информацию об участниках канала;
  • по пользователю (user_id), чтобы запрос к одному шарду давал всю информацию о членстве пользователя.

Поскольку консолидация таблиц завершилась недавно, я помнила, что большинство запросов касались получения членства для данного канала, а не для данного пользователя. Многие из них имели решающее значение для приложения, обеспечивая поиск и возможность упоминания всех участников канала (@channel или here).

Тогда (и по сей день) мы регистрировали образцы всех запросов к базе данных в хранилище данных, чтобы отслеживать использование MySQL при обращениях к нашим производственным системам. Для подтверждения своего предположения, что большая часть обращений к таблице channels_members связана с ключом channel_id, я посмотрела выборку связанных с членством запросов за месяц и передала результат (рис. 11.1) команде.

image

Но один из сотрудничавших с нами инженеров по продукту, имеющий больший опыт работы с Vitess, указал, что будет лучше провести шардирование по пользователям. В том же самом журнале он показал нам десять самых частых запросов к таблице, отфильтрованных по channel_id (рис. 11.2). Для обеспечения эффективной работы приложения нужно было предусмотреть это поведение.

image

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

Разработка новой схемы


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

Пример 11.1. Оператор CREATE TABLE показывает таблицу channels_members, шардированную по рабочим пространствам

CREATE TABLE `channels_members` (
`user_id` bigint(20) unsigned NOT NULL,
`channel_id` bigint(20) unsigned NOT NULL,
`team_id` bigint(20) unsigned NOT NULL,
`date_joined` int(10) unsigned NOT NULL,
`date_deleted` int(10) unsigned NOT NULL,
`last_read` bigint(20) unsigned NOT NULL,
...
`channel_type` tinyint(3) unsigned NOT NULL,
`channel_privacy_type` tinyint(4) unsigned NOT NULL,
...
`user_team_id` bigint(20) unsigned NOT NULL,
PRIMARY KEY (`user_id`,`channel_id`)
)

Таблица членства, шардированная по пользователям


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

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

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

В корпорации Acme Corp. для каждого отдела выделено свое рабочее пространство. Одно из них — для команды инженеров и отдела обслуживания клиентов. У каждого сотрудника Acme Corp. есть учетная запись пользователя от организации в целом. Инженеры имеют членство в рабочем пространстве Engineering для общения с товарищами по команде и в Customer Experience (служба поддержки).

Но с точки зрения внутренней структуры то, что выглядит как одна учетная запись Acme Corp., на самом деле — несколько учетных записей. На уровне организации каждый пользователь имеет канонический идентификатор. Одновременно ему сопоставлены несколько локальных для всех рабочих пространств, где он подписан на каналы. Например, у члена рабочих пространств Engineering и Customer Experience три уникальных идентификатора пользователя. Их количество рассчитывается по формуле n + 1, где n — число доступных пользователю рабочих пространств.
Несложно догадаться, что такая система быстро стала очень сложной и благоприятствующей появлению ошибок. Поэтому через год появился план замены всех локальных идентификаторов пользователей каноническими.

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

При шардировании по рабочему пространству в таблице channels_members локальные идентификаторы пользователей хранились в столбце user_id. Проект по замене всех локальных идентификаторов каноническими уже стартовал. Мы решили работать совместно с ними и проследить за сохранением во все столбцы идентификаторов канонических user ID.

Схема шардированной по каналам таблицы


Нас волновали не только идентификаторы пользователей, но и пропускная способность записи во вторую таблицу членства, шардированную по каналам. Мы изучили, какие запросы планировались к этим шардам, для определения способов снижения трафика. В процессе обнаружилось, что большинство столбцов исходной таблицы вообще не использовались. Причем к ним относились и столбцы, обновлявшиеся чаще всего, например дата последнего чтения пользователем материалов канала. Если запросить всех пользователей с доступом к какому-то каналу, логическая схема приложения для выполнения этого запроса обычно обращается только к столбцам user_id и user_team_id. Опуская эти неиспользуемые столбцы в новой схеме, мы значительно снижали частоту записи, предоставляя больше места шардам по каналам. Схема шардированной по каналам таблицы членства показана в примере 11.2.

Пример 11.2. Оператор CREATE TABLE для второй из новых таблиц channels_members, шардированной по каналам

CREATE TABLE `channels_members_bychan
`user_id` bigint(20) unsigned NOT NULL,
`channel_id` bigint(20) unsigned NOT NULL,
`user_team_id` bigint(20) unsigned NOT NULL,
`channel_team_id` bigint(20) unsigned NOT NULL, 
`date_joined` int(10) unsigned NOT NULL DEFAULT '0',
PRIMARY KEY (`channel_id`,`user_id`)
)
team_id переименовано в channel_team_id.

Разбиение запросов с оператором JOIN


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

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

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

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

Для наглядности рассмотрим процедуру разделения запроса (пример 11.3). Этот запрос показывал, есть ли у пользователя разрешение на просмотр определенного файла.

Пример 11.3. Запрос с оператором JOIN, который нужно было разбить на части; после знака % подставляется соответствующее значение

SELECT COUNT(*)
FROM files_shares s
LEFT JOIN channels_members g
   ON g.team_id = s.team_id
   AND g.channel_id = s.channel_id
   AND g.user_id = %USER_ID
   AND g.date_deleted = 0
WHERE
   s.team_id = %TEAM_ID
AND s.file_id = %FILE_ID
LIMIT 1


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

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

По факту оплаты бумажной версии книги на e-mail высылается электронная книга.

Для Хаброжителей скидка 30% по купону — Лемер

Комментарии (1)


  1. Rober
    04.05.2022 18:53

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

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

    Полагаю, по контексту предыдущих глав можно понять, что за "смешанные организованные структуры" такие, но раз за разом встречать подобные неоднозначности неприятно. К сожалению, в этом же предложении ситуация повторяется: под "отдельными клиентами Slack" подразумеваются мессенджеры или клиенты компании Slack? Сразу и не осознать, про что речь.

    Далее тоже путано как-то. Что за барьеры упоминаются (изоляция?), что именно поддерживалось какими-то способами, мешало ли или нет объединение рабочих пространств крупнейших клиентов?

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

    Сама книга интересная. Надеюсь, в оригинале таких проблем нет.