從項(xiàng)目實(shí)際問題引發(fā)的思考
“
閱讀本文大概需要 5 分鐘。
沒錯(cuò),轉(zhuǎn)載的公眾號(hào)——崔慶才丨靜覓。
崔老師靈魂三解答
該公眾號(hào)為新開的同名個(gè)人公眾號(hào),崔老師打算發(fā)點(diǎn)閱讀量沒那么可愛的純技術(shù)的解 bug 過程和工作感悟。
崔慶才的名字其實(shí)是“崔慶才丨(gun)靜覓“,中間是中文字。
崔老師這周末的體重是 139 斤,同比去年這個(gè)時(shí)間胖了 23 斤。讓我們?yōu)樗恼啤?/span>

最近在開發(fā)過程中遇到了這么一個(gè)問題:
現(xiàn)在有一個(gè) Web 項(xiàng)目,前端是使用 Vue.js 開發(fā)的,整個(gè)前端需要部署到 K8S 上,后端和前端分開,同樣也需要部署到 K8S 上,因此二者需要打包為 Docker 鏡像。
對(duì)前端來說,打包 Docker 就遇到了一個(gè)問題:跨域訪問問題。
因此一個(gè)普遍的解決方案就是使用 Nginx 做反向代理。
一般來說,我們需要在打包時(shí)配置一下 nginx.conf 文件,然后在 Dockerfile 里面指定即可。
Dockerfile
首先看下 Dockerfile:
# build stageFROM node:lts-alpine as build-stageWORKDIR /appCOPY package*.json ./RUN npm installCOPY . .RUN npm run build# production stageFROM nginx:lts-alpine as production-stageCOPY --from=build-stage /app/dist /usr/share/nginx/htmlCOPY 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.confEXPOSE 80CMD ["nginx", "-g", "daemon off;"]
一般來說,對(duì)于常規(guī)的 Vue.js 前端項(xiàng)目,Dockerfile 就這么寫就行了。
簡(jiǎn)單介紹一下:
?第一步,使用 Node.js 鏡像,在 Node.js 環(huán)境下對(duì)項(xiàng)目進(jìn)行編譯,默認(rèn)會(huì)輸出到 dist 文件夾下。?第二步,使用新的 Nginx 鏡像,將編譯得到的前端文件拷貝到 nginx 默認(rèn) serve 的目錄,然后把自定義的 nginx.conf 文件替換為 Nginx 默認(rèn)的 conf 文件,運(yùn)行即可。
反向代理
這里比較關(guān)鍵的就是 nginx.conf 文件了,為了解決跨域問題,我們一般會(huì)將后端的接口進(jìn)行反向代理。
一般來說,后端的 API 接口都是以 api 為開頭的,所以我們需要代理 api 開頭的接口地址,nginx.conf 內(nèi)容一般可以這么寫:
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 進(jìn)行設(shè)置,轉(zhuǎn)發(fā)到后端服務(wù)器。
如果你這么寫,打包 Docker 之后,測(cè)試沒有遇到問題,那就完事了。
問題
但我遇到了一個(gè)奇怪的問題,某個(gè)接口在請(qǐng)求的時(shí)候,狀態(tài)碼還是 200,但其返回值總是為空,即 Response Data 的內(nèi)容完全為空。
但是服務(wù)器端看 Log 確實(shí)有正常返回 Response,使用 Vue 的 devServer 也是正常的,使用 Postman 來請(qǐng)求也是正常的,但是經(jīng)過 Nginx 這么一反向代理就不行了,什么 Response 都接收不到。
部署到 Prod 環(huán)境之后,瀏覽器上面可以得到這么個(gè)錯(cuò)誤:
ERR_INCOMPLETE_CHUNKED_ENCODING
最后經(jīng)排查,發(fā)現(xiàn)后端接口使用時(shí)設(shè)定了?Transfer-Encoding: chunked?響應(yīng)頭:
Transfer-Encoding: chunked這是啥?這時(shí)候就需要引出 Keep-Alive 的相關(guān)問題了。
什么是 Keep-Alive?
我們知道 HTTP 協(xié)議采用「請(qǐng)求-應(yīng)答」模式,當(dāng)使用普通模式,即非 Keep-Alive 模式時(shí),每個(gè)請(qǐng)求/應(yīng)答客戶和服務(wù)器都要新建一個(gè)連接,完成之后立即斷開連接(HTTP 協(xié)議為無連接的協(xié)議)。當(dāng)使用 Keep-Alive 模式(又稱持久連接、連接重用)時(shí),Keep-Alive 功能使客戶端到服務(wù)器端的連接持續(xù)有效,當(dāng)出現(xiàn)對(duì)服務(wù)器的后繼請(qǐng)求時(shí),Keep-Alive 功能避免了建立或者重新建立連接。
?HTTP 1.0 中默認(rèn)是關(guān)閉 Keep-Alive 的,需要在 HTTP 頭加入Connection: Keep-Alive,才能啟用 Keep-Alive?HTTP 1.1 中默認(rèn)啟用 Keep-Alive,如果請(qǐng)求頭中加入?Connection: close,Keep-Alive 才關(guān)閉。
目前大部分瀏覽器都是用 HTTP 1.1 協(xié)議,也就是說默認(rèn)都會(huì)發(fā)起 Keep-Alive 的連接請(qǐng)求了,所以是否能完成一個(gè)完整的 Keep-Alive 連接就看服務(wù)器設(shè)置情況。
啟用 Keep-Alive 模式肯定更高效,性能更高。因?yàn)楸苊饬私?釋放連接的開銷。
Keep-Alive 模式下如何傳輸數(shù)據(jù)
Keep-Alive 模式,客戶端如何判斷請(qǐng)求所得到的響應(yīng)數(shù)據(jù)已經(jīng)接收完成呢?或者說如何知道服務(wù)器已經(jīng)發(fā)生完了數(shù)據(jù)?
我們已經(jīng)知道了,Keep-Alive 模式發(fā)送完數(shù)據(jù),HTTP 服務(wù)器不會(huì)自動(dòng)斷開連接,所有不能再使用返回 EOF(-1)來判斷。
那么怎么判斷呢?一個(gè)是使用 Content-Length ,一個(gè)是使用 Transfer-Encoding。
Content-Length
顧名思義,Conent-Length 表示實(shí)體內(nèi)容長度,客戶端(服務(wù)器)可以根據(jù)這個(gè)值來判斷數(shù)據(jù)是否接收完成。
由于?Content-Length?字段必須真實(shí)反映實(shí)體長度,但實(shí)際應(yīng)用中,有些時(shí)候?qū)嶓w長度并沒那么好獲得,例如實(shí)體來自于網(wǎng)絡(luò)文件,或者由動(dòng)態(tài)語言生成。這時(shí)候要想準(zhǔn)確獲取長度,只能開一個(gè)足夠大的 buffer,等內(nèi)容全部生成好再計(jì)算。但這樣做一方面需要更大的內(nèi)存開銷,另一方面也會(huì)讓客戶端等更久。
我們?cè)谧?WEB 性能優(yōu)化時(shí),有一個(gè)重要的指標(biāo)叫 TTFB(Time To First Byte),它代表的是從客戶端發(fā)出請(qǐng)求到收到響應(yīng)的第一個(gè)字節(jié)所花費(fèi)的時(shí)間。大部分瀏覽器自帶的 Network 面板都可以看到這個(gè)指標(biāo),越短的 TTFB 意味著用戶可以越早看到頁面內(nèi)容,體驗(yàn)越好。可想而知,服務(wù)端為了計(jì)算響應(yīng)實(shí)體長度而緩存所有內(nèi)容,跟更短的 TTFB 理念背道而馳。但在 HTTP 報(bào)文中,實(shí)體一定要在頭部之后,順序不能顛倒,為此我們需要一個(gè)新的機(jī)制:不依賴頭部的長度信息,也能知道實(shí)體的邊界。
但是如果消息中沒有 Conent-Length,那該如何來判斷呢?又在什么情況下會(huì)沒有 Conent-Length 呢?
Transfer-Encoding
當(dāng)客戶端向服務(wù)器請(qǐng)求一個(gè)靜態(tài)頁面或者一張圖片時(shí),服務(wù)器可以很清楚地知道內(nèi)容大小,然后通過 Content-length 消息首部字段告訴客戶端需要接收多少數(shù)據(jù)。但是如果是動(dòng)態(tài)頁面等時(shí),服務(wù)器是不可能預(yù)先知道內(nèi)容大小,這時(shí)就可以使用 分塊編碼模式來傳輸數(shù)據(jù)了。即如果要一邊產(chǎn)生數(shù)據(jù),一邊發(fā)給客戶端,服務(wù)器就需要在請(qǐng)求頭中使用Transfer-Encoding: chunked?這樣的方式來代替 Content-Length,這就是分塊編碼。
分塊編碼相當(dāng)簡(jiǎn)單,在頭部加入?Transfer-Encoding: chunked?之后,就代表這個(gè)報(bào)文采用了分塊編碼。這時(shí),報(bào)文中的實(shí)體需要改為用一系列分塊來傳輸。每個(gè)分塊包含十六進(jìn)制的長度值和數(shù)據(jù),長度值獨(dú)占一行,長度不包括它結(jié)尾的 CRLF(\r\n),也不包括分塊數(shù)據(jù)結(jié)尾的 CRLF。最后一個(gè)分塊長度值必須為 0,對(duì)應(yīng)的分塊數(shù)據(jù)沒有內(nèi)容,表示實(shí)體結(jié)束。
回歸問題
那么我說了這么一大通有什么用呢?
OK,在我遇到的業(yè)務(wù)場(chǎng)景中,我發(fā)現(xiàn)服務(wù)器的響應(yīng)頭中就包含了Transfer-Encoding: chunked?這個(gè)字段。
而這個(gè)字段,在 HTTP 1.0 是不被支持的。
而 Nginx 的反向代理,默認(rèn)用的就是 HTTP 1.0,那就導(dǎo)致了數(shù)據(jù)無法獲取的問題,可以參考 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.
所以,我們?nèi)绻鉀Q這個(gè)問題,只需要設(shè)置一下 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;這樣再測(cè)試,反向代理就會(huì)支持?Transfer-Encoding: chunked?模式了,這也就呼應(yīng)了之前在瀏覽器中遇到的 ERR_INCOMPLETE_CHUNKED_ENCODING 錯(cuò)誤。
自此,問題完美解決。
復(fù)盤記錄
一開始本來只想簡(jiǎn)單一記錄就了事的,但一邊寫,發(fā)現(xiàn)某個(gè)地方還可以展開寫得更詳細(xì)。
所以干脆最后我對(duì)這個(gè)問題進(jìn)行了詳細(xì)的復(fù)盤和記錄。在寫本文之前,我其實(shí)只思考到了 Keep-Alive 和 HTTP 1.1 的問題,其實(shí)我對(duì) Transfer-Encoding 這個(gè)并沒有去深入思考。在邊寫邊總結(jié)的過程中,為了把整個(gè)脈絡(luò)講明白,我又查詢了一些 Transfer-Encoding 和 Nginx 的官方文檔,對(duì)這塊的了解變得更加深入,相當(dāng)于我在整個(gè)記錄的過程中,又對(duì)整個(gè)流程梳理了一遍,同時(shí)又有額外的收獲。
所以,遇到問題,深入去思考、總結(jié)和復(fù)盤,是很有幫助的,這會(huì)讓我們對(duì)問題的看法和理解更加透徹。
怎么說呢?在開發(fā)過程中,難免會(huì)遇到一些奇奇怪怪的 Bug,但這其實(shí)只是技術(shù)問題,總會(huì)解決的。
但怎樣在開發(fā)過程中,不斷提高自己的技術(shù)能力,我覺得需要從每一個(gè)細(xì)節(jié)出發(fā),去思考一些事情的來龍去脈。思考得越多,我們對(duì)整個(gè)事件的把握也會(huì)越清晰,以后如果再遇到類似的或者關(guān)聯(lián)的事情,就會(huì)迎刃而解了。
平時(shí)我們可能很多情況下都在寫業(yè)務(wù)代碼,可能比較枯燥,感覺對(duì)技術(shù)沒有實(shí)質(zhì)性的提升,但如果我們能從中提煉出一些核心的問題或解決方案,這才是能真正提高技術(shù)的時(shí)候,這才是最有價(jià)值的。
參考文章
本文部分內(nèi)容改寫或摘自下列內(nèi)容。
?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 打造超溜的前端環(huán)境:https://github.com/axetroy/blog/issues/178?? HTTP 協(xié)議中的 Transfer-Encoding:https://imququ.com/post/transfer-encoding-header-in-http.html
崔慶才丨靜覓
隱形字
同名公眾號(hào)「崔慶才丨靜覓」
在這里分享自己的一些經(jīng)驗(yàn)、想法和見解。


長按識(shí)別二維碼關(guān)注
好文和朋友一起看~