解Bug之路-Nginx 502 Bad Gateway(轉發)


原文:https://www.cnblogs.com/alchemystar/p/13409534.html

 

解Bug之路-Nginx 502 Bad Gateway

前言

事實證明,讀過Linux內核源碼確實有很大的好處,尤其在處理問題的時刻。當你看到報錯的那一瞬間,就能把現象/原因/以及解決方案一股腦的在腦中閃現。甚至一些邊邊角角的現象都能很快的反應過來是為何。筆者讀過一些Linux TCP協議棧的源碼,就在解決下面這個問題的時候有一種非常流暢的感覺。

Bug現場

首先,這個問題其實並不難解決,但是這個問題引發的現象倒是挺有意思。先描述一下現象吧,
筆者要對自研的dubbo協議隧道網關進行壓測(這個網關的設計也挺有意思,准備放到后面的博客里面)。先看下壓測的拓撲吧:

為了壓測筆者gateway的單機性能,兩端僅僅各保留一台網關,即gateway1和gateway2。壓到一定程度就開始報錯,導致壓測停止。很自然的就想到,網關扛不住了。

網關的情況

去Gateway2的機器上看了一下,沒有任何報錯。而Gateway1則有大量的502報錯。502是Bad Gateway,Nginx的經典報錯,首先想到的就是Gateway2不堪重負被Nginx在Upstream中踢掉。

那么,就先看看Gateway2的負載情況把,查了下監控,發現Gateway2在4核8G的機器上只用了一個核,完全看不出來有瓶頸的樣子,難道是IO有問題?看了下小的可憐的網卡流量打消了這個猜想。

Nginx所在機器CPU利用率接近100%

這時候,發現一個有意思的現象,Nginx確用滿了CPU!

再次壓測,去Nginx所在機器上top了一下,發現Nginx的4個Worker分別占了一個核把CPU吃滿-_-!

什么,號稱性能強悍的Nginx竟然這么弱,說好的事件驅動\epoll邊沿觸發\純C打造的呢?一定是用的姿勢不對!

去掉Nginx直接通信毫無壓力

既然猜測是Nginx的瓶頸,就把Nginx去掉吧。Gateway1和Gateway2直連,壓測TPS里面就飆升了,而且Gateway2的CPU最多也就吃了2個核,毫無壓力。

去Nginx上看下日志

由於Nginx機器權限並不在筆者手上,所以一開始沒有關注其日志,現在就聯系一下對應的運維去看一下吧。在accesslog里面發現了大量的502報錯,確實是Nginx的。又看了下錯誤日志,發現有大量的

Cannot assign requested address 

由於筆者讀過TCP源碼,一瞬間就反應過來,是端口號耗盡了!由於Nginx upstream和后端Backend默認是短連接,所以在大量請求流量進來的時候回產生大量TIME_WAIT的連接。

而這些TIME_WAIT是占據端口號的,而且基本要1分鍾左右才能被Kernel回收。

cat /proc/sys/net/ipv4/ip_local_port_range 32768 61000 

也就是說,只要一分鍾之內產生28232(61000-32768)個TIME_WAIT的socket就會造成端口號耗盡,也即470.5TPS(28232/60),只是一個很容易達到的壓測值。事實上這個限制是Client端的,Server端沒有這樣的限制,因為Server端口號只有一個8080這樣的有名端口號。而在
upstream中Nginx扮演的就是Client,而Gateway2就扮演的是Nginx

為什么Nginx的CPU是100%

而筆者也很快想明白了Nginx為什么吃滿了機器的CPU,問題就出來端口號的搜索過程。

讓我們看下最耗性能的一段函數:

