Hành vi đồng thời của httpClient khác nhau khi chạy trong Powershell so với Visual Studio


10

Tôi đang di chuyển hàng triệu người dùng từ AD tại chỗ sang Azure AD B2C bằng MS Graph API để tạo người dùng trong B2C. Tôi đã viết một ứng dụng bảng điều khiển .Net Core 3.1 để thực hiện việc di chuyển này. Để tăng tốc mọi thứ cùng với việc tôi thực hiện các cuộc gọi đồng thời tới API đồ thị. Điều này đang làm việc tuyệt vời - loại.

Trong quá trình phát triển, tôi đã trải nghiệm hiệu năng chấp nhận được khi chạy từ Visual Studio 2019, nhưng để thử nghiệm tôi đang chạy từ dòng lệnh trong Powershell 7. Từ Powershell, hiệu suất của các cuộc gọi đồng thời đến HttpClient là rất tệ. Dường như có giới hạn về số lượng cuộc gọi đồng thời mà HTTPClient cho phép khi chạy từ Powershell, vì vậy các cuộc gọi trong các đợt đồng thời lớn hơn 40 đến 50 yêu cầu bắt đầu xếp chồng lên nhau. Nó dường như đang chạy 40 đến 50 yêu cầu đồng thời trong khi chặn phần còn lại.

Tôi không tìm kiếm sự trợ giúp với lập trình async. Tôi đang tìm cách khắc phục sự khác biệt giữa hành vi thời gian chạy của Visual Studio và hành vi thời gian chạy dòng lệnh của Powershell. Chạy trong chế độ phát hành từ nút mũi tên màu xanh lá cây của Visual Studio hoạt động như mong đợi. Chạy từ dòng lệnh không.

Tôi điền vào một danh sách nhiệm vụ với các cuộc gọi không đồng bộ và sau đó chờ đợi Nhiệm vụ.When ALL (nhiệm vụ). Mỗi cuộc gọi mất từ ​​300 đến 400 mili giây. Khi chạy từ Visual Studio, nó hoạt động như mong đợi. Tôi thực hiện đồng thời 1000 đợt gọi và mỗi cuộc gọi hoàn thành trong thời gian dự kiến. Toàn bộ khối tác vụ chỉ mất vài mili giây so với cuộc gọi cá nhân dài nhất.

Hành vi thay đổi khi tôi chạy cùng một bản dựng từ dòng lệnh Powershell. 40 đến 50 cuộc gọi đầu tiên mất 300 đến 400 mili giây dự kiến ​​nhưng sau đó thời gian gọi riêng lẻ tăng lên 20 giây mỗi cuộc gọi. Tôi nghĩ rằng các cuộc gọi được tuần tự hóa, vì vậy chỉ có 40 đến 50 được thực hiện tại một thời điểm trong khi những người khác chờ đợi.

Sau nhiều giờ dùng thử và lỗi, tôi đã có thể thu hẹp nó xuống thành HTTPClient. Để tách biệt vấn đề, tôi đã mô phỏng các cuộc gọi đến HttpClient.SendAsync bằng một phương thức thực hiện Task.Delay (300) và trả về kết quả giả. Trong trường hợp này, chạy từ bàn điều khiển hoạt động giống hệt như chạy từ Visual Studio.

Tôi đang sử dụng IHttpClientFactory và thậm chí tôi đã thử điều chỉnh giới hạn kết nối trên ServicePointManager.

Đây là mã đăng ký của tôi.

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

Đây là 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;
    }
}

Đây là mã thiết lập các nhiệm vụ.

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

Đây là cách tôi chế nhạo 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();

Dưới đây là số liệu cho 10k người dùng B2C được tạo thông qua GraphAPI bằng 500 yêu cầu đồng thời. 500 yêu cầu đầu tiên dài hơn bình thường vì các kết nối TCP đang được tạo.

Đây là một liên kết đến bảng điều khiển chạy số liệu .

Đây là một liên kết đến các số liệu chạy Visual Studio .

Thời gian chặn trong số liệu chạy VS khác với những gì tôi đã nói trong bài đăng này vì tôi đã chuyển tất cả quyền truy cập tệp đồng bộ vào cuối quy trình trong nỗ lực cô lập mã có vấn đề nhất có thể cho quá trình chạy thử.

Dự án được biên dịch bằng .Net Core 3.1. Tôi đang sử dụng Visual Studio 2019 16.4.5.


2
Bạn đã xem lại trạng thái kết nối của mình với tiện ích netstat sau đợt đầu tiên chưa? Nó có thể cung cấp một số cái nhìn sâu sắc về những gì đang diễn ra sau khi một vài nhiệm vụ đầu tiên được hoàn thành.
Pranav Negandhi

