Хорошо, если у вас небольшие (сотни гигабайт) базы, а ночью или в выходные вы можете себе позволить иметь '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' может занять много часов и даже более суток.

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

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

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

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


  1. Cayp
    00.00.0000 00:00

    Не могли бы вы поделиться опытом борьбы с фрагментацией для MSSQL Standard?

    Тут некоторые люди пытаются лицензироваться и Enterprise не всем по карману, тем более одна фича онлайн ребилда индексов не оправдывает разницу в стоимости (при условии что остальные фичи явным образом не требуются).


    1. Tzimie Автор
      00.00.0000 00:00

      Ну в standard это ночные rebuild для frag более 60, и reorganize для меньшего. Но если базы большие и нет maintenance window, то ква. А если есть, то можно даже ставить simple recovery, и все перестраивать

      Впрочем, я могу открыт одну тайну: в одной стране стоимость enterprise не отличается от standard и равна нулю


      1. mssqlhelp
        00.00.0000 00:00

        ....


  1. mssqlhelp
    00.00.0000 00:00

    Я скорее поверю, что "в два раза больше страниц" - это миф :) Если фрагментация большая - это скорее такой дизайн, спросите народ, как часто они встречают кластеризованный первичные ключи по IDENTITY... Бездумная дефрагментация на SSD нужна/ненужна это не вопрос, она их просто убивает. И много ещё чего дефрагментация убивает, особенно если вспомнить об AlwaysON, который не шустро будет переливать на реплики дефрагментацию огромной таблицы.

    Buffer Cache Hits Ratio - не будет хуже
    full backups - только если без компрессии
    full table scans - добавляйте индексы, где есть такое


    1. Tzimie Автор
      00.00.0000 00:00

      Не миф, в среднем у меня размер баз уменьшается в 1.7 раза - но там не все индексы настолько фрагментированы


      1. mssqlhelp
        00.00.0000 00:00

        Так может в "той стране" можно такие таблицы в памяти "крошить" ;)


        1. Tzimie Автор
          00.00.0000 00:00

          У нас два терабайта памяти, а базы более 30 терабайт, так что увы


          1. mssqlhelp
            00.00.0000 00:00

            Ещё можно попробовать перестроить индексы со сжатием, всё лучше, чем часто их ребилдить...


    1. Tzimie Автор
      00.00.0000 00:00
      +1

      Смерть SSD дисков сильно преувеличена. Это было верно лет 5 назад. После года активной работы диски показывают 97% ресурса,в пятницу как раз смотрели. Это на 30 лет

      Современные SSD очень хороши (а для tempdb NVMe)


      1. Tzimie Автор
        00.00.0000 00:00
        +1

        Чтобы не быть голословным, один из дисков. Один из дисков tempdb


        1. CryptoFermer86
          00.00.0000 00:00

          Не все так радужно. Данный счетчик чаще врет и с реальным миром мало связан. Был случай когда диски показывали за год 1-2% износа, а на второй ушли на покой с показателем <30%. Хорошо что быстро привезли и удалось заменить диски без последствий.


          1. edo1h
            00.00.0000 00:00

            Был случай когда диски показывали за год 1-2% износа, а на второй ушли на покой с показателем <30%

            на всех накопителях, что я видел, это просто отношения числа случившихся циклов erase к расчётному ресурсу; так что такое поведение говорит о изменении нагрузки.


            Данный счетчик чаще врет и с реальным миром мало связан.

            с этим сложно спорить, показатель ну очень примерный.


    1. Tzimie Автор
      00.00.0000 00:00

      Фрагментация большая была потому, что до меня в этой фирме никто не пытался это делать на регулярной основе по причинам, описанным в статье


    1. Tzimie Автор
      00.00.0000 00:00

      Наконец не понял про Buffer Cache Hits Ratio - не будет хуже

      Как это, если число страниц будет меньше?


      1. mssqlhelp
        00.00.0000 00:00

        Buffer Cache Hits Ratio не про заполненность страниц, а про то, насколько хорошо страницы кешируются. Вы бы лучше сослались на PLE или интенсивность листания...


        1. Tzimie Автор
          00.00.0000 00:00

          Именно. И чем меньше страниц, тем лучше они кешируются, потому что больший процент страниц помещается в память


          1. mssqlhelp
            00.00.0000 00:00

            Спорное утверждение. Всё очень специфично. Если дизайн таблицы грамотный, пустоты на страницах будут легко заполняться, может оказаться даже выгоднее - меньше расщеплений... Или наоборот, может оказаться очень много фантомных строк и в качестве "пластыря" потребуется ребилд. ИМХО, нельзя огульно ни отвергать, ни навязывать дефрагментацию. Но бесспорно, на SSD в большинстве случаев дефрагментация паразитна.


            1. Tzimie Автор
              00.00.0000 00:00

              Вот на практике у меня (было -> стало)

              Rows:       3995946761 -> 4074573862 (выросла за время анализа)
              Page count: 64164949   -> 40253602
              Frag count: 56089774   -> 1168526
              frag_pct:   86.30%     -> 2.17%
              Space, Mb:  504122     -> 319043

              Что касается SSD, то при современном времни жизни SSD это предубеждение (имеется в виду конечно enterprise level SSD)


              1. mssqlhelp
                00.00.0000 00:00

                ...я так думал до того, как у меня одновременно "умерли" восемь enterprise level SSD в массиве. Причём, это были ещё TLC семилетней давности, не те, что сейчас клепают, число циклов перезаписи ячеек не увеличивается :(

                На практике важно не занимаемое место, а время исполнения. Места, памяти, процессоров не дорого добавить для компенсации. А вот риски от ттого, что очередь REDО сильно растёт из-за дефрагментации и этим может помешать автоматической отработке отказа - заставляют смотреть на мизерные потери во времени исполнения "сквозь пальцы" :)


                1. Tzimie Автор
                  00.00.0000 00:00

                  Одновременно? Совпадение? Не думаю)

                  Я спросил, тут за пять лет умер один диск.

                  Число циклов зависит от технологии TLC/MLC/SLC и насколько больше память диска того размера, который он изображает (что позволяет размазывать writes ровнее). Enterprise диски недаром более дорогие.

                  Что касается времени выполнения, то для больших таблиц (>>размер памяти) время при скане (OLAP нагрузка) растет линейно с размером, при OLTP нагрузке как правило падает логарифмически.

                  "памяти и процессоров" добавить как раз сложно, если 64 cores и 2Tb максимум что можно в эту материнку запихнуть.


                1. Tzimie Автор
                  00.00.0000 00:00

                  P.S. Я также слежу за двумя репликами, last_hardened_time отстает не более чем на 5-10 секунд, Такие же пики бывают и просто от обычной нагрузки


                1. Tzimie Автор
                  00.00.0000 00:00

                  И кстати, TLC же наименее живучие...


                1. edo1h
                  00.00.0000 00:00

                  … я так думал до того, как у меня одновременно "умерли" восемь enterprise level SSD в массиве

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


    1. Lazarev-MK
      00.00.0000 00:00

      > full table scans

      Стесняюсь спросить, а как может случится фрагментация индекса на таблице без индекса?


      1. Tzimie Автор
        00.00.0000 00:00

        Я имел в виду full index scans, а index scan кластерного это full table scan по сути.


        1. Lazarev-MK
          00.00.0000 00:00

          дело конечно хозяйское, но как по мне, то лучше всё же так и писать - full clustered index scan


  1. mssqlhelp
    00.00.0000 00:00

    ...и да, REORGANIZE это ну очень медленно, для больших таблиц не предлагать :)


    1. Tzimie Автор
      00.00.0000 00:00

      Верно, у меня получилось 5-10-15 Mb/sec для несильно фрагментированных таблицу, чаще всего 10.

      То есть на терабайт нужно 27 часов.