async HttpClient จาก. Net 4.5 เป็นตัวเลือกที่ไม่ดีสำหรับแอปพลิเคชันที่มีภาระงานมากหรือไม่?


130

ฉันเพิ่งสร้างแอปพลิเคชันง่ายๆสำหรับทดสอบปริมาณงานการโทร 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 ดูเหมือนจะมั่นใจในความน่าเชื่อถือ (อย่างน้อยสำหรับสถานการณ์ที่มีการโทรทั้งหมดไปยังโฮสต์เดียวกัน) แต่ดูเหมือนว่าประสิทธิภาพของมันจะได้รับผลกระทบในทางลบจากการขาดกลไกการควบคุมปริมาณที่เหมาะสม สิ่งที่จะ จำกัด จำนวนคำขอพร้อมกันให้เป็นค่าที่กำหนดค่าได้และวางส่วนที่เหลือไว้ในคิวจะทำให้เหมาะสมมากขึ้นสำหรับสถานการณ์ที่มีความสามารถในการปรับขนาดได้สูง


4
HttpClientServicePointManager.DefaultConnectionLimitควรเคารพ
Stephen Cleary

2
การสังเกตของคุณดูเหมือนจะคุ้มค่าที่จะตรวจสอบ มีสิ่งหนึ่งที่รบกวนจิตใจฉัน: ฉันคิดว่ามันมีความจำเป็นอย่างมากที่จะออก async IO หลายพันตัวพร้อมกัน ฉันจะไม่ทำสิ่งนี้ในการผลิต ความจริงที่ว่าคุณไม่ตรงกันไม่ได้หมายความว่าคุณสามารถใช้ทรัพยากรที่หลากหลาย (ตัวอย่างอย่างเป็นทางการของ Microsoft นั้นทำให้เข้าใจผิดเล็กน้อยในเรื่องนี้เช่นกัน)
usr

1
อย่าเค้นเวลาด้วยความล่าช้า เค้นระดับการทำงานพร้อมกันคงที่ที่คุณกำหนดโดยประจักษ์ ทางออกง่ายๆคือ SemaphoreSlim.WaitAsync แม้ว่าจะไม่เหมาะสำหรับงานจำนวนมากโดยพลการ
usr

1
@FlorinDumitrescu สำหรับการควบคุมปริมาณคุณสามารถใช้SemaphoreSlimตามที่กล่าวไว้แล้วหรือActionBlock<T>จาก TPL Dataflow
svick

1
@svick ขอบคุณสำหรับคำแนะนำ ฉันไม่สนใจที่จะใช้กลไกสำหรับการควบคุมปริมาณ / การ จำกัด การทำงานพร้อมกันด้วยตนเอง ดังที่ได้กล่าวไปแล้วการใช้งานที่รวมอยู่ในคำถามของฉันเป็นเพียงการทดสอบและเพื่อตรวจสอบทฤษฎีเท่านั้น ฉันไม่ได้พยายามปรับปรุงเพราะมันจะไม่ได้รับการผลิต สิ่งที่ฉันสนใจคือถ้า. Net framework มีกลไกในตัวสำหรับ จำกัด การทำงานพร้อมกันของ async IO (รวม HttpClient)
Florin Dumitrescu

คำตอบ:


64

นอกจากการทดสอบที่กล่าวถึงในคำถามแล้วฉันเพิ่งสร้างรายการใหม่ที่เกี่ยวข้องกับการโทร HTTP น้อยลงมาก (5,000 เทียบกับ 1 ล้านครั้งก่อนหน้านี้) แต่สำหรับคำขอที่ใช้เวลาดำเนินการนานกว่ามาก (500 มิลลิวินาทีเทียบกับประมาณ 1 มิลลิวินาทีก่อนหน้านี้) แอปพลิเคชันผู้ทดสอบทั้งสองแอปพลิเคชันมัลติเธรดแบบซิงโครนัส (อิงตาม HttpWebRequest) และ I / O แบบอะซิงโครนัส (ขึ้นอยู่กับไคลเอนต์ HTTP) ให้ผลลัพธ์ที่คล้ายกัน: ประมาณ 10 วินาทีในการดำเนินการโดยใช้ CPU ประมาณ 3% และหน่วยความจำ 30 MB ความแตกต่างเพียงอย่างเดียวระหว่างผู้ทดสอบทั้งสองคือเครื่องมัลติเธรดใช้ 310 เธรดในการดำเนินการในขณะที่อะซิงโครนัสเพียง 22

