Параллельный план выполнения существует ради одной идеи: разделить работу между несколькими потоками, чтобы сократить время запроса. Если запрос выполняется на DOP 8, вы ожидаете, что он будет примерно в восемь раз быстрее последовательного — с поправкой на то, что параллелизм масштабируется не всегда линейно. Но нередко бывает иначе. Запрос получает восемь потоков, а почти вся работа достаётся одному из них. Остальные семь простаивают, а вы платите за CPU и ждёте.
Хорошая новость в том, что это состояние оставляет следы. Нужно только знать, куда смотреть: на веса ожиданий CXCONSUMER и CXPACKET, на новый тип ожидания CXSYNC_PORT, появившийся в SQL Server 2022, на статистику времени запроса и на фактические планы выполнения, где видно распределение строк по потокам.
Как параллелизм распределяет работу
В параллельном плане есть координатор — нулевой поток — и несколько рабочих потоков, число которых определяется степенью параллелизма (DOP). Координатор раздаёт строкам работу, а рабочие потоки её выполняют.
Иногда распределение строк по потокам близко к идеальному. Иногда оно резко перекошено: один поток получает, скажем, 90% строк, а остальным достаются крохи. Причины бывают разными, но важнее другое: вы должны уметь определять, происходит ли это на вашем сервере.
Что такое CXCONSUMER и CXPACKET
Некоторое время назад Microsoft разделила измерение параллелизма внутри запроса на два веса ожиданий:
- CXCONSUMER — потребительский поток ждёт, пока параллельные потоки выполнят работу.
- CXPACKET — параллельные потоки обмениваются данными через обменные операторы.
Проблема в том, что CXCONSUMER долгое время считался безобидным, и многие скрипты для анализа ожиданий просто отфильтровывают его. Хуже того, из фактических планов выполнения он тоже выпадает: CXPACKET вы там увидите, а CXCONSUMER — нет. А ведь именно CXCONSUMER часто объясняет, почему параллельный запрос оказался медленным.
Хорошая новость: в SQL Server 2022 появился новый тип ожидания — CXSYNC_PORT, который пришёл из Azure Managed Instance и Azure SQL DB. В нём CXCONSUMER уже не списывается со счетов как незначительный, и по нему можно найти случаи неэффективного параллелизма прямо в планах запросов.
Пример: параллельный запрос, который на самом деле последовательный
Например, запрос генерирует параллельный план выполнения, но все строки в итоге оказываются в одном потоке. По сути, это последовательный план, который лишь выглядит параллельным.
План содержит параллельные операторы, и при этом запрос выполняется около 24 секунд. Если раскрыть свойства операторов, видно, что все строки попадают в поток номер один. И так по всему плану — проблема сохраняется на каждой операции.
Отчасти причина в том, что в плане нигде нет операции repartition streams. Есть distribute streams, который попытался равномерно раздать строки по потокам, но тип секционирования round robin сделал это плохо.
Первый признак: время CPU почти равно прошедшему времени
В свойствах запроса, в разделе Query Time Stats, есть два числа, за которыми стоит следить: время CPU и прошедшее время. Если они почти равны, это плохой знак.
Смысл параллельного плана в том, чтобы потратить больше CPU, но за меньшее время. Когда CPU-время и прошедшее время совпадают, параллелизм фактически не дал выигрыша. Складывается впечатление, что всю работу сделал один поток, а остальные просто ждали.
Второй признак: CXSYNC_PORT в статистике ожиданий
Если открыть статистику ожиданий для этого запроса, видно много ожиданий CXSYNC_PORT — нового типа ожидания, доступного в SQL Server 2022, Azure Managed Instance и Azure SQL DB. Он помогает обнаружить перекошенный параллелизм.
Обратите внимание: время этого ожидания почти совпадает с CPU-временем и прошедшим временем запроса. Это сильный индикатор того, что почти всё время выполнения — те самые 23 секунды — мы ждали чего-то, связанного с параллельными потоками, а не делали полезную работу.
Третий признак: строки на одном потоке в фактическом плане
Если копнуть в фактический план выполнения, можно посмотреть на любой оператор, показывающий распределение строк. Например, на Index Seek — и увидеть, что все строки оказались на одном потоке. То же самое видно на Key Lookup. И именно на тех участках плана, где строки ужасно перекошены, накопилось почти всё время выполнения.
По сути, вся работа плана пришлись на один фрагмент. Отсюда практический вывод: всегда смотрите на фактические планы выполнения и всегда обращайте внимание на время операторов. Если параллельный план выполняется медленно, стоит проверить, не получил ли один или два потока почти всю работу, пока остальные простаивали.
Четвёртый признак: зарезервированные и фактически использованные потоки
Ещё один полезный показатель — статистика потоков. Если запросу разрешили иметь три активные ветви, то есть три параллельные ветви могли выполняться одновременно. При этом было зарезервировано 24 потока, а использовано только 17. В отдельные моменты семь потоков (при DOP 8) просто не имели работы.
Это ещё один хороший индикатор того, что параллельный запрос сделал не ту работу, которую вы от него ожидали.
Кэш планов и хранилище запросов
Многое из этой информации доступно не только в фактическом плане, но и в кэше планов и хранилище запросов. Там можно увидеть записи вида: последний DOP 8, последнее прошедшее время, последнее время работы. Если они равны при высоком DOP — это серьёзное предупреждение.
Там же видны и счётчики зарезервированных и использованных потоков: если, например, зарезервировано 24, а использовано 17. Это та же информация, что есть и в фактическом плане.
Но есть важное ограничение: в планах из кэша вы не увидите распределения строк по потокам. Кэш планов и хранилище запросов не сохраняют эту информацию, и оценочные планы её тоже не показывают. Чтобы увидеть, какие строки оказались в каких потоках, нужно копать именно фактические планы выполнения. Именно поэтому полезно относиться к себе как к археологу планов запросов: только в фактических планах есть все нужные детали.
Итог
Параллельный план — это не гарантия того, что работа действительно распределилась по потокам. Иногда почти все строки достаются одному потоку, а остальные ждут. CXCONSUMER, CXSYNC_PORT, соотношение CPU-времени и прошедшего времени, а также число зарезервированных и использованных потоков — всё это помогает вовремя заметить такую ситуацию. Главное правило простое: смотрите на фактические планы выполнения и не считайте, что высокий DOP сам по себе означает эффективный параллелизм.

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