मैंने हाल ही में HTTP कॉल थ्रूपुट का परीक्षण करने के लिए एक सरल एप्लिकेशन बनाया है जो कि एक असंगत तरीके बनाम शास्त्रीय बहुपरत दृष्टिकोण में उत्पन्न किया जा सकता है।
एप्लिकेशन HTTP कॉल की एक पूर्वनिर्धारित संख्या करने में सक्षम है और अंत में यह उन्हें प्रदर्शन करने के लिए आवश्यक कुल समय प्रदर्शित करता है। मेरे परीक्षणों के दौरान, सभी HTTP कॉल मेरे स्थानीय IIS को गंभीर बना दिया गया था और उन्होंने एक छोटी पाठ फ़ाइल (आकार में 12 बाइट्स) को पुनर्प्राप्त किया था।
अतुल्यकालिक कार्यान्वयन के लिए कोड का सबसे महत्वपूर्ण हिस्सा नीचे सूचीबद्ध है:
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();
}
}
}
मल्टीथ्रेडिंग कार्यान्वयन का सबसे महत्वपूर्ण हिस्सा नीचे सूचीबद्ध है:
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();
}
}
परीक्षणों को चलाने से पता चला कि मल्टीथ्रेडेड संस्करण तेज था। 10k अनुरोधों को पूरा करने के लिए इसे लगभग 0.6 सेकंड का समय लगा, जबकि async एक को लोड की समान मात्रा के लिए लगभग 2 सेकंड का समय लगा। यह थोड़ा आश्चर्यचकित करने वाला था, क्योंकि मुझे उम्मीद थी कि एसिंक्स एक तेज होगा। शायद यह इस तथ्य के कारण था कि मेरी HTTP कॉल बहुत तेज थीं। एक वास्तविक विश्व परिदृश्य में, जहां सर्वर को अधिक सार्थक संचालन करना चाहिए और जहां कुछ नेटवर्क विलंबता भी होनी चाहिए, परिणाम उलट हो सकते हैं।
हालांकि, लोड बढ़ने पर HttpClient के व्यवहार के तरीके से मुझे वास्तव में चिंता होती है। चूँकि 10k संदेशों को देने में इसे लगभग 2 सेकंड का समय लगता है, मैंने सोचा कि संदेशों की संख्या को 10 गुना करने के लिए इसे 20 सेकंड के आसपास ले जाएगा, लेकिन परीक्षण चलाने से पता चला कि 100k संदेशों को वितरित करने के लिए लगभग 50 सेकंड की आवश्यकता है। इसके अलावा, आमतौर पर 200k संदेश देने में 2 मिनट से अधिक समय लगता है और अक्सर, उनमें से कुछ हजारों (3-4k) निम्नलिखित अपवाद के साथ विफल हो जाते हैं:
सॉकेट पर एक ऑपरेशन नहीं किया जा सका क्योंकि सिस्टम में पर्याप्त बफर स्थान की कमी थी या क्योंकि एक कतार भरी हुई थी।
मैंने IIS लॉग और ऑपरेशंस को चेक किया जो विफल हो गया जो कभी भी सर्वर को नहीं मिला। वे ग्राहक के भीतर विफल रहे। मैंने एक विंडोज 7 मशीन पर 49152 से 65535 के अल्पकालिक बंदरगाहों की डिफ़ॉल्ट सीमा के साथ परीक्षणों को चलाया। नेटस्टैट चलाने से पता चला कि परीक्षण के दौरान लगभग 5-6k बंदरगाहों का उपयोग किया जा रहा था, इसलिए सिद्धांत रूप में कई और उपलब्ध होने चाहिए थे। यदि बंदरगाहों की कमी वास्तव में अपवादों का कारण थी, तो इसका मतलब है कि या तो नेटस्टैट ने स्थिति को ठीक से रिपोर्ट नहीं किया या HttClient केवल अधिकतम संख्या में पोर्ट का उपयोग करता है जिसके बाद वह अपवादों को फेंकना शुरू कर देता है।
इसके विपरीत, HTTP कॉल जनरेट करने के बहुपरत दृष्टिकोण ने बहुत पूर्वानुमानित व्यवहार किया। मैंने इसे 10k संदेशों के लिए 0.6 सेकंड के आसपास, 100k संदेशों के लिए 5.5 सेकंड के आसपास और 1 मिलियन संदेशों के लिए 55 सेकंड के आसपास अपेक्षित के रूप में लिया। कोई भी संदेश विफल नहीं हुआ। इसके अलावा, जब यह चलता था, तो यह 55 एमबी से अधिक रैम (विंडोज टास्क मैनेजर के अनुसार) का उपयोग नहीं करता था। मैसेज भेजने के दौरान उपयोग की जाने वाली मेमोरी, एसिंक्रोनसली लोड के साथ आनुपातिक रूप से बढ़ती है। यह 200k संदेश परीक्षणों के दौरान लगभग 500 एमबी रैम का उपयोग करता था।
मुझे लगता है कि उपरोक्त परिणामों के दो मुख्य कारण हैं। पहला यह है कि HttpClient सर्वर के साथ नए कनेक्शन बनाने में बहुत लालची लगता है। Netstat द्वारा रिपोर्ट किए गए उपयोग किए गए बंदरगाहों की उच्च संख्या का मतलब है कि यह संभवतः HTTP कीप-लाइव से बहुत लाभ नहीं करता है।
दूसरा यह है कि HttpClient में थ्रॉटलिंग तंत्र नहीं है। वास्तव में यह async ऑपरेशंस से जुड़ी एक सामान्य समस्या प्रतीत होती है। यदि आपको बहुत बड़ी संख्या में ऑपरेशन करने की आवश्यकता है, तो वे सभी एक ही बार में शुरू हो जाएंगे और फिर उपलब्ध होने पर उनकी निरंतरता को निष्पादित किया जाएगा। सिद्धांत रूप में यह ठीक होना चाहिए, क्योंकि एसिंक्रोनस ऑपरेशन में लोड बाहरी प्रणालियों पर होता है लेकिन जैसा कि ऊपर साबित हुआ है यह पूरी तरह से मामला नहीं है। बड़ी संख्या में अनुरोध एक बार शुरू होने से मेमोरी का उपयोग बढ़ेगा और संपूर्ण निष्पादन धीमा हो जाएगा।
मैं एक सरल, लेकिन प्रारंभिक विलंब तंत्र के साथ अतुल्यकालिक अनुरोधों की अधिकतम संख्या को सीमित करके, बेहतर परिणाम, स्मृति और निष्पादन के समय को प्राप्त करने में कामयाब रहा:
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 समवर्ती अनुरोधों की संख्या को सीमित करने के लिए एक तंत्र शामिल करता है। टास्क क्लास का उपयोग करते समय (जो कि .Net थ्रेड पूल पर आधारित है) थ्रॉटलिंग स्वचालित रूप से समवर्ती धागे की संख्या को सीमित करके प्राप्त किया जाता है।
संपूर्ण अवलोकन के लिए, मैंने HttpWebRequest के आधार पर Async परीक्षण का एक संस्करण भी बनाया है बजाय HttpClient के और बेहतर परिणाम प्राप्त करने में कामयाब रहा। एक शुरुआत के लिए, यह समवर्ती कनेक्शन की संख्या पर एक सीमा निर्धारित करने की अनुमति देता है (ServicePointManager.DefaultConnectionLimit या config के माध्यम से), जिसका अर्थ है कि यह बंदरगाहों से कभी नहीं भागा और किसी भी अनुरोध पर विफल नहीं हुआ (HttpClient, डिफ़ॉल्ट रूप से, HttpWebRequest पर आधारित है) , लेकिन यह कनेक्शन सीमा सेटिंग की उपेक्षा करता है)।
Async HttpWebRequest दृष्टिकोण अभी भी मल्टीथ्रेडिंग की तुलना में लगभग 50 - 60% धीमा था, लेकिन यह अनुमानित और विश्वसनीय था। इसका एकमात्र नकारात्मक पहलू यह था कि इसमें बड़े लोड के तहत भारी मात्रा में मेमोरी का उपयोग किया गया था। उदाहरण के लिए 1 मिलियन अनुरोध भेजने के लिए इसे लगभग 1.6 GB की आवश्यकता थी। समवर्ती अनुरोधों की संख्या को सीमित करके (जैसे मैंने HttpClient के लिए ऊपर किया था) मैं इस्तेमाल की गई मेमोरी को सिर्फ 20 एमबी तक कम करने में कामयाब रहा और मल्टीथ्रेडिंग दृष्टिकोण की तुलना में सिर्फ 10% धीमी गति से निष्पादन का समय प्राप्त किया।
इस लम्बी प्रस्तुति के बाद, मेरे प्रश्न हैं: क्या .net 4.5 से HttpClient वर्ग गहन लोड अनुप्रयोगों के लिए एक बुरा विकल्प है? क्या इसे थ्रॉटल करने का कोई तरीका है, जिसके बारे में मुझे बताई गई समस्याओं को ठीक करना चाहिए? कैसे HttpWebRequest के async स्वाद के बारे में?
अपडेट (धन्यवाद @ स्टीफन क्ली)
जैसा कि यह पता चलता है, HttpWebRequest (जिस पर यह डिफ़ॉल्ट रूप से आधारित है) की तरह, HttpClient, उसी तरह के समवर्ती कनेक्शनों की संख्या ServicePointManager.DefaultConnectionLitit के साथ सीमित हो सकती है। अजीब बात यह है कि MSDN के अनुसार , कनेक्शन सीमा के लिए डिफ़ॉल्ट मान 2 है। मैंने यह भी जाँच की कि डिबगर का उपयोग करते हुए मेरी तरफ से बताया गया है कि वास्तव में 2 डिफ़ॉल्ट मान है। हालाँकि, ऐसा लगता है कि जब तक स्पष्ट रूप से ServicePointManager.DefaultConnectionLimit का मान सेट नहीं किया जाता है, तब तक डिफ़ॉल्ट मान को अनदेखा किया जाएगा। चूंकि मैंने अपने HttpClient परीक्षणों के दौरान स्पष्ट रूप से इसके लिए कोई मूल्य निर्धारित नहीं किया था, इसलिए मुझे लगा कि इसे अनदेखा किया गया है।
ServicePointManager.DefaultConnectionLimit को 100 HttpClient पर सेट करने के बाद विश्वसनीय और पूर्वानुमेय हो गया (netstat पुष्टि करता है कि केवल 100 पोर्ट का उपयोग किया जाता है)। यह अभी भी async HttpWebRequest (लगभग 40%) की तुलना में धीमा है, लेकिन अजीब तरह से, यह कम मेमोरी का उपयोग करता है। परीक्षण के लिए जिसमें 1 मिलियन अनुरोध शामिल हैं, यह async HttpWebbequest में 1.6 GB की तुलना में, अधिकतम 550 MB का उपयोग करता है।
तो, जबकि संयोजन में HttpClient ServicePointManager.DefaultConnectionLimit विश्वसनीयता सुनिश्चित करने के लिए लगता है (कम से कम उस परिदृश्य के लिए जहां सभी कॉल एक ही होस्ट की ओर किए जा रहे हैं), यह अभी भी ऐसा लगता है कि एक उचित थ्रॉटलिंग तंत्र की कमी से इसका प्रदर्शन नकारात्मक रूप से प्रभावित होता है। ऐसा कुछ जो एक विन्यास मूल्य के लिए अनुरोधों की समवर्ती संख्या को सीमित करेगा और बाकी को एक कतार में रखेगा, यह उच्च स्केलेबिलिटी परिदृश्यों के लिए अधिक उपयुक्त होगा।
SemaphoreSlim
पहले से बताए गए, या ActionBlock<T>
TPL डेटाफ़्लो से उपयोग कर सकते हैं ।
HttpClient
सम्मान करना चाहिएServicePointManager.DefaultConnectionLimit
।