Apa overhead untuk membuat HttpClient baru per panggilan dalam klien WebAPI?

162

Apa yang seharusnya menjadi masa HttpClienthidup klien WebAPI?
Apakah lebih baik memiliki satu contoh HttpClientuntuk beberapa panggilan?

Berapa overhead untuk membuat dan membuang HttpClientpermintaan, seperti dalam contoh di bawah ini (diambil dari http://www.asp.net/web-api/overview/web-api-clients/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);
    }
}
Bruno Pessanha
sumber
Saya tidak yakin, Anda bisa menggunakan Stopwatchkelas untuk membandingkannya. Perkiraan saya akan lebih masuk akal untuk memiliki satu HttpClient, dengan asumsi semua contoh tersebut digunakan dalam konteks yang sama.
Matius

Jawaban:

215

HttpClienttelah dirancang untuk digunakan kembali untuk beberapa panggilan . Bahkan di banyak utas. The HttpClientHandlermemiliki Kredensial dan Cookie yang dimaksudkan untuk digunakan kembali di seluruh panggilan. Memiliki HttpClientinstance baru memerlukan pengaturan ulang semua hal itu. Selain itu, DefaultRequestHeadersproperti berisi properti yang dimaksudkan untuk beberapa panggilan. Harus mengatur ulang nilai-nilai itu pada setiap permintaan mengalahkan intinya.

Manfaat utama lainnya HttpClientadalah kemampuan untuk menambahkan HttpMessageHandlerske dalam pipa permintaan / respons untuk menerapkan masalah lintas sektoral. Ini bisa untuk pencatatan, audit, pembatasan, penanganan pengalihan, penanganan offline, metrik pengambilan. Segala macam hal berbeda. Jika HttpClient baru dibuat pada setiap permintaan, maka semua penangan pesan ini harus disiapkan pada setiap permintaan dan entah bagaimana keadaan tingkat aplikasi yang dibagi antara permintaan untuk penangan ini juga perlu disediakan.

Semakin banyak Anda menggunakan fitur HttpClient, semakin Anda akan melihat bahwa menggunakan kembali contoh yang ada masuk akal.

Namun, masalah terbesar, menurut saya adalah ketika sebuah HttpClientkelas dibuang, ia dibuang HttpClientHandler, yang kemudian secara paksa menutup TCP/IPkoneksi di kumpulan koneksi yang dikelola oleh ServicePointManager. Ini berarti bahwa setiap permintaan dengan yang baru HttpClientmemerlukan membangun kembali TCP/IPkoneksi baru .

Dari pengujian saya, menggunakan HTTP biasa pada LAN, kinerja hit cukup diabaikan. Saya menduga ini karena ada keepalive TCP yang mendasarinya yang menahan koneksi terbuka bahkan ketika HttpClientHandlermencoba untuk menutupnya.

Atas permintaan yang masuk internet, saya telah melihat cerita yang berbeda. Saya telah melihat hit kinerja 40% karena harus membuka kembali permintaan setiap waktu.

Saya menduga hit pada HTTPSkoneksi akan lebih buruk.

Saran saya adalah untuk membuat instance HttpClient untuk aplikasi Anda seumur hidup untuk setiap API berbeda yang Anda sambungkan.

Darrel Miller
sumber
5
which then forcibly closes the TCP/IP connection in the pool of connections that is managed by ServicePointManagerSeberapa yakin Anda tentang pernyataan ini? Itu sulit dipercaya. HttpClientbagiku tampak seperti unit kerja yang seharusnya sering dipakai.
usr
2
@vkelman Ya Anda masih dapat menggunakan kembali contoh HttpClient bahkan jika Anda membuatnya dengan HttpClientHandler baru. Perhatikan juga ada konstruktor khusus untuk HttpClient yang memungkinkan Anda untuk menggunakan kembali HttpClientHandler dan membuang HttpClient tanpa mematikan koneksi.
Darrel Miller
2
@ vkelman Saya lebih suka membiarkan HttpClient tetap ada, tetapi jika Anda lebih suka menjaga HttpClientHandler tetap ada, itu akan menjaga koneksi tetap terbuka ketika parameter kedua salah.
Darrel Miller
2
@DarrelMiller Jadi sepertinya koneksi terikat ke HttpClientHandler. Saya tahu bahwa untuk skala saya tidak ingin menghancurkan koneksi jadi saya harus menyimpan HttpClientHandler dan membuat semua instance HttpClient saya dari itu ATAU membuat instance HttpClient statis. Namun, jika CookieContainer terikat dengan HttpClientHandler, dan cookie saya perlu berbeda per permintaan, apa yang Anda rekomendasikan? Saya ingin menghindari sinkronisasi utas pada HttpClientHandler statis dengan memodifikasi CookieContainer untuk setiap permintaan.
Dave Black
2
@ Sana.91 Anda bisa. Akan lebih baik untuk mendaftar sebagai singleton dalam pengumpulan layanan dan mengaksesnya dengan cara itu.
Darrel Miller
69

Jika Anda ingin skala aplikasi Anda, perbedaannya BESAR! Tergantung pada bebannya, Anda akan melihat angka kinerja yang sangat berbeda. Seperti yang disebutkan Darrel Miller, HttpClient dirancang untuk digunakan kembali di seluruh permintaan. Ini dikonfirmasi oleh orang-orang di tim BCL yang menulisnya.

Sebuah proyek baru-baru ini yang saya miliki adalah untuk membantu pengecer komputer online yang sangat besar dan terkenal untuk meningkatkan traffic Black Friday / holiday untuk beberapa sistem baru. Kami mengalami beberapa masalah kinerja seputar penggunaan HttpClient. Sejak diterapkanIDisposable , para devs melakukan apa yang biasanya Anda lakukan dengan membuat sebuah instance dan menempatkannya di dalam sebuah using()pernyataan. Setelah kami mulai memuat pengujian, aplikasi membuat server bertekuk lutut - ya, server bukan hanya aplikasi. Alasannya adalah bahwa setiap instance HttpClient membuka port pada server. Karena finalisasi GC yang non-deterministik dan fakta bahwa Anda bekerja dengan sumber daya komputer yang menjangkau beberapa lapisan OSI , menutup port jaringan dapat memakan waktu cukup lama. Bahkan OS Windows sendiridapat memakan waktu hingga 20 detik untuk menutup port (per Microsoft). Kami membuka port lebih cepat daripada yang bisa ditutup - kelelahan port server yang memalu CPU hingga 100%. Perbaikan saya adalah mengubah HttpClient menjadi instance statis yang menyelesaikan masalah. Ya, ini adalah sumber daya sekali pakai, tetapi setiap overhead jauh lebih besar daripada perbedaan dalam kinerja. Saya mendorong Anda untuk melakukan beberapa pengujian beban untuk melihat bagaimana aplikasi Anda berperilaku.

Anda juga dapat memeriksa halaman Panduan WebAPI untuk dokumentasi dan contoh di https://www.asp.net/web-api/overview/advanced/calling-a-web-api-from-a-net-client

Berikan perhatian khusus pada info ini:

HttpClient dimaksudkan untuk dipakai satu kali dan digunakan kembali sepanjang umur aplikasi. Khususnya dalam aplikasi server, membuat instance HttpClient baru untuk setiap permintaan akan menghabiskan jumlah soket yang tersedia di bawah beban berat. Ini akan menghasilkan kesalahan SocketException.

Jika Anda perlu menggunakan statis HttpClientdengan tajuk, alamat dasar, dll. Yang perlu Anda lakukan adalah membuat HttpRequestMessagesecara manual dan mengatur nilai-nilai tersebut di HttpRequestMessage. Lalu, gunakanHttpClient:SendAsync(HttpRequestMessage requestMessage, ...)

PEMBARUAN untuk .NET Core : Anda harus menggunakan IHttpClientFactoryInjeksi Ketergantungan via untuk membuat HttpClientinstance. Ini akan mengatur masa pakai untuk Anda dan Anda tidak perlu membuangnya secara eksplisit. Lihat Membuat permintaan HTTP menggunakan IHttpClientFactory di ASP.NET Core

Dave Black
sumber
1
posting ini berisi wawasan yang bermanfaat bagi mereka yang akan melakukan stress testing ..!
Sana.91
9

Sebagai jawaban lain menyatakan, HttpClientdimaksudkan untuk digunakan kembali. Namun, menggunakan kembali satu HttpClientinstance di aplikasi multi-threaded berarti Anda tidak dapat mengubah nilai properti stateful, seperti BaseAddressdan DefaultRequestHeaders(jadi Anda hanya dapat menggunakannya jika mereka konstan di aplikasi Anda).

Salah satu pendekatan untuk mengatasi batasan ini adalah membungkus HttpClientdengan kelas yang menduplikasi semuaHttpClient metode yang Anda butuhkan ( GetAsync, PostAsyncdll) dan mendelegasikannya ke singleton HttpClient. Namun itu cukup membosankan (Anda perlu membungkus metode ekstensi juga), dan untungnya ada cara lain - terus membuat HttpClientcontoh baru , tetapi gunakan kembali yang mendasarinya HttpClientHandler. Pastikan Anda tidak membuang penangannya:

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) 
       } 
    };
}
Ohad Schneider
sumber
2
Cara yang lebih baik adalah menyimpan satu instance HttpClient, dan kemudian buat instance HttpRequestMessage lokal Anda sendiri dan kemudian gunakan metode .SendAsync () pada HttpClient. Dengan cara ini, benang akan tetap aman. Setiap HttpRequestMessage akan memiliki nilai Otentikasi / URL sendiri.
Tim P.
@TimP. mengapa lebih baik SendAsyncjauh lebih tidak nyaman daripada metode khusus seperti PutAsync, PostAsJsonAsyncdll.
Ohad Schneider
2
SendAsync memungkinkan Anda mengubah URL dan properti lainnya seperti header dan masih aman utas.
Tim P.
2
Ya, pawang adalah kuncinya. Selama itu dibagi antara instance HttpClient Anda baik-baik saja. Saya salah membaca komentar Anda sebelumnya.
Dave Black
1
Jika kita menyimpan handler bersama, apakah kita masih harus mengurus masalah DNS basi?
shanti
5

Terkait dengan situs web bervolume tinggi tetapi tidak langsung ke HttpClient. Kami memiliki cuplikan kode di bawah ini di semua layanan kami.

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

Dari https://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k(System.Net.ServicePoint.ConnectionLeaseTimeout);k(TargetFrameworkMoniker-.Version%3Dv4.5.2); k (DevLang-csharp) & rd = true

"Anda bisa menggunakan properti ini untuk memastikan bahwa koneksi aktif objek ServicePoint tidak tetap terbuka tanpa batas waktu. Properti ini ditujukan untuk skenario di mana koneksi harus dihapus dan dibangun kembali secara berkala, seperti skenario penyeimbangan beban.

Secara default, ketika KeepAlive berlaku untuk permintaan, properti MaxIdleTime menetapkan batas waktu untuk menutup koneksi ServicePoint karena tidak aktif. Jika ServicePoint memiliki koneksi aktif, MaxIdleTime tidak berpengaruh dan koneksi tetap terbuka tanpa batas.

Ketika properti ConnectionLeaseTimeout diatur ke nilai selain -1, dan setelah waktu yang ditentukan berlalu, koneksi ServicePoint aktif ditutup setelah melayani permintaan dengan mengatur KeepAlive ke false dalam permintaan itu. Mengatur nilai ini memengaruhi semua koneksi yang dikelola oleh objek ServicePoint. "

Ketika Anda memiliki layanan di belakang CDN atau titik akhir lain yang Anda ingin gagal, maka pengaturan ini membantu penelepon mengikuti Anda ke tujuan baru Anda. Dalam contoh ini 60 detik setelah kegagalan, semua penelepon harus menghubungkan kembali ke titik akhir yang baru. Itu mengharuskan Anda untuk mengetahui layanan dependen Anda (layanan-layanan yang ANDA panggil) dan titik akhir mereka.

Tanpa Pengembalian Uang Tanpa Pengembalian
sumber
Anda masih menaruh banyak beban di server dengan membuka dan menutup koneksi. Jika Anda menggunakan HttpClients berbasis instance dengan HttpClientHandlers berbasis instance, Anda masih akan mengalami kelelahan pelabuhan jika Anda tidak berhati-hati.
Dave Black
Bukan tidak setuju. Semuanya adalah tradeoff. Bagi kami mengikuti CDN atau DNS re-routed adalah uang di bank vs pendapatan yang hilang.
Tanpa Pengembalian Uang Tanpa Pengembalian
1

Anda mungkin juga ingin merujuk ke posting blog ini oleh Simon Timms: https://aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/

Tetapi HttpClientberbeda. Meskipun mengimplementasikan IDisposableantarmuka itu sebenarnya adalah objek bersama. Ini berarti bahwa di bawah selimut itu reentrant) dan aman. Alih-alih membuat instance baru HttpClientuntuk setiap eksekusi, Anda harus membagikan instance tunggal HttpClientuntuk seumur hidup aplikasi. Mari kita lihat alasannya.

SvenAelterman
sumber
1

Satu hal yang perlu diperhatikan, bahwa tidak ada catatan blog "jangan gunakan menggunakan" adalah bahwa itu bukan hanya BaseAddress dan DefaultHeader yang perlu Anda pertimbangkan. Setelah Anda membuat HttpClient statis, ada kondisi internal yang akan dibawa melintasi permintaan. Contoh: Anda mengautentikasi ke pihak ke-3 dengan HttpClient untuk mendapatkan token FedAuth (abaikan mengapa tidak menggunakan OAuth / OWIN / dll), bahwa pesan Respons memiliki header Set-Cookie untuk FedAuth, ini ditambahkan ke status HttpClient Anda. Pengguna selanjutnya yang masuk ke API Anda akan mengirimkan cookie FedAuth orang terakhir kecuali Anda mengelola cookie ini pada setiap permintaan.

pelarianc
sumber
0

Sebagai masalah pertama, walaupun kelas ini dapat dibuang, menggunakannya dengan usingpernyataan bukanlah pilihan terbaik karena bahkan ketika Anda membuangnyaHttpClient objek, soket yang mendasarinya tidak segera dilepaskan dan dapat menyebabkan masalah serius yang bernama 'sockets exhaustion.

Tapi ada masalah kedua dengan HttpClientyang bisa Anda miliki ketika Anda menggunakannya sebagai objek tunggal atau statis. Dalam hal ini, singleton atau statis HttpClienttidak menghormatiDNS perubahan.

dalam .net core Anda dapat melakukan hal yang sama dengan HttpClientFactory seperti ini:

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

Konfigurasikan Layanan

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

dokumentasi dan contoh di sini

Reza Jenabi
sumber