Связанные с производительностью: как SQL Server обрабатывает одновременные запросы от нескольких подключений из одного настольного приложения .NET?

Описание однопоточной версии:

  1. Программа собирает список вопросов.
  2. Для каждого вопроса получите типовые ответы и пропустите каждый из них через модуль оценки.

  • Модуль подсчета очков делает ряд запросов к базе данных (только для чтения).
  • Последовательная обработка, подключение к одной базе данных.

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

Каждый поток открывает собственное соединение с базой данных и работает со своим списком вопросов (около 95 вопросов в каждом из 6 потоков). Приложение ожидает завершения всех потоков, а затем объединяет результаты для отображения.

К моему удивлению, многопоточная версия отработала примерно столько же, заняв около 16 секунд вместо 17.

Вопросы:

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

Будет ли SQL Server обрабатывать запросы одновременно, когда они поступают из одного приложения, или он (или сам .net) будет их сериализовать?

Может быть, что-то неправильно сконфигурировано, что ускорит его работу, или я просто довожу SQL Server до предела вычислительных возможностей?

Текущая конфигурация:

Microsoft SQL Server Developer Edition 9.0.1406 RTM
ОС: Windows Server 2003 Standard
Процессоры: 8
ОЗУ: 4 ГБ


person Triynko    schedule 21.05.2009    source источник


Ответы (6)


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

Я предполагаю, что ваш код С# действительно правильный, и вы действительно запускаете отдельные потоки и выполняете каждый запрос параллельно. Без обид, но я видел, как многие заявляют об этом, и код на самом деле является последовательным в клиенте по разным причинам. Вы должны проверить это, наблюдая за сервером (через Profiler или используя sys.dm_exec_requests и sys.dm_exec_sessions).

Также я предполагаю, что ваши запросы имеют аналогичный вес. т. е. у вас нет одного потока, который длится 15 секунд, и 5, которые 100 мс.

Симптомы, которые вы описываете, при отсутствии более подробной информации указывают на то, что у вас есть операция записи в начале каждого потока, который блокирует X на каком-либо ресурсе. Первый поток запускается и блокирует ресурс, остальные 5 ждут. 1-й поток выполнен, освобождает ресурс, затем его захватывает следующий, остальные 4 ждут. Таким образом, последний поток должен ждать выполнения всех остальных 5. Это было бы чрезвычайно легко устранить, посмотрев sys.dm_exec_requests и отслеживайте, что блокирует запросы.

Кстати, вам следует рассмотреть возможность использования асинхронной обработки = true и полагаться на асинхронные методы, такие как BeginExecuteReader, для запуска ваших команд в параллельном выполнении без накладных расходов на потоки на стороне клиента.

