Chi phí chung của việc tạo một httpClient mới cho mỗi cuộc gọi trong máy khách WebAPI là gì?


162

Điều gì sẽ là HttpClienttrọn đời của một khách hàng WebAPI?
Có tốt hơn để có một phiên bản HttpClientcho nhiều cuộc gọi không?

Chi phí chung của việc tạo và xử lý một HttpClientyêu cầu, như trong ví dụ dưới đây (lấy từ http://www.asp.net/web-api/overview/web-api-clents/calling-a-web-api-from- a-net-client ):

using (var client = new HttpClient())
{
    client.BaseAddress = new Uri("http://localhost:9000/");
    client.DefaultRequestHeaders.Accept.Clear();
    client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

    // New code:
    HttpResponseMessage response = await client.GetAsync("api/products/1");
    if (response.IsSuccessStatusCode)
    {
        Product product = await response.Content.ReadAsAsync<Product>();
        Console.WriteLine("{0}\t${1}\t{2}", product.Name, product.Price, product.Category);
    }
}

Tôi không chắc chắn Stopwatch, tuy nhiên , bạn có thể sử dụng lớp để đánh giá nó. Ước tính của tôi sẽ có ý nghĩa hơn khi có một HttpClient, giả sử tất cả các trường hợp đó được sử dụng trong cùng một bối cảnh.
Matthew

Câu trả lời:


215

HttpClientđã được thiết kế để được sử dụng lại cho nhiều cuộc gọi . Thậm chí trên nhiều chủ đề. Các HttpClientHandlerThông tin xác thực và Cookie được dự định sẽ được sử dụng lại qua các cuộc gọi. Có một phiên bản mới HttpClientđòi hỏi phải thiết lập lại tất cả những thứ đó. Ngoài ra, DefaultRequestHeadersthuộc tính chứa các thuộc tính được dành cho nhiều cuộc gọi. Phải đặt lại các giá trị đó trên mỗi yêu cầu đánh bại điểm.

Một lợi ích lớn khác HttpClientlà khả năng thêm HttpMessageHandlersvào đường ống yêu cầu / phản hồi để áp dụng các mối quan tâm xuyên suốt. Chúng có thể được ghi nhật ký, kiểm toán, điều chỉnh, xử lý chuyển hướng, xử lý ngoại tuyến, nắm bắt các số liệu. Tất cả các loại khác nhau. Nếu một httpClient mới được tạo cho mỗi yêu cầu, thì tất cả các trình xử lý thông báo này cần được thiết lập theo từng yêu cầu và bằng cách nào đó, bất kỳ trạng thái cấp ứng dụng nào được chia sẻ giữa các yêu cầu cho các trình xử lý này cũng cần được cung cấp.

Bạn càng sử dụng các tính năng của HttpClient, bạn sẽ càng thấy rằng việc sử dụng lại một cá thể hiện có có ý nghĩa.

Tuy nhiên, vấn đề lớn nhất, theo tôi là khi một HttpClientlớp bị loại bỏ, nó sẽ xử lý HttpClientHandler, sau đó buộc phải đóng TCP/IPkết nối trong nhóm kết nối được quản lý bởi ServicePointManager. Điều này có nghĩa là mỗi yêu cầu với một HttpClientyêu cầu mới đòi hỏi phải thiết lập lại một TCP/IPkết nối mới .

Từ các thử nghiệm của tôi, sử dụng HTTP đơn giản trên mạng LAN, hiệu năng đạt được là không đáng kể. Tôi nghi ngờ điều này là do có một trình giữ TCP cơ bản đang giữ kết nối mở ngay cả khi HttpClientHandlercố gắng đóng nó.

Theo yêu cầu qua internet, tôi đã thấy một câu chuyện khác. Tôi đã thấy một hiệu suất 40% đạt được do phải mở lại yêu cầu mỗi lần.

Tôi nghi ngờ cú đánh vào một HTTPSkết nối sẽ còn tồi tệ hơn.

Lời khuyên của tôi là giữ một phiên bản của HTTPClient trong suốt thời gian ứng dụng của bạn cho từng API riêng biệt mà bạn kết nối.


5
which then forcibly closes the TCP/IP connection in the pool of connections that is managed by ServicePointManagerLàm thế nào chắc chắn bạn về tuyên bố này? Điều đó thật khó tin. HttpClientđối với tôi giống như một đơn vị công việc được cho là thường xuyên được khởi tạo.
usr

2
@vkelman Có, bạn vẫn có thể sử dụng lại một thể hiện của HTTPClient ngay cả khi bạn đã tạo nó bằng một HTTPClientHandler mới. Ngoài ra, xin lưu ý rằng có một hàm tạo đặc biệt dành cho HTTPClient cho phép bạn sử dụng lại HTTPClientHandler và loại bỏ HTTPClient mà không làm mất kết nối.
Darrel Miller

2
@vkelman Tôi thích giữ HTTPClient xung quanh, nhưng nếu bạn thích giữ httpClientHandler, nó sẽ giữ kết nối mở khi tham số thứ hai sai.
Darrel Miller

2
@DarrelMiller Vì vậy, có vẻ như kết nối được liên kết với HttpClientHandler. Tôi biết rằng để mở rộng quy mô, tôi không muốn phá hủy kết nối vì vậy tôi cần phải giữ một HTTPClientHandler và tạo tất cả các phiên bản HTTPClient của mình từ đó HOẶC tạo một cá thể httpClient tĩnh. Tuy nhiên, nếu CookieContainer bị ràng buộc với HttpClientHandler và cookie của tôi cần khác nhau cho mỗi yêu cầu, bạn khuyên gì? Tôi muốn tránh đồng bộ hóa luồng trên một HTTPClientHandler tĩnh bằng cách sửa đổi CookieContainer của nó cho mỗi yêu cầu.
Dave Black

2
@ Sana.91 Bạn có thể. Sẽ là tốt hơn để đăng ký nó như là một singleton trong bộ sưu tập dịch vụ và truy cập nó theo cách đó.
Darrel Miller

69

Nếu bạn muốn ứng dụng của mình mở rộng quy mô, sự khác biệt là LỚN! Tùy thuộc vào tải, bạn sẽ thấy số hiệu suất rất khác nhau. Như Darrel Miller đã đề cập, HttpClient được thiết kế để được sử dụng lại trong các yêu cầu. Điều này đã được xác nhận bởi những người trong nhóm BCL đã viết nó.

Một dự án gần đây tôi đã có là để giúp một nhà bán lẻ máy tính trực tuyến rất lớn và nổi tiếng mở rộng quy mô cho lưu lượng Thứ Sáu Đen / ngày lễ cho một số hệ thống mới. Chúng tôi gặp phải một số vấn đề về hiệu suất xung quanh việc sử dụng HttpClient. Vì nó thực hiện IDisposable, các nhà phát triển đã làm những gì bạn thường làm bằng cách tạo một thể hiện và đặt nó bên trong một using()câu lệnh. Khi chúng tôi bắt đầu tải thử nghiệm, ứng dụng đưa máy chủ đến đầu gối - vâng, máy chủ không chỉ là ứng dụng. Lý do là mọi phiên bản của HTTPClient đều mở một cổng trên máy chủ. Do việc hoàn thiện không xác định của GC và thực tế là bạn đang làm việc với các tài nguyên máy tính trải rộng trên nhiều lớp OSI , việc đóng các cổng mạng có thể mất một thời gian. Trong thực tế hệ điều hành Windows bản thâncó thể mất tới 20 giây để đóng một cổng (theo Microsoft). Chúng tôi đã mở các cổng nhanh hơn mức chúng có thể bị đóng - cạn kiệt cổng máy chủ khiến CPU tăng 100%. Cách khắc phục của tôi là thay đổi httpClient thành một thể hiện tĩnh để giải quyết vấn đề. Vâng, nó là một tài nguyên dùng một lần, nhưng bất kỳ chi phí nào cũng lớn hơn nhiều so với sự khác biệt về hiệu suất. Tôi khuyến khích bạn thực hiện một số thử nghiệm tải để xem ứng dụng của bạn hoạt động như thế nào.

Bạn cũng có thể xem trang Hướng dẫn về WebAPI để biết tài liệu và ví dụ tại https://www.asp.net/web-api/overview/advified/calling-a-web-api-from-a-net-client

Đặc biệt chú ý đến cuộc gọi này:

HttpClient dự định sẽ được khởi tạo một lần và được sử dụng lại trong suốt vòng đời của một ứng dụng. Đặc biệt là trong các ứng dụng máy chủ, việc tạo một cá thể httpClient mới cho mọi yêu cầu sẽ làm cạn kiệt số lượng ổ cắm có sẵn trong các tải nặng. Điều này sẽ dẫn đến lỗi SocketException.

Nếu bạn thấy rằng bạn cần sử dụng một tĩnh HttpClientvới các tiêu đề, địa chỉ cơ sở khác nhau, v.v. điều bạn cần làm là tạo HttpRequestMessagethủ công và đặt các giá trị đó lên HttpRequestMessage. Sau đó, sử dụngHttpClient:SendAsync(HttpRequestMessage requestMessage, ...)

CẬP NHẬT cho .NET Core : Bạn nên sử dụng IHttpClientFactorythông qua Dependency Injection để tạo HttpClientphiên bản. Nó sẽ quản lý suốt đời cho bạn và bạn không cần phải loại bỏ nó một cách rõ ràng. Xem Tạo yêu cầu HTTP bằng IHttpClientFactory trong ASP.NET Core


1
bài đăng này chứa cái nhìn sâu sắc hữu ích cho những người sẽ làm bài kiểm tra căng thẳng ..!
Sana.91

9

Như các câu trả lời khác, HttpClientcó nghĩa là để tái sử dụng. Tuy nhiên, sử dụng lại một HttpClientcá thể trong một ứng dụng đa luồng có nghĩa là bạn không thể thay đổi các giá trị của các thuộc tính trạng thái của nó, như BaseAddressDefaultRequestHeaders(vì vậy bạn chỉ có thể sử dụng chúng nếu chúng không đổi trong ứng dụng của bạn).

Một cách tiếp cận để vượt qua giới hạn này là kết HttpClienthợp với một lớp nhân đôi tất cả các HttpClientphương thức bạn cần ( GetAsync, PostAsyncv.v.) và ủy thác chúng cho một singleton HttpClient. Tuy nhiên, điều đó khá tẻ nhạt (bạn cũng sẽ cần phải bọc các phương thức mở rộng ), và may mắn thay, có một cách khác - tiếp tục tạo các HttpClientphiên bản mới , nhưng sử dụng lại cơ sở HttpClientHandler. Chỉ cần đảm bảo rằng bạn không vứt bỏ trình xử lý:

HttpClientHandler _sharedHandler = new HttpClientHandler(); //never dispose this
HttpClient GetClient(string token)
{
    //client code can dispose these HttpClient instances
    return new HttpClient(_sharedHandler, disposeHandler: false)         
    {
       DefaultRequestHeaders = 
       {
            Authorization = new AuthenticationHeaderValue("Bearer", token) 
       } 
    };
}

2
Cách tốt hơn để đi là giữ một cá thể HttpClient, sau đó tạo các cá thể httpRequestMessage cục bộ của riêng bạn và sau đó sử dụng phương thức .SendAsync () trên HttpClient. Bằng cách này, nó sẽ vẫn an toàn cho chủ đề. Mỗi httpRequestMessage sẽ có các giá trị Xác thực / URL riêng.
Tim P.

@TimP. tại sao nó tốt hơn SendAsyncít tiện lợi hơn nhiều so với các phương pháp chuyên dụng như PutAsync, PostAsJsonAsyncv.v.
Ohad Schneider

2
SendAsync cho phép bạn thay đổi URL và các thuộc tính khác như tiêu đề và vẫn an toàn cho chuỗi.
Tim P.

2
Vâng, xử lý là chìa khóa. Miễn là điều đó được chia sẻ giữa các trường hợp HTTPClient, bạn vẫn ổn. Tôi đọc sai nhận xét trước đó của bạn.
Dave Black

1
Nếu chúng tôi giữ một trình xử lý được chia sẻ, chúng tôi vẫn cần quan tâm đến vấn đề DNS cũ?
shanti

5

Liên quan đến các trang web có khối lượng lớn nhưng không trực tiếp đến HttpClient. Chúng tôi có đoạn mã dưới đây trong tất cả các dịch vụ của chúng tôi.

        // number of milliseconds after which an active System.Net.ServicePoint connection is closed.
        const int DefaultConnectionLeaseTimeout = 60000;

        ServicePoint sp =
                ServicePointManager.FindServicePoint(new Uri("http://<yourServiceUrlHere>"));
        sp.ConnectionLeaseTimeout = DefaultConnectionLeaseTimeout;

Từ https://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k(System.Net.ServicePoint.ConnectionLeaseTimeout);k(TargetFrameworkMoniker-.NETFramework,V k (DevLang-csharp) & rd = true

"Bạn có thể sử dụng thuộc tính này để đảm bảo rằng các kết nối hoạt động của đối tượng ServicePoint không mở vô thời hạn. Thuộc tính này dành cho các tình huống nên bỏ kết nối và thiết lập lại định kỳ, chẳng hạn như các kịch bản cân bằng tải.

Theo mặc định, khi KeepAlive đúng với yêu cầu, thuộc tính MaxIdleTime sẽ đặt thời gian chờ để đóng các kết nối Servicepoint do không hoạt động. Nếu ServicePoint có kết nối hoạt động, MaxIdleTime không có hiệu lực và các kết nối vẫn mở vô thời hạn.

Khi thuộc tính ConnectionLeaseTimeout được đặt thành giá trị khác -1 và sau khi hết thời gian đã chỉ định, kết nối Servicepoint đang hoạt động sẽ bị đóng sau khi phục vụ yêu cầu bằng cách đặt KeepAlive thành false trong yêu cầu đó. Đặt giá trị này ảnh hưởng đến tất cả các kết nối được quản lý bởi đối tượng Servicepoint. "

Khi bạn có các dịch vụ đằng sau CDN hoặc điểm cuối khác mà bạn muốn chuyển đổi dự phòng thì cài đặt này sẽ giúp người gọi theo bạn đến đích mới. Trong ví dụ này 60 giây sau khi chuyển đổi dự phòng, tất cả người gọi sẽ kết nối lại với điểm cuối mới. Nó yêu cầu bạn phải biết các dịch vụ phụ thuộc của mình (những dịch vụ mà BẠN gọi) và các điểm cuối của chúng.


Bạn vẫn đặt rất nhiều tải cho máy chủ bằng cách mở và đóng kết nối. Nếu bạn sử dụng các HTTPClient dựa trên cá thể với các HTTPClientHandlers dựa trên cá thể, bạn vẫn sẽ bị cạn kiệt cổng nếu không cẩn thận.
Dave Black

Không đồng ý. Mọi thứ đều là sự đánh đổi. Đối với chúng tôi, theo sau CDN hoặc DNS được định tuyến lại là tiền trong ngân hàng so với doanh thu bị mất.
Không hoàn lại tiền Không trả lại

1

Bạn cũng có thể muốn tham khảo bài đăng trên blog này của Simon Timms: https://aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/

Nhưng HttpClientlà khác nhau. Mặc dù nó thực hiện IDisposablegiao diện nhưng nó thực sự là một đối tượng chia sẻ. Điều này có nghĩa là dưới vỏ bọc nó được reentrant) và chủ đề an toàn. Thay vì tạo một phiên bản mới HttpClientcho mỗi lần thực hiện, bạn nên chia sẻ một phiên bản duy nhất HttpClientcho toàn bộ vòng đời của ứng dụng. Hãy xem tại sao.


