從項目實際問題引發的思考
- 2019 年 12 月 11 日
- 筆記
閱讀本文大概需要 5 分鐘。
沒錯,轉載的公眾號——崔慶才丨靜覓。
崔老師靈魂三解答
- 該公眾號為新開的同名個人公眾號,崔老師打算髮點閱讀量沒那麼可愛的純技術的解 bug 過程和工作感悟。
- 崔慶才的名字其實是「崔慶才丨(gun)靜覓「,中間是中文字。
- 崔老師這周末的體重是 139 斤,同比去年這個時間胖了 23 斤。讓我們為他鼓掌。

最近在開發過程中遇到了這麼一個問題:
現在有一個 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 功能避免了建立或者重新建立連接。
•HTTP 1.0 中默認是關閉 Keep-Alive 的,需要在 HTTP 頭加入Connection: Keep-Alive,才能啟用 Keep-Alive•HTTP 1.1 中默認啟用 Keep-Alive,如果請求頭中加入 Connection: close,Keep-Alive 才關閉。
目前大部分瀏覽器都是用 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 錯誤。
自此,問題完美解決。
復盤記錄
一開始本來只想簡單一記錄就了事的,但一邊寫,發現某個地方還可以展開寫得更詳細。
所以乾脆最後我對這個問題進行了詳細的復盤和記錄。在寫本文之前,我其實只思考到了 Keep-Alive 和 HTTP 1.1 的問題,其實我對 Transfer-Encoding 這個並沒有去深入思考。在邊寫邊總結的過程中,為了把整個脈絡講明白,我又查詢了一些 Transfer-Encoding 和 Nginx 的官方文檔,對這塊的了解變得更加深入,相當於我在整個記錄的過程中,又對整個流程梳理了一遍,同時又有額外的收穫。
所以,遇到問題,深入去思考、總結和復盤,是很有幫助的,這會讓我們對問題的看法和理解更加透徹。
怎麼說呢?在開發過程中,難免會遇到一些奇奇怪怪的 Bug,但這其實只是技術問題,總會解決的。
但怎樣在開發過程中,不斷提高自己的技術能力,我覺得需要從每一個細節出發,去思考一些事情的來龍去脈。思考得越多,我們對整個事件的把握也會越清晰,以後如果再遇到類似的或者關聯的事情,就會迎刃而解了。
平時我們可能很多情況下都在寫業務程式碼,可能比較枯燥,感覺對技術沒有實質性的提升,但如果我們能從中提煉出一些核心的問題或解決方案,這才是能真正提高技術的時候,這才是最有價值的。
參考文章
本文部分內容改寫或摘自下列內容。
•HTTP Keep-Alive模式:https://www.cnblogs.com/skynet/archive/2010/12/11/1903347.html•Nginx proxy_set_header 理解:https://www.jianshu.com/p/cc5167032525•使用 Docker 打造超溜的前端環境:https://github.com/axetroy/blog/issues/178• HTTP 協議中的 Transfer-Encoding:https://imququ.com/post/transfer-encoding-header-in-http.html
以上文章來源於崔慶才丨靜覓,作者崔慶才