person Remus Rusanu    schedule 21.05.2009
comment
Я уверен, что код правильный. Каждому из потоков отправляется объект инициализации, который содержит уникальный объект подключения вместе с уникальным списком элементов для обработки. Цикл, запускающий потоки, завершается за 5,4875 мс. Нет операций записи — только выборочные запросы. Запросы имеют одинаковый вес, о чем свидетельствует одинаковое время выполнения каждого потока (16,639 с, 16,481 с, 16,544, 16,712). Общее время работы многопоточного приложения составляет около 16 с по сравнению с 17 с для однопоточной версии. - person Triynko; 23.05.2009
comment
Так что, на самом деле, я уже думал обо всем этом. Чего я не понимаю, так это почему SQL Server тратит одинаковое количество времени на обработку сотен запросов, независимо от того, отправляются ли они параллельно или нет. Действительно, кажется, что в SQL Server происходит какая-то сериализация, но почему? С запросами только для чтения мне действительно нужно указывать «nolock», как некоторые предлагали? SQL Server должен иметь возможность обрабатывать запросы только для чтения (выбор) одновременно без синтаксиса «nolock». - person Triynko; 23.05.2009
comment
Если параметр nolock указан для всех строк запроса (всего 3), время выполнения каждого потока сокращается примерно на 2 секунды (14,398, 14,213, 14,212, 14,475). Тем не менее, время работы однопоточной версии также снижается, но все еще всего на секунду меньше, чем у многопоточной версии. Я провел трассировку в профилировщике, и было выполнено чуть менее 5000 запросов, все из которых имеют ноль в столбце «Запись». - person Triynko; 24.05.2009
comment
Вы не найдете ответ, угадывая. Используйте представление sys.dm_exec_requests, чтобы увидеть, что делают ваши запросы. - person Remus Rusanu; 24.05.2009
comment
Я уже запускал dm_exec_requests и не угадаю. Трассировка показывает время начала и окончания всех запросов, а график для всех 5000 значений показывает, что они выполняются одновременно (т. е. начальные и конечные сегменты явно перекрываются для разных spid). - person Triynko; 24.05.2009
comment
Это похоже на то, что SQL Server указывает, что он выполняет запросы одновременно, но время не подтверждает это. Это заставляет меня поверить, что на самом деле он не использует несколько процессоров для одновременных запросов... - person Triynko; 24.05.2009
comment
... конечно, это тоже не подходит, потому что диспетчер задач показывает загрузку ЦП более 90%, и все 8 графиков для всех 8 процессоров максимальны за 14 секунд, которые он работает. Я просто подталкиваю сервер к его вычислительному пределу или что? - person Triynko; 24.05.2009
comment
dm_exec_requests кстати показывает одновременные запросы. Я запускаю его снова и снова, и параллельные запросы приходят и уходят, но я обычно ловлю все четыре из них в приостановленном состоянии с типом ожидания CXPACKET. Это дает какие-то идеи? - person Triynko; 24.05.2009
comment
Я думал об использовании методов BeginExecuteReader, но алгоритм оценки требует синхронного завершения для продолжения и использует такие вызовы API, как «List‹Response› responses = Query.Responses.Load(conn, userid, questionid);' Интересно, что недавно я прочитал статью в Game Developer, в которой предлагается придерживаться синхронного API, который под капотом использует волокна и асинхронные вызовы. Он позволяет писать код аккуратно в однопоточном режиме с преимуществами параллелизма, но требует тщательного контроля за тем, чтобы переключения оптоволокна происходили достаточно часто. - person Triynko; 24.05.2009
comment
смысл dm_exec_requests заключался в проверке состояния запроса: заблокирован или готов к выполнению. Если они заблокированы, то на чем заблокированы? Что такое «ресурс последнего ожидания» и «тип ресурса последнего ожидания». - person Remus Rusanu; 24.05.2009
comment
А также кто такой блокирующий спид и что он делает. - person Remus Rusanu; 24.05.2009
comment
CXPACKET означает, что ваш запрос разбит на параллельные подзапросы, и управляющий поток ожидает подзапросов для получения данных. Параллельный план имеет смысл только в том случае, если в запросах есть много данных для сканирования, поэтому это укажет на плохо спроектированные запросы и индексы. Это также может объяснить время, поскольку ваши запросы ни в коем случае не связаны с ЦП, но связаны с вводом-выводом. Выполнение их параллельно приводит к тому же времени, потому что столько же требуется для выборки необходимых страниц в пуле буферов. - person Remus Rusanu; 24.05.2009
comment
Запросы оптимальны, как и индексы. Например, второй из трех запросов — это простой оператор select с одним целочисленным полем в предложении where, которое имеет кластеризованный индекс. Первый запрос выполняется заранее в таблице с менее чем сотней строк, до запуска любого из потоков/таймеров, поэтому он не имеет значения. Единственный другой запрос выполняется для таблицы с полумиллионом строк, чтобы получить одиночный модельный ответ на вопрос; у него есть 2 поля в предложении where, которые имеют один кластерный индекс с несколькими столбцами для этих полей, указанных в том же порядке. - person Triynko; 26.05.2009
comment
Запросы были «приостановлены», но не заблокированы и не выполнялись. Блокирующего SPID нет, и любое поле, относящееся к блокировке, равно нулю. Тип ожидания (текущий и последний), как я уже сказал, был CXPACKET. Поле «last_wait_resource» отсутствует, но поле «wait_resource» всегда пустое. Время ожидания составляет от 0 до 125, но обычно меньше 50. - person Triynko; 26.05.2009
comment
вам нужно посмотреть потоки, выполняющие параллельный план, если они заблокированы или приостановлены и почему. Проверьте sys.dm_os_workers и sys.dm_os_tasks. - person Remus Rusanu; 26.05.2009
comment
Я не вижу ни одного столбца в этих представлениях с пометкой, почему статус приостановлен, лол, но я только что опубликовал ответ с результатами. Я также взглянул на прогнозы плана запроса в студии управления, и это указывает на то, что оба основных запроса являются простым поиском по кластеризованному индексу. - person Triynko; 27.05.2009

