Дефрагментация таблиц в высоко нагруженных базах данных (MSSQL)

от автора

Хорошо, если у вас небольшие (сотни гигабайт) базы, а ночью или в выходные вы можете себе позволить иметь ‘maintenance window‘ и дефрагментировать таблицы. А если нет? В любом случае дефрагментация многих терабайт может занять дни, так что существование maintenance window становится непринципиальным.

Case study: многие терабайты данных, деятельность связанная с процессингом карт (24/7, maintenance window нет в принципе), MSSQL. Разумеется, Enterprise Edition, разумеется AlwaysOn.

Миф: у нас SSD, поэтому дефрагментация нам не нужна. Еще как нужна! Часто в высоко нагруженных системах не делают дефрагментацию, потому что это сложно. В итоге процент фрагментации выходит на уровень почти 100%, и таблицы занимают в два раза больше страниц, чем нужно. В два раза больше места — это в два раза хуже Buffer Cache Hits Ratio. Это в два раза больше размер full backups. Это в два раза дольше full table scans. Это выше CPU (потому что страницы перемещаются с помощью процессора, а не сами по себе)

Итак, приступим.

Немного теории

В данном случае — без Enterprise edition никуда. А значит, у нас есть замечательная опция ALTER INDEX … REBUILD (ONLINE=ON). И даже еще более замечательная опция (ONLINE=ON, RESUMABLE=ON). Не для всех индексов возможно использовать RESUMABLE, и даже ONLINE можно использовать не для всех, но мы будем оптимистами.

Если идет rebuild, то мы можем его приостановить командой

ALTER INDEX ...  PAUSE

В таком состоянии недоперестроенный индекс виден в таблице

SELECT total_execution_time, percent_complete, name,state_desc,   last_pause_time,page_count   FROM sys.index_resumable_operations;

В таком состоянии rebuild может даже переехать на другую ноду в AlwaysOn! Проверено!

Также обратите внимание на замечательное поле percent_complete. (для filtered индексов, правда, это значение врет тем больше, чем сильнее отфильтрован индекс)

Командой ALTER INDEX … RESUME мы можем продолжить выполнение операции (на самом деле RESUME это синтаксический сахар, просто повторяется команда REBUILD и перестроение продолжается. Вы можете это использовать исходную команду REBUILD вместо RESUME, но важно указать в точности все те же опции — по ходу дела поменять опции уже не получится, например, начав с MAXDOP=4 уже не получится его поменять), а командой ALTER INDEX … ABORT можно прибить операцию.

В режиме RESUMABLE транзакции очень малы, так что нет причин боятся того, что лог (LDF) должен будет вместить в себя всю операцию по созданию новой версии индекса.

Первая попытка, неудачная

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

Можно закрывать ноут. А что это люди забегали? Про тормоза какие-то говорят… И что это за алерты пришли по длине очереди AlwaysOn? Интересно, связано это с тем, что я делаю? Так, а это уже серьезнее, какие-то крики про «все висит, блокировки, идут таймауты». Ох, они нашли мою коннекцию с REBUILD как ту, что блокирует других. А что это за паника что ‘вообще все не работает, все висит и все индикаторы в Zabbix красные?’

И вот уже начальство с налитыми кровью глазами требует прибить этот жуткий REBUILD и больше никогда — слышите, никогда! его не запускать…

Я хочу спасти вас от этой ситуации. Итак, мы словили 4 проблемы, и будем с ними разбираться:

  • Сильная нагрузка на CPU (и IO)

  • Блокировки (!!!)

  • Рост размера LDF

  • Проблемы с AlwaysOn Queue

Придушивание (throttling)

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

В смысле не совсем то, но почти так:

  • MAXDOP=1 — ласково и нежно (но медленно)

  • MAXDOP=2 — нормальный режим

  • MAXDOP=4 — агрессивный режим

  • (без ограничения) — NIGHTMARE!

Стало много лучше, мы решили проблему с CPU (при MAXDOP=2 вы сожрете максимум две коровы, а у вас на боевом сервере их много). Но другие проблемы остались.

Например, в режиме MAXDOP=4 мощный сервер способен заполнять LDF со скоростью 1Gb/sec и более (гигабайт, а не гигабит). Это означает, что за 10 минут (что может быть промежутком между transaction log backup), мы заполним 600Gb в LDF, что довольно много. Хуже того, 1Gb/sec в LDF это 10 гигабит в секунду для AlwaysOn. 10 гигабит не так много для локальной сети, но ведь у вас реплики AlwaysOn расположены далеко, в других ДЦ?

На практике у нас ночью иногда забивался канал между ДЦ, и очередь росла. Итак, мы должны наблюдать за тем, сколько заполнено в LDF и каков размер очереди AlwaysOn. Если $db — это база, то рекомендую запрос:

