Perilaku bersamaan HttpClient berbeda ketika berjalan di Powershell daripada di Visual Studio

10

Saya memigrasikan jutaan pengguna dari on-prem AD ke Azure AD B2C menggunakan MS Graph API untuk membuat pengguna di B2C. Saya telah menulis aplikasi konsol .Net Core 3.1 untuk melakukan migrasi ini. Untuk mempercepat, saya melakukan panggilan bersamaan ke Graph API. Ini bekerja dengan baik - semacam.

Selama pengembangan saya mengalami kinerja yang dapat diterima saat menjalankan dari Visual Studio 2019, tetapi untuk pengujian saya menjalankan dari baris perintah di Powershell 7. Dari Powershell kinerja panggilan bersamaan ke HttpClient sangat buruk. Tampaknya ada batas jumlah panggilan bersamaan yang diizinkan HttpClient saat berjalan dari Powershell, jadi panggilan dalam kumpulan bersamaan yang lebih besar dari 40 hingga 50 permintaan mulai menumpuk. Tampaknya menjalankan 40 hingga 50 permintaan bersamaan sekaligus memblokir sisanya.

Saya tidak mencari bantuan dengan pemrograman async. Saya sedang mencari cara untuk memecahkan masalah perbedaan antara perilaku run-time Visual Studio dan perilaku run-time baris perintah Powershell. Berjalan dalam mode rilis dari tombol panah hijau Visual Studio berperilaku seperti yang diharapkan. Menjalankan dari baris perintah tidak.

Saya mengisi daftar tugas dengan panggilan async dan kemudian menunggu Task.WhenAll (tugas). Setiap panggilan membutuhkan antara 300 dan 400 milidetik. Saat menjalankan dari Visual Studio berfungsi seperti yang diharapkan. Saya melakukan batch 1000 panggilan secara bersamaan dan masing-masing individu selesai dalam waktu yang diharapkan. Seluruh blok tugas hanya membutuhkan beberapa milidetik lebih lama dari panggilan individu terlama.

Perilaku berubah ketika saya menjalankan build yang sama dari baris perintah Powershell. 40 hingga 50 panggilan pertama mengambil 300 hingga 400 milidetik yang diharapkan, tetapi kemudian waktu panggilan individu masing-masing tumbuh hingga 20 detik. Saya pikir panggilan serialisasi, jadi hanya 40 hingga 50 sedang dieksekusi sekaligus sementara yang lain menunggu.

Setelah berjam-jam mencoba-coba, saya bisa mempersempitnya ke HttpClient. Untuk mengisolasi masalah, saya mengejek panggilan ke HttpClient.SendAsync dengan metode yang melakukan Task.Delay (300) dan mengembalikan hasil tiruan. Dalam hal ini menjalankan dari konsol berperilaku identik dengan berjalan dari Visual Studio.

Saya menggunakan IHttpClientFactory dan saya bahkan sudah mencoba menyesuaikan batas koneksi pada ServicePointManager.

Ini kode registrasi saya.

    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;
    }

Inilah 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;
    }
}

Inilah kode yang mengatur tugas.

        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();

Inilah cara saya mengejek 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();

Berikut adalah metrik untuk pengguna 10k B2C yang dibuat melalui GraphAPI menggunakan 500 permintaan bersamaan. 500 permintaan pertama lebih panjang dari biasanya karena koneksi TCP sedang dibuat.

Berikut tautan ke metrik jalankan konsol .

Berikut tautan ke metrik menjalankan Visual Studio .

Waktu blok dalam metrik VS menjalankan berbeda dari apa yang saya katakan dalam posting ini karena saya memindahkan semua akses file sinkron ke akhir proses dalam upaya untuk mengisolasi kode bermasalah sebanyak mungkin untuk menjalankan tes.

Proyek ini dikompilasi menggunakan Net Core 3.1. Saya menggunakan Visual Studio 2019 16.4.5.

Mark Lauter
sumber
2
Sudahkah Anda meninjau keadaan koneksi Anda dengan utilitas netstat setelah batch pertama? Mungkin memberikan beberapa wawasan tentang apa yang terjadi setelah beberapa tugas pertama selesai.
Pranav Negandhi
Jika Anda tidak menyelesaikannya dengan cara ini (Async permintaan HTTP), Anda selalu dapat menggunakan panggilan HTTP sinkronisasi untuk setiap pengguna dalam paralelisme konsumen / produsen ConcurrentQueue [objek]. Baru-baru ini saya melakukan ini untuk sekitar 200 juta file di PowerShell.
thepip3r
1
@ thepip3r Saya baru saja membaca ulang pujian Anda dan memahaminya kali ini. Saya akan mengingatnya.
Mark Lauter
1
Tidak, saya katakan, jika Anda ingin menggunakan PowerShell alih-alih c #: leeholmes.com/blog/2018/09/05/… .
thepip3r
1
@ thepip3r Baca saja entri blog dari Stephen Cleary. Saya harus baik.
Mark Lauter

Jawaban:

3

Dua hal muncul di benak saya. Kebanyakan microsoft powershell ditulis dalam versi 1 dan 2. Versi 1 dan 2 memiliki System.Threading.Thread.ApartmentState of MTA. Dalam versi 3 hingga 5 keadaan apartemen diubah menjadi STA secara default.

Pikiran kedua adalah sepertinya mereka menggunakan System.Threading.ThreadPool untuk mengelola utas. Seberapa besar threadpool Anda?

Jika itu tidak menyelesaikan masalah, mulailah menggali di bawah System.Threading.

Ketika saya membaca pertanyaan Anda, saya memikirkan blog ini. https://devblogs.microsoft.com/oldnewthing/20170623-00/?p=96455

Seorang kolega berdemonstrasi dengan program sampel yang menciptakan seribu item pekerjaan, yang masing-masing mensimulasikan panggilan jaringan yang membutuhkan 500 ms untuk menyelesaikannya. Dalam demonstrasi pertama, panggilan jaringan memblokir panggilan sinkron, dan program sampel membatasi kumpulan utas hingga sepuluh utas agar efeknya lebih jelas. Di bawah konfigurasi ini, beberapa item kerja pertama dengan cepat dikirim ke utas, tetapi kemudian latensi mulai dibangun karena tidak ada lagi utas yang tersedia untuk melayani item kerja baru, sehingga item kerja yang tersisa harus menunggu lebih lama dan lebih lama untuk sebuah utas untuk menjadi tersedia untuk melayaninya. Latensi rata-rata hingga awal item kerja lebih dari dua menit.

Pembaruan 1: Saya menjalankan PowerShell 7.0 dari menu mulai dan status utasnya adalah STA. Apakah status utas berbeda dalam dua versi?

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

Pembaruan 2: Saya berharap jawaban yang lebih baik tetapi, Anda harus membandingkan dua lingkungan sampai ada sesuatu yang menonjol.

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

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

Pembaruan 3:

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

Selain itu, setiap instance HttpClient menggunakan kumpulan koneksi sendiri, mengisolasi permintaannya dari permintaan yang dieksekusi oleh instance HttpClient lainnya.

Jika aplikasi menggunakan HttpClient dan kelas terkait di namespace Windows.Web.Http mengunduh sejumlah besar data (50 megabita atau lebih), maka aplikasi tersebut harus mengalirkan unduhan tersebut dan tidak menggunakan buffering default. Jika buffering default digunakan, penggunaan memori klien akan menjadi sangat besar, berpotensi mengakibatkan kinerja berkurang.

Teruslah membandingkan kedua lingkungan dan masalahnya harus menonjol

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
Harun
sumber
Saat berjalan di Powershell 7.0 System.Threading.Thread.CurrentThread.GetApartmentState () mengembalikan MTA dari dalam Program.Main ()
Mark Lauter
Kumpulan utas min default adalah 12, saya mencoba meningkatkan ukuran kumpulan min ke ukuran kumpulan saya (500 untuk pengujian). Ini tidak berpengaruh pada perilaku.
Mark Lauter
Berapa banyak utas yang dihasilkan di kedua lingkungan?
Aaron
Saya bertanya-tanya berapa banyak utas yang dimiliki 'HttpClient' karena ia melakukan semuanya pada pekerjaan.
Aaron
Apa kondisi apartemen di kedua versi Anda?
Aaron