Вы можете просто проверить диспетчер задач, когда процесс запущен. Если он показывает загрузку ЦП на 100%, значит, он привязан к ЦП. В противном случае его IO Bound.

Для гиперпоточности 50% использования ЦП примерно равно 100% использованию!

Вау, я и не знал, сколько лет теме. Я думаю, что всегда хорошо оставить ответ для других.

person WebDev01    schedule 13.11.2009

Насколько велика ваша база данных? Насколько быстры ваши жесткие диски / RAID / Другое хранилище

Возможно, ваша БД связана с вводом-выводом?

person Tom Leys    schedule 21.05.2009
comment
БД составляет несколько ГБ. Дисков 5, (четыре по 73Гб и один 300Гб), Serial Attached SCSI, 3, 15К, 3.5, SG2, 15К5. Честно говоря, рейд - загадка (сервер не перезапускал, чтобы проверить), и меня здесь не было, когда он был установлен. Они отображаются как один диск на 135 ГБ в Windows. Есть ли способ определить, привязана ли БД к вводу-выводу? - person Triynko; 26.05.2009

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

Сказав это, вы, вероятно, испытываете некоторую блокировку базы данных, которая вызывает медлительность. Поскольку вы говорите о запросах только для чтения, попробуйте использовать подсказку with (nolock) в своих запросах, чтобы увидеть, поможет ли это.

Что касается обработки SQL-сервера, насколько я понимаю, SQL Server будет пытаться обрабатывать как можно больше подключений одновременно (по одному оператору за раз для каждого подключения), вплоть до максимального количества подключений, разрешенного конфигурацией. Проблема if, которую вы видите, почти никогда не связана с проблемой потока и почти всегда связана с блокировкой или проблемой ввода-вывода.

person Chris    schedule 21.05.2009
comment
Потоки были введены специально для того, чтобы разрешить обработку во время выполнения транзакций базы данных и попытаться обеспечить параллельное выполнение запросов, чтобы использовать преимущества нескольких процессоров, поскольку без многопоточности каждый вопрос блокировался бы в ожидании активности базы данных. Потоки определенно помогают в этой области, но IO действительно является отдельным фактором среди других. - person Triynko; 22.05.2009
comment
Хотя использование nolock может повысить общую производительность запросов (как для последовательного, так и для параллельного выполнения), меня это не интересует; Меня интересует, почему параллельная версия не работает намного быстрее. Запросы включают доступ к структурам «дерева», поэтому я не могу рисковать чтением незафиксированных строк с помощью nolock, поскольку другие утилиты могут изменить деревья оценки в любое время. - person Triynko; 22.05.2009
comment
Суть nolock в том, чтобы избежать операций блокировки, которые могут задерживать ваши запросы. Если запрос1 обращается к 15 записям, он может заблокировать еще несколько. Если query2 попытается получить доступ к какой-либо из ближайших заблокированных записей, он не сможет работать параллельно — ему придется ждать освобождения блокировки. - person Chris; 22.05.2009
comment
Запросы только для чтения по умолчанию не должны мешать друг другу; блокировка чтения не должна препятствовать чтению тех же строк другим запросом, а только записи в них. Полное избегание блокировки чтения с помощью «nolock» сокращает общее время работы на пару секунд, но это все еще не тот прирост производительности, который я ожидаю от распараллеливания вещей на 8-процессорной машине. Возможно, это просто проблема с вводом-выводом. - person Triynko; 24.05.2009