int __inet_hash_connect(...) { // 注意,這邊是static變量 static u32 hint; // hint有助於不從0開始搜索,而是從下一個待分配的端口號搜索 u32 offset = hint + port_offset; ..... inet_get_local_port_range(&low, &high); // 這邊remaining就是61000 - 32768 remaining = (high - low) + 1 ...... for (i = 1; i <= remaining; i++) { port = low + (i + offset) % remaining; /* port是否占用check */ .... goto ok; } ....... ok: hint += i; ...... } 

看上面那段代碼,如果一直沒有端口號可用的話,則需要循環remaining次才能宣告端口號耗盡,也就是28232次。而如果按照正常的情況,因為有hint的存在,所以每次搜索從下一個待分配的端口號開始計算,以個位數的搜索就能找到端口號。如下圖所示:

所以當端口號耗盡后,Nginx的Worker進程就沉浸在上述for循環中不可自拔,把CPU吃滿。

為什么Gateway1調用Nginx沒有問題

很簡單,因為筆者在Gateway1調用Nginx的時候設置了Keepalived,所以采用的是長連接,就沒有這個端口號耗盡的限制。

Nginx 后面有多台機器的話

由於是因為端口號搜索導致CPU 100%,而且但凡有可用端口號,因為hint的原因,搜索次數可能就是1和28232的區別。

因為端口號限制是針對某個特定的遠端server:port的。
所以,只要Nginx的Backend有多台機器,甚至同一個機器上的多個不同端口號,只要不超過臨界點,Nginx就不會有任何壓力。

把端口號范圍調大

比較無腦的方案當然是把端口號范圍調大,這樣就能抗更多的TIME_WAIT。同時將tcp_max_tw_bucket調小,tcp_max_tw_bucket是kernel中最多存在的TIME_WAIT數量,只要port范圍 - tcp_max_tw_bucket大於一定的值,那么就始終有port端口可用,這樣就可以避免再次到調大臨界值得時候繼續擊穿臨界點。

cat /proc/sys/net/ipv4/ip_local_port_range 22768 61000 cat /proc/sys/net/ipv4/tcp_max_tw_buckets 20000 

開啟tcp_tw_reuse

這個問題Linux其實早就有了解決方案,那就是tcp_tw_reuse這個參數。

echo '1' > /proc/sys/net/ipv4/tcp_tw_reuse 

事實上TIME_WAIT過多的原因是其回收時間竟然需要1min,這個1min其實是TCP協議中規定的2MSL時間,而Linux中就固定為1min。

#define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT * state, about 60 seconds */ 

2MSL的原因就是排除網絡上還殘留的包對新的同樣的五元組的Socket產生影響,也就是說在2MSL(1min)之內重用這個五元組會有風險。為了解決這個問題,Linux就采取了一些列措施防止這樣的情況,使得在大部分情況下1s之內的TIME_WAIT就可以重用。下面這段代碼,就是檢測此TIME_WAIT是否重用。

__inet_hash_connect
	|->__inet_check_established
static int __inet_check_established(......) { ...... /* Check TIME-WAIT sockets first. */ sk_nulls_for_each(sk2, node, &head->twchain) { tw = inet_twsk(sk2); // 如果在time_wait中找到一個match的port,就判斷是否可重用 if (INET_TW_MATCH(sk2, net, hash, acookie, saddr, daddr, ports, dif)) { if (twsk_unique(sk, sk2, twp)) goto unique; else goto not_unique; } } ...... } 

而其中的核心函數就是twsk_unique,它的判斷邏輯如下:

int tcp_twsk_unique(......)
{
	......
	if (tcptw->tw_ts_recent_stamp && (twp == NULL || (sysctl_tcp_tw_reuse && get_seconds() - tcptw->tw_ts_recent_stamp > 1))) { // 對write_seq設置為snd_nxt+65536+2 // 這樣能夠確保在數據傳輸速率<=80Mbit/s的情況下不會被回繞 tp->write_seq = tcptw->tw_snd_nxt + 65535 + 2 ...... return 1; } return 0; } 

上面這段代碼邏輯如下所示:

在開啟了tcp_timestamp以及tcp_tw_reuse的情況下,在Connect搜索port時只要比之前用這個port的TIME_WAIT狀態的Socket記錄的最近時間戳>1s,就可以重用此port,即將之前的1分鍾縮短到1s。同時為了防止潛在的序列號沖突,直接將write_seq加上在65537,這樣,在單Socket傳輸速率小於80Mbit/s的情況下,不會造成序列號重疊(沖突)。
同時這個tw_ts_recent_stamp設置的時機如下圖所示:

所以如果Socket進入TIME_WAIT狀態后,如果一直有對應的包發過來,那么會影響此TIME_WAIT對應的port是否可用的時間。
開啟了這個參數之后,由於從1min縮短到1s,那么Nginx單台對單Upstream可承受的TPS就從原來的470.5TPS(28232/60)一躍提升為28232TPS,增長了60倍。
如果還嫌性能不夠,可以配上上面的端口號范圍調大以及tcp_max_tw_bucket調小繼續提升tps,不過tcp_max_tw_bucket調小可能會有序列號重疊的風險,畢竟Socket不經過2MSL階段就被重用了。

不要開啟tcp_tw_recycle

開啟tcp_tw_recyle這個參數會在NAT環境下造成很大的影響,建議不開啟。

Nginx upstream改成長連接

事實上,上面的一系列問題都是由於Nginx對Backend是短連接導致。
Nginx從 1.1.4 開始,實現了對后端機器的長連接支持功能。在Upstream中這樣配置可以開啟長連接的功能:

upstream backend { server 127.0.0.1:8080; # It should be particularly noted that the keepalive directive does not limit the total number of connections to upstream servers that an nginx worker process can open. The connections parameter should be set to a number small enough to let upstream servers process new incoming connections as well. keepalive 32; keepalive_timeout 30s; # 設置后端連接的最大idle時間為30s } 

這樣前端和后端都是長連接,大家又可以愉快的玩耍了。

由此產生的風險點

由於對單個遠端ip:port耗盡會導致CPU吃滿這種現象。所以在Nginx在配置Upstream時候需要格外小心。假設一種情況,PE擴容了一台Nginx,為防止有問題,就先配一台Backend看看情況,這時候如果量比較大的話擊穿臨界點就會造成大量報錯(而應用本身確毫無壓力,畢竟臨界值是470.5TPS(28232/60)),甚至在同Nginx上的非此域名的請求也會因為CPU被耗盡而得不到響應。多配幾台Backend/開啟tcp_tw_reuse或許是不錯的選擇。

總結

應用再強大也還是承載在內核之上,始終逃不出Linux內核的樊籠。所以對於Linux內核本身參數的調優還是非常有意義的。如果讀過一些內核源碼,無疑對我們排查線上問題有着很大的助力,同時也能指導我們避過一些坑!

 


免責聲明!

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



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