高並發httpClient原理與優化使用詳解


HTTP長連接、短連接究竟是什么?

1. HTTP協議與TCP/IP協議的關系

HTTP的長連接和短連接本質上是TCP長連接和短連接。HTTP屬於應用層協議,在傳輸層使用TCP協議,在網絡層使用IP協議。 IP協議主要解決網絡路由和尋址問題,TCP協議主要解決如何在IP層之上可靠地傳遞數據包,使得網絡上接收端收到發送端所發出的所有包,並且順序與發送順序一致。TCP協議是可靠的、面向連接的。

2. 如何理解HTTP協議是無狀態的

HTTP協議是無狀態的,指的是協議對於事務處理沒有記憶能力,服務器不知道客戶端是什么狀態。也就是說,打開一個服務器上的網頁和上一次打開這個服務器上的網頁之間沒有任何聯系。HTTP是一個無狀態的面向連接的協議,無狀態不代表HTTP不能保持TCP連接,更不能代表HTTP使用的是UDP協議(無連接)。

3. 什么是長連接、短連接?

在HTTP/1.0中默認使用短連接。也就是說,客戶端和服務器每進行一次HTTP操作,就建立一次連接,任務結束就中斷連接。當客戶端瀏覽器訪問的某個HTML或其他類型的Web頁中包含有其他的Web資源(如JavaScript文件、圖像文件、CSS文件等),每遇到這樣一個Web資源,瀏覽器就會重新建立一個HTTP會話。

而從HTTP/1.1起,默認使用長連接,用以保持連接特性。使用長連接的HTTP協議,會在響應頭加入這行代碼:

Connection:keep-alive

在使用長連接的情況下,當一個網頁打開完成后,客戶端和服務器之間用於傳輸HTTP數據的TCP連接不會關閉,客戶端再次訪問這個服務器時,會繼續使用這一條已經建立的連接。Keep-Alive不會永久保持連接,它有一個保持時間,可以在不同的服務器軟件(如Apache)中設定這個時間。實現長連接需要客戶端和服務端都支持長連接。

HTTP協議的長連接和短連接,實質上是TCP協議的長連接和短連接。

3.1. TCP連接

當網絡通信時采用TCP協議時,在真正的讀寫操作之前,客戶端與服務器端之間必須建立一個連接,當讀寫操作完成后,雙方不再需要這個連接時可以釋放這個連接。連接的建立依靠“三次握手”,而釋放則需要“四次握手”,所以每個連接的建立都是需要資源消耗和時間消耗的。

經典的三次握手建立連接示意圖:

經典的四次握手關閉連接示意圖:

3.2. TCP短連接

模擬一下TCP短連接的情況:client向server發起連接請求,server接到請求,然后雙方建立連接。client向server發送消息,server回應client,然后一次請求就完成了。這時候雙方任意都可以發起close操作,不過一般都是client先發起close操作。上述可知,短連接一般只會在 client/server間傳遞一次請求操作。

短連接的優點是:管理起來比較簡單,存在的連接都是有用的連接,不需要額外的控制手段。

3.3. TCP長連接

我們再模擬一下長連接的情況:client向server發起連接,server接受client連接,雙方建立連接,client與server完成一次請求后,它們之間的連接並不會主動關閉,后續的讀寫操作會繼續使用這個連接。

TCP的保活功能主要為服務器應用提供。如果客戶端已經消失而連接未斷開,則會使得服務器上保留一個半開放的連接,而服務器又在等待來自客戶端的數據,此時服務器將永遠等待客戶端的數據。保活功能就是試圖在服務端器端檢測到這種半開放的連接。

如果一個給定的連接在兩小時內沒有任何動作,服務器就向客戶發送一個探測報文段,根據客戶端主機響應探測4個客戶端狀態:

  • 客戶主機依然正常運行,且服務器可達。此時客戶的TCP響應正常,服務器將保活定時器復位。
  • 客戶主機已經崩潰,並且關閉或者正在重新啟動。上述情況下客戶端都不能響應TCP。服務端將無法收到客戶端對探測的響應。服務器總共發送10個這樣的探測,每個間隔75秒。若服務器沒有收到任何一個響應,它就認為客戶端已經關閉並終止連接。
  • 客戶端崩潰並已經重新啟動。服務器將收到一個對其保活探測的響應,這個響應是一個復位,使得服務器終止這個連接。
  • 客戶機正常運行,但是服務器不可達。這種情況與第二種狀態類似。

