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


162

สิ่งที่ควรเป็นHttpClientอายุการใช้งานของไคลเอนต์ WebAPI
มันจะดีกว่าถ้ามีอินสแตนซ์หนึ่งHttpClientสำหรับการโทรหลายสาย

ค่าใช้จ่ายในการสร้างและการกำจัดHttpClientต่อคำขอเป็นอย่างไรเช่นในตัวอย่างด้านล่าง (นำมาจาก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);
    }
}

ฉันไม่แน่ใจคุณสามารถใช้Stopwatchคลาสเพื่อเปรียบเทียบมันได้ การประมาณของฉันน่าจะเหมาะสมกว่าถ้ามีสักHttpClientกรณีสมมติว่าทุกกรณีถูกใช้ในบริบทเดียวกัน
Matthew

1
ที่เกี่ยวข้อง: stackoverflow.com/questions/11178220/... , stackoverflow.com/questions/15705092/...
Ohad Schneider

คำตอบ:


215

HttpClientถูกออกแบบมาให้ใช้ซ้ำสำหรับการโทรหลายครั้ง แม้ในหลายกระทู้ HttpClientHandlerมีข้อมูลประจำตัวและคุกกี้ที่มีวัตถุประสงค์เพื่อนำกลับมาใช้ในการโทร การมีHttpClientอินสแตนซ์ใหม่นั้นจำเป็นต้องตั้งค่าทุกสิ่งใหม่ นอกจากนี้ยังDefaultRequestHeadersมีคุณสมบัติที่มีไว้สำหรับการโทรหลายสาย ต้องรีเซ็ตค่าเหล่านั้นในแต่ละคำขอเอาชนะจุด

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

ยิ่งคุณใช้คุณลักษณะของHttpClientเครื่องมากเท่าไรคุณจะยิ่งเห็นว่าการนำอินสแตนซ์ที่มีอยู่กลับมาใช้ใหม่ได้อย่างเหมาะสม

แต่ปัญหาที่ใหญ่ที่สุดในความคิดของฉันคือว่าเมื่อHttpClientระดับเป็นที่จำหน่ายก็พ้นHttpClientHandlerที่แล้วบังคับให้ปิดการเชื่อมต่อในสระว่ายน้ำของการเชื่อมต่อที่มีการจัดการโดยTCP/IP ServicePointManagerซึ่งหมายความว่าคำขอแต่ละรายการที่มีใหม่HttpClientจะต้องสร้างการTCP/IPเชื่อมต่อใหม่อีกครั้ง

จากการทดสอบของฉันโดยใช้ HTTP ธรรมดาบน LAN ประสิทธิภาพในการทำงานนั้นค่อนข้างเล็กน้อย ฉันสงสัยว่านี่เป็นเพราะมี TCP keepalive พื้นฐานที่ถือการเชื่อมต่อเปิดแม้ว่าHttpClientHandlerจะพยายามปิด

เมื่อฉันร้องขอผ่านอินเทอร์เน็ตฉันได้เห็นเรื่องราวที่แตกต่าง ฉันเห็นผลการดำเนินงาน 40% เนื่องจากต้องเปิดคำขอใหม่ทุกครั้ง

ฉันสงสัยว่าการHTTPSเชื่อมต่อจะยิ่งแย่ลง

คำแนะนำของฉันคือเก็บ HttpClient เป็นระยะเวลาตลอดอายุการใช้งานแอปพลิเคชันของคุณสำหรับแต่ละ API ที่คุณเชื่อมต่อ


5
which then forcibly closes the TCP/IP connection in the pool of connections that is managed by ServicePointManagerคุณแน่ใจเกี่ยวกับคำชี้แจงนี้แค่ไหน? นั่นเป็นเรื่องยากที่จะเชื่อ HttpClientดูเหมือนว่าฉันจะเป็นหน่วยงานที่ควรได้รับการยกตัวอย่างบ่อยครั้ง
usr

2
@vkelman ใช่คุณยังสามารถใช้อินสแตนซ์ของ HttpClient ได้แม้ว่าคุณจะสร้างด้วย HttpClientHandler ใหม่ นอกจากนี้โปรดทราบว่ามีตัวสร้างพิเศษสำหรับ HttpClient ที่ให้คุณใช้ HttpClientHandler อีกครั้งและกำจัด HttpClient โดยไม่ทำลายการเชื่อมต่อ
Darrel Miller

2
@vkelman ฉันต้องการเก็บ HttpClient ไว้รอบ ๆ แต่ถ้าคุณต้องการเก็บ HttpClientHandler ไว้รอบ ๆ ก็จะคงการเชื่อมต่อไว้เมื่อพารามิเตอร์ตัวที่สองเป็นเท็จ
Darrel Miller

2
@DarrelMiller ดังนั้นดูเหมือนว่าการเชื่อมต่อจะถูกผูกไว้กับ HttpClientHandler ฉันรู้ว่าการขยายขนาดฉันไม่ต้องการทำลายการเชื่อมต่อดังนั้นฉันต้องเก็บ HttpClientHandler ไว้รอบ ๆ และสร้างอินสแตนซ์ HttpClient ทั้งหมดของฉันจากนั้นหรือสร้างอินสแตนซ์ HttpClient แบบคงที่ อย่างไรก็ตามหาก CookieContainer ถูกผูกไว้กับ HttpClientHandler และคุกกี้ของฉันต้องแตกต่างกันตามคำขอคุณแนะนำอะไร ฉันต้องการหลีกเลี่ยงการซิงโครไนซ์เธรดบน HttpClientHandler แบบคงที่โดยการแก้ไข CookieContainer สำหรับแต่ละคำขอ
เดฟสีดำ

2
@ Sana.91 คุณทำได้ มันจะเป็นการดีกว่าถ้าคุณลงทะเบียนเป็นซิงเกิลตันในคอลเล็กชันบริการและเข้าถึงด้วยวิธีนั้น
Darrel Miller

69

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

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

นอกจากนี้คุณยังสามารถตรวจสอบหน้าคำแนะนำ WebAPI เพื่อรับเอกสารและตัวอย่างได้ที่ https://www.asp.net/web-api/overview/advanced/calling-a-web-api-from-a-net-client

ให้ความสนใจเป็นพิเศษกับการโทรออกนี้:

HttpClient มีวัตถุประสงค์เพื่อสร้างอินสแตนซ์หนึ่งครั้งและนำกลับมาใช้ใหม่ตลอดอายุการใช้งานของแอปพลิเคชัน โดยเฉพาะอย่างยิ่งในแอปพลิเคชันเซิร์ฟเวอร์การสร้างอินสแตนซ์ HttpClient ใหม่สำหรับทุกคำขอจะทำให้จำนวนซ็อกเก็ตหมดลงภายใต้การโหลดจำนวนมาก สิ่งนี้จะส่งผลให้เกิดข้อผิดพลาด SocketException

หากคุณพบว่าคุณจำเป็นต้องใช้คงที่HttpClientมีส่วนหัวที่แตกต่างกันอยู่ฐาน ฯลฯ สิ่งที่คุณจะต้องทำคือการสร้างด้วยตนเองและการตั้งค่าผู้ที่อยู่ในHttpRequestMessage HttpRequestMessageจากนั้นใช้HttpClient:SendAsync(HttpRequestMessage requestMessage, ...)

อัปเดตสำหรับ. NET Core : คุณควรใช้การIHttpClientFactoryพึ่งพาการฉีดเพื่อสร้างHttpClientอินสแตนซ์ มันจะจัดการอายุการใช้งานสำหรับคุณและคุณไม่จำเป็นต้องกำจัดอย่างชัดเจน ดูที่การสร้างคำขอ HTTP โดยใช้ IHttpClientFactory ใน ASP.NET Core


1
โพสต์นี้มีข้อมูลเชิงลึกที่เป็นประโยชน์สำหรับผู้ที่จะทำการทดสอบความเครียด .. !
Sana.91

9

ในฐานะที่เป็นคำตอบอื่น ๆ รัฐHttpClientมีวัตถุประสงค์เพื่อนำมาใช้ใหม่ อย่างไรก็ตามการใช้HttpClientอินสแตนซ์เดียวซ้ำในแอปพลิเคชันแบบมัลติเธรดหมายความว่าคุณไม่สามารถเปลี่ยนค่าของคุณสมบัติที่เป็นสถานะเช่นBaseAddressและDefaultRequestHeaders(เพื่อให้คุณสามารถใช้งานได้เฉพาะในกรณีที่แอปพลิเคชันของคุณคงที่)