จากข้อสรุปของการทดสอบของฉันการเรียก HTTP แบบอะซิงโครนัสไม่ใช่ตัวเลือกที่ดีที่สุดเมื่อจัดการกับคำขอที่รวดเร็วมาก เหตุผลเบื้องหลังก็คือเมื่อรันงานที่มีการเรียก I / O แบบอะซิงโครนัสเธรดที่เริ่มงานจะหยุดทำงานทันทีที่มีการโทรแบบอะซิงโครนัสและส่วนที่เหลือของงานจะถูกลงทะเบียนเป็นการเรียกกลับ จากนั้นเมื่อการดำเนินการ I / O เสร็จสิ้นการเรียกกลับจะถูกจัดคิวเพื่อดำเนินการกับเธรดแรกที่มีอยู่ ทั้งหมดนี้สร้างค่าใช้จ่ายซึ่งทำให้การดำเนินการ I / O ที่รวดเร็วมีประสิทธิภาพมากขึ้นเมื่อดำเนินการกับเธรดที่เริ่มต้น

การเรียก HTTP แบบอะซิงโครนัสเป็นตัวเลือกที่ดีเมื่อจัดการกับการดำเนินการ I / O ที่ยาวหรืออาจจะยาวเพราะไม่ทำให้เธรดใด ๆ ยุ่งอยู่กับการรอการดำเนินการ I / O ให้เสร็จสมบูรณ์ ซึ่งจะลดจำนวนเธรดโดยรวมที่ใช้โดยแอปพลิเคชันทำให้ใช้เวลา CPU มากขึ้นโดยการดำเนินการที่เชื่อมโยงกับ CPU นอกจากนี้ในแอปพลิเคชันที่จัดสรรเธรดจำนวน จำกัด เท่านั้น (เช่นเดียวกับในกรณีของเว็บแอ็พพลิเคชัน) I / O แบบอะซิงโครนัสจะป้องกันการหมดเธรดพูลเธรดซึ่งอาจเกิดขึ้นได้หากทำการเรียก I / O พร้อมกัน

ดังนั้น async HttpClient จึงไม่ใช่คอขวดสำหรับแอปพลิเคชันที่ใช้งานหนัก โดยธรรมชาติแล้วมันไม่เหมาะอย่างยิ่งสำหรับการร้องขอ HTTP ที่เร็วมาก แต่มันเหมาะสำหรับคำขอยาว ๆ หรืออาจจะยาวโดยเฉพาะอย่างยิ่งภายในแอปพลิเคชันที่มีเธรดจำนวน จำกัด เท่านั้น นอกจากนี้เป็นแนวทางปฏิบัติที่ดีในการ จำกัด การทำงานพร้อมกันผ่าน ServicePointManager.DefaultConnectionLimit ด้วยค่าที่สูงพอที่จะทำให้แน่ใจว่ามีการขนานกันในระดับที่ดี แต่ต่ำพอที่จะป้องกันการพร่องของพอร์ตชั่วคราว คุณสามารถค้นหารายละเอียดเพิ่มเติมเกี่ยวกับการทดสอบและข้อสรุปนำเสนอสำหรับคำถามนี้ที่นี่


3
"เร็วมาก" แค่ไหน? 1 มิลลิวินาที? 100ms? 1,000ms?
Tim P.

