設置
  • 日夜間
    隨系統(tǒng)
    淺色
    深色
  • 主題色

502 問題怎么排查?

小白debug 2022/11/11 19:27:05 責編:子非

剛工作那會,有一次,上游調(diào)用我服務的老哥說,你的服務報 "502 錯誤了,快去看看是為什么吧"。

當時那個服務里正好有個調(diào)用日志,平時會記錄各種 200,4xx 狀態(tài)碼的信息。于是我跑到服務日志里去搜索了一下 502 這個數(shù)字,毫無發(fā)現(xiàn)。于是跟老哥說," 服務日志里并沒有 502 的記錄,你是不是搞錯啦?"

現(xiàn)在想來,多少有些不好意思。

不知道有多少老哥是跟當時的我是一樣的,這篇文章,就來聊聊 502 錯誤是什么?

我們從狀態(tài)碼是什么開始聊起。

HTTP 狀態(tài)碼

我們平時在瀏覽器里逛的某寶和某度,其實都是一個個前端網(wǎng)頁。

一般來說,前端并不存儲太多數(shù)據(jù),大部分時候都需要從后端服務器那獲取數(shù)據(jù)。

于是前后端之間需要通過 TCP 協(xié)議去建立連接,然后在 TCP 的基礎上傳輸數(shù)據(jù)。

而 TCP 是基于數(shù)據(jù)流的協(xié)議,傳輸數(shù)據(jù)時,并不會為每個消息加入數(shù)據(jù)邊界,直接使用裸的 TCP 進行數(shù)據(jù)傳輸會有 "粘包" 問題。

因此需要用特地的協(xié)議格式去對數(shù)據(jù)進行解析。于是在此基礎上設計了 HTTP 協(xié)議。詳細的內(nèi)容可以看我之前寫的《既然有 HTTP 協(xié)議,為什么還要有 RPC》。

比如,我想要看某個商品的具體信息,其實就是前端發(fā)的 HTTP 請求中傳入商品的 id,后端返回的 HTTP 響應中返回商品的價格,商店名,發(fā)貨地址的信息等。

通過 id 獲取商品詳情

這樣,表面上,我們是在刷著各種網(wǎng)頁,實際上背后正有多次 HTTP 消息在不斷進行收發(fā)。

用戶在網(wǎng)上瀏覽商品

但問題就來了,上面提到的都是正常情況,如果有異常情況呢,比如前端發(fā)的數(shù)據(jù),根本就不是個商品 id,而是一張圖片,這對于后端服務端來說是不可能給出正常響應的,于是就需要設計一套 HTTP 狀態(tài)碼,用來標識這次 HTTP 請求響應流程是否正常。通過這個可以影響瀏覽器的行為。

比方說一切正常,那服務端返回個 200 狀態(tài)碼,前端收到后,可以放心使用響應的數(shù)據(jù)。但如果服務端發(fā)現(xiàn)客戶端發(fā)的東西異常,就響應個 4xx 狀態(tài)碼,意思是這是個客戶端的錯誤,4xx 里頭的 xx 可以根據(jù)錯誤的類型,再細分成各種碼,比如 401 是客戶端沒權限,404 是客戶端請求了一個根本不存在的網(wǎng)頁。反過來,如果是服務器有問題,就返回 5xx 狀態(tài)碼。

4xx 和 5xx 的區(qū)別

但問題就來了。

服務端都有問題了,搞嚴重點,服務器可能直接就崩潰了,那它還怎么給你返回狀態(tài)碼?

是的,這種情況,服務端是不可能給客戶端返回狀態(tài)碼的。所以說,一般情況下 5xx 的狀態(tài)碼其實并不是服務器返回給客戶端的。

它們是由網(wǎng)關返回的,常見的網(wǎng)關,比如 nginx。

nginx 的作用