4. 長連接和短連接的優點和缺點

由上可以看出,長連接可以省去較多的TCP建立和關閉的操作,減少浪費,節約時間。對於頻繁請求資源的客戶端適合使用長連接。在長連接的應用場景下,client端一般不會主動關閉連接,當client與server之間的連接一直不關閉,隨着客戶端連接越來越多,server會保持過多連接。這時候server端需要采取一些策略,如關閉一些長時間沒有請求發生的連接,這樣可以避免一些惡意連接導致server端服務受損;如果條件允許則可以限制每個客戶端的最大長連接數,這樣可以完全避免惡意的客戶端拖垮整體后端服務。

短連接對於服務器來說管理較為簡單,存在的連接都是有用的連接,不需要額外的控制手段。但如果客戶請求頻繁,將在TCP的建立和關閉操作上浪費較多時間和帶寬。

長連接和短連接的產生在於client和server采取的關閉策略。不同的應用場景適合采用不同的策略。

由上可以看出,長連接可以省去較多的TCP建立和關閉的操作,減少浪費,節約時間。對於頻繁請求資源的客戶來說,較適用長連接。不過這里存在一個問題存活功能的探測周期太長,還有就是它只是探測TCP連接的存活,屬於比較斯文的做法,遇到惡意的連接時,保活功能就不夠使了。在長連接的應用場景下,client端一般不會主動關閉它們之間的連接,Client與server之間的連接如果一直不關閉的話,會存在一個問題,隨着客戶端連接越來越多,server早晚有扛不住的時候,這時候server端需要采取一些策略,如關閉一些長時間沒有讀寫事件發生的連接,這樣可 以避免一些惡意連接導致server端服務受損;如果條件再允許就可以以客戶端機器為顆粒度,限制每個客戶端的最大長連接數,這樣可以完全避免某個蛋疼的客戶端連累后端服務。

短連接對於服務器來說管理較為簡單,存在的連接都是有用的連接,不需要額外的控制手段。但如果客戶請求頻繁,將在TCP的建立和關閉操作上浪費時間和帶寬

長連接和短連接的產生在於client和server采取的關閉策略,具體的應用場景采用具體的策略,沒有十全十美的選擇,只有合適的選擇。

長連接短連接操作過程

短連接的操作步驟是:
建立連接——數據傳輸——關閉連接...建立連接——數據傳輸——關閉連接
長連接的操作步驟是:
建立連接——數據傳輸...(保持連接)...數據傳輸——關閉連接

什么時候用長連接,短連接?    

長連接多用於操作頻繁,點對點的通訊,而且連接數不能太多情況,。每個TCP連接都需要三步握手,這需要時間,如果每個操作都是先連接,再操作的話那么處理速度會降低很多,所以每個操作完后都不斷開,次處理時直接發送數據包就OK了,不用建立TCP連接。例如:數據庫的連接用長連接, 如果用短連接頻繁的通信會造成socket錯誤,而且頻繁的socket 創建也是對資源的浪費。 
  
  而像WEB網站的http服務一般都用短鏈接,因為長連接對於服務端來說會耗費一定的資源,而像WEB網站這么頻繁的成千上萬甚至上億客戶端的連接用短連接會更省一些資源,如果用長連接,而且同時有成千上萬的用戶,如果每個用戶都占用一個連接的話,那可想而知吧。所以並發量大,但每個用戶無需頻繁操作情況下需用短連好。

 

http和socket之長連接和短連接區別

 ==============================================================================

TCP/IP

TCP/IP是個協議組,可分為三個層次:網絡層、傳輸層和應用層。

在網絡層有IP協議、ICMP協議、ARP協議、RARP協議和BOOTP協議。

在傳輸層中有TCP協議與UDP協議。

在應用層有:TCP包括FTP、HTTP、TELNET、SMTP等協議

UDP包括DNS、TFTP等協議

短連接

連接->傳輸數據->關閉連接

HTTP是無狀態的,瀏覽器和服務器每進行一次HTTP操作,就建立一次連接,但任務結束就中斷連接。

也可以這樣說:短連接是指SOCKET連接后發送后接收完數據后馬上斷開連接。

長連接