Nếu bạn không giải quyết theo cách này (Không đồng bộ yêu cầu HTTP), bạn luôn có thể sử dụng các cuộc gọi HTTP đồng bộ hóa cho mỗi người dùng trong chế độ song song của người tiêu dùng / nhà sản xuất [đối tượng]. Gần đây tôi đã làm điều này cho khoảng 200 triệu tệp trong PowerShell.
thepip3r

1
@ thepip3r Tôi vừa đọc lại lời khen của bạn và hiểu nó lần này. Tôi sẽ ghi nhớ điều đó.
Mark Lauter

1
Không, tôi đang nói, nếu bạn muốn đi PowerShell thay vì c #: leeholmes.com/blog/2018/09/05/ .
thepip3r

1
@ thepip3r Chỉ cần đọc mục blog từ Stephen Cleary. Tôi nên tốt
Đánh dấu Lauter

Câu trả lời:


3

Hai điều tôi suy nghĩ. Hầu hết các quyền hạn của microsoft được viết trong phiên bản 1 và 2. Phiên bản 1 và 2 có System.Threading.Thread.ApemonyState của MTA. Trong phiên bản 3 đến 5, trạng thái căn hộ thay đổi thành STA theo mặc định.

Ý nghĩ thứ hai là có vẻ như họ đang sử dụng System.Threading.ThreadPool để quản lý các luồng. Chủ đề của bạn lớn như thế nào?

Nếu những vấn đề đó không giải quyết được thì hãy bắt đầu đào theo System.Threading.

Khi tôi đọc câu hỏi của bạn, tôi nghĩ về blog này. https://devbloss.microsoft.com/oldnewthing/20170623-00/?p=96455

Một đồng nghiệp đã trình diễn với một chương trình mẫu tạo ra một nghìn mục công việc, mỗi mục mô phỏng một cuộc gọi mạng mất 500ms để hoàn thành. Trong phần trình diễn đầu tiên, các cuộc gọi mạng đã chặn các cuộc gọi đồng bộ và chương trình mẫu đã giới hạn nhóm luồng thành mười luồng để làm cho hiệu ứng rõ ràng hơn. Theo cấu hình này, một vài mục công việc đầu tiên đã nhanh chóng được gửi đến các luồng, nhưng sau đó độ trễ bắt đầu được xây dựng do không có nhiều luồng để phục vụ các mục công việc mới, vì vậy các mục công việc còn lại phải chờ lâu hơn và lâu hơn cho một luồng trở nên có sẵn để phục vụ nó. Thời gian trễ trung bình để bắt đầu mục công việc là hơn hai phút.

Cập nhật 1: Tôi đã chạy PowerShell 7.0 từ menu start và trạng thái luồng là STA. Là trạng thái chủ đề khác nhau trong hai phiên bản?

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

Cập nhật 2: Tôi muốn câu trả lời tốt hơn nhưng, bạn sẽ so sánh hai môi trường cho đến khi một cái gì đó nổi bật.

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

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

Cập nhật 3:

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

Ngoài ra, mọi phiên bản HTTPClient đều sử dụng nhóm kết nối riêng, tách biệt các yêu cầu của nó khỏi các yêu cầu được thực hiện bởi các phiên bản httpClient khác.

Nếu một ứng dụng sử dụng HttpClient và các lớp liên quan trong không gian tên Windows.Web.Http tải xuống một lượng lớn dữ liệu (50 megabyte trở lên), thì ứng dụng sẽ truyền phát các bản tải xuống đó và không sử dụng bộ đệm mặc định. Nếu bộ đệm mặc định được sử dụng, việc sử dụng bộ nhớ máy khách sẽ rất lớn, có khả năng dẫn đến hiệu suất giảm.

Chỉ cần tiếp tục so sánh hai môi trường và vấn đề sẽ nổi bật

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

Khi chạy trong Powershell 7.0 System.Threading.Thread.CurrentThread.GetApartmentState () trả về MTA từ bên trong Program.Main ()
Đánh dấu Lauter

Nhóm luồng mặc định tối thiểu là 12, tôi đã thử tăng kích thước nhóm tối thiểu lên kích thước lô của mình (500 để thử nghiệm). Điều này không có tác dụng đối với hành vi.
Mark Lauter

Có bao nhiêu chủ đề được tạo ra trong cả hai môi trường?
Aaron

Tôi đã tự hỏi có bao nhiêu chủ đề 'HttpClient' vì nó đang làm tất cả trong công việc.
Aaron

Trạng thái căn hộ trong cả hai phiên bản của bạn là gì?
Aaron
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.