ฉันกำลังใช้วิธีการ "async" ของคุณในการเล่นซ้ำการโหลดบนเว็บเซิร์ฟเวอร์ WebLogic ที่ติดตั้งบน Windows แต่ฉันได้รับปัญหาการสูญเสียพอร์ตแบบ ephemical ค่อนข้างเร็ว ฉันไม่ได้แตะ ServicePointManager.DefaultConnectionLimit และฉันกำลังกำจัดและสร้างใหม่ทุกอย่าง (HttpClient และการตอบกลับ) ในแต่ละคำขอ คุณมีความคิดใดบ้างที่อาจทำให้การเชื่อมต่อยังคงเปิดอยู่และทำให้พอร์ตหมดลงหรือไม่?
Iravanchi

@TimP สำหรับการทดสอบของฉันดังที่กล่าวไว้ข้างต้น "เร็วมาก" เป็นคำขอที่ใช้เวลาเพียง 1 มิลลิวินาทีในการดำเนินการให้เสร็จสมบูรณ์ ในโลกแห่งความเป็นจริงสิ่งนี้มักจะเป็นเรื่องส่วนตัว จากมุมมองของฉันสิ่งที่เทียบเท่ากับข้อความค้นหาขนาดเล็กบนฐานข้อมูลเครือข่ายท้องถิ่นถือได้ว่ารวดเร็วในขณะที่สิ่งที่เทียบเท่ากับการเรียก API ทางอินเทอร์เน็ตถือได้ว่าช้าหรืออาจช้า
Florin Dumitrescu

1
@Iravanchi ในแนวทาง "async" การส่งคำขอและการจัดการการตอบกลับจะดำเนินการแยกกัน หากคุณมีสายโทรเข้าจำนวนมากคำขอทั้งหมดจะถูกส่งอย่างรวดเร็วและการตอบกลับจะได้รับการจัดการเมื่อมาถึง เนื่องจากคุณสามารถยกเลิกการเชื่อมต่อได้หลังจากที่มีการตอบกลับเท่านั้นการเชื่อมต่อพร้อมกันจำนวนมากจึงสามารถสะสมและทำให้พอร์ตชั่วคราวของคุณหมดลงได้ คุณควร จำกัด จำนวนสูงสุดของการเชื่อมต่อพร้อมกันโดยใช้ ServicePointManager.DefaultConnectionLimit
Florin Dumitrescu

1
@FlorinDumitrescu ฉันจะเพิ่มเข้าไปด้วยว่าการโทรผ่านเครือข่ายนั้นไม่สามารถคาดเดาได้โดยธรรมชาติ สิ่งที่ทำงานใน 10ms 90% ของเวลาอาจทำให้เกิดปัญหาการบล็อกเมื่อทรัพยากรเครือข่ายนั้นคับคั่งหรือไม่สามารถใช้งานได้ในอีก 10% ของเวลา
Tim P.

27

สิ่งหนึ่งที่ควรพิจารณาที่อาจส่งผลต่อผลลัพธ์ของคุณคือด้วย HttpWebRequest คุณจะไม่ได้รับ ResponseStream และใช้งานสตรีมนั้น ด้วย HttpClient โดยค่าเริ่มต้นจะคัดลอกสตรีมเครือข่ายไปยังสตรีมหน่วยความจำ ในการใช้งาน HttpClient ในลักษณะเดียวกับที่คุณกำลังใช้ HttpWebRquest คุณจะต้องทำ

var requestMessage = new HttpRequestMessage() {RequestUri = URL};
Task<HttpResponseMessage> getTask = httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead);

อีกอย่างก็คือฉันไม่แน่ใจว่าอะไรคือความแตกต่างที่แท้จริงจากมุมมองของเธรดคุณกำลังทดสอบอยู่ หากคุณเจาะลึกลงไปในส่วนลึกของ HttpClientHandler มันก็แค่ทำการ Task.Factory.StartNew เพื่อดำเนินการตามคำขอ async ลักษณะการทำงานของเธรดถูกมอบหมายให้กับบริบทการซิงโครไนซ์ในลักษณะเดียวกับตัวอย่างของคุณด้วยตัวอย่าง HttpWebRequest

ไม่ต้องสงสัย HttpClient เพิ่มค่าใช้จ่ายบางส่วนตามค่าเริ่มต้นโดยใช้ HttpWebRequest เป็นไลบรารีการขนส่ง ดังนั้นคุณจะสามารถได้รับความสมบูรณ์แบบที่ดีขึ้นเสมอด้วย HttpWebRequest โดยตรงในขณะที่ใช้ HttpClientHandler ประโยชน์ที่ HttpClient นำเสนอคือคลาสมาตรฐานเช่น HttpResponseMessage, HttpRequestMessage, HttpContent และส่วนหัวที่พิมพ์หนักแน่นทั้งหมด ในตัวมันเองไม่ใช่การเพิ่มประสิทธิภาพอย่างสมบูรณ์แบบ


(คำตอบเก่า แต่) HttpClientดูเหมือนจะใช้งานง่ายและฉันคิดว่าอะซิงโครนัสเป็นวิธีที่จะไป แต่ดูเหมือนจะมี "buts and ifs" มากมายรอบ ๆ นี้ บางทีHttpClientควรเขียนใหม่เพื่อให้ใช้งานง่ายขึ้น? หรือว่าเอกสารนั้นเน้นย้ำถึงสิ่งสำคัญเกี่ยวกับการใช้งานให้มีประสิทธิภาพมากที่สุด?
จำนอง

@mortb, Flurl.Http flurl.ioเป็นกระดาษห่อหุ้มที่ใช้งานง่ายกว่าของ HttpClient
Michael Freidgeim

1
@MichaelFreidgeim: ขอบคุณแม้ว่าฉันจะเรียนรู้ที่จะอยู่กับ HttpClient แล้วในตอนนี้ ...

17

แม้ว่าสิ่งนี้จะไม่ได้ตอบคำถามของ OP โดยตรงในส่วน "async" แต่ก็เป็นการระบุข้อผิดพลาดในการใช้งานที่เขาใช้อยู่

หากคุณต้องการให้แอปพลิเคชันของคุณปรับขนาดให้หลีกเลี่ยงการใช้ HttpClients ที่ใช้อินสแตนซ์ ความแตกต่างมหาศาล! ขึ้นอยู่กับการโหลดคุณจะเห็นตัวเลขประสิทธิภาพที่แตกต่างกันมาก HttpClient ได้รับการออกแบบมาเพื่อใช้ซ้ำกับคำขอต่างๆ สิ่งนี้ได้รับการยืนยันโดยคนในทีม BCL ที่เขียนมัน

โครงการล่าสุดที่ฉันมีคือการช่วยร้านค้าปลีกคอมพิวเตอร์ออนไลน์ขนาดใหญ่และมีชื่อเสียงในการขยายการรับส่งข้อมูลในวัน Black Friday / วันหยุดสำหรับระบบใหม่ ๆ เราพบปัญหาด้านประสิทธิภาพบางอย่างเกี่ยวกับการใช้งาน HttpClient เนื่องจากมีการใช้งานIDisposabledevs จึงทำสิ่งที่คุณทำตามปกติโดยการสร้างอินสแตนซ์และวางไว้ในusing()คำสั่ง เมื่อเราเริ่มทดสอบการโหลดแอพก็ทำให้เซิร์ฟเวอร์คุกเข่าใช่เซิร์ฟเวอร์ไม่ใช่แค่แอพ เหตุผลก็คือทุกอินสแตนซ์ของ HttpClient เปิด I / O Completion Port บนเซิร์ฟเวอร์ เนื่องจากการสรุป GC แบบไม่กำหนดและการที่คุณกำลังทำงานกับทรัพยากรคอมพิวเตอร์ที่ครอบคลุมOSIหลายเลเยอร์การปิดพอร์ตเครือข่ายอาจใช้เวลาสักครู่ ในความเป็นจริง Windows OS เองอาจใช้เวลาถึง 20 วินาทีในการปิดพอร์ต (ตาม Microsoft) เราเปิดพอร์ตเร็วกว่าที่จะปิดได้ - พอร์ตเซิร์ฟเวอร์หมดซึ่งส่งผลกระทบต่อ CPU ถึง 100% การแก้ไขของฉันคือเปลี่ยน HttpClient เป็นอินสแตนซ์แบบคงที่ซึ่งช่วยแก้ปัญหาได้ ใช่มันเป็นทรัพยากรที่ใช้แล้วทิ้ง แต่ค่าใช้จ่ายใด ๆ มีมากกว่าความแตกต่างของประสิทธิภาพอย่างมาก ขอแนะนำให้คุณทำการทดสอบการโหลดเพื่อดูว่าแอปของคุณทำงานอย่างไร

