Я перемещаю миллионы пользователей из локальной AD в Azure AD B2 C, используя MS Graph API для создания пользователей в B2 C. Я написал консольное приложение. Net Core 3.1 для выполнения этой миграции. Чтобы ускорить процесс, я делаю параллельные вызовы Graph API. Это работает отлично - вроде.
Во время разработки я испытал приемлемую производительность при запуске из Visual Studio 2019, но для тестирования я запускаю из командной строки в Powershell 7. Из Powershell производительность параллельных вызовов HttpClient очень плохо. Похоже, что существует ограничение на количество одновременных вызовов, которые HttpClient разрешает при запуске из Powershell, поэтому вызовы в одновременных пакетах, превышающих 40-50 запросов, начинают складываться. Кажется, он выполняет от 40 до 50 одновременных запросов при блокировке остальных.
Я не ищу помощи в асинхронном программировании c. Я ищу способ устранить разницу между поведением во время выполнения Visual Studio и поведением командной строки Powershell. Работа в режиме выпуска из зеленой кнопки Visual Studio ведет себя как ожидалось. Запуск из командной строки не выполняется.
Я заполняю список задач асинхронными вызовами c, а затем жду Task.WhenAll (tasks). Каждый вызов занимает от 300 до 400 миллисекунд. При запуске из Visual Studio все работает как положено. Я делаю одновременные партии по 1000 звонков, и каждый из них выполняется индивидуально в течение ожидаемого времени. Весь блок задач занимает всего несколько миллисекунд дольше, чем самый длинный индивидуальный вызов.
Поведение меняется, когда я запускаю ту же сборку из командной строки Powershell. Первые 40–50 вызовов занимают ожидаемые 300–400 миллисекунд, но затем время отдельного вызова увеличивается до 20 секунд каждый. Я думаю, что вызовы сериализуются, поэтому только 40–50 выполняются одновременно, в то время как остальные ждут.
После нескольких часов проб и ошибок я смог сузить его до HttpClient. Чтобы изолировать проблему, я смоделировал вызовы HttpClient.SendAsyn c с помощью метода, который выполняет 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 B2 C, созданных с помощью GraphAPI с использованием 500 одновременных запросов. Первые 500 запросов длиннее, чем обычно, поскольку создаются соединения TCP.
Вот ссылка на метрики запуска консоли .
Вот ссылка на Метрики выполнения Visual Studio .
Время блокировки в метриках запуска VS отличается от того, что я говорил в этом посте, потому что я перенес весь синхронный доступ к файлу в конец процесса, пытаясь максимально изолировать проблемный код c для тестовых прогонов.
Проект компилируется с использованием. Net Core 3.1. Я использую Visual Studio 2019 16.4.5.