問題
之前工作中遇到一個KeepAlive的問題,現(xiàn)在把它記錄下來,場景是這樣的:
從上圖可以看出,用戶通過Client訪問的是LVS的VIP, VIP后端掛載的RealServer是Nginx服務器。 Client可以是瀏覽器也可以是一個客戶端程序。一般情況下, 這種架構(gòu)不會出現(xiàn)問題,但是如果Client端把請求發(fā)送給Nginx,Nginx的后端需要一段時間才能返回結(jié)果,超過1分30秒就會有問題,使用LVS作為負載均衡設(shè)備看到的現(xiàn)象就是1分30秒之后, Client和Nginx鏈接被斷開,沒有數(shù)據(jù)返回。
分析
原因是LVS默認保持TCP的Session為90s,超過90s沒有TCP報文在鏈接上傳輸,LVS就會給兩端發(fā)送RESET報文斷開鏈接。LVS這么做的原因相信大家都知道一二,我所知道的原因主要有兩點:
1.節(jié)省負載均衡設(shè)備資源,每一個TCP/UDP的鏈接都會在負載均衡設(shè)備上創(chuàng)建一個Session的結(jié)構(gòu), 鏈接如果一直不斷開,這種Session結(jié)構(gòu)信息最終會消耗掉所有的資源,所以必須釋放掉。
2.另外釋放掉能保護后端的資源,如果攻擊者通過空鏈接,鏈接到Nginx上,如果Nginx沒有做合適 的保護,Nginx會因為鏈接數(shù)過多而無法提供服務。
這種問題不只是在LVS上有,之前在商用負載均衡設(shè)備F5上遇到過同樣的問題,F(xiàn)5的Session斷開方式和LVS有點區(qū)別,F(xiàn)5不會主動發(fā)送RESET給鏈接的兩端,Session消失之后,當鏈接中一方再次發(fā)送報文時會接收到F5的RESET,
之后的現(xiàn)象是再次發(fā)送報文的一端TCP鏈接狀態(tài)已經(jīng)斷開,而另外一端卻還是ESTABLISH狀態(tài)。
知道是負載均衡設(shè)備原因之后,第一反應就是通過開啟KeepAlive來解決。到此這個問題應該是結(jié)束了,但是我發(fā)現(xiàn)過一段時間總又有人提起KeepAlive的問題,甚至發(fā)現(xiàn)由于KeepAlive的理解不正確浪費了很多資源,原本能使用LVS的應用放在了公網(wǎng)下沉區(qū),或者換成了商用F5設(shè)備(F5設(shè)備的Session斷開時間要長一點,默認應該是5分鐘)。所以我決定把我知道的KeepAlive知識點寫篇博客分享出來。
為什么要有KeepAlive?
在談KeepAlive之前,我們先來了解下簡單TCP知識(知識很簡單,高手直接忽略)。首先要明確的是在TCP層是沒有“請求”一說的,經(jīng)常聽到在TCP層發(fā)送一個請求,這種說法是錯誤的。TCP是一種通信的方式,“請求”一詞是事務上的概念,HTTP協(xié)議是一種事務協(xié)議,如果說發(fā)送一個HTTP請求,這種說法就沒有問題。也經(jīng)常聽到面試官反饋有些面試運維的同學,基本的TCP三次握手的概念不清楚, 面試官問TCP是如何建立鏈接,面試者上來就說,假如我是客戶端我發(fā)送一個請求給服務端,服務端發(fā)送一個請求給我。。。這種一聽就知道對TCP基本概念不清楚。下面是我通過wireshark抓取的一個TCP建立握手的過程。(命令行基本上用TCPdump,后面我們還會用這張圖說明問題):
現(xiàn)在我看只要看前3行,這就是TCP三次握手的完整建立過程,第一個報文SYN從發(fā)起方發(fā)出,第二個報文SYN,ACK是從被連接方發(fā)出,第三個報文ACK確認對方的SYN,ACK已經(jīng)收到,如下圖:
但是數(shù)據(jù)實際上并沒有傳輸,請求是有數(shù)據(jù)的,第四個報文才是數(shù)據(jù)傳輸開始的過程,細心的讀者應該能夠發(fā)現(xiàn)wireshark把第四個報文解析成HTTP協(xié)議,HTTP協(xié)議的GET方法和URI也解析出來,所以說TCP層是沒有請求的概念,HTTP協(xié)議是事務性協(xié)議才有請求的概念,TCP報文承載HTTP協(xié)議的請求(Request)和響應(Response)。
現(xiàn)在才是開始說明為什么要有KeepAlive。 鏈接建立之后,如果應用程序或者上層協(xié)議一直不發(fā)送數(shù)據(jù),或者隔很長時間才發(fā)送一次數(shù)據(jù),當鏈接很久沒有數(shù)據(jù)報文傳輸時如何去確定對方還在線,到底是掉線了還是確實沒有數(shù)據(jù)傳輸,鏈接還需不需要保持,這種情況在TCP協(xié)議設(shè)計中是需要考慮到的。TCP協(xié)議通過一種巧妙的方式去解決這個問題,當超過一段時間之后,TCP自動發(fā)送一個數(shù)據(jù)為空的報文給對方,如果對方回應了這個報文,說明對方還在線,鏈接可以繼續(xù)保持,如果對方?jīng)]有報文返回,并且重試了多次之后則認為鏈接丟失,沒有必要保持鏈接。
如何開啟KeepAlive
KeepAlive并不是默認開啟的,在Linux系統(tǒng)上沒有一個全局的選項去開啟TCP的KeepAlive。需要開啟KeepAlive的應用必須在TCP的socket中單獨開啟。Linux Kernel有三個選項影響到KeepAlive的行為:
1.net.ipv4.tcp_keepalive_intvl = 75
2.net.ipv4.tcp_keepalive_probes = 9
3.net.ipv4.tcp_keepalive_time = 7200
tcp_keepalive_time的單位是秒,表示TCP鏈接在多少秒之后沒有數(shù)據(jù)報文傳輸啟動探測報文; tcp_keepalive_intvl單位是也秒,表示前一個探測報文和后一個探測報文之間的時間間隔,tcp_keepalive_probes表示探測的次數(shù)。
TCP socket也有三個選項和內(nèi)核對應,通過setsockopt系統(tǒng)調(diào)用針對單獨的socket進行設(shè)置:
TCP_KEEPCNT: 覆蓋 tcp_keepalive_probes
TCP_KEEPIDLE: 覆蓋 tcp_keepalive_time
TCP_KEEPINTVL: 覆蓋 tcp_keepalive_intvl
舉個例子,以我的系統(tǒng)默認設(shè)置為例,kernel默認設(shè)置的tcp_keepalive_time是7200s, 如果我在應用程序中針對socket開啟了KeepAlive,然后設(shè)置的TCP_KEEPIDLE為60,那么TCP協(xié)議棧在發(fā)現(xiàn)TCP鏈接空閑了60s沒有數(shù)據(jù)傳輸?shù)臅r候就會發(fā)送第一個探測報文。
TCP KeepAlive和HTTP的Keep-Alive是一樣的嗎?
估計很多人乍看下這個問題才發(fā)現(xiàn)其實經(jīng)常說的KeepAlive不是這么回事,實際上在沒有特指是TCP還是HTTP層的KeepAlive,不能混為一談。TCP的KeepAlive和HTTP的Keep-Alive是完全不同的概念。TCP層的KeepAlive上面已經(jīng)解釋過了。 HTTP層的Keep-Alive是什么概念呢? 在講述TCP鏈接建立的時候,我畫了一張三次握手的示意圖,TCP在建立鏈接之后, HTTP協(xié)議使用TCP傳輸HTTP協(xié)議的請求(Request)和響應(Response)數(shù)據(jù),一次完整的HTTP事務如下圖:
各位看官請注意,這張圖我簡化了HTTP(Req)和HTTP(Resp),實際上的請求和響應需要多個TCP報文。從圖中可以發(fā)現(xiàn)一個完整的HTTP事務,有鏈接的建立, 請求的發(fā)送,響應接收,斷開鏈接這四個過程,早期通過HTTP協(xié)議傳輸?shù)臄?shù)據(jù)以文本為主,一個請求可能就把所有要返回的數(shù)據(jù)取到,但是,現(xiàn)在要展現(xiàn)一張完整的頁面需要很多個請求才能完成,如圖片,JS,CSS等,如果每一個HTTP請求都需要新建并斷開一個TCP,這個開銷是完全沒有必要的,開啟HTTP Keep-Alive之后,能復用已有的TCP鏈接,當前一個請求已經(jīng)響應完畢,服務器端沒有立即關(guān)閉TCP鏈接,而是等待一段時間接收瀏覽器端可能發(fā)送過來的第二個請求,通常瀏覽器在第一個請求返回之后會立即發(fā)送第二個請求,如果某一時刻只能有一個鏈接,同一個TCP鏈接處理的請求越多,開啟KeepAlive能節(jié)省的TCP建立和關(guān)閉的消耗就越多。當然通常會啟用多個鏈接去從服務器器上請求資源,但是開啟了Keep-Alive之后,仍然能加快資源的加載速度。HTTP/1.1之后默認開啟Keep-Alive, 在HTTP的頭域中增加Connection選項。當設(shè)置為Connection:keep-alive表示開啟,設(shè)置為Connection:close表示關(guān)閉。實際上HTTP的KeepAlive寫法是Keep-Alive,跟TCP的KeepAlive寫法上也有不同。 所以TCP KeepAlive和HTTP的Keep-Alive不是同一回事情。
Nginx的TCP KeepAlive如何設(shè)置
開篇提到我最近遇到的問題,Client發(fā)送一個請求到Nginx服務端,服務端需要經(jīng)過一段時間的計算才會返回, 時間超過了LVS Session保持的90s,在服務端使用Tcpdump抓包,本地通過wireshark分析顯示的結(jié)果如第二副圖所示,第5條報文和最后一條報文之間的時間戳大概差了90s。在確定是LVS的Session保持時間到期的問題之后,我開始在尋找Nginx的TCP KeepAlive如何設(shè)置,最先找到的選項是keepalive_timeout,從同事那里得知keepalive_timeout的用法是當keepalive_timeout的值為0時表示關(guān)閉keepalive,當keepalive_timeout的值為一個正整數(shù)值時表示鏈接保持多少秒,于是把keepalive_timeout設(shè)置成75s,但是實際的測試結(jié)果表明并不生效。顯然keepalive_timeout不能解決TCP層面的KeepAlive問題,實際上Nginx涉及到keepalive的選項還不少,Nginx通常的使用方式如下:
從TCP層面Nginx不僅要和Client關(guān)心KeepAlive,而且還要和Upstream關(guān)心KeepAlive, 同時從HTTP協(xié)議層面,Nginx需要和Client關(guān)心Keep-Alive,如果Upstream使用的HTTP協(xié)議,還要關(guān)心和Upstream的Keep-Alive,總而言之,還比較復雜。所以搞清楚TCP層的KeepAlive和HTTP的Keep-Alive之后,就不會對于Nginx的KeepAlive設(shè)置錯。我當時解決這個問題時候不確定Nginx有配置TCP keepAlive的選項,于是我打開Ngnix的源代碼,在源代碼里面搜索TCP_KEEPIDLE,相關(guān)的代碼如下:
519 #if (NGX_HAVE_KEEPALIVE_TUNABLE)
520
521 if (ls[i].keepidle) {
522 if (setsockopt(ls[i].fd, IPPROTO_TCP, TCP_KEEPIDLE,
523 (const void *) &ls[i].keepidle, sizeof(int))
524 == -1)
525 {
526 ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_socket_errno,
527 "setsockopt(TCP_KEEPIDLE, %d) %V failed, ignored",
528 ls[i].keepidle, &ls[i].addr_text);
529 }
530 }
從代碼的上下文我發(fā)現(xiàn)TCP KeepAlive可以配置,所以我接著查找通過哪個選項配置,最后發(fā)現(xiàn)listen指令的so_keepalive選項能對TCP socket進行KeepAlive的配置。
so_keepalive=on|off|[keepidle]:[keepintvl]:[keepcnt]
// on表示開啟
// off表示關(guān)閉
有些系統(tǒng)提供跟精確的控制,比如linux:
keepidle表示等待時間,keepintvl表示探測報的發(fā)送間隔,keepcnt表示探測報文發(fā)送的次數(shù)。
以上三個參數(shù)只能使用一個,不能同時使用, 比如so_keepalive=on, so_keepalive=off或者so_keepalive=30s::(表示等待30s沒有數(shù)據(jù)報文發(fā)送探測報文)。通過設(shè)置listen 80,so_keepalive=60s::之后成功解決Nginx在LVS保持長鏈接的問題,避免了使用其他高成本的方案。在商用負載設(shè)備上如果遇到類似的問題同樣也可以通過這種方式解決。
Apache中KeepAlive和KeepAliveTimeOut
調(diào)優(yōu)apache參數(shù)一般都是根據(jù)場景去設(shè)置,這里分享下KeepAlive和KeepAliveTimeOut的關(guān)系與區(qū)別。
在Apache的httpd.conf中,KeepAlive指的是保持連接活躍,類似于數(shù)據(jù)庫中的永久連接。若將KeepAlive設(shè)置為On,那么來自同一客戶端的請求就不需要再一次連接,避免每次請求都要新建一個連接而加重服務器的負擔。
KeepAlive的連接活躍時間當然是受KeepAliveTimeOut限制的。如果第二次請求和第一次請求之間超過KeepAliveTimeOut的時間的話,第一次連接就會中斷,再新建第二個連接。所以,一般情況下,圖片較多的網(wǎng)站應該把KeepAlive設(shè)為On。但是KeepAliveTimeOut應該設(shè)置為多少秒就是一個值得討論的問題了。這里如果KeepAliveTimeOut設(shè)置的時間過短,比如設(shè)置=1秒,那么Apache就會頻繁的建立New Link,就會耗費不少的資源;反過來,如果KeepAliveTimeOut設(shè)置的時間過長,比如設(shè)置超過200秒,那么APACHE中肯定有很多無用的連接會占用服務器的資源,也不是一件好事。所以,到底要把KeepAliveTimeOut設(shè)置為多少,要看網(wǎng)站的流量、服務器的配置而定。
其實,這和數(shù)據(jù)庫比如mysql中的連接機制有點類似,KeepAlive相當于mysql_connectmysql_pconnect,KeepAliveTimeOut相當于wait_timeout。文章來源:http://www.zghlxwxcb.cn/news/detail-428351.html
參考資料
-《TCP/IP協(xié)議詳解VOL1》–網(wǎng)絡(luò)基礎(chǔ)知識詳盡介紹文章來源地址http://www.zghlxwxcb.cn/news/detail-428351.html
- http://tldp.org/HOWTO/html_single/TCP-Keepalive-HOWTO/#overview
- http://nginx.org/en/docs/http/ngx_http_core_module.html
- Nginx Source code: https://github.com/alibaba/tengine
到了這里,關(guān)于聊一聊nginx中KeepAlive的設(shè)置的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!