問題排查:nginx的反向代理感覺失效了一樣

背景

最近,負責基礎設施的同事,要對一批測試環境機器進行回收,回收就涉及到應用遷移,問題是整個過程一團亂。比如服務器A上一堆應用要調用服務器B上一堆服務,結果服務器B被回收了,然後服務器A上一堆應用報錯。

今天就是負責查一個問題,app上一個頭像上傳的接口,之前都好好的,不知道怎麼就不能訪問了,報錯現象是在請求後等待n秒超時,然後服務端報錯502。

這個服務也不知道誰維護的,可能維護的人早已離職了也說不定,這也是這邊的常態吧,人走了,負責的服務還在服務器上跑,也沒有交接文檔。

問題現象

鏈路梳理

先上個圖,再解釋整個鏈路:

image-20230824200723942

現象就是,app端調用外網ip(記作A): xxxx端口的某個接口,超時後報502錯誤,因爲是http協議,能從響應中看出來是Apache。

然後,就是去找網絡同事,問外網ip:xxx端口對應的內網ip和端口,得到了內網ip(記作B):80端口。接下來,又是找負責服務器的同事,要服務器B的密碼,一開始以爲是linux機器,沒想到還是windows的。vnc登錄進去後,根據端口號找到對應的進程,發現是Apache HTTP Server,這個東西我也不熟悉,知道它類似於nginx,功能類似,但是幾乎一直沒用過,所幸,在程序的根目錄下,找到了一個配置文件,配置文件中配置了反向代理,將請求反向代理到了服務器C:8088端口。

這個服務器C,基本就是今天的主角了。

於是,又去找同事要服務器C的密碼,這次還好,是個linux機器,查詢8088端口對應的服務,是個nginx進程,然後查看該進程的配置文件,發現請求被反向代理到了本機的9901端口。

問題現象

梳理完整個鏈路後,我決定去看看最後的java服務的日誌,因爲是第一次看到這個服務,也不知道日誌文件在哪裏。cd到/proc/服務pid/fd目錄下,看到了其打開的文件,裏面有個日誌文件,但是,打開日誌文件,發現裏面空空如也。

我又去調了調日誌級別,然後app發起請求,發現還是沒啥日誌。

然後開始懷疑請求沒到服務這裏,行吧,那還是跟着鏈路排查下,看看怎麼回事。

於是在linux機器上開啓java服務的9901端口的抓包,然後重試,發現還是空空如也,什麼包都沒有。

tcpdump -i any tcp port 9901 -Ann

這就奇怪了,沒到java服務,那到了nginx沒有呢?然後開始抓nginx這塊:

tcpdump -i any tcp port 8808 -Ann

這次發現包還挺多的,於是根據接口名(url包括Upload關鍵字)加了個過濾條件:

tcpdump -i any tcp port 8808 -Ann |grep Upload

這次發現,能抓到包。這,意思是,看起來nginx是收到包了,但是,沒往java服務發啊。這倒是奇了怪了,看起來,反正是nginx的問題,於是,去看nginx的access日誌和error日誌,發現access日誌裏並沒有該接口的記錄,error日誌裏也啥都沒有。

於是我調整了nginx error日誌級別爲info,如下(從上而下,越來越詳細):

alert - 系統級別緊急信息
critical - 關鍵錯誤信息
error - 一般性錯誤信息
warn - 警告信息
notice - 一些特殊信息
info - 一般信息
debug - 調試信息

error_log /var/log/nginx/error.log info;

結果,發現error日誌還是啥都沒有。

然後,我想着是不是我配置文件沒看對,我以爲會走某個location,該不會沒匹配上,走到別的location了,然後轉發到其他後端去了?

後面仔細觀察了請求接口的url,感覺還是沒問題。

當時,基於兩個原因,決定採用strace去看看nginx的系統調用:

  • 看看是不是我把location看錯了,nginx把請求發到其他機器去了,所以在9901的java服務纔看不到日誌
  • 看看是不是nginx內部報啥錯了,error日誌沒體現出來

然後找到nginx的pid後,使用如下命令查看網絡調用:

strace -p nginx-pid -q -f  -s 10000 -e trace=network
命令我也是查了自己當年的文章,不然誰記得住:
https://www.cnblogs.com/grey-wolf/articles/13139308.html

結果,發現系統感覺有問題,執行命令後,啥結果都沒有。

換了nginx的worker進程的pid,還是沒效果。後邊再換了個pid,直接卡死了,ctrl c也沒用。

我他麼就是感覺這機器有點怪,之前執行lsof也卡住不動,現在strace又這樣,真的服了。

一看時間,到午飯時間了,喫飯吧。

來了一點靈感

喫完飯,我又去把之前抓的windows apache和nginx之間的網絡包打開分析了一會。

包的前面幾個報文如下:

image-20230824205015484

前三個報文是三次握手,8088是我們的nginx服務端。因爲我的包就是服務端抓的,看起來,一切正常,服務端是正常完成了三次握手了。

包4,客戶端發了個報文過來,包長1516字節,這個包,其實也就是包含了http請求(見下圖);理論上,下一個包應該是我們回覆ack,表示包4收到了。

image-20230824205721980

但是,下面的包5、包6,看起來是客戶端發生了重傳,爲啥要重傳呢?不知道,接着看下面。

image-20230824205407186