回到前后端交互數(shù)據(jù)的話題上,如果前端用戶少,那后端處理起請求來,游刃有余。但隨著用戶越來越多,后端服務器受資源限制,cpu 或者內(nèi)存都可能會嚴重不足,這時候解決方案也很簡單,多搞幾臺一樣的服務器,這樣就能將這些前端請求均攤給幾個服務器,從而提升處理能力。

但要實現(xiàn)這樣的效果,前端就得知道后端具體有哪些個服務器,并一一跟他們建立 TCP 連接。

前端與多個服務器之間建立連接

也不是不行,但就是麻煩。

但這時候如果能有個中間層擋在它們中間就好了,這樣客戶端只需要跟中間層連接,中間層再和服務器建立連接。

于是,這個中間層就成了這幫服務器的一個代理人一樣,客戶端有啥事都找代理人,只管發(fā)出自己的請求,再由代理人去找某個服務器去完成響應。整個過程下來,客戶端只知道自己的請求被代理人幫忙搞定了,但代理人具體找了那個服務器去完成,客戶端并不知道,也不需要知道。

像這種,屏蔽掉具體有哪些服務器的代理方式就是所謂的反向代理

反向代理

反過來,屏蔽掉具體有哪些客戶端的代理方式,就是所謂的正向代理。

而這個中間層的角色,一般由 nginx 這類網(wǎng)關來充當。

另外,由于背后的服務器可能性能配置各不相同,有些 4 核 8G,有些 2 核 4G,nginx 能為它們加上不同的訪問權重,權重高的多轉(zhuǎn)發(fā)點請求,通過這個方式實現(xiàn)不同的負載均衡策略。

nginx 返回 5xx 狀態(tài)碼

有了 nginx 這一中間層后,客戶端從直連服務端,變成客戶端直連 nginx,再由 nginx 直連服務端。從一個 TCP 連接變成兩個 TCP 連接。

于是,當服務器發(fā)生異常時,nginx 發(fā)送給服務器的那條 TCP 連接就不能正常響應,nginx 在得到這一信息后,就會返回 5xx 錯誤碼給客戶端,也就是說 5xx 的報錯,其實是由 nginx 識別出來,并返回給客戶端的,服務端本身,并不會有 5xx 的日志信息。所以才會出現(xiàn)文章開頭的一幕,上游收到了我服務的 502 報錯,但我在自己的服務日志里卻搜索不到這一信息。

產(chǎn)生 502 的常見原因

在 rfc7231 中有關于 502 錯誤碼的官方解釋是

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ǎng)關或代理時,在嘗試滿足請求時從它訪問的入站服務器接收到無效響應。

汝聽,人言否?

這對于大部分編程小白來說,不僅沒解釋到問題,反而只會冒出更多的問號。比如,這上面提到的無效響應到底指的是什么?

我來解釋下,它其實是說,502 其實是由網(wǎng)關代理(nginx)發(fā)出的,是因為網(wǎng)關代理把客戶端的請求轉(zhuǎn)發(fā)給了服務端,但服務端卻發(fā)出了無效響應,而這里的無效響應,一般是指 TCP 的 RST 報文或四次揮手的 FIN 報文。

四次揮手估計大家背的很熟了,所以略過,我們來重點說下 RST 報文是什么。

RST 是什么?

我們都知道 TCP 正常情況下斷開連接是用四次揮手,那是正常時候的優(yōu)雅做法。

異常情況下,收發(fā)雙方都不一定正常,連揮手這件事本身都可能做不到,所以就需要一個機制去強行關閉連接。

RST 就是用于這種情況,一般用來異常地關閉一個連接。它是 TCP 包頭中的一個標志位,在收到置這個標志位的數(shù)據(jù)包后,連接就會被關閉,此時接收到 RST 的一方,在應用層會看到一個 connection reset 或  connection refused 的報錯。

TCP 報頭 RST 位

而之所以發(fā)出 RST 報文,一般有兩個常見原因。

服務端過早斷開連接

nginx 與服務端之間有一條 TCP 連接,在 nginx 將客戶端請求轉(zhuǎn)發(fā)給服務端時,他兩之間按道理會一直保持這條連接,直到服務端將結果正常返回后,再斷開連接。