Возможно ли, что потоки имеют общее соединение? Вы проверили, что при запуске создается несколько SPID (sp_who)?

person KM.    schedule 21.05.2009
comment
Каждый поток вызывает метод, который создает новый объект SqlConnection и открывает его; поэтому, если фреймворк не делает что-то глупое с пулом соединений (может быть, это так?), тогда потоки не используют соединение. - person Triynko; 22.05.2009
comment
@Triyko, пока ты не проверишь, ты не можешь быть уверен, что происходит. запустите sp_who, затем запустите свою программу, и, пока она работает, снова запустите sp_who, вы сможете увидеть, есть ли у вас 4 процесса в базе данных. - person KM.; 22.05.2009
comment
@KM, я запускал sp_who до/во время выполнения, и появляются новые spid, равные количеству запущенных потоков. Каждый spid имеет 9 записей с ecids от 0 до 8. Все они являются командами «выбрать». Таким образом, похоже, что действительно существует несколько одновременных подключений, как и ожидалось. - person Triynko; 23.05.2009
comment
@Triyko, так что теперь вы можете сосредоточиться на базе данных, у вас есть 4 пользователя, которые одновременно обращаются к одним и тем же таблицам. вы можете использовать традиционные методы БД, чтобы убедиться, что они не замедляют работу друг друга. - person KM.; 26.05.2009

Я выполнил запрос соединения между sys.dm_os_workers, sys.dm_os_tasks и sys.dm_exec_requests по адресу task_address, и вот результаты (некоторые неинтересные поля с нулевым значением исключены, другие снабжены префиксом ex или os для устранения неоднозначности):

-COL_NAME-  -Thread_1-  -Thread_2-  -Thread_3-  -Thread_4-

