剛工作那會(huì),有一次,上游調(diào)用我服務(wù)的老哥說(shuō),你的服務(wù)報(bào)"502錯(cuò)誤了,快去看看是為什么吧"。
當(dāng)時(shí)那個(gè)服務(wù)里正好有個(gè)調(diào)用日志,平時(shí)會(huì)記錄各種200,4xx狀態(tài)碼的信息。于是我跑到服務(wù)日志里去搜索了一下502這個(gè)數(shù)字,毫無(wú)發(fā)現(xiàn)。于是跟老哥說(shuō),"服務(wù)日志里并沒(méi)有502的記錄,你是不是搞錯(cuò)啦?"
現(xiàn)在想來(lái),多少有些不好意思。
不知道有多少老哥是跟當(dāng)時(shí)的我是一樣的,這篇文章,就來(lái)聊聊502錯(cuò)誤是什么?
我們從狀態(tài)碼是什么開(kāi)始聊起。
HTTP狀態(tài)碼
我們平時(shí)在瀏覽器里逛的某寶和某度,其實(shí)都是一個(gè)個(gè)前端網(wǎng)頁(yè)。
一般來(lái)說(shuō),前端并不存儲(chǔ)太多數(shù)據(jù),大部分時(shí)候都需要從后端服務(wù)器那獲取數(shù)據(jù)。
于是前后端之間需要通過(guò)TCP協(xié)議去建立連接,然后在TCP的基礎(chǔ)上傳輸數(shù)據(jù)。
而TCP是基于數(shù)據(jù)流的協(xié)議,傳輸數(shù)據(jù)時(shí),并不會(huì)為每個(gè)消息加入數(shù)據(jù)邊界,直接使用裸的TCP進(jìn)行數(shù)據(jù)傳輸會(huì)有"粘包"問(wèn)題。
因此需要用特地的協(xié)議格式去對(duì)數(shù)據(jù)進(jìn)行解析。于是在此基礎(chǔ)上設(shè)計(jì)了HTTP協(xié)議。詳細(xì)的內(nèi)容可以看我之前寫的《既然有HTTP協(xié)議,為什么還要有RPC》。
比如,我想要看某個(gè)商品的具體信息,其實(shí)就是前端發(fā)的HTTP請(qǐng)求中傳入商品的id,后端返回的HTTP響應(yīng)中返回商品的價(jià)格,商店名,發(fā)貨地址的信息等。
通過(guò)id獲取商品詳情
這樣,表面上,我們是在刷著各種網(wǎng)頁(yè),實(shí)際上背后正有多次HTTP消息在不斷進(jìn)行收發(fā)。
用戶在網(wǎng)上瀏覽商品
但問(wèn)題就來(lái)了,上面提到的都是正常情況,如果有異常情況呢,比如前端發(fā)的數(shù)據(jù),根本就不是個(gè)商品id,而是一張圖片,這對(duì)于后端服務(wù)端來(lái)說(shuō)是不可能給出正常響應(yīng)的,于是就需要設(shè)計(jì)一套HTTP狀態(tài)碼,用來(lái)標(biāo)識(shí)這次HTTP請(qǐng)求響應(yīng)流程是否正常。通過(guò)這個(gè)可以影響瀏覽器的行為。
比方說(shuō)一切正常,那服務(wù)端返回個(gè)200狀態(tài)碼,前端收到后,可以放心使用響應(yīng)的數(shù)據(jù)。但如果服務(wù)端發(fā)現(xiàn)客戶端發(fā)的東西異常,就響應(yīng)個(gè)4xx狀態(tài)碼,意思是這是個(gè)客戶端的錯(cuò)誤,4xx里頭的xx可以根據(jù)錯(cuò)誤的類型,再細(xì)分成各種碼,比如401是客戶端沒(méi)權(quán)限,404是客戶端請(qǐng)求了一個(gè)根本不存在的網(wǎng)頁(yè)。反過(guò)來(lái),如果是服務(wù)器有問(wèn)題,就返回5xx狀態(tài)碼。
4xx和5xx的區(qū)別
但問(wèn)題就來(lái)了。
服務(wù)端都有問(wèn)題了,搞嚴(yán)重點(diǎn),服務(wù)器可能直接就崩潰了,那它還怎么給你返回狀態(tài)碼?
是的,這種情況,服務(wù)端是不可能給客戶端返回狀態(tài)碼的。所以說(shuō),一般情況下5xx的狀態(tài)碼其實(shí)并不是服務(wù)器返回給客戶端的。
它們是由網(wǎng)關(guān)返回的,常見(jiàn)的網(wǎng)關(guān),比如nginx。
nginx的作用
回到前后端交互數(shù)據(jù)的話題上,如果前端用戶少,那后端處理起請(qǐng)求來(lái),游刃有余。但隨著用戶越來(lái)越多,后端服務(wù)器受資源限制,cpu或者內(nèi)存都可能會(huì)嚴(yán)重不足,這時(shí)候解決方案也很簡(jiǎn)單,多搞幾臺(tái)一樣的服務(wù)器,這樣就能將這些前端請(qǐng)求均攤給幾個(gè)服務(wù)器,從而提升處理能力。
但要實(shí)現(xiàn)這樣的效果,前端就得知道后端具體有哪些個(gè)服務(wù)器,并一一跟他們建立TCP連接。
前端與多個(gè)服務(wù)器之間建立連接
也不是不行,但就是麻煩。
但這時(shí)候如果能有個(gè)中間層擋在它們中間就好了,這樣客戶端只需要跟中間層連接,中間層再和服務(wù)器建立連接。
于是,這個(gè)中間層就成了這幫服務(wù)器的一個(gè)代理人一樣,客戶端有啥事都找代理人,只管發(fā)出自己的請(qǐng)求,再由代理人去找某個(gè)服務(wù)器去完成響應(yīng)。整個(gè)過(guò)程下來(lái),客戶端只知道自己的請(qǐng)求被代理人幫忙搞定了,但代理人具體找了那個(gè)服務(wù)器去完成,客戶端并不知道,也不需要知道。
像這種,屏蔽掉具體有哪些服務(wù)器的代理方式就是所謂的反向代理。
反向代理
反過(guò)來(lái),屏蔽掉具體有哪些客戶端的代理方式,就是所謂的正向代理。
而這個(gè)中間層的角色,一般由nginx這類網(wǎng)關(guān)來(lái)充當(dāng)。
另外,由于背后的服務(wù)器可能性能配置各不相同,有些4核8G,有些2核4G,nginx能為它們加上不同的訪問(wèn)權(quán)重,權(quán)重高的多轉(zhuǎn)發(fā)點(diǎn)請(qǐng)求,通過(guò)這個(gè)方式實(shí)現(xiàn)不同的負(fù)載均衡策略。
nginx返回5xx狀態(tài)碼
有了nginx這一中間層后,客戶端從直連服務(wù)端,變成客戶端直連nginx,再由nginx直連服務(wù)端。從一個(gè)TCP連接變成兩個(gè)TCP連接。
于是,當(dāng)服務(wù)器發(fā)生異常時(shí),nginx發(fā)送給服務(wù)器的那條TCP連接就不能正常響應(yīng),nginx在得到這一信息后,就會(huì)返回5xx錯(cuò)誤碼給客戶端,也就是說(shuō)5xx的報(bào)錯(cuò),其實(shí)是由nginx識(shí)別出來(lái),并返回給客戶端的,服務(wù)端本身,并不會(huì)有5xx的日志信息。所以才會(huì)出現(xiàn)文章開(kāi)頭的一幕,上游收到了我服務(wù)的502報(bào)錯(cuò),但我在自己的服務(wù)日志里卻搜索不到這一信息。
產(chǎn)生502的常見(jiàn)原因
在rfc7231中有關(guān)于502錯(cuò)誤碼的官方解釋是
502BadGateway The502(BadGateway)statuscodeindicatesthattheserver,whileactingasagatewayorproxy,receivedaninvalidresponsefromaninboundserveritaccessedwhileattemptingtofulfilltherequest.
翻譯一下就是,502 (Bad Gateway) 狀態(tài)代碼表示服務(wù)器在充當(dāng)網(wǎng)關(guān)或代理時(shí),在嘗試滿足請(qǐng)求時(shí)從它訪問(wèn)的入站服務(wù)器接收到無(wú)效響應(yīng)。
汝聽(tīng),人言否?
這對(duì)于大部分編程小白來(lái)說(shuō),不僅沒(méi)解釋到問(wèn)題,反而只會(huì)冒出更多的問(wèn)號(hào)。比如,這上面提到的無(wú)效響應(yīng)到底指的是什么?
我來(lái)解釋下,它其實(shí)是說(shuō),502其實(shí)是由網(wǎng)關(guān)代理(nginx)發(fā)出的,是因?yàn)榫W(wǎng)關(guān)代理把客戶端的請(qǐng)求轉(zhuǎn)發(fā)給了服務(wù)端,但服務(wù)端卻發(fā)出了無(wú)效響應(yīng),而這里的無(wú)效響應(yīng),一般是指TCP的RST報(bào)文或四次揮手的FIN報(bào)文。
四次揮手估計(jì)大家背的很熟了,所以略過(guò),我們來(lái)重點(diǎn)說(shuō)下RST報(bào)文是什么。
RST是什么?
我們都知道TCP正常情況下斷開(kāi)連接是用四次揮手,那是正常時(shí)候的優(yōu)雅做法。
但異常情況下,收發(fā)雙方都不一定正常,連揮手這件事本身都可能做不到,所以就需要一個(gè)機(jī)制去強(qiáng)行關(guān)閉連接。
RST就是用于這種情況,一般用來(lái)異常地關(guān)閉一個(gè)連接。它是TCP包頭中的一個(gè)標(biāo)志位,在收到置這個(gè)標(biāo)志位的數(shù)據(jù)包后,連接就會(huì)被關(guān)閉,此時(shí)接收到 RST的一方,在應(yīng)用層會(huì)看到一個(gè)connection reset或 connection refused的報(bào)錯(cuò)。
TCP報(bào)頭RST位
而之所以發(fā)出RST報(bào)文,一般有兩個(gè)常見(jiàn)原因。
服務(wù)端過(guò)早斷開(kāi)連接
nginx與服務(wù)端之間有一條TCP連接,在nginx將客戶端請(qǐng)求轉(zhuǎn)發(fā)給服務(wù)端時(shí),他兩之間按道理會(huì)一直保持這條連接,直到服務(wù)端將結(jié)果正常返回后,再斷開(kāi)連接。
但如果服務(wù)端過(guò)早斷開(kāi)連接,而nginx卻還繼續(xù)發(fā)消息過(guò)去,nginx就會(huì)收到服務(wù)端內(nèi)核返回的RST報(bào)文或四次揮手的FIN報(bào)文,迫使nginx那邊的連接結(jié)束。
過(guò)早斷開(kāi)連接的原因常見(jiàn)的有兩個(gè)。
第一個(gè)是,服務(wù)端設(shè)置的超時(shí)時(shí)間過(guò)短。不管是用的哪種編程語(yǔ)言,一般都有現(xiàn)成的HTTP庫(kù),服務(wù)端一般都會(huì)有幾個(gè)timeout參數(shù),比如golang的HTTP服務(wù)框架里有個(gè)寫超時(shí)(WriteTimeout),假設(shè)設(shè)置了2s,那它的含義就是,服務(wù)端在收到請(qǐng)求后需要在2s內(nèi)處理完并將結(jié)果寫到響應(yīng)中,如果等不到,就會(huì)將連接給斷掉。
比如你的接口處理時(shí)間是5s,而你的WriteTimeout卻只有2s,在沒(méi)等到響應(yīng)寫完之前,HTTP框架就會(huì)主動(dòng)將連接給斷開(kāi)。nginx此時(shí)就有可能收到四次揮手的FIN報(bào)文(有些框架也可能發(fā)RST報(bào)文),然后斷開(kāi)連接,于是客戶端就會(huì)收到一個(gè)502報(bào)錯(cuò)。
遇到這種問(wèn)題,將WriteTimeout的時(shí)間調(diào)大一些就好了。
FIN與502的關(guān)系
第二個(gè)原因,也是造成502狀態(tài)碼最常見(jiàn)的原因,就是服務(wù)端應(yīng)用進(jìn)程崩了(crash)。
服務(wù)端崩了,也就是當(dāng)前沒(méi)有一個(gè)進(jìn)程在監(jiān)聽(tīng)服務(wù)器端口,而此時(shí)你卻嘗試向一個(gè)不存在的端口發(fā)數(shù)據(jù),服務(wù)器的linux內(nèi)核協(xié)議棧就會(huì)響應(yīng)一個(gè)RST數(shù)據(jù)包。同樣,這時(shí)候nginx也會(huì)給客戶端一個(gè)502。
RST和502
在開(kāi)發(fā)過(guò)程中,這種情況是最常見(jiàn)的。
現(xiàn)在我們大部分的服務(wù)器都會(huì)將掛掉的服務(wù)重啟,因此我們需要判斷下服務(wù)是否曾經(jīng)崩潰過(guò)。
如果你有對(duì)服務(wù)端的cpu或者內(nèi)存做過(guò)監(jiān)控,可以看下CPU或內(nèi)存的監(jiān)控圖是否出現(xiàn)過(guò)斷崖式的突然下跌。如果有,十有八九百,就是你的服務(wù)端應(yīng)用程序曾經(jīng)崩潰過(guò)。
cpu突然暴跌
除此之外你還通過(guò)下面的命令,看下進(jìn)程上次的啟動(dòng)時(shí)間是什么時(shí)候。
ps-olstart{pid}
比如我要看的進(jìn)程id是13515,命令就需要像下面這樣。
#ps-olstart13515 STARTED WedAug3114532022
可以看到它上次的啟動(dòng)時(shí)間是8月31日,這個(gè)時(shí)間如果跟你印象中的操作時(shí)間有差距,那說(shuō)明進(jìn)程可能是崩了之后被重新拉起了。
遇到這種問(wèn)題,最重要的是找出崩潰的原因,崩潰的原因就多種多樣了,比如,對(duì)未初始化的內(nèi)存地址進(jìn)行寫操作,或者內(nèi)存訪問(wèn)越界(數(shù)組arr長(zhǎng)度明明只有2,代碼卻讀arr[3])。
這種情況幾乎都是程序有代碼邏輯問(wèn)題,崩潰一般也會(huì)留下代碼堆棧,可以根據(jù)堆棧報(bào)錯(cuò)去排查問(wèn)題,修復(fù)之后就好了。比如下面這張圖是golang的報(bào)錯(cuò)堆棧信息,其他語(yǔ)言的也類似。
報(bào)錯(cuò)堆棧
不打印堆棧的情況
但有一些情況,有時(shí)候根本不留下堆棧。
比如內(nèi)存泄露導(dǎo)致進(jìn)程占用內(nèi)存越來(lái)越多,最后導(dǎo)致超過(guò)服務(wù)器的最大內(nèi)存限制,觸發(fā)OOM(out of memory), 進(jìn)程直接就被操作系統(tǒng)kill掉。
還有更隱蔽的,代碼邏輯里隱藏了主動(dòng)退出進(jìn)程的操作。比如golang的日志打印里有個(gè)方法叫l(wèi)og.Fatalln(),打印完日志還會(huì)順便執(zhí)行os.Exit()直接退出進(jìn)程,對(duì)源碼不了解的新手很容易犯這個(gè)錯(cuò)。
打印完順便還退出進(jìn)程
如果你很明確,你的服務(wù)沒(méi)有崩過(guò)。那繼續(xù)往下看。
網(wǎng)關(guān)將請(qǐng)求打到了一個(gè)不存在的IP上
nginx是通過(guò)配置的形式來(lái)代理多個(gè)服務(wù)器。這個(gè)配置一般是放在/etc/nginx/nginx.conf中。
打開(kāi)它,你可能會(huì)看到類似下面這樣的信息。
upstreamxiaobaidebug.top{ server10.14.12.19:9235weight=2; server10.14.16.13:8145weight=5; server10.14.12.133:9702weight=8; server10.14.11.15:7035weight=10; }
上面配置的含義是,如果客戶端訪問(wèn)xiaobaidebug.top域名,nginx就會(huì)將客戶端的請(qǐng)求轉(zhuǎn)發(fā)到下面的4個(gè)服務(wù)器ip上,ip邊上還有個(gè)weight權(quán)重,權(quán)重越高,被轉(zhuǎn)發(fā)到的次數(shù)就越多。
可以看出,nginx具有相當(dāng)豐富的配置能力。但要注意的是,這些個(gè)文件是需要自己手動(dòng)配置的。對(duì)于服務(wù)器少,且不怎么變化的情況,這當(dāng)然沒(méi)問(wèn)題。
但現(xiàn)在已經(jīng)是云原生時(shí)代了,很多公司內(nèi)部都有自己的云產(chǎn)品,服務(wù)自然也會(huì)上云。一般來(lái)說(shuō)每次更新服務(wù),都可能會(huì)將服務(wù)部署到一臺(tái)新的機(jī)器上。而這個(gè)ip也會(huì)隨著改變,難道每發(fā)布一次服務(wù),都需要手動(dòng)去nginx上改配置嗎?這顯然不現(xiàn)實(shí)。
如果能在服務(wù)啟動(dòng)時(shí),讓服務(wù)主動(dòng)將自己的ip告訴nginx,然后nginx自己生成這樣的一個(gè)配置并重新加載,那事情就簡(jiǎn)單多了。
為了實(shí)現(xiàn)這樣一個(gè)服務(wù)注冊(cè)的功能,不少公司都會(huì)基于nginx進(jìn)行二次開(kāi)發(fā)。
但如果這個(gè)服務(wù)注冊(cè)功能有問(wèn)題,比方說(shuō)服務(wù)啟動(dòng)后,新服務(wù)沒(méi)注冊(cè)上,但老服務(wù)已經(jīng)被銷毀了。這時(shí)候nginx還將請(qǐng)求打到老服務(wù)的IP上,由于老服務(wù)所在的機(jī)器已經(jīng)沒(méi)有這個(gè)服務(wù)了,所以服務(wù)器內(nèi)核就會(huì)響應(yīng)RST,nginx收到RST后回復(fù)502給客戶端。
實(shí)例已經(jīng)銷毀但配置沒(méi)刪IP
要排查這種問(wèn)題也不難。
這個(gè)時(shí)候,你可以看下nginx側(cè)是否有打印相關(guān)的日志,看下轉(zhuǎn)發(fā)的IP端口是否符合預(yù)期。
如果不符合預(yù)期,可以去找找做這個(gè)基礎(chǔ)組件的同事,進(jìn)行一波友好的交流。
總結(jié)
HTTP狀態(tài)碼用來(lái)表示響應(yīng)結(jié)果的狀態(tài),其中200是正常響應(yīng),4xx是客戶端錯(cuò)誤,5xx是服務(wù)端錯(cuò)誤。
客戶端和服務(wù)端之間加入nginx,可以起到反向代理和負(fù)載均衡的作用,客戶端只管向nginx請(qǐng)求數(shù)據(jù),并不關(guān)心這個(gè)請(qǐng)求具體由哪個(gè)服務(wù)器來(lái)處理。
后端服務(wù)端應(yīng)用如果發(fā)生崩潰,nginx在訪問(wèn)服務(wù)端時(shí)會(huì)收到服務(wù)端返回的RST報(bào)文,然后給客戶端返回502報(bào)錯(cuò)。502并不是服務(wù)端應(yīng)用發(fā)出的,而是nginx發(fā)出的。因此發(fā)生502時(shí),后端服務(wù)端很可能沒(méi)有沒(méi)有相關(guān)的502日志,需要在nginx側(cè)才能看到這條502日志。
如果發(fā)現(xiàn)502,優(yōu)先通過(guò)監(jiān)控排查服務(wù)端應(yīng)用是否發(fā)生過(guò)崩潰重啟,如果是的話,再看下是否留下過(guò)崩潰堆棧日志,如果沒(méi)有日志,看下是否可能是oom或者是其他原因?qū)е逻M(jìn)程主動(dòng)退出。如果進(jìn)程也沒(méi)崩潰過(guò),去排查下nginx的日志,看下是否將請(qǐng)求打到了某個(gè)不知名IP端口上。
審核編輯 :李倩
-
HTTP
+關(guān)注
關(guān)注
0文章
510瀏覽量
31297 -
TCP
+關(guān)注
關(guān)注
8文章
1362瀏覽量
79112 -
服務(wù)端
+關(guān)注
關(guān)注
0文章
66瀏覽量
7019
原文標(biāo)題:502問(wèn)題怎么排查?
文章出處:【微信號(hào):良許Linux,微信公眾號(hào):良許Linux】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論