HttpClient ทำงานพร้อมกันแตกต่างกันเมื่อทำงานใน Powershell กว่าใน Visual Studio


10

ฉันกำลังโยกย้ายผู้ใช้นับล้านจากโฆษณาในสถานที่ไปยัง Azure AD B2C โดยใช้ MS Graph API เพื่อสร้างผู้ใช้ใน B2C ฉันได้เขียนแอปพลิเคชันคอนโซล. Net Core 3.1 เพื่อทำการย้ายข้อมูลนี้ เพื่อเพิ่มความเร็วของสิ่งต่าง ๆ ในขณะที่ฉันกำลังเรียกใช้กราฟ API พร้อมกัน มันใช้งานได้ดีมาก

ในระหว่างการพัฒนาฉันพบว่าประสิทธิภาพที่ยอมรับได้ในขณะที่เรียกใช้จาก Visual Studio 2019 แต่สำหรับการทดสอบฉันกำลังเรียกใช้จากบรรทัดคำสั่งใน Powershell 7 จาก Powershell ประสิทธิภาพของการโทรพร้อมกันไปยัง HttpClient นั้นแย่มาก ปรากฏว่ามีการ จำกัด จำนวนการโทรพร้อมกันที่ HttpClient อนุญาตเมื่อเรียกใช้จาก Powershell ดังนั้นการโทรแบบ Batches พร้อมกันที่มีมากกว่า 40 ถึง 50 คำขอจะเริ่มซ้อนกัน ดูเหมือนว่าจะเรียกใช้คำขอที่เกิดขึ้นพร้อมกัน 40 ถึง 50 รายการในขณะที่ปิดกั้นส่วนที่เหลือ

ฉันไม่ต้องการความช่วยเหลือเกี่ยวกับการเขียนโปรแกรม async ฉันกำลังมองหาวิธีที่จะแก้ปัญหาในการถ่ายความแตกต่างระหว่างพฤติกรรมการใช้งาน Visual Studio และการทำงานแบบบรรทัดคำสั่ง Powershell การทำงานในโหมดนำออกใช้จากปุ่มลูกศรสีเขียวของ Visual Studio จะทำงานตามที่คาดไว้ เรียกใช้จากบรรทัดคำสั่งไม่ได้

ฉันเติมรายการงานด้วยการโทรแบบ async จากนั้นรอ Task.WhenAll (งาน) การโทรแต่ละครั้งใช้เวลาระหว่าง 300 และ 400 มิลลิวินาที เมื่อเรียกใช้จาก Visual Studio จะทำงานตามที่คาดไว้ ฉันทำการโทรพร้อมกัน 1,000 การโทรและแต่ละการโทรจะเสร็จสิ้นภายในเวลาที่กำหนด บล็อกงานทั้งหมดใช้เวลาเพียงไม่กี่มิลลิวินาทีนานกว่าการโทรแต่ละครั้งที่ยาวที่สุด

พฤติกรรมเปลี่ยนไปเมื่อฉันเรียกใช้บิลด์เดียวกันจากบรรทัดคำสั่ง 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();

นี่คือตัวชี้วัดสำหรับผู้ใช้ B2C 10 พันคนที่สร้างผ่าน GraphAPI โดยใช้คำขอ 500 คำขอพร้อมกัน คำขอ 500 ครั้งแรกยาวกว่าปกติเนื่องจากกำลังสร้างการเชื่อมต่อ TCP

นี่คือการเชื่อมโยงไปยังตัวชี้วัดคอนโซลทำงาน

นี่คือการเชื่อมโยงไปยังภาพและตัวชี้วัดการทำงานสตูดิโอ

เวลาบล็อกในตัวชี้วัดการทำงาน VS แตกต่างจากที่ฉันพูดในโพสต์นี้เนื่องจากฉันย้ายการเข้าถึงไฟล์แบบซิงโครนัสทั้งหมดไปยังจุดสิ้นสุดของกระบวนการเพื่อพยายามแยกรหัสที่มีปัญหาออกให้มากที่สุดสำหรับการทดสอบการทำงาน

โครงการถูกคอมไพล์โดยใช้. Net Core 3.1 ฉันใช้ Visual Studio 2019 16.4.5