連接->傳輸數據->保持連接 -> 傳輸數據-> 。。。 ->關閉連接。

長連接指建立SOCKET連接后不管是否使用都保持連接,但安全性較差。

http的長連接

HTTP也可以建立長連接的,使用Connection:keep-alive,HTTP 1.1默認進行持久連接。HTTP1.1和HTTP1.0相比較而言,最大的區別就是增加了持久連接支持(貌似最新的 http1.0 可以顯示的指定 keep-alive),但還是無狀態的,或者說是不可以信任的。

什么時候用長連接,短連接?

長連接多用於操作頻繁,點對點的通訊,而且連接數不能太多情況,。每個TCP連接都需要三步握手,這需要時間,如果每個操作都是先連接,再操作的話那么處理速度會降低很多,所以每個操作完后都不斷開,次處理時直接發送數據包就OK了,不用建立TCP連接。例如:數據庫的連接用長連接, 如果用短連接頻繁的通信會造成socket錯誤,而且頻繁的socket 創建也是對資源的浪費。

而像WEB網站的http服務一般都用短鏈接,因為長連接對於服務端來說會耗費一定的資源,而像WEB網站這么頻繁的成千上萬甚至上億客戶端的連接用短連接會更省一些資源,如果用長連接,而且同時有成千上萬的用戶,如果每個用戶都占用一個連接的話,那可想而知吧。所以並發量大,但每個用戶無需頻繁操作情況下需用短連好。

總之,長連接和短連接的選擇要視情況而定。

發送接收方式

1、異步

報文發送和接收是分開的,相互獨立的,互不影響。這種方式又分兩種情況:

(1)異步雙工:接收和發送在同一個程序中,由兩個不同的子進程分別負責發送和接收

(2)異步單工:接收和發送是用兩個不同的程序來完成。

2、同步

報文發送和接收是同步進行,既報文發送后等待接收返回報文。 同步方式一般需要考慮超時問題,即報文發出去后不能無限等待,需要設定超時時間,超過該時間發送方不再等待讀返回報文,直接通知超時返回。

在長連接中一般是沒有條件能夠判斷讀寫什么時候結束,所以必須要加長度報文頭。讀函數先是讀取報文頭的長度,再根據這個長度去讀相應長度的報文。

Socket是什么

Socket是應用層與TCP/IP協議族通信的中間軟件抽象層,它是一組接口。在設計模式中,Socket其實就是一個門面模式,它把復雜的TCP/IP協議族隱藏在Socket接口后面,對用戶來說,一組簡單的接口就是全部,讓Socket去組織數據,以符合指定的協議。

通信過程:

主機 A 的應用程序要能和主機 B 的應用程序通信,必須通過 Socket 建立連接,而建立 Socket 連接必須需要底層 TCP/IP 協議來建立 TCP 連接。建立 TCP 連接需要底層 IP 協議來尋址網絡中的主機。我們知道網絡層使用的 IP 協議可以幫助我們根據 IP 地址來找到目標主機,但是一台主機上可能運行着多個應用程序,如何才能與指定的應用程序通信就要通過 TCP 或 UPD 的地址也就是端口號來指定。這樣就可以通過一個 Socket 實例唯一代表一個主機上的一個應用程序的通信鏈路了。

建立通信鏈路

當客戶端要與服務端通信,客戶端首先要創建一個 Socket 實例,操作系統將為這個 Socket 實例分配一個沒有被使用的本地端口號,並創建一個包含本地和遠程地址和端口號的套接字數據結構,這個數據結構將一直保存在系統中直到這個連接關閉。在創建 Socket 實例的構造函數正確返回之前,將要進行 TCP 的三次握手協議,TCP 握手協議完成后,Socket 實例對象將創建完成,否則將拋出 IOException 錯誤。

與之對應的服務端將創建一個 ServerSocket 實例,ServerSocket 創建比較簡單只要指定的端口號沒有被占用,一般實例創建都會成功,同時操作系統也會為 ServerSocket 實例創建一個底層數據結構,這個數據結構中包含指定監聽的端口號和包含監聽地址的通配符,通常情況下都是“*”即監聽所有地址。之后當調用 accept() 方法時,將進入阻塞狀態,等待客戶端的請求。當一個新的請求到來時,將為這個連接創建一個新的套接字數據結構,該套接字數據的信息包含的地址和端口信息正是請求源地址和端口。這個新創建的數據結構將會關聯到 ServerSocket 實例的一個未完成的連接數據結構列表中,注意這時服務端與之對應的 Socket 實例並沒有完成創建,而要等到與客戶端的三次握手完成后,這個服務端的 Socket 實例才會返回,並將這個 Socket 實例對應的數據結構從未完成列表中移到已完成列表中。所以 ServerSocket 所關聯的列表中每個數據結構,都代表與一個客戶端的建立的 TCP 連接。

備注:

Windows 下單機最大TCP連接數

調整系統參數來調整單機的最大TCP連接數,Windows 下單機的TCP連接數有多個參數共同決定:

以下都是通過修改注冊表[HKEY_LOCAL_MACHINE \System \CurrentControlSet \Services \Tcpip \Parameters]

1.最大TCP連接數      TcpNumConnections

2.TCP關閉延遲時間    TCPTimedWaitDelay    (30-240)s

3.最大動態端口數  MaxUserPort  (Default = 5000, Max = 65534) TCP客戶端和服務器連接時,客戶端必須分配一個動態端口,默認情況下這個動態端口的分配范圍為 1024-5000 ,也就是說默認情況下,客戶端最多可以同時發起3977 Socket 連接

4.最大TCB 數量  MaxFreeTcbs

系統為每個TCP 連接分配一個TCP 控制塊(TCP control block or TCB),這個控制塊用於緩存TCP連接的一些參數,每個TCB需要分配 0.5 KB的pagepool 和 0.5KB 的Non-pagepool,也就說,每個TCP連接會占用 1KB 的系統內存。

非Server版本,MaxFreeTcbs 的默認值為1000 (64M 以上物理內存)Server 版本,這個的默認值為 2000。也就是說,默認情況下,Server 版本最多同時可以建立並保持2000個TCP 連接。

5. 最大TCB Hash table 數量  MaxHashTableSize TCB 是通過Hash table 來管理的。

這個值指明分配 pagepool 內存的數量,也就是說,如果MaxFreeTcbs = 1000 , 則 pagepool 的內存數量為 500KB那么 MaxHashTableSize 應大於 500 才行。這個數量越大,則Hash table 的冗余度就越高,每次分配和查找 TCP  連接用時就越少。這個值必須是2的冪,且最大為65536.

IBM WebSphere Voice Server 在windows server 2003 下的典型配置

MaxUserPort = 65534 (Decimal)

MaxHashTableSize = 65536 (Decimal)

MaxFreeTcbs = 16000 (Decimal)

這里我們可以看到 MaxHashTableSize 被配置為比MaxFreeTcbs 大4倍,這樣可以大大增加TCP建立的速度


鏈接:https://www.jianshu.com/p/b68d2b26f5f4 

==============================================================================

 

HTTP 的長連接和短連接

==========================================================================

一、什么是長連接

HTTP1.1規定了默認保持長連接(HTTP persistent connection ,也有翻譯為持久連接),數據傳輸完成了保持TCP連接不斷開(不發RST包、不四次握手),等待在同域名下繼續用這個通道傳輸數據;相反的就是短連接。

HTTP首部的Connection: Keep-alive是HTTP1.0瀏覽器和服務器的實驗性擴展,當前的HTTP1.1 RFC2616文檔沒有對它做說明,因為它所需要的功能已經默認開啟,無須帶着它,但是實踐中可以發現,瀏覽器的報文請求都會帶上它。如果HTTP1.1版本的HTTP請求報文不希望使用長連接,則要在HTTP請求報文首部加上Connection: close。《HTTP權威指南》提到,有部分古老的HTTP1.0 代理不理解Keep-alive,而導致長連接失效:客戶端–>代理–>服務端,客戶端帶有Keep-alive,而代理不認識,於是將報文原封不動轉給了服務端,服務端響應了Keep-alive,也被代理轉發給了客戶端,於是保持了“客戶端–>代理”連接和“代理–>服務端”連接不關閉,但是,當客戶端第發送第二次請求時,代理會認為當前連接不會有請求了,於是忽略了它,長連接失效。書上也介紹了解決方案:當發現HTTP版本為1.0時,就忽略Keep-alive,客戶端就知道當前不該使用長連接。其實,在實際使用中不需要考慮這么多,很多時候代理是我們自己控制的,如Nginx代理,代理服務器有長連接處理邏輯,服務端無需做patch處理,常見的是客戶端跟Nginx代理服務器使用HTTP1.1協議&長連接,而Nginx代理服務器跟后端服務器使用HTTP1.0協議&短連接。

在實際使用中,HTTP頭部有了Keep-Alive這個值並不代表一定會使用長連接,客戶端和服務器端都可以無視這個值,也就是不按標准來,譬如我自己寫的HTTP客戶端多線程去下載文件,就可以不遵循這個標准,並發的或者連續的多次GET請求,都分開在多個TCP通道中,每一條TCP通道,只有一次GET,GET完之后,立即有TCP關閉的四次握手,這樣寫代碼更簡單,這時候雖然HTTP頭有Connection: Keep-alive,但不能說是長連接。正常情況下客戶端瀏覽器、web服務端都有實現這個標准,因為它們的文件又小又多,保持長連接減少重新開TCP連接的開銷很有價值。

以前使用libcurl做的上傳/下載,就是短連接,抓包可以看到:1、每一條TCP通道只有一個POST;2、在數據傳輸完畢可以看到四次握手包。只要不調用curl_easy_cleanup,curl的handle就可能一直有效,可復用。這里說可能,因為連接是雙方的,如果服務器那邊關掉了,那么我客戶端這邊保留着也不能實現長連接。

如果是使用windows的WinHTTP庫,則在POST/GET數據的時候,雖然我關閉了句柄,但這時候TCP連接並不會立即關閉,而是等一小會兒,這時候是WinHTTP庫底層支持了跟Keep-alive所需要的功能:即便沒有Keep-alive,WinHTTP庫也可能會加上這種TCP通道復用的功能,而其它的網絡庫像libcurl則不會這么做。以前觀察過WinHTTP庫不會及時斷開TCP連接

二、長連接的過期時間

客戶端的長連接不可能無限期的拿着,會有一個超時時間,服務器有時候會告訴客戶端超時時間,譬如:

上圖中的Keep-Alive: timeout=20,表示這個TCP通道可以保持20秒。另外還可能有max=XXX,表示這個長連接最多接收XXX次請求就斷開。對於客戶端來說,如果服務器沒有告訴客戶端超時時間也沒關系,服務端可能主動發起四次握手斷開TCP連接,客戶端能夠知道該TCP連接已經無效;另外TCP還有心跳包來檢測當前連接是否還活着,方法很多,避免浪費資源。

三、長連接的數據傳輸完成識別

使用長連接之后,客戶端、服務端怎么知道本次傳輸結束呢?兩部分:1是判斷傳輸數據是否達到了Content-Length指示的大小;2動態生成的文件沒有Content-Length,它是分塊傳輸(chunked),這時候就要根據chunked編碼來判斷,chunked編碼的數據在最后有一個空chunked塊,表明本次傳輸數據結束。更細節的介紹可以看這篇文章

四、並發連接數的數量限制

在web開發中需要關注瀏覽器並發連接的數量,RFC文檔說,客戶端與服務器最多就連上兩通道,但服務器、個人客戶端要不要這么做就隨人意了,有些服務器就限制同時只能有1個TCP連接,導致客戶端的多線程下載(客戶端跟服務器連上多條TCP通道同時拉取數據)發揮不了威力,有些服務器則沒有限制。瀏覽器客戶端就比較規矩,知乎這里有分析,限制了同域名下能啟動若干個並發的TCP連接去下載資源。並發數量的限制也跟長連接有關聯,打開一個網頁,很多個資源的下載可能就只被放到了少數的幾條TCP連接里,這就是TCP通道復用(長連接)。如果並發連接數少,意味着網頁上所有資源下載完需要更長的時間(用戶感覺頁面打開卡了);並發數多了,服務器可能會產生更高的資源消耗峰值。瀏覽器只對同域名下的並發連接做了限制,也就意味着,web開發者可以把資源放到不同域名下,同時也把這些資源放到不同的機器上,這樣就完美解決了。

五、容易混淆的概念——TCP的keep alive和HTTP的Keep-alive

TCP的keep alive是檢查當前TCP連接是否活着;HTTP的Keep-alive是要讓一個TCP連接活久點。它們是不同層次的概念。

TCP keep alive的表現:

當一個連接“一段時間”沒有數據通訊時,一方會發出一個心跳包(Keep Alive包),如果對方有回包則表明當前連接有效,繼續監控。

這個“一段時間”可以設置。

WinHttp庫的設置:

WINHTTP_OPTION_WEB_SOCKET_KEEPALIVE_INTERVAL
Sets the interval, in milliseconds, to send a keep-alive packet over the connection. The default interval is 30000 (30 seconds). The minimum interval is 15000 (15 seconds). Using WinHttpSetOption to set a value lower than 15000 will return with ERROR_INVALID_PARAMETER.

libcurl的設置:

http://curl.haxx.se/libcurl/c/curl_easy_setopt.html

CURLOPT_TCP_KEEPALIVE

Pass a long. If set to 1, TCP keepalive probes will be sent. The delay and frequency of these probes can be controlled by the CURLOPT_TCP_KEEPIDLE and CURLOPT_TCP_KEEPINTVL options, provided the operating system supports them. Set to 0 (default behavior) to disable keepalive probes (Added in 7.25.0).

CURLOPT_TCP_KEEPIDLE

Pass a long. Sets the delay, in seconds, that the operating system will wait while the connection is idle before sending keepalive probes. Not all operating systems support this option. (Added in 7.25.0)

CURLOPT_TCP_KEEPINTVL

Pass a long. Sets the interval, in seconds, that the operating system will wait between sending keepalive probes. Not all operating systems support this option. (Added in 7.25.0)

CURLOPT_TCP_KEEPIDLE是空閑多久發送一個心跳包,CURLOPT_TCP_KEEPINTVL是心跳包間隔多久發一個。

打開網頁抓包,發送心跳包和關閉連接如下:

從上圖可以看到,大概過了44秒,客戶端發出了心跳包,服務器及時回應,本TCP連接繼續保持。到了空閑60秒的時候,服務器主動發起FIN包,斷開連接。

六、HTTP 流水線技術

使用了HTTP長連接(HTTP persistent connection )之后的好處,包括可以使用HTTP 流水線技術(HTTP pipelining,也有翻譯為管道化連接),它是指,在一個TCP連接內,多個HTTP請求可以並行,下一個HTTP請求在上一個HTTP請求的應答完成之前就發起。從wiki上了解到這個技術目前並沒有廣泛使用,使用這個技術必須要求客戶端和服務器端都能支持,目前有部分瀏覽器完全支持,而服務端的支持僅需要:按HTTP請求順序正確返回Response(也就是請求&響應采用FIFO模式),wiki里也特地指出,只要服務器能夠正確處理使用HTTP pipelinning的客戶端請求,那么服務器就算是支持了HTTP pipelining。

由於要求服務端返回響應數據的順序必須跟客戶端請求時的順序一致,這樣也就是要求FIFO,這容易導致Head-of-line blocking:第一個請求的響應發送影響到了后邊的請求,因為這個原因導致HTTP流水線技術對性能的提升並不明顯(wiki提到,這個問題會在HTTP2.0中解決)。另外,使用這個技術的還必須是冪等的HTTP方法,因為客戶端無法得知當前已經處理到什么地步,重試后可能發生不可預測的結果。POST方法不是冪等的:同樣的報文,第一次POST跟第二次POST在服務端的表現可能會不一樣。

在HTTP長連接的wiki中提到了HTTP1.1的流水線技術對RFC規定一個用戶最多兩個連接的指導意義:流水線技術實現好了,那么多連接並不能提升性能。我也覺得如此,並發已經在單個連接中實現了,多連接就沒啥必要,除非瓶頸在於單個連接上的資源限制迫使不得不多開連接搶資源。

目前瀏覽器並不太重視這個技術,畢竟性能提升有限。

七、學習資料

========================================================================

http://blog.jobbole.com/104108/

 

HTTP持久連接

https://zh.wikipedia.org/wiki/HTTP%E6%8C%81%E4%B9%85%E8%BF%9E%E6%8E%A5

長連接

http://baike.baidu.com/view/2831907.htm

 

1.背景

我們有個業務,會調用其他部門提供的一個基於http的服務,日調用量在千萬級別。使用了httpclient來完成業務。之前因為qps上不去,就看了一下業務代碼,並做了一些優化,記錄在這里。