วิธีการหนึ่งในการรับรอบข้อ จำกัด นี้เป็นห่อHttpClientด้วยชั้นเรียนที่ซ้ำกันทั้งหมดHttpClientวิธีการที่คุณต้องการ ( GetAsync, PostAsyncฯลฯ ) HttpClientและได้รับมอบหมายให้พวกเขาเดี่ยว แต่ที่สวยน่าเบื่อ (คุณจะต้องห่อวิธีการขยายมากเกินไป) และโชคดีที่มีวิธีอื่น - ให้สร้างใหม่HttpClientกรณี HttpClientHandlerแต่นำมาใช้อ้างอิง ตรวจสอบให้แน่ใจว่าคุณไม่ได้กำจัดผู้ดูแล:

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

2
วิธีที่ดีกว่าที่จะไปคือเก็บอินสแตนซ์ HttpClient หนึ่งอินสแตนซ์จากนั้นสร้างอินสแตนซ์ HttpRequestMessage ในเครื่องของคุณแล้วใช้เมธอด. SendAsync () บน HttpClient วิธีนี้จะยังคงปลอดภัยต่อเธรด แต่ละ HttpRequestMessage จะมีค่าการตรวจสอบสิทธิ์ / URL ของตนเอง
ทิมพี

@TimP ทำไมถึงดีกว่า SendAsyncมีมากน้อยสะดวกกว่าวิธีเฉพาะเช่นPutAsync, PostAsJsonAsyncฯลฯ
Ohad Schneider

2
SendAsync ให้คุณเปลี่ยน URL และคุณสมบัติอื่น ๆ เช่นส่วนหัวและยังคงปลอดภัยต่อเธรด
ทิมพี

2
ใช่ตัวจัดการคือกุญแจ ตราบใดที่มันถูกแชร์ระหว่างอินสแตนซ์ HttpClient คุณก็ใช้ได้ ฉันอ่านความคิดเห็นก่อนหน้านี้ผิด
เดฟสีดำ

1
หากเราจัดการผู้ดูแลร่วมกันเรายังต้องดูแลปัญหา DNS เก่าหรือไม่
shanti

5

เกี่ยวข้องกับเว็บไซต์ที่มีปริมาณมาก แต่ไม่เกี่ยวข้องกับ HttpClient โดยตรง เรามีข้อมูลโค้ดด้านล่างในบริการทั้งหมดของเรา

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

จากhttps://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k(System.Net.ServicePoint.ConnectionLeaseTimeout);k(TargetFrameworkMoniker-.NETFramework,Version%3Dv4.5.2); k (DevLang-csharp) และถ = true

"คุณสามารถใช้คุณสมบัตินี้เพื่อให้แน่ใจว่าการเชื่อมต่อที่แอ็คทีฟของวัตถุ ServicePoint จะไม่เปิดตลอดไปคุณสมบัตินี้มีไว้สำหรับสถานการณ์ที่การเชื่อมต่อควรถูกปล่อยและสร้างใหม่อีกครั้งเป็นระยะเช่นสถานการณ์โหลดบาลานซ์

ตามค่าเริ่มต้นเมื่อ KeepAlive เป็นจริงสำหรับคำขอคุณสมบัติ MaxIdleTime จะตั้งค่าการหมดเวลาสำหรับการปิดการเชื่อมต่อ ServicePoint เนื่องจากไม่มีการใช้งาน ถ้า ServicePoint มีการเชื่อมต่อที่ใช้งานอยู่ MaxIdleTime จะไม่มีผลใด ๆ และการเชื่อมต่อจะยังคงเปิดอยู่โดยไม่มีกำหนด

เมื่อคุณสมบัติ ConnectionLeaseTimeout ถูกตั้งค่าเป็นค่าอื่นที่ไม่ใช่ -1 และหลังจากเวลาที่กำหนดผ่านไปการเชื่อมต่อ ServicePoint ที่ใช้งานจะถูกปิดหลังจากให้บริการคำขอโดยตั้งค่า KeepAlive เป็นเท็จในคำขอนั้น การตั้งค่านี้มีผลต่อการเชื่อมต่อทั้งหมดที่จัดการโดยวัตถุ ServicePoint "