task_state  SUSPENDED   SUSPENDED   SUSPENDED   SUSPENDED
context_switches_count  2   2   2   2
worker_address  0x3F87A0E8  0x5993E0E8  0x496C00E8  0x366FA0E8
is_in_polling_io_completion_routine 0   0   0   0
pending_io_count    0   0   0   0
pending_io_byte_count   0   0   0   0
pending_io_byte_average 0   0   0   0
wait_started_ms_ticks   1926478171  1926478187  1926478171  1926478187
wait_resumed_ms_ticks   1926478171  1926478187  1926478171  1926478187
task_bound_ms_ticks 1926478171  1926478171  1926478156  1926478171
worker_created_ms_ticks 1926137937  1923739218  1921736640  1926137890
locale  1033    1033    1033    1033
affinity    1   4   8   32
state   SUSPENDED   SUSPENDED   SUSPENDED   SUSPENDED
start_quantum   3074730327955210    3074730349757920    3074730321989030    3074730355017750
end_quantum 3074730334339210    3074730356141920    3074730328373030    3074730361401750
quantum_used    6725    11177   11336   6284
max_quantum 4   15  5   20
boost_count 999 999 999 999
tasks_processed_count   765 1939    1424    314
os.task_address 0x006E8A78  0x00AF12E8  0x00B84C58  0x00D2CB68
memory_object_address   0x3F87A040  0x5993E040  0x496C0040  0x366FA040
thread_address  0x7FF08E38  0x7FF8CE38  0x7FF0FE38  0x7FF92E38
signal_worker_address   0x4D7DC0E8  0x571360E8  0x2F8560E8  0x4A9B40E8
scheduler_address   0x006EC040  0x00AF4040  0x00B88040  0x00E40040
os.request_id   0   0   0   0
start_time  2009-05-26 19:39    39:43.2 39:43.2 39:43.2
ex.status   suspended   suspended   suspended   suspended
command SELECT  SELECT  SELECT  SELECT
sql_handle  0x020000009355F1004BDC90A51664F9174D245A966E276C61  0x020000009355F1004D8095D234D39F77117E1BBBF8108B26  0x020000009355F100FC902C84A97133874FBE4CA6614C80E5  0x020000009355F100FC902C84A97133874FBE4CA6614C80E5
statement_start_offset  94  94  94  94
statement_end_offset    -1  -1  -1  -1
plan_handle 0x060007009355F100B821C414000000000000000000000000  0x060007009355F100B8811331000000000000000000000000  0x060007009355F100B801B259000000000000000000000000  0x060007009355F100B801B259000000000000000000000000
database_id 7   7   7   7
user_id 1   1   1   1
connection_id   BABF5455-409B-4F4C-9BA5-B53B35B11062    A2BBCACF-D227-466A-AB08-6EBB56F34FF2    D330EDFE-D49B-4148-B7C5-8D26FE276D30    649F0EC5-CB97-4B37-8D4E-85761847B403
blocking_session_id 0   0   0   0
wait_type   CXPACKET    CXPACKET    CXPACKET    CXPACKET
wait_time   46  31  46  31
ex.last_wait_type   CXPACKET    CXPACKET    CXPACKET    CXPACKET
wait_resource               
open_transaction_count  0   0   0   0
open_resultset_count    1   1   1   1
transaction_id  3052202 3052211 3052196 3052216
context_info    0x  0x  0x  0x
percent_complete    0   0   0   0
estimated_completion_time   0   0   0   0
cpu_time    0   0   0   0
total_elapsed_time  54  41  65  39
reads   0   0   0   0
writes  0   0   0   0
logical_reads   78745   123090  78672   111966
text_size   2147483647  2147483647  2147483647  2147483647
arithabort  0   0   0   0
transaction_isolation_level 2   2   2   2
lock_timeout    -1  -1  -1  -1
deadlock_priority   0   0   0   0
row_count   6   0   1   1
prev_error  0   0   0   0
nest_level  2   2   2   2
granted_query_memory    512 512 512 512

Предсказатель плана запроса для всех запросов показывает пару узлов, 0% для выбора и 100% для поиска по кластеризованному индексу.

Изменить: поля и значения, которые я пропустил, где (одинаковые для всех 4 потоков, кроме context_switch_count): exec_context_id(0), host_address(0x00000000), status(0), is_preemptive(0), is_fiber(0), is_sick(0), is_in_cc_exception(0), is_fatal_exception(0), is_inside_catch(0), context_switch_count(3-89078), exception_num(0), exception_Severity(0), exception_address(0x00000000), return_code(0), fiber_address(NULL), language(us_english), date_format(mdy), date_first(7), quoted_identifier(1), ansi_defaults(0), ansi_warnings(1), ansi_padding(1), ansi_nulls(1), concat_null_yields_null(1), executing_managed_code(0)

person Triynko    schedule 27.05.2009
comment
Другие темы, где все было до того, как появились мои четыре темы, и все они имеют статус «фон». Итак, перед запуском моего приложения было 10 потоков и 14 потоков во время его работы. Исходные 10 потоков имели следующие команды: RESOURCE MONITOR, LAZY WRITER, LOCK MONITOR, SIGNAL HANDLER, TRACE, QUEUE TASK, BRKR TASK, TASK MANAGER, BRKR EVENT HNDLR, BRKR TASK. Все мои 4 потока имели команду: SELECT. - person Triynko; 28.05.2009