Я мігрую мільйони користувачів з наперед 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 одночасних запитів, блокуючи решту.
Я не шукаю допомоги з програмуванням async. Я шукаю спосіб усунути різницю між поведінкою під час виконання Visual Studio та поведінкою командного рядка Powershell. Запуск у режимі випуску із зеленої кнопки зі стрілкою Visual Studio веде себе як очікувалося. Запуск з командного рядка не робить.
Я заповнюю список завдань асинхронними дзвінками, а потім чекаю Task.WhenAll (завдання). Кожен дзвінок займає від 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.