USE [$db];   SELECT      convert(int,sum(size/128.0)) AS CurrentSizeMB,       convert(int,sum(size/128.0 - CAST(FILEPROPERTY(name, 'SpaceUsed') AS INT)/128.0))        AS FreeSpaceMB,     (select sum(log_send_queue_size)+sum(redo_queue_size)        from sys.dm_hadr_database_replica_states        where database_id=DB_ID('$db')) as QueueLen     FROM sys.database_files WHERE type=1

Таким образом, мы не можем воспользоваться sqlcmd, студией или SQL server job — мы должны написать скрипт с двумя тредами — один будет выполнять команды, а другой наблюдать за обстановкой и, если надо, делать PAUSE/RESUME. Коннекцию, которая делает REBUILD, лучше пометить program_name=уникальное имя, чтобы любым скриптам было бы легко ее находить.

По запросу выше имеет смысл ввести thresholds:

  • Максимальный размер занятого места в LDF (CurrentSizeMb-FreeSpaceMb)

  • Процент занятого места, если вы не предполагаете autogrowth LDF (FreeSpacemb/CurrentSizeMb)

  • Максимальный размер очереди QueueLen, рекомендую поиграть значениями в несколько миллионов.

Вот как это выглядит у меня, скрипт написан на PowerShell, будь он неладен:

Секунду, скажете вы. А почему вместо PAUSE автор использует команду kill? Потому что она короче и позволяет сэкономить несколько байт (нет). На самом деле причина важнейшая, и будет описана в следующем разделе.

Блокировки

Несмотря на то, что мы делаем REBUILD с ONLINE=ON, возможны блокировки с обеих сторон:

процесс(ы) в базе —> INDEX REBUILD —> процесс в базе

Правая часть нас не беспокоит. Наш rebuild может и подождать. Единственная проблема, это вопли плохо настроенной системы алертов, которая обнаружит процесс, который долго ждет блокировки. Вы помните, что мы пометили нашу коннекцию с помощью program_name=Rebuild? Это позволит нам добавить исключение (WHERE … AND program_name not like ‘Rebuild%’) в систему алертов, чтобы игнорировать наш процесс. Если ваша система алертов полагается на датчики PerfMon, а не на запросы, то по этому поводу я уже ругался тут.

А вот левая часть важнее. Если мы блокируем какой-то процесс или процессы, то мы должны немедленно уступить, сделав PAUSE, а потом, через некоторое время, можно снова попробовать сделать RESUME.

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

Именно здесь нас выручает kill. Мы теряем последнюю порцию работы (порядка пары секунд), но мы гарантированно и быстро уступаем. После kill индексирование корректно переходит в статус PAUSED и его можно продолжить.

На скриншоте вы видите, как процесс уступает по блокировке (после kill коннекция завершилась не сразу (spidnct — количество записей в sysprocesses, running и paused — это количество операций в sys.index_resumable_operations в разных статусах)

Больше всего блокировок возникают на этапе подмены разделов, когда 100% работы выполнено и операция пытается завершиться

Общий план работ

Мы рассмотрели как делать INDEX REBUILD. А когда его лучше делать? Когда делать REORGANIZE, а когда REBUILD? Какие уровни фрагментации смотреть?

Начнем того, что не подвергается сомнению.

  • COLUMNSTORE лучше дефрагментировать с помощью REORGANIZE (список эвристик ниже только для обычных таблиц)

  • REORGANIZE всегда идет спокойно потому что работает в MAXDOP=1

  • REORGANIZE имеет смысл обернуть в тот же скрипт слежения.

  • Для маленьких таблиц (page_count<50) нормально иметь высокий уровень фрагментации даже после REBUILD

Теперь то, с чем вы можете не согласиться — это эвристики, которые я выработал для себя:

  • Если frag_pct менее 7%, то ничего делать и не надо

  • Если frag_pct в диапазоне 7%-20%, то делаем REORGANIZE

  • Если frag_cpt в диапазоне 20%-40%, то мы не делаем ничего, хотя это и может показаться странным. Для REORG уже слишком поздно, а для REBUILD слишком рано. Таблица выскочила за наш первую линию защиты, ну пусть тогда фрагментация растет дальше

  • Если frag_pct больше 40%, то делаем REBUILD ONLINE=ON, RESUMABLE=ON.

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

  • Проверим состояние таблиц, например, через неделю. Учтите, что анализ уровня фрагментации даже в самом простом режиме ‘LIMITED‘ может занять много часов и даже более суток.

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

Вообще борьба с фрагментированными индексами выглядит так:

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


ссылка на оригинал статьи https://habr.com/ru/post/724702/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *