Kısa bir süre önce, klasik bir çok iş parçacıklı yaklaşıma kıyasla zaman uyumsuz bir şekilde üretilebilen HTTP çağrı verimini test etmek için basit bir uygulama oluşturdum.
Uygulama, önceden tanımlanmış sayıda HTTP çağrısı gerçekleştirebilir ve sonunda bunları gerçekleştirmek için gereken toplam süreyi görüntüler. Testlerim sırasında, tüm HTTP çağrıları yerel IIS sunucuma yapıldı ve küçük bir metin dosyası (boyut olarak 12 bayt) aldılar.
Eşzamansız uygulama için kodun en önemli kısmı aşağıda listelenmiştir:
public async void TestAsync()
{
this.TestInit();
HttpClient httpClient = new HttpClient();
for (int i = 0; i < NUMBER_OF_REQUESTS; i++)
{
ProcessUrlAsync(httpClient);
}
}
private async void ProcessUrlAsync(HttpClient httpClient)
{
HttpResponseMessage httpResponse = null;
try
{
Task<HttpResponseMessage> getTask = httpClient.GetAsync(URL);
httpResponse = await getTask;
Interlocked.Increment(ref _successfulCalls);
}
catch (Exception ex)
{
Interlocked.Increment(ref _failedCalls);
}
finally
{
if(httpResponse != null) httpResponse.Dispose();
}
lock (_syncLock)
{
_itemsLeft--;
if (_itemsLeft == 0)
{
_utcEndTime = DateTime.UtcNow;
this.DisplayTestResults();
}
}
}
Çoklu okuma uygulamasının en önemli kısmı aşağıda listelenmiştir:
public void TestParallel2()
{
this.TestInit();
ServicePointManager.DefaultConnectionLimit = 100;
for (int i = 0; i < NUMBER_OF_REQUESTS; i++)
{
Task.Run(() =>
{
try
{
this.PerformWebRequestGet();
Interlocked.Increment(ref _successfulCalls);
}
catch (Exception ex)
{
Interlocked.Increment(ref _failedCalls);
}
lock (_syncLock)
{
_itemsLeft--;
if (_itemsLeft == 0)
{
_utcEndTime = DateTime.UtcNow;
this.DisplayTestResults();
}
}
});
}
}
private void PerformWebRequestGet()
{
HttpWebRequest request = null;
HttpWebResponse response = null;
try
{
request = (HttpWebRequest)WebRequest.Create(URL);
request.Method = "GET";
request.KeepAlive = true;
response = (HttpWebResponse)request.GetResponse();
}
finally
{
if (response != null) response.Close();
}
}
Testleri çalıştırmak, çok iş parçacıklı sürümün daha hızlı olduğunu ortaya çıkardı. 10.000 istek için tamamlanması yaklaşık 0.6 saniye sürerken, zaman uyumsuz olanın aynı miktarda yükleme için tamamlanması yaklaşık 2 saniye sürdü. Bu biraz şaşırtıcıydı, çünkü asenkron olanın daha hızlı olmasını bekliyordum. Belki de HTTP aramalarımın çok hızlı olmasından kaynaklanıyordu. Sunucunun daha anlamlı bir işlem gerçekleştirmesi gereken ve ayrıca bir miktar ağ gecikmesi olması gereken gerçek dünya senaryosunda, sonuçlar tersine çevrilebilir.
Bununla birlikte, beni gerçekten endişelendiren, HttpClient'in yük arttığında nasıl davranacağıdır. 10.000 mesaj iletmek yaklaşık 2 saniye sürdüğü için, mesaj sayısının 10 katı kadar mesaj göndermenin yaklaşık 20 saniye süreceğini düşündüm, ancak testi çalıştırmak, 100.000 mesajı iletmek için yaklaşık 50 saniyeye ihtiyacı olduğunu gösterdi. Ayrıca, 200.000 mesajın teslim edilmesi genellikle 2 dakikadan uzun sürer ve genellikle birkaç bin mesaj (3-4k) aşağıdaki istisna dışında başarısız olur:
Bir soket üzerinde işlem, sistemde yeterli arabellek alanı olmadığından veya bir kuyruk dolu olduğundan gerçekleştirilemedi.
IIS günlüklerini kontrol ettim ve başarısız olan işlemler sunucuya hiç ulaşmadı. Müşteri içinde başarısız oldular. Testleri, varsayılan geçici bağlantı noktası aralığı 49152 ila 65535 olan bir Windows 7 makinesinde çalıştırdım. Netstat'ı çalıştırmak, testler sırasında yaklaşık 5-6 bin bağlantı noktasının kullanıldığını gösterdi, bu nedenle teoride çok daha fazla kullanılabilir olması gerekirdi. Bağlantı noktalarının olmaması gerçekten istisnaların nedeni ise, bu, ya netstat'ın durumu düzgün bir şekilde rapor etmediği ya da HttClient'in yalnızca maksimum sayıda bağlantı noktası kullandığı ve ardından istisnaları atmaya başladığı anlamına gelir.
Bunun tersine, HTTP çağrıları oluşturmanın çok iş parçacıklı yaklaşımı oldukça tahmin edilebilir davrandı. 10.000 mesaj için yaklaşık 0.6 saniye, 100.000 mesaj için yaklaşık 5.5 saniye ve 1 milyon mesaj için yaklaşık 55 saniye bekledim. Mesajların hiçbiri başarısız oldu. Dahası, çalışırken hiçbir zaman 55 MB'den fazla RAM kullanmadı (Windows Görev Yöneticisine göre). Eşzamansız olarak mesaj gönderirken kullanılan bellek, yük ile orantılı olarak büyüdü. 200k mesaj testleri sırasında yaklaşık 500 MB RAM kullandı.
Yukarıdaki sonuçların iki ana nedeni olduğunu düşünüyorum. Birincisi, HttpClient'in sunucuyla yeni bağlantılar oluşturmada çok açgözlü görünmesi. Netstat tarafından bildirilen çok sayıda kullanılan bağlantı noktası, HTTP'nin canlı tutma özelliğinden büyük olasılıkla yararlanmadığı anlamına gelir.
İkincisi, HttpClient'in bir azaltma mekanizmasına sahip olmadığıdır. Aslında bu, zaman uyumsuz işlemlerle ilgili genel bir sorun gibi görünüyor. Çok fazla sayıda işlem yapmanız gerekiyorsa, bunların tümü aynı anda başlatılacak ve mevcut olduklarında devamları yürütülecektir. Teoride bu tamam olmalıdır, çünkü asenkron işlemlerde yük harici sistemlerdedir, ancak yukarıda kanıtlandığı gibi durum tamamen böyle değildir. Aynı anda çok sayıda isteğin başlatılması, bellek kullanımını artıracak ve tüm yürütmeyi yavaşlatacaktır.
Basit ama ilkel bir gecikme mekanizmasıyla maksimum eşzamansız istek sayısını sınırlayarak daha iyi sonuçlar, bellek ve yürütme süresi elde etmeyi başardım:
public async void TestAsyncWithDelay()
{
this.TestInit();
HttpClient httpClient = new HttpClient();
for (int i = 0; i < NUMBER_OF_REQUESTS; i++)
{
if (_activeRequestsCount >= MAX_CONCURENT_REQUESTS)
await Task.Delay(DELAY_TIME);
ProcessUrlAsyncWithReqCount(httpClient);
}
}
HttpClient, eşzamanlı isteklerin sayısını sınırlamak için bir mekanizma eklemiş olsaydı gerçekten yararlı olurdu. Task sınıfı (.Net iş parçacığı havuzuna dayalı) kullanılırken, eşzamanlı iş parçacığı sayısı sınırlandırılarak azaltma otomatik olarak gerçekleştirilir.
Tam bir genel bakış için, HttpClient yerine HttpWebRequest'e dayalı bir zaman uyumsuz test sürümü de oluşturdum ve çok daha iyi sonuçlar elde etmeyi başardım. Bir başlangıç için, eşzamanlı bağlantıların sayısına (ServicePointManager.DefaultConnectionLimit ile veya config aracılığıyla) bir sınır koymaya izin verir; bu, bağlantı noktalarının hiçbir zaman bitmediği ve hiçbir istekte asla başarısız olmadığı anlamına gelir (HttpClient, varsayılan olarak, HttpWebRequest'e dayanır. , ancak bağlantı sınırı ayarını yok sayıyor gibi görünüyor).
Eşzamansız HttpWebRequest yaklaşımı hala çok iş parçacıklı olandan yaklaşık% 50 - 60 daha yavaştı, ancak öngörülebilir ve güvenilirdi. Tek dezavantajı, büyük yük altında büyük miktarda bellek kullanmasıydı. Örneğin 1 milyon istek göndermek için yaklaşık 1,6 GB gerekiyordu. Eşzamanlı isteklerin sayısını sınırlandırarak (yukarıda HttpClient için yaptığım gibi) kullanılan belleği sadece 20 MB'ye düşürmeyi ve çoklu okuma yaklaşımından yalnızca% 10 daha yavaş bir yürütme süresi elde etmeyi başardım.
Bu uzun sunumdan sonra sorularım: .Net 4.5'ten gelen HttpClient sınıfı, yoğun yük uygulamaları için kötü bir seçim mi? Onu kısmanın bir yolu var mı, bahsettiğim sorunları çözen hangisi? HttpWebRequest'in eşzamansız özelliği nasıl olur?
Güncelleme (teşekkürler @Stephen Cleary)
Görünüşe göre, HttpClient, tıpkı HttpWebRequest gibi (varsayılan olarak dayalıdır), aynı ana bilgisayarda ServicePointManager.DefaultConnectionLimit ile sınırlı eşzamanlı bağlantı sayısına sahip olabilir. Garip olan şu ki, MSDN'ye göre bağlantı limiti için varsayılan değer 2'dir. Ayrıca bunu benim tarafımda, aslında 2'nin varsayılan değer olduğunu gösteren hata ayıklayıcıyı kullanarak kontrol ettim. Ancak, ServicePointManager.DefaultConnectionLimit'e açıkça bir değer ayarlamadıkça, varsayılan değer yok sayılacak gibi görünüyor. HttpClient testlerim sırasında bunun için açıkça bir değer belirlemediğimden, bunun göz ardı edildiğini düşündüm.
ServicePointManager.DefaultConnectionLimit'i 100 HttpClient olarak ayarladıktan sonra güvenilir ve öngörülebilir hale geldi (netstat yalnızca 100 bağlantı noktasının kullanıldığını doğrular). Halen eşzamansız HttpWebRequest'ten daha yavaştır (yaklaşık% 40), ancak garip bir şekilde daha az bellek kullanır. 1 milyon istek içeren test için, asenkron HttpWebRequest'teki 1,6 GB'ye kıyasla maksimum 550 MB kullandı.
Dolayısıyla, ServicePointManager.DefaultConnectionLimit kombinasyonundaki HttpClient güvenilirliği sağlıyor gibi görünse de (en azından tüm çağrıların aynı ana bilgisayara doğru yapıldığı senaryo için), yine de performansı uygun bir azaltma mekanizmasının eksikliğinden olumsuz etkileniyor gibi görünüyor. Eşzamanlı istek sayısını yapılandırılabilir bir değerle sınırlayacak ve gerisini bir sıraya koyacak bir şey, yüksek ölçeklenebilirlik senaryoları için onu çok daha uygun hale getirecektir.
SemaphoreSlim
, daha önce belirtildiği gibi veya ActionBlock<T>
TPL Dataflow'dan kullanabilirsiniz.
HttpClient
saygı duymalıServicePointManager.DefaultConnectionLimit
.