但如果服務端過早斷開連接,而 nginx 卻還繼續(xù)發(fā)消息過去,nginx 就會收到服務端內(nèi)核返回的 RST 報文或四次揮手的 FIN 報文,迫使 nginx 那邊的連接結束。

過早斷開連接的原因常見的有兩個。

第一個是,服務端設置的超時時間過短。不管是用的哪種編程語言,一般都有現(xiàn)成的 HTTP 庫,服務端一般都會有幾個 timeout 參數(shù),比如 golang 的 HTTP 服務框架里有個寫超時(WriteTimeout),假設設置了 2s,那它的含義就是,服務端在收到請求后需要在 2s 內(nèi)處理完并將結果寫到響應中,如果等不到,就會將連接給斷掉。

比如你的接口處理時間是 5s,而你的 WriteTimeout 卻只有 2s,在沒等到響應寫完之前,HTTP 框架就會主動將連接給斷開。nginx 此時就有可能收到四次揮手的 FIN 報文(有些框架也可能發(fā) RST 報文),然后斷開連接,于是客戶端就會收到一個 502 報錯。

遇到這種問題,將 WriteTimeout 的時間調(diào)大一些就好了。

FIN 與 502 的關系

第二個原因,也是造成 502 狀態(tài)碼最常見的原因,就是服務端應用進程崩了(crash)。

服務端崩了,也就是當前沒有一個進程在監(jiān)聽服務器端口,而此時你卻嘗試向一個不存在的端口發(fā)數(shù)據(jù),服務器的 linux 內(nèi)核協(xié)議棧就會響應一個 RST 數(shù)據(jù)包。同樣,這時候 nginx 也會給客戶端一個 502。

RST 和 502

在開發(fā)過程中,這種情況是最常見的

現(xiàn)在我們大部分的服務器都會將掛掉的服務重啟,因此我們需要判斷下服務是否曾經(jīng)崩潰過。

如果你有對服務端的 cpu 或者內(nèi)存做過監(jiān)控,可以看下 CPU 或內(nèi)存的監(jiān)控圖是否出現(xiàn)過斷崖式的突然下跌。如果有,十有八九百,就是你的服務端應用程序曾經(jīng)崩潰過。

cpu 突然暴跌

除此之外你還通過下面的命令,看下進程上次的啟動時間是什么時候。

ps -o lstart {pid}

比如我要看的進程 id 是 13515,命令就需要像下面這樣。

# ps -o lstart 13515
                 STARTED
Wed Aug 31 14:28:53 2022

可以看到它上次的啟動時間是 8 月 31 日,這個時間如果跟你印象中的操作時間有差距,那說明進程可能是崩了之后被重新拉起了。

遇到這種問題,最重要的是找出崩潰的原因,崩潰的原因就多種多樣了,比如,對未初始化的內(nèi)存地址進行寫操作,或者內(nèi)存訪問越界(數(shù)組 arr 長度明明只有 2,代碼卻讀 arr [3])。

這種情況幾乎都是程序有代碼邏輯問題,崩潰一般也會留下代碼堆棧,可以根據(jù)堆棧報錯去排查問題,修復之后就好了。比如下面這張圖是 golang 的報錯堆棧信息,其他語言的也類似。

報錯堆棧

不打印堆棧的情況

但有一些情況,有時候根本不留下堆棧。

比如內(nèi)存泄露導致進程占用內(nèi)存越來越多,最后導致超過服務器的最大內(nèi)存限制,觸發(fā) OOM(out of memory), 進程直接就被操作系統(tǒng) kill 掉。

還有更隱蔽的,代碼邏輯里隱藏了主動退出進程的操作。比如 golang 的日志打印里有個方法叫 log.Fatalln (),打印完日志還會順便執(zhí)行 os.Exit () 直接退出進程,對源碼不了解的新手很容易犯這個錯。

打印完順便還退出進程

如果你很明確,你的服務沒有崩過。那繼續(xù)往下看。

網(wǎng)關將請求打到了一個不存在的 IP 上

nginx 是通過配置的形式來代理多個服務器。這個配置一般是放在 /etc/ nginx / nginx.conf 中。

打開它,你可能會看到類似下面這樣的信息。

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 就會將客戶端的請求轉(zhuǎn)發(fā)到下面的 4 個服務器 ip 上,ip 邊上還有個 weight 權重,權重越高,被轉(zhuǎn)發(fā)到的次數(shù)就越多。

可以看出,nginx 具有相當豐富的配置能力。但要注意的是,這些個文件是需要自己手動配置的。對于服務器少,且不怎么變化的情況,這當然沒問題。

但現(xiàn)在已經(jīng)是云原生時代了,很多公司內(nèi)部都有自己的云產(chǎn)品,服務自然也會上云。一般來說每次更新服務,都可能會將服務部署到一臺新的機器上。而這個 ip 也會隨著改變,難道每發(fā)布一次服務,都需要手動去 nginx 上改配置嗎?這顯然不現(xiàn)實。

如果能在服務啟動時,讓服務主動將自己的 ip 告訴 nginx,然后 nginx 自己生成這樣的一個配置并重新加載,那事情就簡單多了。

為了實現(xiàn)這樣一個服務注冊的功能,不少公司都會基于 nginx 進行二次開發(fā)。

但如果這個服務注冊功能有問題,比方說服務啟動后,新服務沒注冊上,但老服務已經(jīng)被銷毀了。這時候 nginx 還將請求打到老服務的 IP 上,由于老服務所在的機器已經(jīng)沒有這個服務了,所以服務器內(nèi)核就會響應 RST,nginx 收到 RST 后回復 502 給客戶端

實例已經(jīng)銷毀但配置沒刪 IP

要排查這種問題也不難。

這個時候,你可以看下 nginx 側(cè)是否有打印相關的日志,看下轉(zhuǎn)發(fā)的 IP 端口是否符合預期。

如果不符合預期,可以去找找做這個基礎組件的同事,進行一波友好的交流。

總結

HTTP 狀態(tài)碼用來表示響應結果的狀態(tài),其中 200 是正常響應,4xx 是客戶端錯誤,5xx 是服務端錯誤。

客戶端和服務端之間加入 nginx,可以起到反向代理和負載均衡的作用,客戶端只管向 nginx 請求數(shù)據(jù),并不關心這個請求具體由哪個服務器來處理。

后端服務端應用如果發(fā)生崩潰,nginx 在訪問服務端時會收到服務端返回的 RST 報文,然后給客戶端返回 502 報錯。502 并不是服務端應用發(fā)出的,而是 nginx 發(fā)出的。因此發(fā)生 502 時,后端服務端很可能沒有相關的 502 日志,需要在 nginx 側(cè)才能看到這條 502 日志。

如果發(fā)現(xiàn) 502,優(yōu)先通過監(jiān)控排查服務端應用是否發(fā)生過崩潰重啟,如果是的話,再看下是否留下過崩潰堆棧日志,如果沒有日志,看下是否可能是 oom 或者是其他原因?qū)е逻M程主動退出。如果進程也沒崩潰過,去排查下 nginx 的日志,看下是否將請求打到了某個不知名 IP 端口上。

本文來自微信公眾號:小白 debug (ID:xiaobaidebug),作者:小白

廣告聲明:文內(nèi)含有的對外跳轉(zhuǎn)鏈接(包括不限于超鏈接、二維碼、口令等形式),用于傳遞更多信息,節(jié)省甄選時間,結果僅供參考,IT之家所有文章均包含本聲明。

相關文章

軟媒旗下網(wǎng)站: IT之家 最會買 - 返利返現(xiàn)優(yōu)惠券 iPhone之家 Win7之家 Win10之家 Win11之家

軟媒旗下軟件: 軟媒手機APP應用 魔方 最會買 要知