Nginx 反向代理返回結果為空的問題


最近在開發過程中遇到了這么一個問題:

現在有一個 Web 項目,前端是使用 vue.js 開發的,整個前端需要部署到 K8S 上,后端和前端分開,同樣也需要部署到 K8S 上,因此二者需要打包為 Docker 鏡像。

對前端來說,打包 Docker 就遇到了一個問題:跨域訪問問題。

因此一個普遍的解決方案就是使用 Nginx 做反向代理。

一般來說,我們需要在打包時配置一下 nginx.conf 文件,然后在 Dockerfile 里面指定即可。

 

Dockerfile

首先看下 Dockerfile:

# build stage FROM node:lts-alpine as build-stage WORKDIR /app COPY package*.json ./ RUN npm install COPY . . RUN npm run build # production stage FROM nginx:lts-alpine as production-stage COPY --from=build-stage /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/ RUN rm /etc/nginx/conf.d/default.conf && mv /etc/nginx/conf.d/nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80 CMD ["nginx", "-g", "daemon off;"] 

一般來說,對於常規的 vue.js 前端項目,Dockerfile 就這么寫就行了。

簡單介紹一下:

  • 第一步,使用 Node.js 鏡像,在 Node.js 環境下對項目進行編譯,默認會輸出到 dist 文件夾下。
  • 第二步,使用新的 Nginx 鏡像,將編譯得到的前端文件拷貝到 nginx 默認 serve 的目錄,然后把自定義的 nginx.conf 文件替換為 Nginx 默認的 conf 文件,運行即可。

 

反向代理

這里比較關鍵的就是 nginx.conf 文件了,為了解決跨域問題,我們一般會將后端的接口進行反向代理。

一般來說,后端的 API 接口都是以 api 為開頭的,所以我們需要代理 api 開頭的接口地址,nginx.conf 內容一般可以這么寫:

server { listen 80; server_name localhost; location /api/ { proxy_pass http://domain.com/api/; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; } location / { root /usr/share/nginx/html; index index.html index.htm; } location = /50x.html { root /usr/share/nginx/html; } error_page 404 /404.html; error_page 500 502 503 504 /50x.html; } 

一般來說,以上的寫法是沒有問題的,proxy_set_header 也把一些 Header 進行設置,轉發到后端服務器。

如果你這么寫,打包 Docker 之后,測試沒有遇到問題,那就完事了。

 

問題

但我遇到了一個奇怪的問題,某個接口在請求的時候,狀態碼還是 200,但其返回值總是為空,即 Response Data 的內容完全為空。

但是服務器端看 Log 確實有正常返回 Response,使用 Vue 的 devServer 也是正常的,使用 Postman 來請求也是正常的,但是經過 Nginx 這么一反向代理就不行了,什么 Response 都接收不到。

部署到 Prod 環境之后,瀏覽器上面可以得到這么個錯誤:

ERR_INCOMPLETE_CHUNKED_ENCODING 

最后經排查,發現后端接口使用時設定了 Transfer-Encoding: chunked 響應頭:

Transfer-Encoding: chunked 

這是啥?這時候就需要引出 Keep-Alive 的相關問題了。

 

什么是 Keep-Alive?

我們知道 HTTP 協議采用「請求-應答」模式,當使用普通模式,即非 Keep-Alive 模式時,每個請求/應答客戶和服務器都要新建一個連接,完成之后立即斷開連接(HTTP 協議為無連接的協議)。當使用 Keep-Alive 模式(又稱持久連接、連接重用)時,Keep-Alive 功能使客戶端到服務器端的連接持續有效,當出現對服務器的后繼請求時,Keep-Alive 功能避免了建立或者重新建立連接。

Connection: Keep-Alive Connection: close 

目前大部分瀏覽器都是用 HTTP 1.1 協議,也就是說默認都會發起 Keep-Alive 的連接請求了,所以是否能完成一個完整的 Keep-Alive 連接就看服務器設置情況。

啟用 Keep-Alive 模式肯定更高效,性能更高。因為避免了建立/釋放連接的開銷。

 

Keep-Alive 模式下如何傳輸數據

Keep-Alive 模式,客戶端如何判斷請求所得到的響應數據已經接收完成呢?或者說如何知道服務器已經發生完了數據?

我們已經知道了,Keep-Alive 模式發送完數據,HTTP 服務器不會自動斷開連接,所有不能再使用返回 EOF(-1)來判斷。

那么怎么判斷呢?一個是使用 Content-Length ,一個是使用 Transfer-Encoding。

Content-Length

顧名思義,Conent-Length 表示實體內容長度,客戶端(服務器)可以根據這個值來判斷數據是否接收完成。

由於 Content-Length 字段必須真實反映實體長度,但實際應用中,有些時候實體長度並沒那么好獲得,例如實體來自於網絡文件,或者由動態語言生成。這時候要想准確獲取長度,只能開一個足夠大的 buffer,等內容全部生成好再計算。但這樣做一方面需要更大的內存開銷,另一方面也會讓客戶端等更久。

我們在做 WEB 性能優化時,有一個重要的指標叫 TTFB(Time To First Byte),它代表的是從客戶端發出請求到收到響應的第一個字節所花費的時間。大部分瀏覽器自帶的 Network 面板都可以看到這個指標,越短的 TTFB 意味着用戶可以越早看到頁面內容,體驗越好。可想而知,服務端為了計算響應實體長度而緩存所有內容,跟更短的 TTFB 理念背道而馳。但在 HTTP 報文中,實體一定要在頭部之后,順序不能顛倒,為此我們需要一個新的機制:不依賴頭部的長度信息,也能知道實體的邊界。

但是如果消息中沒有 Conent-Length,那該如何來判斷呢?又在什么情況下會沒有 Conent-Length 呢?

Transfer-Encoding

當客戶端向服務器請求一個靜態頁面或者一張圖片時,服務器可以很清楚地知道內容大小,然后通過 Content-length 消息首部字段告訴客戶端需要接收多少數據。但是如果是動態頁面等時,服務器是不可能預先知道內容大小,這時就可以使用 分塊編碼模式來傳輸數據了。即如果要一邊產生數據,一邊發給客戶端,服務器就需要在請求頭中使用 Transfer-Encoding: chunked 這樣的方式來代替 Content-Length,這就是分塊編碼。

分塊編碼相當簡單,在頭部加入 Transfer-Encoding: chunked 之后,就代表這個報文采用了分塊編碼。這時,報文中的實體需要改為用一系列分塊來傳輸。每個分塊包含十六進制的長度值和數據,長度值獨占一行,長度不包括它結尾的 CRLF(rn),也不包括分塊數據結尾的 CRLF。最后一個分塊長度值必須為 0,對應的分塊數據沒有內容,表示實體結束。

 

回歸問題

那么我說了這么一大通有什么用呢?

OK,在我遇到的業務場景中,我發現服務器的響應頭中就包含了 Transfer-Encoding: chunked這個字段。

而這個字段,在 HTTP 1.0 是不被支持的。

而 Nginx 的反向代理,默認用的就是 HTTP 1.0,那就導致了數據無法獲取的問題,可以參考 Nginx 的官方文檔說明: http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_pass 。

原文中:

Syntax: proxy_http_version 1.0 | 1.1; Default: proxy_http_version 1.0; By default, version 1.0 is used. Version 1.1 is recommended for use with keepalive connections and NTLM authentication. 

所以,我們如果要解決這個問題,只需要設置一下 HTTP 版本為 1.1 就好了:

修改 nginx.conf 文件如下:

location /api/ { proxy_pass http://domain.com/api/; proxy_http_version 1.1; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; } 

這里就增加了一行:

proxy_http_version 1.1; 

這樣再測試,反向代理就會支持 Transfer-Encoding: chunked 模式了,這也就呼應了之前在瀏覽器中遇到的 ERR_INCOMPLETE_CHUNKED_ENCODING 錯誤。

自此,問題完美解決。

佛山vi設計https://www.houdianzi.com/fsvi/ 豌豆資源搜索大全https://55wd.com

復盤記錄

一開始本來只想簡單一記錄就了事的,但一邊寫,發現某個地方還可以展開寫得更詳細。

所以干脆最后我對這個問題進行了詳細的復盤和記錄。在寫本文之前,我其實只思考到了 Keep-Alive 和 HTTP 1.1 的問題,其實我對 Transfer-Encoding 這個並沒有去深入思考。在邊寫邊總結的過程中,為了把整個脈絡講明白,我又查詢了一些 Transfer-Encoding 和 Nginx 的官方文檔,對這塊的了解變得更加深入,相當於我在整個記錄的過程中,又對整個流程梳理了一遍,同時又有額外的收獲。

所以,遇到問題,深入去思考、總結和復盤,是很有幫助的,這會讓我們對問題的看法和理解更加透徹。

怎么說呢?在開發過程中,難免會遇到一些奇奇怪怪的 Bug,但這其實只是技術問題,總會解決的。

但怎樣在開發過程中,不斷提高自己的技術能力,我覺得需要從每一個細節出發,去思考一些事情的來龍去脈。思考得越多,我們對整個事件的把握也會越清晰,以后如果再遇到類似的或者關聯的事情,就會迎刃而解了。

平時我們可能很多情況下都在寫業務代碼,可能比較枯燥,感覺對技術沒有實質性的提升,但如果我們能從中提煉出一些核心的問題或解決方案,這才是能真正提高技術的時候,這才是最有價值的。


免責聲明!

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



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