剛工作那會(huì),有一次,上游調(diào)用我服務(wù)的老哥說,你的服務(wù)報(bào) "502 錯(cuò)誤了,快去看看是為什么吧"。
當(dāng)時(shí)那個(gè)服務(wù)里正好有個(gè)調(diào)用日志,平時(shí)會(huì)記錄各種 200,4xx 狀態(tài)碼的信息。于是我跑到服務(wù)日志里去搜索了一下 502 這個(gè)數(shù)字,毫無發(fā)現(xiàn)。于是跟老哥說," 服務(wù)日志里并沒有 502 的記錄,你是不是搞錯(cuò)啦?"
現(xiàn)在想來,多少有些不好意思。
不知道有多少老哥是跟當(dāng)時(shí)的我是一樣的,這篇文章,就來聊聊 502 錯(cuò)誤是什么?
我們從狀態(tài)碼是什么開始聊起。
HTTP 狀態(tài)碼
我們平時(shí)在瀏覽器里逛的某寶和某度,其實(shí)都是一個(gè)個(gè)前端網(wǎng)頁。
一般來說,前端并不存儲(chǔ)太多數(shù)據(jù),大部分時(shí)候都需要從后端服務(wù)器那獲取數(shù)據(jù)。
于是前后端之間需要通過 TCP 協(xié)議去建立連接,然后在 TCP 的基礎(chǔ)上傳輸數(shù)據(jù)。
而 TCP 是基于數(shù)據(jù)流的協(xié)議,傳輸數(shù)據(jù)時(shí),并不會(huì)為每個(gè)消息加入數(shù)據(jù)邊界,直接使用裸的 TCP 進(jìn)行數(shù)據(jù)傳輸會(huì)有 "粘包" 問題。
因此需要用特地的協(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ā)貨地址的信息等。
這樣,表面上,我們是在刷著各種網(wǎng)頁,實(shí)際上背后正有多次 HTTP 消息在不斷進(jìn)行收發(fā)。
但問題就來了,上面提到的都是正常情況,如果有異常情況呢,比如前端發(fā)的數(shù)據(jù),根本就不是個(gè)商品 id,而是一張圖片,這對(duì)于后端服務(wù)端來說是不可能給出正常響應(yīng)的,于是就需要設(shè)計(jì)一套 HTTP 狀態(tài)碼,用來標(biāo)識(shí)這次 HTTP 請(qǐng)求響應(yīng)流程是否正常。通過這個(gè)可以影響瀏覽器的行為。
比方說一切正常,那服務(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 是客戶端沒權(quán)限,404 是客戶端請(qǐng)求了一個(gè)根本不存在的網(wǎng)頁。反過來,如果是服務(wù)器有問題,就返回 5xx 狀態(tài)碼。
但問題就來了。
服務(wù)端都有問題了,搞嚴(yán)重點(diǎn),服務(wù)器可能直接就崩潰了,那它還怎么給你返回狀態(tài)碼?
是的,這種情況,服務(wù)端是不可能給客戶端返回狀態(tài)碼的。所以說,一般情況下 5xx 的狀態(tài)碼其實(shí)并不是服務(wù)器返回給客戶端的。
它們是由網(wǎng)關(guān)返回的,常見的網(wǎng)關(guān),比如 nginx。
nginx 的作用
回到前后端交互數(shù)據(jù)的話題上,如果前端用戶少,那后端處理起請(qǐng)求來,游刃有余。但隨著用戶越來越多,后端服務(wù)器受資源限制,cpu 或者內(nèi)存都可能會(huì)嚴(yán)重不足,這時(shí)候解決方案也很簡單,多搞幾臺(tái)一樣的服務(wù)器,這樣就能將這些前端請(qǐng)求均攤給幾個(gè)服務(wù)器,從而提升處理能力。
但要實(shí)現(xiàn)這樣的效果,前端就得知道后端具體有哪些個(gè)服務(wù)器,并一一跟他們建立 TCP 連接。
也不是不行,但就是麻煩。
但這時(shí)候如果能有個(gè)中間層擋在它們中間就好了,這樣客戶端只需要跟中間層連接,中間層再和服務(wù)器建立連接。
于是,這個(gè)中間層就成了這幫服務(wù)器的一個(gè)代理人一樣,客戶端有啥事都找代理人,只管發(fā)出自己的請(qǐng)求,再由代理人去找某個(gè)服務(wù)器去完成響應(yīng)。整個(gè)過程下來,客戶端只知道自己的請(qǐng)求被代理人幫忙搞定了,但代理人具體找了那個(gè)服務(wù)器去完成,客戶端并不知道,也不需要知道。
像這種,屏蔽掉具體有哪些服務(wù)器的代理方式就是所謂的反向代理。
反過來,屏蔽掉具體有哪些客戶端的代理方式,就是所謂的正向代理。
而這個(gè)中間層的角色,一般由 nginx 這類網(wǎng)關(guān)來充當(dāng)。
另外,由于背后的服務(wù)器可能性能配置各不相同,有些 4 核 8G,有些 2 核 4G,nginx 能為它們加上不同的訪問權(quán)重,權(quán)重高的多轉(zhuǎn)發(fā)點(diǎn)請(qǐng)求,通過這個(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ò)誤碼給客戶端,也就是說 5xx 的報(bào)錯(cuò),其實(shí)是由 nginx 識(shí)別出來,并返回給客戶端的,服務(wù)端本身,并不會(huì)有 5xx 的日志信息。所以才會(huì)出現(xiàn)文章開頭的一幕,上游收到了我服務(wù)的 502 報(bào)錯(cuò),但我在自己的服務(wù)日志里卻搜索不到這一信息。
產(chǎn)生 502 的常見原因
在 rfc7231 中有關(guān)于 502 錯(cuò)誤碼的官方解釋是
502 Bad Gateway The 502 (Bad Gateway) status code indicates that the server, while acting as a gateway or proxy, received an invalid response from an inbound server it accessed while attempting to fulfill the request.
翻譯一下就是,502 (Bad Gateway) 狀態(tài)代碼表示服務(wù)器在充當(dāng)網(wǎng)關(guān)或代理時(shí),在嘗試滿足請(qǐng)求時(shí)從它訪問的入站服務(wù)器接收到無效響應(yīng)。
汝聽,人言否?
這對(duì)于大部分編程小白來說,不僅沒解釋到問題,反而只會(huì)冒出更多的問號(hào)。比如,這上面提到的無效響應(yīng)到底指的是什么?
我來解釋下,它其實(shí)是說,502 其實(shí)是由網(wǎng)關(guān)代理(nginx)發(fā)出的,是因?yàn)榫W(wǎng)關(guān)代理把客戶端的請(qǐng)求轉(zhuǎn)發(fā)給了服務(wù)端,但服務(wù)端卻發(fā)出了無效響應(yīng),而這里的無效響應(yīng),一般是指 TCP 的 RST 報(bào)文或四次揮手的 FIN 報(bào)文。
四次揮手估計(jì)大家背的很熟了,所以略過,我們來重點(diǎn)說下 RST 報(bào)文是什么。
RST 是什么?
我們都知道 TCP 正常情況下斷開連接是用四次揮手,那是正常時(shí)候的優(yōu)雅做法。
但異常情況下,收發(fā)雙方都不一定正常,連揮手這件事本身都可能做不到,所以就需要一個(gè)機(jī)制去強(qiáng)行關(guān)閉連接。
RST 就是用于這種情況,一般用來異常地關(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ò)。
而之所以發(fā)出 RST 報(bào)文,一般有兩個(gè)常見原因。
服務(wù)端過早斷開連接
nginx 與服務(wù)端之間有一條 TCP 連接,在 nginx 將客戶端請(qǐng)求轉(zhuǎn)發(fā)給服務(wù)端時(shí),他兩之間按道理會(huì)一直保持這條連接,直到服務(wù)端將結(jié)果正常返回后,再斷開連接。
但如果服務(wù)端過早斷開連接,而 nginx 卻還繼續(xù)發(fā)消息過去,nginx 就會(huì)收到服務(wù)端內(nèi)核返回的 RST 報(bào)文或四次揮手的 FIN 報(bào)文,迫使 nginx 那邊的連接結(jié)束。
過早斷開連接的原因常見的有兩個(gè)。
第一個(gè)是,服務(wù)端設(shè)置的超時(shí)時(shí)間過短。不管是用的哪種編程語言,一般都有現(xiàn)成的 HTTP 庫,服務(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,在沒等到響應(yīng)寫完之前,HTTP 框架就會(huì)主動(dòng)將連接給斷開。nginx 此時(shí)就有可能收到四次揮手的 FIN 報(bào)文(有些框架也可能發(fā) RST 報(bào)文),然后斷開連接,于是客戶端就會(huì)收到一個(gè) 502 報(bào)錯(cuò)。
遇到這種問題,將 WriteTimeout 的時(shí)間調(diào)大一些就好了。
第二個(gè)原因,也是造成 502 狀態(tài)碼最常見的原因,就是服務(wù)端應(yīng)用進(jìn)程崩了(crash)。
服務(wù)端崩了,也就是當(dāng)前沒有一個(gè)進(jìn)程在監(jiān)聽服務(wù)器端口,而此時(shí)你卻嘗試向一個(gè)不存在的端口發(fā)數(shù)據(jù),服務(wù)器的 linux 內(nèi)核協(xié)議棧就會(huì)響應(yīng)一個(gè) RST 數(shù)據(jù)包。同樣,這時(shí)候 nginx 也會(huì)給客戶端一個(gè) 502。
在開發(fā)過程中,這種情況是最常見的。
現(xiàn)在我們大部分的服務(wù)器都會(huì)將掛掉的服務(wù)重啟,因此我們需要判斷下服務(wù)是否曾經(jīng)崩潰過。
如果你有對(duì)服務(wù)端的 cpu 或者內(nèi)存做過監(jiān)控,可以看下 CPU 或內(nèi)存的監(jiān)控圖是否出現(xiàn)過斷崖式的突然下跌。如果有,十有八九百,就是你的服務(wù)端應(yīng)用程序曾經(jīng)崩潰過。
除此之外你還通過下面的命令,看下進(jìn)程上次的啟動(dòng)時(shí)間是什么時(shí)候。
ps -o lstart {pid}
比如我要看的進(jìn)程 id 是 13515,命令就需要像下面這樣。
# ps -o lstart 13515 STARTED Wed Aug 31 14:28:53 2022
可以看到它上次的啟動(dòng)時(shí)間是 8 月 31 日,這個(gè)時(shí)間如果跟你印象中的操作時(shí)間有差距,那說明進(jìn)程可能是崩了之后被重新拉起了。
遇到這種問題,最重要的是找出崩潰的原因,崩潰的原因就多種多樣了,比如,對(duì)未初始化的內(nèi)存地址進(jìn)行寫操作,或者內(nèi)存訪問越界(數(shù)組 arr 長度明明只有 2,代碼卻讀 arr [3])。
這種情況幾乎都是程序有代碼邏輯問題,崩潰一般也會(huì)留下代碼堆棧,可以根據(jù)堆棧報(bào)錯(cuò)去排查問題,修復(fù)之后就好了。比如下面這張圖是 golang 的報(bào)錯(cuò)堆棧信息,其他語言的也類似。
不打印堆棧的情況
但有一些情況,有時(shí)候根本不留下堆棧。
比如內(nèi)存泄露導(dǎo)致進(jìn)程占用內(nèi)存越來越多,最后導(dǎo)致超過服務(wù)器的最大內(nèi)存限制,觸發(fā) OOM(out of memory), 進(jìn)程直接就被操作系統(tǒng) kill 掉。
還有更隱蔽的,代碼邏輯里隱藏了主動(dòng)退出進(jìn)程的操作。比如 golang 的日志打印里有個(gè)方法叫 log.Fatalln (),打印完日志還會(huì)順便執(zhí)行 os.Exit () 直接退出進(jìn)程,對(duì)源碼不了解的新手很容易犯這個(gè)錯(cuò)。
如果你很明確,你的服務(wù)沒有崩過。那繼續(xù)往下看。
網(wǎng)關(guān)將請(qǐng)求打到了一個(gè)不存在的 IP 上
nginx 是通過配置的形式來代理多個(gè)服務(wù)器。這個(gè)配置一般是放在 /etc/ nginx / nginx.conf 中。
打開它,你可能會(huì)看到類似下面這樣的信息。
upstream xiaobaidebug.top { server 10.14.12.19:9235 weight=2; server 10.14.16.13:8145 weight=5; server 10.14.12.133:9702 weight=8; server 10.14.11.15:7035 weight=10; }
上面配置的含義是,如果客戶端訪問 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)然沒問題。
但現(xiàn)在已經(jīng)是云原生時(shí)代了,很多公司內(nèi)部都有自己的云產(chǎn)品,服務(wù)自然也會(huì)上云。一般來說每次更新服務(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è)配置并重新加載,那事情就簡單多了。
為了實(shí)現(xiàn)這樣一個(gè)服務(wù)注冊(cè)的功能,不少公司都會(huì)基于 nginx 進(jìn)行二次開發(fā)。
但如果這個(gè)服務(wù)注冊(cè)功能有問題,比方說服務(wù)啟動(dòng)后,新服務(wù)沒注冊(cè)上,但老服務(wù)已經(jīng)被銷毀了。這時(shí)候 nginx 還將請(qǐng)求打到老服務(wù)的 IP 上,由于老服務(wù)所在的機(jī)器已經(jīng)沒有這個(gè)服務(wù)了,所以服務(wù)器內(nèi)核就會(huì)響應(yīng) RST,nginx 收到 RST 后回復(fù) 502 給客戶端。
要排查這種問題也不難。
這個(gè)時(shí)候,你可以看下 nginx 側(cè)是否有打印相關(guān)的日志,看下轉(zhuǎn)發(fā)的 IP 端口是否符合預(yù)期。
如果不符合預(yù)期,可以去找找做這個(gè)基礎(chǔ)組件的同事,進(jìn)行一波友好的交流。
總結(jié)
HTTP 狀態(tà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ù)器來處理。
后端服務(wù)端應(yīng)用如果發(fā)生崩潰,nginx 在訪問服務(wù)端時(shí)會(huì)收到服務(wù)端返回的 RST 報(bào)文,然后給客戶端返回 502 報(bào)錯(cuò)。502 并不是服務(wù)端應(yīng)用發(fā)出的,而是 nginx 發(fā)出的。因此發(fā)生 502 時(shí),后端服務(wù)端很可能沒有相關(guān)的 502 日志,需要在 nginx 側(cè)才能看到這條 502 日志。
如果發(fā)現(xiàn) 502,優(yōu)先通過監(jiān)控排查服務(wù)端應(yīng)用是否發(fā)生過崩潰重啟,如果是的話,再看下是否留下過崩潰堆棧日志,如果沒有日志,看下是否可能是 oom 或者是其他原因?qū)е逻M(jìn)程主動(dòng)退出。如果進(jìn)程也沒崩潰過,去排查下 nginx 的日志,看下是否將請(qǐng)求打到了某個(gè)不知名 IP 端口上。
本文來自微信公眾號(hào):小白 debug (ID:xiaobaidebug),作者:小白
廣告聲明:文內(nèi)含有的對(duì)外跳轉(zhuǎn)鏈接(包括不限于超鏈接、二維碼、口令等形式),用于傳遞更多信息,節(jié)省甄選時(shí)間,結(jié)果僅供參考,IT之家所有文章均包含本聲明。