先對比前后:優化之前,平均執行時間是250ms;優化之后,平均執行時間是80ms,降低了三分之二的消耗,容器不再動不動就報警線程耗盡了,清爽~

2.分析

項目的原實現比較粗略,就是每次請求時初始化一個httpclient,生成一個httpPost對象,執行,然后從返回結果取出entity,保存成一個字符串,最后顯式關閉response和client。我們一點點分析和優化:

2.1 httpclient反復創建開銷

httpclient是一個線程安全的類,沒有必要由每個線程在每次使用時創建,全局保留一個即可。

2.2 反復創建tcp連接的開銷

tcp的三次握手與四次揮手兩大裹腳布過程,對於高頻次的請求來說,消耗實在太大。試想如果每次請求我們需要花費5ms用於協商過程,那么對於qps為100的單系統,1秒鍾我們就要花500ms用於握手和揮手。又不是高級領導,我們程序員就不要搞這么大做派了,改成keep alive方式以實現連接復用!

2.3 重復緩存entity的開銷

原本的邏輯里,使用了如下代碼:

HttpEntity entity = httpResponse.getEntity();
String response = EntityUtils.toString(entity);

這里我們相當於額外復制了一份content到一個字符串里,而原本的httpResponse仍然保留了一份content,需要被consume掉,在高並發且content非常大的情況下,會消耗大量內存。

3.實現

按上面的分析,我們主要要做三件事:一是單例的client,二是緩存的保活連接,三是更好的處理返回結果。一就不說了,來說說二。

提到連接緩存,很容易聯想到數據庫連接池。httpclient4提供了一個PoolingHttpClientConnectionManager 作為連接池。接下來我們通過以下步驟來優化:

3.1 定義一個keep alive strategy

關於keep-alive,本文不展開說明,只提一點,是否使用keep-alive要根據業務情況來定,它並不是靈丹妙葯。還有一點,keep-alive和time_wait/close_wait之間也有不少故事。

在本業務場景里,我們相當於有少數固定客戶端,長時間極高頻次的訪問服務器,啟用keep-alive非常合適

再多提一嘴,http的keep-alive 和tcp的KEEPALIVE不是一個東西。回到正文,定義一個strategy如下:


ConnectionKeepAliveStrategy myStrategy = new ConnectionKeepAliveStrategy() {
    @Override
    public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
        HeaderElementIterator it = new BasicHeaderElementIterator
            (response.headerIterator(HTTP.CONN_KEEP_ALIVE));
        while (it.hasNext()) {
            HeaderElement he = it.nextElement();
            String param = he.getName();
            String value = he.getValue();
            if (value != null && param.equalsIgnoreCase
               ("timeout")) {
                return Long.parseLong(value) * 1000;
            }
        }
        return 60 * 1000;//如果沒有約定,則默認定義時長為60s
    }
};

3.2 配置一個PoolingHttpClientConnectionManager

PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
connectionManager.setMaxTotal(500);
connectionManager.setDefaultMaxPerRoute(50);//例如默認每路由最高50並發,具體依據業務來定

也可以針對每個路由設置並發數。

3.3 生成httpclient

httpClient = HttpClients.custom()
                .setConnectionManager(connectionManager)
                .setKeepAliveStrategy(kaStrategy)
                .setDefaultRequestConfig(RequestConfig.custom().setStaleConnectionCheckEnabled(true).build())
                .build();

注意:使用setStaleConnectionCheckEnabled方法來逐出已被關閉的鏈接不被推薦。更好的方式是手動啟用一個線程,定時運行closeExpiredConnections 和closeIdleConnections方法,如下所示。


public static class IdleConnectionMonitorThread extends Thread {
    
    private final HttpClientConnectionManager connMgr;
    private volatile boolean shutdown;
    
    public IdleConnectionMonitorThread(HttpClientConnectionManager connMgr) {
        super();
        this.connMgr = connMgr;
    }

    @Override
    public void run() {
        try {
            while (!shutdown) {
                synchronized (this) {
                    wait(5000);
                    // Close expired connections
                    connMgr.closeExpiredConnections();
                    // Optionally, close connections
                    // that have been idle longer than 30 sec
                    connMgr.closeIdleConnections(30, TimeUnit.SECONDS);
                }
            }
        } catch (InterruptedException ex) {
            // terminate
        }
    }
    