2
คุณได้ตรวจสอบสถานะการเชื่อมต่อของคุณด้วยยูทิลิตี netstat หลังจากแบทช์แรกแล้วหรือยัง? อาจให้ข้อมูลเชิงลึกเกี่ยวกับสิ่งที่เกิดขึ้นหลังจากงานสองสามงานแรกเสร็จสมบูรณ์
Pranav Negandhi

หากคุณไม่ได้รับการแก้ไขด้วยวิธีนี้ (Async การร้องขอ HTTP) คุณสามารถใช้การโทรแบบ HTTP HTTP สำหรับผู้ใช้แต่ละคนใน ConcurrentQueue [object] consumer / โปรดสร้างความเท่าเทียมกัน ฉันเพิ่งทำสิ่งนี้เพื่อประมาณ 200 ล้านไฟล์ใน PowerShell
thepip3r

1
@ thepip3r ฉันเพิ่งอ่านคำชื่นชมของคุณอีกครั้งและเข้าใจในครั้งนี้ ฉันจะจำไว้
Mark Lauter

1
ไม่มีผมว่าถ้าคุณอยากจะไป PowerShell แทน C #: leeholmes.com/blog/2018/09/05/...
thepip3r

1
@ thepip3r เพียงแค่อ่านบล็อกจาก Stephen Cleary ฉันควรจะดี
Mark Lauter

คำตอบ:


3

นึกถึงสองสิ่ง microsoft powershell ส่วนใหญ่เขียนในเวอร์ชัน 1 และ 2 เวอร์ชัน 1 และ 2 มี System.Threading.Thread.Thread ส่วนสถานะของ MTA ในเวอร์ชัน 3 ถึง 5 สถานะของอพาร์ทเมนท์จะเปลี่ยนเป็น STA โดยค่าเริ่มต้น

ความคิดที่สองคือดูเหมือนว่าพวกเขากำลังใช้ System.Threading.ThreadPool เพื่อจัดการเธรด เธรดพูลของคุณใหญ่แค่ไหน?

หากผู้ที่ไม่แก้ปัญหาให้เริ่มขุดใต้ System.Threading

เมื่อฉันอ่านคำถามของคุณฉันคิดถึงบล็อกนี้ https://devblogs.microsoft.com/oldnewthing/20170623-00/?p=96455

เพื่อนร่วมงานสาธิตด้วยโปรแกรมตัวอย่างที่สร้างรายการงานหนึ่งพันรายการซึ่งแต่ละรายการจำลองการโทรผ่านเครือข่ายที่ใช้เวลาดำเนินการ 500ms ในการสาธิตครั้งแรกการเรียกเครือข่ายกำลังปิดกั้นการโทรแบบซิงโครนัสและโปรแกรมตัวอย่าง จำกัด เธรดพูลสิบเธรดเพื่อให้มีผลชัดเจนยิ่งขึ้น ภายใต้การกำหนดค่านี้ไอเท็มงานแรกสองสามรายการจะถูกส่งไปยังเธรดอย่างรวดเร็ว แต่เวลาแฝงเริ่มสร้างเนื่องจากไม่มีเธรดเพิ่มเติมที่พร้อมใช้งานเพื่อให้บริการไอเท็มงานใหม่ดังนั้นไอเท็มงานที่เหลือต้องรออีกต่อไป พร้อมให้บริการแล้ว เวลาเฉลี่ยในการเริ่มต้นของรายการงานล่าช้ากว่าสองนาที

อัปเดต 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 ()
Mark Lauter

กลุ่มเธรดขั้นต่ำเริ่มต้นคือ 12 ฉันพยายามเพิ่มขนาดกลุ่มขั้นต่ำเป็นขนาดแบทช์ของฉัน (500 สำหรับการทดสอบ) สิ่งนี้ไม่มีผลต่อพฤติกรรม
Mark Lauter

มีเธรดกี่ตัวที่สร้างขึ้นในทั้งสองสภาวะแวดล้อม
แอรอน

ฉันสงสัยว่าเธรด 'HttpClient' มีกี่เธรดเพราะมันทำงานทั้งหมด
แอรอน

สถานะพาร์ทเมนต์ในทั้งสองเวอร์ชันของคุณคืออะไร
แอรอน
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.