пятница, 4 сентября 2015 г.

Крупный рефакторинг приложения. Часть 2: База

В первой части я рассказывал о том как что нас сподвигло на большой рефакторинг приложения и как в общих чертах происходило переписывание кода. Хотя рефакторингом это (300+ файлов для чекина в main) назвать нельзя. Термин "рефакторинг" подразумевает "небольшие контроллируемые изменения", а не "отрежем пациенту ноги и пришьём новые, попутно переделав зрение, слух и часть внутренних органов".
Но как ни странно, изменения в коде были в целом довольно скучными и предсказуемыми, пусть и работы там было очень много.
А вот с базой дело обстояло несколько иначе.
Первоначально план был примерно таким:
1. Создать миграцию, которая сделает новые таблицы с префиксом "new", новый код будет работать с ними (на этапе отладки)
2. Написать приложение мигрирующее данные подокументно (отмечать старые документы в новой колонке Migrated), смигрировать их кроме последних месяцев.
3.  Быстро смигировать последние нескольком месяцев
4. Запустить миграцию добавляющую старым таблицам префикс "old", переименовать новые таблицы без префикса. Опять же с возможностью rollback-а миграции.
5. Вылить код, который ссылается на таблицы без префикса.

Эта схема мне показалась несколько громоздкой и я поинтересовался у коллег что они могут мне посоветовать. А посоветовали хорошую вещь - использовать namespace-ы у MS SQL сервера. Т.е. вместо колдования с переименованием таблиц просто создавать новые таблицы в другом namespace в духе "at.Document" вместо "dbo.Document". И имя у таблицы нормальное и работы меньше и меньше шанс что в процессе что-то пойдёт не так.
Так что план действий существенно сократился:
1. Написать миграцию для новых таблиц (с возможностью rollback). Код сразу будет смотреть на новые таблицы, нет необходимости что-то менять в последствии.
2.Написать приложение мигрирующее данные подокументно (отмечать старые документы в новой колонке Migrated), смигрировать их кроме последних месяцев.
3. Вылить код, быстро смигировать оставшиеся данные.

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

Приложение для миграции я писал "на коленке" - для чтения использовав Dapper, а для записи сделав friendly сборку с новым DAL, непосредственно манипулируя сущностями-отображением базы через Entity Framework. Причем первая реализация была написана довольно быстро (за несколько часов) и я довольный ушел домой на выходные (ведь практически всё готово, а я целую неделю на это закладывал!). Наивный.

В понедельник, слегка допилив то что я сделал в пятницу, я решил прогнать пробную миграцию. И сразу же упёрся в довольно плохую производительность - в тестовой базе около 7к документов и миграция шла несколько часов. А в продакшн базе 130к документов и 75 Гб файлы базы, что даже с учетом более мощного сервера грозило несколькими сутками. И это в случае если не будет деградации производительности.
А самое ужасное, что по ощущениям деградация производительности была - первые 20-30% документов проходили быстро, а потом всё становилось всё хуже и хуже.
Первое что я сделал - разбил миграцию меняющую структуру данных на две - первая создаёт таблицы с Primary Key, вторая (должна запускаться уже после миграции данных) создаёт индексы и внешние ключи.
Запускаю миграцию снова... И всё работает по прежнему медленно!
Начинаю думать, гуглить и спрашивать коллег. В итоге подкидывают идею насчет clustered (т.е. упорядоченных индексов) у первичных ключей. Учитывая что они преимущественно GUID-ы, получаем что на вставку каждой новой записи требуется всё больше и больше времени. Радостно переписываю индекс на non-clustered... И снова всё существенно замедляется через какое-то время!
Начинаю копать с точки зрения администрирования базы. Выясняю что рост базы настроен не попроцентно, а +1 Мб . Учитывая что многие документы сами по себе занимают более мегабайта, получается что довольно много времени может уходить на увеличение размера базы, которое будет происходить довольно часто. Меняю эту настройку... Да, да, ничего существенно не поменялось.
Уже проходит выделенная на миграцию базы неделя, а у нас по прежнему необъяснимая деградация после приблизительно 30% документов. Обращаемся к админам, те дают нам "почти продакшен" сервер, который в общем-то практически идентичен продакшену, но имеет не 140, а около 30 Гб оперативной памяти. Заодно накатывает туда нам копию базы из продакшена.
Теперь экспериментируем с настоящими данными (без риска их испортить по настоящему), первые 40к документов проходят буквально за 30 минут! Отлично, раз уж на тестовой базе всё начинало подтормаживать уже после 2к документов, то значит всё идёт хорошо и проблема в производительности сервера и его занятости. Как бы не так, после 40к записей скорость миграции снова снижается в разы.
Заодно на реальных данных всплыли 2 проблемы:
1. Зачастую приложение по миграции данных потребляет слишком много памяти (на очень больших документах). Становится понятно что идея "а давай будем использовать один контекст базы для миграции всех документов" была явно плохой. Да и производительность совсем не улучшалась.
2. Есть несколько документов (возможно битые данные в базе, точно не знаю), у которых аж по 160к сегментов. Тут даже подокументное использование контекста не помогает. К тому же чем больше документ, тем больше времени на его миграцию требуется (похоже что нелинейно). В общем, работа с контекстом базы была переписана на использование кусков по 1к сегментов, после чего идёт пересоздание контекста.

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

Через какое-то время я вспоминаю что ковырял непонятный и кажется неиспользуемый код со странным названием "деархивация документа". И тут меня осиняет догадка, которую я сразу же проверяю. Ну конечно же - код по архивации документа делал следующее - он вытаскивал все сегменты документа, сохранял их в файл (кстати, надо бы выяснить куда) и удалял их из базы. И конечно же, на продакшен сервере эта операция последний раз выполнялась примерно так в 2012 году и количество заархивированных документов как раз приблизительно равняется сорока тысячам! Т.е. по факту никакой деградации производительности не было - просто самые старые записи (а начинал я от старых к новым для того чтобы по максимум избежать возможных коллизий) были без данных. И только потом начиналась нормальная работа.
Кстати, по мере тестирования нашего кода выяснилось что некоторые операции выполнялись очень даже медленно (там был подсчёт статистики по нескольким документам за раз). Я уже боялся что придётся хранить агрегированную статистику внутри документа (а очень не хотелось, т.к. добавляло много работы и денормализовывало данные), но добавление нескольких индексов улучшило ситуацию на 2 порядка до довольно приемлемых половины секунды (на тестовом сервере, всё равно эту функциональность редко используют). Для меня было новостью что внешние ключи оказывается не обязательно создают индексы и их стоит создавать вручную. Ну и последующее создание индексов на полной базе занимало порядка 40-50 минут, что тоже стоит учитывать и выливаться в ночь на выходные, чтобы утром создать индексы не мешая пользователям. Кстати, учитывая что сервер изрядно вытаскивает базу в память, скорее всего стоит еще и очистить статистику для старых таблиц + перезапустить его.

Комментариев нет:

Отправить комментарий