ยังตอบที่ลิงค์ด้านล่าง:

ค่าใช้จ่ายในการสร้าง HttpClient ใหม่ต่อการโทรในไคลเอนต์ WebAPI คืออะไร

https://www.asp.net/web-api/overview/advanced/calling-a-web-api-from-a-net-client


ฉันพบปัญหาเดียวกันกับการสร้างพอร์ต TCP หมดบนไคลเอนต์ วิธีแก้ปัญหาคือการเช่าอินสแตนซ์ HttpClient เป็นระยะเวลานานซึ่งมีการเรียกแบบวนซ้ำไม่ใช่สร้างและกำจัดสำหรับการโทรแต่ละครั้ง ข้อสรุปที่ฉันได้รับคือ "เพียงเพราะมันใช้ Dispose ไม่ได้หมายความว่ามันถูกที่จะทิ้ง"
PhillipH

ดังนั้นหาก HttpClient เป็นแบบคงที่และฉันจำเป็นต้องเปลี่ยนส่วนหัวในคำขอถัดไปจะทำอย่างไรกับคำขอแรก มีอันตรายใด ๆ ในการเปลี่ยน HttpClient เนื่องจากเป็นแบบคงที่ - เช่นการออก HttpClient.DefaultRequestHeaders.Accept.Clear (); ? ตัวอย่างเช่นหากฉันมีผู้ใช้ที่ตรวจสอบสิทธิ์ผ่านโทเค็นต้องเพิ่มโทเค็นเหล่านั้นเป็นส่วนหัวในคำขอไปยัง API ซึ่งเป็นโทเค็นที่แตกต่างกัน การไม่มี HttpClient เป็นแบบคงที่แล้วการเปลี่ยนส่วนหัวนี้บน HttpClient จะส่งผลเสียหรือไม่?
crizzwald

หากคุณจำเป็นต้องใช้สมาชิกอินสแตนซ์ HttpClient เช่นส่วนหัว / คุกกี้เป็นต้นคุณไม่ควรใช้ HttpClient แบบคงที่ มิฉะนั้นข้อมูลอินสแตนซ์ของคุณ (ส่วนหัว, คุกกี้) จะเหมือนกันสำหรับทุกคำขอ - ไม่ใช่สิ่งที่คุณต้องการอย่างแน่นอน
Dave Black

เนื่องจากเป็นกรณีนี้ ... คุณจะป้องกันสิ่งที่คุณอธิบายข้างต้นในโพสต์ของคุณได้อย่างไร - จากการโหลด โหลดบาลานเซอร์และเพิ่มเซิร์ฟเวอร์ที่มัน?
crizzwald

@crizzwald - ในโพสต์ของฉันฉันสังเกตเห็นวิธีแก้ปัญหาที่ใช้ ใช้อินสแตนซ์แบบคงที่ของ HttpClient หากคุณจำเป็นต้องใช้ส่วนหัว / คุกกี้บน HttpClient ฉันต้องการใช้ทางเลือกอื่น
Dave Black
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.