1

Một điều cần chỉ ra, không ai trong số các blog "không sử dụng" lưu ý rằng đó không chỉ là BaseAddress và DefaultHeader mà bạn cần xem xét. Khi bạn tạo HTTPClient tĩnh, có các trạng thái bên trong sẽ được chuyển qua các yêu cầu. Một ví dụ: Bạn đang xác thực với bên thứ 3 bằng HttpClient để nhận mã thông báo FedAuth (bỏ qua lý do tại sao không sử dụng OAuth / OWIN / etc), thông báo Phản hồi có tiêu đề Set-Cookie cho FedAuth, điều này được thêm vào trạng thái HttpClient của bạn. Người dùng tiếp theo đăng nhập vào API của bạn sẽ gửi cookie FedAuth của người cuối cùng trừ khi bạn đang quản lý các cookie này trên mỗi yêu cầu.


0

Là một vấn đề đầu tiên, trong khi lớp này là dùng một lần, sử dụng nó với usingcâu lệnh không phải là lựa chọn tốt nhất bởi vì ngay cả khi bạn vứt bỏ HttpClientđối tượng, ổ cắm bên dưới không được giải phóng ngay lập tức và có thể gây ra sự cố nghiêm trọng có tên là kiệt sức ổ cắm.

Nhưng có một vấn đề thứ hai HttpClientmà bạn có thể gặp phải khi sử dụng nó dưới dạng đơn lẻ hoặc đối tượng tĩnh. Trong trường hợp này, một singleton hoặc tĩnh HttpClientkhông tôn trọng DNSthay đổi.

trong lõi .net, bạn có thể làm tương tự với HttpClientFactory một cái gì đó như thế này:

public interface IBuyService
{
    Task<Buy> GetBuyItems();
}
public class BuyService: IBuyService
{
    private readonly HttpClient _httpClient;

    public BuyService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<Buy> GetBuyItems()
    {
        var uri = "Uri";

        var responseString = await _httpClient.GetStringAsync(uri);

        var buy = JsonConvert.DeserializeObject<Buy>(responseString);
        return buy;
    }
}

ConfigureService

services.AddHttpClient<IBuyService, BuyService>(client =>
{
     client.BaseAddress = new Uri(Configuration["BaseUrl"]);
});

tài liệu và ví dụ tại đây

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.