看我上圖標紅的下面那一行,是我們服務端nginx往客戶端發的,68個字節,也有個重傳字樣,看起來,意思是我們也發生了重傳,重傳了哪個包呢,就是包2,也就是握手時候的我方回覆的syn+ack那個包。

再加個過濾,看看我方到底給對方發了些啥包:

image-20230824205620749

結果,我方貌似一直在給對方重傳第二次握手的消息。

我想了半天,終於想差不多了,看來是客戶端的第三次握手的ack,被我們忽略了,所以,我們這邊,連接一直不是established狀態,而是syn received狀態。而客戶端呢,發完第三次的ack後,就進入了established狀態,所以就開始發http請求過來了,我方由於狀態不是established,所以一直給對方重發syn + ack。

Tcp_state_diagram

爲啥會忽略第三次的ack呢,我突然想起來,如果接收了ack,連接就會正式建立,連接就會放入accept隊列(全連接隊列),等待應用去accept了。現在反過來想,既然沒往accept隊列放,會不是隊列滿了,所以乾脆就不添堵了,所以不放了,直接丟棄ack呢?

然後我開始搜索全連接隊列滿相關的文章,看了幾篇,基本感覺有戲。

解決問題

午休結束後,去到測試機(沒法在本地直接ssh)上根據文章查驗。
參考文章:
https://blog.51cto.com/u_15181572/6172585
https://blog.csdn.net/Octopus21/article/details/132124481

其實全連接隊列這個,幾年前學習過這個,但是久了沒碰到這個場景,早已淡忘,這次還真遇上了。
每一個listen狀態的socket,都有個全連接隊列,隊列大小受到兩個參數控制,一個是linux的內核參數net.core.somaxconn,可通過sysctl -a |grep somaxconn查看,我看了我們機器,值爲128;另一個參數是應用執行listen時,可以指定一個叫做backlog的int類型參數,nginx中默認爲512.
全連接隊列大小呢,就是取min(net.core.somaxconn, 應用listen時的backlog值),我這裏,兩者取小,就是128.

這個值怎麼查看呢,可以通過:

[root@168assi logs]# ss -lnt |egrep "State|8088"
State      Recv-Q Send-Q Local Address:Port               Peer Address:Port              
LISTEN     129    128          *:8088     

這裏可以看到Send-Q的值,就表示隊列的最大值爲128. 而Recv-Q呢,就是當前全連接隊列的長度,129,可以看到,已經大於128了,說明隊列滿了。

這裏的Recv-Q和Send-Q的值,僅當socket處於listen時表示該意思,非listen時,表示其他意思。這裏給個官方解釋:

Recv-Q
Established: The count of bytes not copied by the user program connected to this socket.

Listening: Since Kernel 2.6.18  this  column
contains the current syn backlog.

Send-Q
Established:  The count of bytes not acknowledged by the remote host.  

Listening: Since Kernel 2.6.18 this column contains the maximum
size of the syn backlog.

另外,再根據文章提到的命令:

netstat -s | grep overflow

果然看到數字一直在增長,見下面網圖:

image-20230824211750204

基本認定這個問題後,就是修改了,我是直接將內核參數改成了65535:

[root@168assi 12556]# vim /etc/sysctl.conf
net.core.somaxconn = 65535
然後如下命令生效:
sysctl -p

接下來,重啓nginx,查看隊列長度,已經是511了(nginx 默認的listen的backlog值):

[root@168assi 12556]# ss -lnt|grep 8088
LISTEN     0      511          *:8088                     *:*

另外,補充一點,再遇到該隊列滿時,我們的linxu機器是直接忽略了ack,也可以配置如下參數(值爲1,默認爲0,表示忽略報文),讓其給客戶端回覆rst報文:

[root@168assi logs]# sysctl -a |grep tcp_abort_on_overflow
net.ipv4.tcp_abort_on_overflow = 0

官方解釋如下(man tcp,如提示沒安裝,yum install man-pages):

tcp_abort_on_overflow (Boolean; default: disabled; since Linux 2.4)

Enable resetting connections if the listening service is too slow and unable to keep up and accept  them.   It  means  that  if overflow occurred due to a burst, the connection will recover.  Enable this option only if you are really sure that the listening daemon cannot be tuned to accept connections faster.  Enabling this option can harm the clients of your server.

改完再測試,抓包查看,報文很清爽,再沒有一堆重傳了:

image-20230824212737517

補充

如需查看nginx在location衆多時,到底發給了哪個後端upstream,不用像我上面那樣用strace,太複雜了,我查了下,可以這樣:

http://nginx.org/en/docs/http/ngx_http_log_module.html

官方文檔的access_log中,默認包含了一個日誌format爲combined,內容:

The configuration always includes the predefined “combined” format:

log_format combined '$remote_addr - $remote_user [$time_local] '
                    '"$request" $status $body_bytes_sent '
                    '"$http_referer" "$http_user_agent"';

我們可以增加一個屬性$upstream_addr,即可展示轉發到哪個upstream了:

http://nginx.org/en/docs/http/ngx_http_upstream_module.html

log_format combined1 '$remote_addr - $remote_user [$time_local] '
                '"$request" $status $body_bytes_sent '
                '"$http_referer" "$http_user_agent" $upstream_addr' ;
access_log  logs/access.log  combined1;

效果如下:

image-20230824213137598

參考文檔

https://mp.weixin.qq.com/s/2qN0ulyBtO2I67NB_RnJbg

http://04007.cn/article/323.html nginx配置listen的backlog

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章