เมื่อคุณมีบริการที่อยู่ด้านหลัง CDN หรือจุดปลายอื่น ๆ ที่คุณต้องการล้มเหลวการตั้งค่านี้จะช่วยให้ผู้โทรติดต่อติดตามคุณไปยังปลายทางใหม่ของคุณ ในตัวอย่างนี้ 60 วินาทีหลังจากเกิดความล้มเหลวผู้เรียกทั้งหมดควรเชื่อมต่อกับจุดปลายใหม่อีกครั้ง มันต้องการให้คุณรู้ว่าบริการที่ต้องพึ่งพา (บริการที่คุณโทรหา) และจุดสิ้นสุดของบริการนั้น ๆ


คุณยังคงมีภาระมากมายบนเซิร์ฟเวอร์โดยการเปิดและปิดการเชื่อมต่อ ถ้าคุณใช้ HttpClients ที่อิงตามอินสแตนซ์กับ HttpClientHandlers ตามอินสแตนซ์คุณจะยังคงทำงานในพอร์ตที่อ่อนล้าหากคุณไม่ระวัง
เดฟสีดำ

ไม่เห็นด้วย ทุกอย่างเป็นการแลกเปลี่ยน สำหรับเราหลังจาก CDN หรือ DNS ที่กำหนดเส้นทางใหม่เป็นเงินในธนาคารเทียบกับรายได้ที่หายไป
ไม่มีการคืนเงินไม่ส่งคืน

1

คุณอาจต้องการอ้างอิงโพสต์บล็อกนี้โดย Simon Timms: https://aspnetmonsters.com 2016/08/2016-08-27-httpclientwrong /

แต่HttpClientต่างกัน แม้ว่ามันจะใช้IDisposableอินเตอร์เฟสมันเป็นวัตถุที่ใช้ร่วมกัน ซึ่งหมายความว่าภายใต้ครอบคลุมมันเป็น reentrant) และด้ายปลอดภัย แทนที่จะสร้างอินสแตนซ์ใหม่HttpClientสำหรับการดำเนินการแต่ละครั้งคุณควรแบ่งปันอินสแตนซ์เดียวHttpClientตลอดอายุการใช้งานของแอปพลิเคชัน ลองดูสาเหตุ


1

สิ่งหนึ่งที่ชี้ให้เห็นว่าไม่มีข้อความ "ไม่ได้ใช้โดยใช้" ในบล็อกคือมันไม่ใช่แค่ BaseAddress และ DefaultHeader ที่คุณต้องพิจารณา เมื่อคุณสร้าง HttpClient คงมีสถานะภายในที่จะดำเนินการข้ามคำขอ ตัวอย่าง: คุณกำลังตรวจสอบสิทธิ์กับบุคคลที่สามด้วย HttpClient เพื่อรับโทเค็น FedAuth (ไม่ต้องสนใจว่าทำไมไม่ใช้ OAuth / OWIN / ฯลฯ ) ข้อความตอบกลับนั้นมีหัวข้อ Set-Cookie สำหรับ FedAuth ซึ่งจะเพิ่มในสถานะ HttpClient ของคุณ ผู้ใช้รายถัดไปที่ลงชื่อเข้าใช้ API ของคุณจะส่งคุกกี้ FedAuth ของบุคคลสุดท้ายเว้นแต่คุณจะจัดการคุกกี้เหล่านี้ในแต่ละคำขอ


0

เป็นปัญหาแรกในขณะที่คลาสนี้ใช้แล้วทิ้งการใช้usingคำสั่งนั้นไม่ใช่ทางเลือกที่ดีที่สุดเพราะแม้ว่าคุณจะทิ้งHttpClientวัตถุซ็อกเก็ตที่อยู่ข้างใต้จะไม่ถูกปล่อยออกมาทันทีและอาจทำให้เกิดปัญหาร้ายแรงที่ชื่อว่า

แต่มีปัญหาที่สองHttpClientที่คุณสามารถทำได้เมื่อคุณใช้มันเป็นซิงเกิลหรือวัตถุคงที่ ในกรณีนี้ singleton หรือ static HttpClientไม่เคารพDNSการเปลี่ยนแปลง

ใน. net coreคุณสามารถทำสิ่งเดียวกันกับHttpClientFactory ได้ ดังนี้:

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

ConfigureServices

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

เอกสารและตัวอย่างที่นี่

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