    public void shutdown() {
        shutdown = true;
        synchronized (this) {
            notifyAll();
        }
    }
    
}

3.4 使用httpclient執行method時降低開銷

這里要注意的是,不要關閉connection。

一種可行的獲取內容的方式類似於,把entity里的東西復制一份:

res = EntityUtils.toString(response.getEntity(),"UTF-8");
EntityUtils.consume(response1.getEntity());

但是,更推薦的方式是定義一個ResponseHandler,方便你我他,不再自己catch異常和關閉流。在此我們可以看一下相關的源碼:


public <T> T execute(final HttpHost target, final HttpRequest request,
            final ResponseHandler<? extends T> responseHandler, final HttpContext context)
            throws IOException, ClientProtocolException {
        Args.notNull(responseHandler, "Response handler");

        final HttpResponse response = execute(target, request, context);

        final T result;
        try {
            result = responseHandler.handleResponse(response);
        } catch (final Exception t) {
            final HttpEntity entity = response.getEntity();
            try {
                EntityUtils.consume(entity);
            } catch (final Exception t2) {
                // Log this exception. The original exception is more
                // important and will be thrown to the caller.
                this.log.warn("Error consuming content after an exception.", t2);
            }
            if (t instanceof RuntimeException) {
                throw (RuntimeException) t;
            }
            if (t instanceof IOException) {
                throw (IOException) t;
            }
            throw new UndeclaredThrowableException(t);
        }

        // Handling the response was successful. Ensure that the content has
        // been fully consumed.
        final HttpEntity entity = response.getEntity();
        EntityUtils.consume(entity);//看這里看這里
        return result;
    }

可以看到,如果我們使用resultHandler執行execute方法,會最終自動調用consume方法,而這個consume方法如下所示:


public static void consume(final HttpEntity entity) throws IOException {
        if (entity == null) {
            return;
        }
        if (entity.isStreaming()) {
            final InputStream instream = entity.getContent();
            if (instream != null) {
                instream.close();
            }
        }
    }

可以看到最終它關閉了輸入流。

4.其他

通過以上步驟,基本就完成了一個支持高並發的httpclient的寫法,下面是一些額外的配置和提醒:

4.1 httpclient的一些超時配置

CONNECTION_TIMEOUT是連接超時時間,SO_TIMEOUT是socket超時時間,這兩者是不同的。連接超時時間是發起請求前的等待時間;socket超時時間是等待數據的超時時間。


HttpParams params = new BasicHttpParams();
//設置連接超時時間
Integer CONNECTION_TIMEOUT = 2 * 1000; //設置請求超時2秒鍾 根據業務調整
Integer SO_TIMEOUT = 2 * 1000; //設置等待數據超時時間2秒鍾 根據業務調整

//定義了當從ClientConnectionManager中檢索ManagedClientConnection實例時使用的毫秒級的超時時間
//這個參數期望得到一個java.lang.Long類型的值。如果這個參數沒有被設置,默認等於CONNECTION_TIMEOUT,因此一定要設置。
Long CONN_MANAGER_TIMEOUT = 500L; //在httpclient4.2.3中我記得它被改成了一個對象導致直接用long會報錯,后來又改回來了
 
params.setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, CONNECTION_TIMEOUT);
params.setIntParameter(CoreConnectionPNames.SO_TIMEOUT, SO_TIMEOUT);
params.setLongParameter(ClientPNames.CONN_MANAGER_TIMEOUT, CONN_MANAGER_TIMEOUT);
//在提交請求之前 測試連接是否可用
params.setBooleanParameter(CoreConnectionPNames.STALE_CONNECTION_CHECK, true);
 
//另外設置http client的重試次數,默認是3次;當前是禁用掉(如果項目量不到,這個默認即可)
httpClient.setHttpRequestRetryHandler(new DefaultHttpRequestRetryHandler(0, false));

4.2 如果配置了nginx的話,nginx也要設置面向兩端的keep-alive

現在的業務里,沒有nginx的情況反而比較稀少。nginx默認和client端打開長連接而和server端使用短鏈接。注意client端的keepalive_timeout和keepalive_requests參數,以及upstream端的keepalive參數設置,這三個參數的意義在此也不再贅述。

以上是全部設置。通過這些設置,成功地將原本每次請求250ms的耗時降低到了80左右,效果顯著。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM