В настоящее время я выполняю некоторые тесты серверного приложения, которое я разработал, в значительной степени полагаясь на конструкции async/await C # 5.
Это консольное приложение, поэтому контекст синхронизации отсутствует, и в коде явно не создаются потоки. Приложение удаляет запросы из очереди MSMQ так быстро, как только может (асинхронный цикл исключения из очереди), и обрабатывает каждый запрос перед отправкой обработанных запросов через HttpClient.
Операции ввода-вывода, основанные на async/await, удаляются из очереди из MSMSQ, считываются/записываются данные в базу данных SQL-сервера и, наконец, отправляется запрос HttpClient в конце цепочки.
В настоящее время для моих тестов БД полностью подделана (результаты возвращаются напрямую через Task.FromResult), а также подделан HttpClient (ждите случайного Task.Delay между 0-50 мс и возвращайте ответ), поэтому только реальный Ввод-вывод — это удаление из очереди из MSMQ.
Я уже значительно улучшил производительность приложения, увидев, что много времени уходит на сборщик мусора, поэтому я использовал CLR Profiler и выяснил, где можно оптимизировать.
Сейчас я пытаюсь понять, могу ли я еще улучшить пропускную способность, и я думаю, что это возможно.
Есть две вещи, которые я не понимаю, и, возможно, за этим стоит какая-то возможность улучшения пропускной способности:
1) У меня 4 ядра процессора (на самом деле всего 2 настоящих... ЦП i7), и когда приложение работает, оно использует максимум 3 ядра ЦП (в визуализаторе параллелизма VS2012 я могу ясно видно, что используются только 3 ядра, а в Windows Perfmon я вижу, что загрузка ЦП составляет ~ 75/80%). Любая идея, почему? У меня нет контроля над потоками, поскольку я не создаю их явно, а полагаюсь только на задачи, так почему же планировщик задач не максимизирует использование ЦП в моем случае? Кто-нибудь испытал это?
2) Используя визуализатор параллелизма VS2012, я вижу очень большое время синхронизации (примерно 20% выполнения и 80% синхронизации). F.Y.I Создается около 15 тем.
Приблизительно 60% синхронизации происходит из следующего стека вызовов:
clr.dll!ThreadPoolMgr::WorkerThreadStart
clr.dll!CLRSemaphore::Wait
kernelbase.dll!WaitForSingleObjectEx
и
clr.dll!ThreadPoolMgr::WorkerThreadStart
clr.dll!ThreadPoolMgr::UnfairSemaphore::Wait
clr.dll!CLRSemaphore::Wait
kernelbase.dll!WaitForSingleObjectEx
И примерно 30% синхронизации исходит от:
clr.dll!ThreadPoolMgr::CompletionPortThreadStart
kernel32.dll!GetQueueCompletionStatusStub
kernelbase.dll!GetQueuedCompletionStatus
ntdll.dll!ZwRemoveIoCompletion
..... blablabla
ntoskrnl.exe!KeRemoveQueueEx
Я не знаю, нормально ли испытывать такую высокую синхронизацию или нет.
EDIT: Основываясь на ответе Стивена, я добавляю более подробную информацию о своей реализации:
Действительно, мой сервер полностью асинхронный. Однако некоторая работа процессора выполняется для обработки каждого сообщения (не так много, я признаю, но все же). После того, как сообщение получено из очереди MSMQ, оно сначала десериализуется (большая часть затрат ЦП/памяти, по-видимому, происходит в этот момент), затем оно проходит через различные этапы обработки/проверки, которые требуют некоторого количества ЦП, прежде чем, наконец, достичь «конца». канала», где обработанное сообщение отправляется во внешний мир через HttpClient.
Моя реализация не ожидает полной обработки сообщения, прежде чем исключить следующее из очереди. Действительно, мой насос сообщений, извлекающий сообщения из очереди, очень прост и сразу же «пересылает» сообщение, чтобы иметь возможность исключить из очереди следующее. Упрощенный код выглядит так (без учета управления исключениями, отмены...):
while (true)
{
var message = await this.queue.ReceiveNextMessageAsync();
this.DeserializeDispatchMessageAsync();
}
private async void DeserializeDispatchMessageAsync()
{
// Immediately yield to avoid blocking the asynchronous messaging pump
// while deserializing the body which would otherwise impact the throughput.
await Task.Yield();
this.messageDispatcher.DispatchAsync(message).ForgetSafely();
}
ReceiveNextMessageAsync
— это пользовательский метод, использующий TaskCompletionSource
, поскольку .NET MessageQueue
не предлагал асинхронный метод в .NET Framework 4.5. Так что я просто использую пару BeginReceive
/ EndReceive
с TaskCompletionSource
.
Это одно из немногих мест в моем коде, где я не жду асинхронного метода. Цикл удаляется из очереди так быстро, как только может. Он даже не ожидает десериализации сообщения (десериализация сообщения лениво выполняется реализацией .NET FCL Message при явном доступе к свойству Body). Я немедленно делаю Task.Yield(), чтобы разветвить десериализацию/обработку сообщения на другую задачу и немедленно освободить цикл.
Прямо сейчас, в контексте моих стендов, как я уже говорил ранее, все операции ввода-вывода (только доступ к БД) являются поддельными. Все вызовы асинхронных методов для получения данных из БД просто возвращают Task.FromResult с поддельными данными. Во время обработки сообщения происходит что-то около 20 вызовов БД, и все они подделаны прямо сейчас / синхронно. Единственная точка асинхронности находится в конце обработки сообщения, когда оно отправляется через HttpClient. Отправка HttpClient также подделана, но в этот момент я выполняю случайное (0-50 мс) «ожидание Task.Delay». В любом случае, из-за подделки БД обработка каждого сообщения может рассматриваться как одна задача.
Для моих стендов я сохраняю в очереди около 300 тыс. сообщений, а затем запускаю серверное приложение. Он удаляется из очереди довольно быстро, переполняя серверное приложение, и все сообщения обрабатываются одновременно. Вот почему я не понимаю, почему я не достигаю 100% ЦП и 4 ядер, а только 75% и 3 ядра (вопросы синхронизации в сторону).
Когда я только удаляю очередь без какой-либо десериализации или обработки сообщений (комментируя вызов DeserializeDispatchMessageAsync
, я достигаю пропускной способности примерно 20 000 сообщений/сек. Когда я выполняю всю обработку, я достигаю пропускной способности примерно 10 000 сообщений/сек.
Тот факт, что сообщения быстро удаляются из очереди и что десериализация + обработка сообщений выполняется в отдельной задаче, заставляет меня визуализировать в моей голове множество задач (по одной на сообщение), поставленных в очередь в планировщике задач (пул потоков здесь... нет контекста синхронизации), поэтому я ожидаю, что пул потоков будет отправлять все эти сообщения на максимальное количество ядер, и все 4 ядра будут полностью заняты для обработки всех задач, но мне кажется, что это не так.
В любом случае, любой ответ приветствуется, я ищу любые идеи/советы.