Я перевожу миллионы пользователей из предварительной AD в Azure AD B2C, используя MS Graph API для создания пользователей в B2C. Я написал консольное приложение .Net Core 3.1 для выполнения этой миграции. Чтобы ускорить процесс, я делаю параллельные вызовы Graph API. Это работает отлично - вроде.
Во время разработки я испытывал приемлемую производительность при запуске из Visual Studio 2019, но для тестирования я запускаю из командной строки в Powershell 7. Из Powershell производительность одновременных вызовов HttpClient очень плохая. Похоже, что существует ограничение на количество одновременных вызовов, которые HttpClient разрешает при запуске из Powershell, поэтому вызовы в одновременных пакетах, превышающих 40-50 запросов, начинают складываться. Кажется, что выполняется от 40 до 50 одновременных запросов, а остальные блокируются.
Я не ищу помощи в асинхронном программировании. Я ищу способ устранить разницу между поведением во время выполнения Visual Studio и поведением командной строки Powershell. Работа в режиме выпуска из зеленой кнопки Visual Studio ведет себя как ожидалось. Запускать из командной строки нет.
Я заполняю список задач асинхронными вызовами, а затем жду Task.WhenAll (tasks). Каждый вызов занимает от 300 до 400 миллисекунд. При запуске из Visual Studio все работает как положено. Я делаю одновременные партии по 1000 звонков, и каждый из них выполняется индивидуально в течение ожидаемого времени. Весь блок задач занимает всего несколько миллисекунд дольше, чем самый длинный индивидуальный вызов.
Поведение меняется, когда я запускаю ту же сборку из командной строки Powershell. Первые 40-50 звонков занимают ожидаемые 300-400 миллисекунд, но затем время отдельного звонка увеличивается до 20 секунд каждый. Я думаю, что звонки сериализуются, поэтому только 40-50 выполняются одновременно, пока остальные ждут.
После нескольких часов проб и ошибок я смог сузить его до HttpClient. Чтобы изолировать проблему, я смоделировал вызовы HttpClient.SendAsync с помощью метода, который выполняет Task.Delay (300) и возвращает ложный результат. В этом случае запуск из консоли ведет себя так же, как запуск из Visual Studio.
Я использую IHttpClientFactory, и я даже пытался отрегулировать лимит соединения на ServicePointManager.
Вот мой регистрационный код.
public static IServiceCollection RegisterHttpClient(this IServiceCollection services, int batchSize)
{
ServicePointManager.DefaultConnectionLimit = batchSize;
ServicePointManager.MaxServicePoints = batchSize;
ServicePointManager.SetTcpKeepAlive(true, 1000, 5000);
services.AddHttpClient(MSGraphRequestManager.HttpClientName, c =>
{
c.Timeout = TimeSpan.FromSeconds(360);
c.DefaultRequestHeaders.Add("User-Agent", "xxxxxxxxxxxx");
})
.ConfigurePrimaryHttpMessageHandler(() => new DefaultHttpClientHandler(batchSize));
return services;
}
Вот DefaultHttpClientHandler.
internal class DefaultHttpClientHandler : HttpClientHandler
{
public DefaultHttpClientHandler(int maxConnections)
{
this.MaxConnectionsPerServer = maxConnections;
this.UseProxy = false;
this.AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate;
}
}
Вот код, который устанавливает задачи.
var timer = Stopwatch.StartNew();
var tasks = new Task<(UpsertUserResult, TimeSpan)>[users.Length];
for (var i = 0; i < users.Length; ++i)
{
tasks[i] = this.CreateUserAsync(users[i]);
}
var results = await Task.WhenAll(tasks);
timer.Stop();
Вот как я издевался над HttpClient.
var httpClient = this.httpClientFactory.CreateClient(HttpClientName);
#if use_http
using var response = await httpClient.SendAsync(request);
#else
await Task.Delay(300);
var graphUser = new User { Id = "mockid" };
using var response = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(JsonConvert.SerializeObject(graphUser)) };
#endif
var responseContent = await response.Content.ReadAsStringAsync();
Вот показатели для пользователей 10k B2C, созданных через GraphAPI с использованием 500 одновременных запросов. Первые 500 запросов длиннее обычного, так как создаются соединения TCP.
Вот ссылка на параметры запуска консоли .
Вот ссылка на показатели запуска Visual Studio .
Время блокировки в метриках запуска VS отличается от того, что я сказал в этом посте, потому что я перенес весь синхронный доступ к файлам в конец процесса, чтобы максимально изолировать проблемный код для тестовых запусков.
Проект компилируется с использованием .Net Core 3.1. Я использую Visual Studio 2019 16.4.5.
источник
Ответы:
На ум приходят две вещи. Большинство Microsoft PowerShell было написано в версии 1 и 2. В версиях 1 и 2 System.Threading.Thread.ApartmentState MTA. В версиях с 3 по 5 состояние квартиры по умолчанию изменилось на STA.
Вторая мысль - похоже, они используют System.Threading.ThreadPool для управления потоками. Насколько велика ваша нить?
Если это не помогло, начните копать в System.Threading.
Когда я прочитал ваш вопрос, я подумал об этом блоге. https://devblogs.microsoft.com/oldnewthing/20170623-00/?p=96455
Коллега продемонстрировал пример программы, которая создает тысячу рабочих элементов, каждый из которых имитирует сетевой вызов, выполнение которого занимает 500 мс. В первой демонстрации сетевые вызовы блокировали синхронные вызовы, и пример программы ограничил пул потоков десятью потоками, чтобы сделать эффект более очевидным. В этой конфигурации первые несколько рабочих элементов были быстро отправлены в потоки, но затем задержка начала накапливаться, поскольку больше не было доступных потоков для обслуживания новых рабочих элементов, поэтому оставшиеся рабочие элементы должны были ждать дольше и дольше, чтобы поток стать доступным для обслуживания. Средняя задержка начала рабочего элемента составила более двух минут.
Обновление 1: я запустил PowerShell 7.0 из меню «Пуск», и состояние потока было STA. Отличается ли состояние потока в двух версиях?
Обновление 2: Я хотел бы получить лучший ответ, но вам придется сравнивать два окружения, пока что-то не выделяется.
Обновление 3:
https://docs.microsoft.com/en-us/uwp/api/windows.web.http.httpclient
Просто продолжайте сравнивать две среды, и проблема должна выделяться
источник