ฉันเพิ่งสร้างแอปพลิเคชันง่ายๆสำหรับทดสอบปริมาณงานการโทร 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();
}
}
การดำเนินการทดสอบพบว่าเวอร์ชันมัลติเธรดนั้นเร็วกว่า ใช้เวลาประมาณ 0.6 วินาทีในการดำเนินการสำหรับคำขอ 10k ในขณะที่ async ใช้เวลาประมาณ 2 วินาทีในการโหลดให้เสร็จสมบูรณ์ นี่เป็นเรื่องที่น่าประหลาดใจเล็กน้อยเพราะฉันคาดหวังว่า async จะเร็วขึ้น อาจเป็นเพราะการโทร HTTP ของฉันเร็วมาก ในสถานการณ์จริงที่เซิร์ฟเวอร์ควรดำเนินการที่มีความหมายมากกว่านี้และในจุดที่ควรมีเวลาแฝงของเครือข่ายด้วยผลลัพธ์อาจกลับกัน
อย่างไรก็ตามสิ่งที่ฉันกังวลจริงๆคือวิธีที่ HttpClient ทำงานเมื่อโหลดเพิ่มขึ้น เนื่องจากต้องใช้เวลาประมาณ 2 วินาทีในการส่งข้อความ 10k ฉันคิดว่าจะใช้เวลาประมาณ 20 วินาทีในการส่งข้อความถึง 10 เท่าของจำนวนข้อความ แต่การดำเนินการทดสอบแสดงให้เห็นว่าต้องใช้เวลาประมาณ 50 วินาทีในการส่งข้อความ 100k นอกจากนี้โดยปกติแล้วจะใช้เวลามากกว่า 2 นาทีในการส่งข้อความ 200k และบ่อยครั้งที่มีไม่กี่พันข้อความ (3-4k) ล้มเหลวโดยมีข้อยกเว้นต่อไปนี้:
ไม่สามารถดำเนินการกับซ็อกเก็ตได้เนื่องจากระบบไม่มีพื้นที่บัฟเฟอร์เพียงพอหรือเนื่องจากคิวเต็ม
ฉันตรวจสอบบันทึก IIS และการดำเนินการที่ล้มเหลวไม่เคยเข้าถึงเซิร์ฟเวอร์ พวกเขาล้มเหลวภายในไคลเอนต์ ฉันทำการทดสอบบนเครื่อง Windows 7 ด้วยช่วงเริ่มต้นของพอร์ตชั่วคราวที่ 49152 ถึง 65535 การเรียกใช้ netstat แสดงให้เห็นว่ามีการใช้พอร์ตประมาณ 5-6k ในระหว่างการทดสอบดังนั้นในทางทฤษฎีควรมีอีกมากมาย หากการขาดพอร์ตเป็นสาเหตุของข้อยกเว้นนั่นหมายความว่า netstat ไม่ได้รายงานสถานการณ์อย่างถูกต้องหรือ HttClient ใช้พอร์ตจำนวนสูงสุดเท่านั้นหลังจากนั้นจะเริ่มทิ้งข้อยกเว้น
ในทางตรงกันข้ามวิธีการสร้างการโทร HTTP แบบหลายเธรดมีพฤติกรรมที่คาดเดาได้ง่ายมาก ฉันใช้เวลาประมาณ 0.6 วินาทีสำหรับข้อความ 10,000 ข้อความประมาณ 5.5 วินาทีสำหรับ 100k ข้อความและตามที่คาดไว้ประมาณ 55 วินาทีสำหรับ 1 ล้านข้อความ ไม่มีข้อความใดล้มเหลว ยิ่งไปกว่านั้นในขณะที่ทำงานก็ไม่เคยใช้ RAM เกิน 55 MB (ตาม Windows Task Manager) หน่วยความจำที่ใช้ในการส่งข้อความแบบอะซิงโครนัสเพิ่มขึ้นตามสัดส่วนเมื่อโหลด ใช้ RAM ประมาณ 500 MB ในระหว่างการทดสอบข้อความ 200k
ฉันคิดว่ามีสองเหตุผลหลักสำหรับผลลัพธ์ข้างต้น อย่างแรกคือดูเหมือนว่า HttpClient จะมีความโลภมากในการสร้างการเชื่อมต่อใหม่กับเซิร์ฟเวอร์ จำนวนพอร์ตที่ใช้งานสูงที่รายงานโดย netstat หมายความว่าอาจไม่ได้รับประโยชน์มากนักจาก HTTP keep-alive
อย่างที่สองคือ HttpClient ดูเหมือนจะไม่มีกลไกการควบคุมปริมาณ ในความเป็นจริงปัญหานี้ดูเหมือนจะเป็นปัญหาทั่วไปที่เกี่ยวข้องกับการดำเนินการ async หากคุณจำเป็นต้องดำเนินการจำนวนมากการดำเนินการทั้งหมดจะเริ่มต้นพร้อมกันจากนั้นการดำเนินการต่อจะดำเนินการตามที่มีอยู่ ตามทฤษฎีแล้วสิ่งนี้น่าจะใช้ได้เพราะในการดำเนินการ 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) จะทำได้โดยอัตโนมัติโดยการ จำกัด จำนวนเธรดที่ทำงานพร้อมกัน
สำหรับภาพรวมทั้งหมดฉันได้สร้างเวอร์ชันของการทดสอบ async ตาม HttpWebRequest แทนที่จะเป็น HttpClient และได้รับผลลัพธ์ที่ดีกว่ามาก สำหรับการเริ่มต้นจะอนุญาตให้ตั้งค่าขีด จำกัด จำนวนการเชื่อมต่อพร้อมกัน (ด้วย ServicePointManager.DefaultConnectionLimit หรือผ่าน config) ซึ่งหมายความว่าพอร์ตจะไม่มีวันหมดและไม่เคยล้มเหลวในคำขอใด ๆ (โดยค่าเริ่มต้น HttpClient จะขึ้นอยู่กับ HttpWebRequest แต่ดูเหมือนว่าจะเพิกเฉยต่อการตั้งค่าขีด จำกัด การเชื่อมต่อ)
วิธีการ async HttpWebRequest ยังคงช้ากว่าแบบมัลติเธรดประมาณ 50 - 60% แต่ก็สามารถคาดเดาได้และเชื่อถือได้ ข้อเสียเพียงอย่างเดียวคือมันใช้หน่วยความจำจำนวนมากภายใต้ภาระงานจำนวนมาก ตัวอย่างเช่นต้องการประมาณ 1.6 GB ในการส่งคำขอ 1 ล้านคำขอ ด้วยการ จำกัด จำนวนคำขอพร้อมกัน (เช่นเดียวกับที่ฉันทำไว้ข้างต้นสำหรับ HttpClient) ฉันสามารถลดหน่วยความจำที่ใช้แล้วให้เหลือเพียง 20 MB และได้เวลาดำเนินการช้ากว่าวิธีมัลติเธรดเพียง 10%
หลังจากการนำเสนอที่ยืดเยื้อนี้คำถามของฉันคือ: คลาส HttpClient จาก. Net 4.5 เป็นตัวเลือกที่ไม่ดีสำหรับแอปพลิเคชันที่มีภาระงานมากหรือไม่? มีวิธีใดบ้างที่จะบีบเค้นซึ่งควรแก้ไขปัญหาที่ฉันพูดถึง รสชาติ async ของ HttpWebRequest เป็นอย่างไร?
อัปเดต (ขอบคุณ @Stephen Cleary)
ตามที่ปรากฎ HttpClient เช่นเดียวกับ HttpWebRequest (ซึ่งขึ้นอยู่กับค่าเริ่มต้น) สามารถมีจำนวนการเชื่อมต่อพร้อมกันบนโฮสต์เดียวกันที่ จำกัด ด้วย ServicePointManager.DefaultConnectionLimit สิ่งที่แปลกคือตามMSDNค่าเริ่มต้นสำหรับขีด จำกัด การเชื่อมต่อคือ 2 ฉันยังตรวจสอบด้วยว่าทางฝั่งของฉันใช้ดีบักเกอร์ซึ่งชี้ว่า 2 เป็นค่าเริ่มต้น อย่างไรก็ตามดูเหมือนว่าหากไม่ตั้งค่าเป็น ServicePointManager.DefaultConnectionLimit อย่างชัดเจนค่าเริ่มต้นจะถูกละเว้น เนื่องจากฉันไม่ได้ตั้งค่าอย่างชัดเจนในระหว่างการทดสอบ HttpClient ฉันจึงคิดว่ามันถูกเพิกเฉย
หลังจากตั้งค่า ServicePointManager.DefaultConnectionLimit เป็น 100 HttpClient มีความน่าเชื่อถือและสามารถคาดเดาได้ (netstat ยืนยันว่าใช้เพียง 100 พอร์ต) มันยังช้ากว่า async HttpWebRequest (ประมาณ 40%) แต่ที่แปลกคือมันใช้หน่วยความจำน้อยกว่า สำหรับการทดสอบที่เกี่ยวข้องกับคำขอ 1 ล้านคำขอจะใช้สูงสุด 550 MB เทียบกับ 1.6 GB ใน async HttpWebRequest
ดังนั้นในขณะที่ HttpClient ร่วมกับ ServicePointManager.DefaultConnectionLimit ดูเหมือนจะมั่นใจในความน่าเชื่อถือ (อย่างน้อยสำหรับสถานการณ์ที่มีการโทรทั้งหมดไปยังโฮสต์เดียวกัน) แต่ดูเหมือนว่าประสิทธิภาพของมันจะได้รับผลกระทบในทางลบจากการขาดกลไกการควบคุมปริมาณที่เหมาะสม สิ่งที่จะ จำกัด จำนวนคำขอพร้อมกันให้เป็นค่าที่กำหนดค่าได้และวางส่วนที่เหลือไว้ในคิวจะทำให้เหมาะสมมากขึ้นสำหรับสถานการณ์ที่มีความสามารถในการปรับขนาดได้สูง
SemaphoreSlim
ตามที่กล่าวไว้แล้วหรือActionBlock<T>
จาก TPL Dataflow
HttpClient
ServicePointManager.DefaultConnectionLimit
ควรเคารพ