Паралельна поведінка HttpClient відрізняється під час роботи в Powershell, ніж у Visual Studio


10

Я мігрую мільйони користувачів з наперед 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.


2
Ви перевірили стан своїх зв’язків з утилітою netstat після першої партії? Це може дати деяку інформацію про те, що відбувається після того, як будуть виконані перші кілька завдань.
Пранів Неганді

Якщо ви не вирішите це рішення таким чином (Асинхронізувати HTTP-запит), ви завжди можете використовувати синхронізовані HTTP-дзвінки для кожного користувача у паралелізмі паралелізму споживача / виробника ConcurrentQueue [об'єкт]. Нещодавно я зробив це для близько 200 мільйонів файлів у PowerShell.
thepip3r

1
@ thepip3r Я щойно перечитав вашу оцінку та цього разу зрозумів. Я буду мати це на увазі.
Марк Лаутер

1
Ні , я кажу, якщо ви хочете йти PowerShell замість C #: leeholmes.com/blog/2018/09/05 / ... .
thepip3r

1
@ thepip3r Просто прочитайте запис у блозі від Стівена Клірі. Я повинен бути хорошим.
Марк Лаутер

Відповіді:


3

Дві речі спадають на думку. Більшість програмних засобів Microsoft були написані у версіях 1 та 2. Версії 1 та 2 мають System.Threading.Thread.ApartmentState of MTA. У версіях 3 до 5 стан квартири за замовчуванням змінився на STA.

Друга думка: це звучить так, як вони використовують System.Threading.ThreadPool для управління потоками. Наскільки великий ваш нитковий пул?

Якщо це не вирішило проблему, починайте копати під System.Threading.

Коли я читав ваше запитання, я думав про цей блог. https://devblogs.microsoft.com/oldnewthing/20170623-00/?p=96455

Колега продемонстрував зразок програми, яка створює тисячу робочих елементів, кожен з яких імітує мережевий дзвінок, на який потрібно 500 мс. У першій демонстрації мережеві дзвінки блокували синхронні виклики, а зразок програми обмежував пул потоків до десяти потоків, щоб зробити ефект більш очевидним. За цією конфігурацією перші кілька робочих елементів були швидко відправлені в потоки, але потім затримка почала наростати, оскільки для обслуговування нових робочих елементів більше не було доступних потоків, тому для інших робочих елементів довелося чекати довше і довше, щоб потік стають доступними для його обслуговування. Середня затримка до початку робочого пункту становила понад дві хвилини.

Оновлення 1: я запустив PowerShell 7.0 з меню "Пуск", і стан потоку був STA. Чи відрізняється стан потоку в двох версіях?

PS C:\Program Files\PowerShell\7>  [System.Threading.Thread]::CurrentThread

ManagedThreadId    : 12
IsAlive            : True
IsBackground       : False
IsThreadPoolThread : False
Priority           : Normal
ThreadState        : Running
CurrentCulture     : en-US
CurrentUICulture   : en-US
ExecutionContext   : System.Threading.ExecutionContext
Name               : Pipeline Execution Thread
ApartmentState     : STA

Оновлення 2: Я хочу краще відповісти, але вам доведеться порівняти два середовища, поки щось не виділиться.

PS C:\Windows\system32> [System.Net.ServicePointManager].GetProperties() | select name

Name                               
----                               
SecurityProtocol                   
MaxServicePoints                   
DefaultConnectionLimit             
MaxServicePointIdleTime            
UseNagleAlgorithm                  
Expect100Continue                  
EnableDnsRoundRobin                
DnsRefreshTimeout                  
CertificatePolicy                  
ServerCertificateValidationCallback
ReusePort                          
CheckCertificateRevocationList     
EncryptionPolicy            

Оновлення 3:

https://docs.microsoft.com/en-us/uwp/api/windows.web.http.httpclient

Крім того, кожен екземпляр HttpClient використовує власний пул з'єднань, ізолюючи свої запити від запитів, виконаних іншими екземплярами HttpClient.

Якщо додаток, що використовує HttpClient та пов’язані з ним класи в просторі імен Windows.Web.Http, завантажує велику кількість даних (50 мегабайт або більше), то програма повинна завантажувати завантаження та не використовувати буферизацію за замовчуванням. Якщо використовується буферизація за замовчуванням, використання пам'яті клієнта буде дуже великою, що може призвести до зниження продуктивності.

Просто продовжуйте порівнювати два середовища, і питання повинно виділятися

Add-Type -AssemblyName System.Net.Http
$client = New-Object -TypeName System.Net.Http.Httpclient
$client | format-list *

DefaultRequestHeaders        : {}
BaseAddress                  : 
Timeout                      : 00:01:40
MaxResponseContentBufferSize : 2147483647

Під час запуску в Powershell 7.0 System.Threading.Thread.CurrentThread.GetApartmentState () повертає MTA з програми Program.Main ()
Марк Лаутер

Типовий пул мінімальних потоків становив 12, я намагався збільшити розмір мінімального пулу до мого розміру партії (500 для тестування). Це не впливало на поведінку.
Марк Лаутер

Скільки ниток генерується в обох середовищах?
Аарон

Мені було цікаво, скільки ниток має "HttpClient", тому що він робить все на роботі.
Аарон

Який стан квартири в обох Ваших версіях?
Аарон
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.