<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          NodeJS 服務(wù) Docker 鏡像極致優(yōu)化指北

          共 7035字,需瀏覽 15分鐘

           ·

          2021-09-24 22:42

          這段時間在開發(fā)一個騰訊文檔全品類通用的 HTML 動態(tài)服務(wù),為了方便各品類接入的生成與部署,也順應(yīng)上云的趨勢,考慮使用 Docker 的方式來固定服務(wù)內(nèi)容,統(tǒng)一進行制品版本的管理。本篇文章就將我在服務(wù) Docker 化的過程中積累起來的優(yōu)化經(jīng)驗分享出來,供大家參考。

          以一個例子開頭,大部分剛接觸 Docker 的同學(xué)應(yīng)該都會這樣編寫項目的 Dockerfile,如下所示:

          FROM node:14WORKDIR /app
          COPY . .# 安裝 npm 依賴RUN npm install
          # 暴露端口EXPOSE 8000
          CMD ["npm", "start"]

          構(gòu)建,打包,上傳,一氣呵成。然后看下鏡像狀態(tài),臥槽,一個簡單的 node web 服務(wù)體積居然達到了驚人的 1.3 個 G,并且鏡像傳輸與構(gòu)建速度也很慢:

          要是這個鏡像只需要部署一個實例也就算了,但是這個服務(wù)得提供給所有開發(fā)同學(xué)進行高頻集成并部署環(huán)境的(實現(xiàn)高頻集成的方案可參見我的 上一篇文章)。首先,鏡像體積過大必然會對鏡像的拉取和更新速度造成影響,集成體驗會變差。其次,項目上線后,同時在線的測試環(huán)境實例可能成千上萬,這樣的容器內(nèi)存占用成本對于任何一個項目都是無法接受的。必須找到優(yōu)化的辦法解決。

          發(fā)現(xiàn)問題后,我就開始研究 Docker 的優(yōu)化方案,準(zhǔn)備給我的鏡像動手術(shù)了。

          node 項目生產(chǎn)環(huán)境優(yōu)化

          首先開刀的是當(dāng)然是前端最為熟悉的領(lǐng)域,對代碼本身體積進行優(yōu)化。之前開發(fā)項目時使用了 Typescript,為了圖省事,項目直接使用 tsc 打包生成 es5 后就直接運行起來了。這里的體積問題主要有兩個,一個是開發(fā)環(huán)境 ts 源碼并未處理,并且用于生產(chǎn)環(huán)境的 js 代碼也未經(jīng)壓縮。

          另一個是引用的 node_modules 過于臃腫。仍然包含了許多開發(fā)調(diào)試環(huán)境中的 npm 包,如 ts-node,typescript 等等。既然打包成 js 了,這些依賴自然就該去除。

          一般來說,由于服務(wù)端代碼不會像前端代碼一樣暴露出去,運行在物理機上的服務(wù)更多考慮的是穩(wěn)定性,也不在乎多一些體積,因此這些地方一般也不會做處理。但是 Docker 化后,由于部署規(guī)模變大,這些問題就非常明顯了,在生產(chǎn)環(huán)境下需要優(yōu)化的。

          對于這兩點的優(yōu)化的方式其實我們前端非常熟悉了,不是本文的重點就粗略帶過了。對于第一點,使用 Webpack + babel 降級并壓縮 Typescript 源碼,如果擔(dān)心錯誤排查可以加上 sourcemap,不過對于 docker 鏡像來說有點多余,一會兒會說到。對于第二點,梳理 npm 包的 dependencies 與 devDependencies 依賴,去除不是必要存在于運行時的依賴,方便生產(chǎn)環(huán)境使用 npm install--production 安裝依賴。

          優(yōu)化項目鏡像體積

          使用盡量精簡的基礎(chǔ)鏡像

          我們知道,容器技術(shù)提供的是操作系統(tǒng)級別的進程隔離,Docker 容器本身是一個運行在獨立操作系統(tǒng)下的進程,也就是說,Docker 鏡像需要打包的是一個能夠獨立運行的操作系統(tǒng)級環(huán)境。因此,決定鏡像體積的一個重要因素就顯而易見了:打包進鏡像的 Linux 操作系統(tǒng)的體積。

          一般來說,減小依賴的操作系統(tǒng)的大小主要需要考慮從兩個方面下手,第一個是盡可能去除 Linux 下不需要的各類工具庫,如 python,cmake, telnet 等。第二個是選取更輕量級的 Linux 發(fā)行版系統(tǒng)。正規(guī)的官方鏡像應(yīng)該會依據(jù)上述兩個因素對每個發(fā)行版提供閹割版本。

          以 node 官方提供的版本 node:14 為例,默認版本中,它的運行基礎(chǔ)環(huán)境是 Ubuntu,是一個大而全的 Linux 發(fā)行版,以保證最大的兼容性。去除了無用工具庫的依賴版本稱為 node:14-slim 版本。而最小的鏡像發(fā)行版稱為 node:14-alpine。Linux alpine 是一個高度精簡,僅包含基本工具的輕量級 Linux 發(fā)行版,本身的 Docker 鏡像只有 4 ~ 5M 大小,因此非常適合制作最小版本的 Docker 鏡像。

          在我們的服務(wù)中,由于運行該服務(wù)的依賴是確定的,因此為了盡可能的縮減基礎(chǔ)鏡像的體積,我們選擇 alpine 版本作為生產(chǎn)環(huán)境的基礎(chǔ)鏡像。

          分級構(gòu)建

          這時候,我們遇到了新的問題。由于 alpine 的基本工具庫過于簡陋,而像 webpack 這樣的打包工具背后可能使用的插件庫極多,構(gòu)建項目時對環(huán)境的依賴較大。并且這些工具庫只有編譯時需要用到,在運行時是可以去除的。對于這種情況,我們可以利用 Docker 的 分級構(gòu)建的特性來解決這一問題。

          首先,我們可以在完整版鏡像下進行依賴安裝,并給該任務(wù)設(shè)立一個別名(此處為 build)。

          # 安裝完整依賴并構(gòu)建產(chǎn)物FROM node:14 AS buildWORKDIR /app
          COPY package*.json /app/RUN ["npm", "install"]COPY . /app/
          RUN npm run build

          之后我們可以啟用另一個鏡像任務(wù)來運行生產(chǎn)環(huán)境,生產(chǎn)的基礎(chǔ)鏡像就可以換成 alpine 版本了。其中編譯完成后的源碼可以通過 --from參數(shù)獲取到處于 build任務(wù)中的文件,移動到此任務(wù)內(nèi)。

          FROM node:14-alpine AS releaseWORKDIR /release
          COPY package*.json /RUN ["npm", "install", "--registry=http://r.tnpm.oa.com", "--production"]
          # 移入依賴與源碼COPY public /release/publicCOPY --from=build /app/dist /release/dist
          # 啟動服務(wù)EXPOSE 8000
          CMD ["node", "./dist/index.js"]

          Docker 鏡像的生成規(guī)則是,生成鏡像的結(jié)果僅以最后一個鏡像任務(wù)為準(zhǔn)。因此前面的任務(wù)并不會占用最終鏡像的體積,從而完美解決這一問題。

          當(dāng)然,隨著項目越來越復(fù)雜,在運行時仍可能會遇到工具庫報錯,如果曝出問題的工具庫所需依賴不多,我們可以自行補充所需的依賴,這樣的鏡像體積仍然能保持較小的水平。

          其中最常見的問題就是對 node-gypnode-sass庫的引用。由于這個庫是用來將其他語言編寫的模塊轉(zhuǎn)譯為 node 模塊,因此,我們需要手動增加 g++make python這三個依賴。

          # 安裝生產(chǎn)環(huán)境依賴(為兼容 node-gyp 所需環(huán)境需要對 alpine 進行改造)FROM node:14-alpine AS dependencies
          RUN apk add --no-cache python make g++COPY package*.json /RUN ["npm", "install", "--registry=http://r.tnpm.oa.com", "--production"]RUN apk del .gyp

          詳情可見:https://github.com/nodejs/docker-node/issues/282

          合理規(guī)劃 Docker Layer

          構(gòu)建速度優(yōu)化

          我們知道,Docker 使用 Layer 概念來創(chuàng)建與組織鏡像,Dockerfile 的每條指令都會產(chǎn)生一個新的文件層,每層都包含執(zhí)行命令前后的狀態(tài)之間鏡像的文件系統(tǒng)更改,文件層越多,鏡像體積就越大。而 Docker 使用緩存方式實現(xiàn)了構(gòu)建速度的提升。若 Dockerfile 中某層的語句及依賴未更改,則該層重建時可以直接復(fù)用本地緩存

          如下所示,如果 log 中出現(xiàn) Usingcache字樣時,說明緩存生效了,該層將不會執(zhí)行運算,直接拿原緩存作為該層的輸出結(jié)果。

          Step 2/3 : npm install ---> Using cache ---> efvbf79sd1eb

          通過研究 Docker 緩存算法,發(fā)現(xiàn)在 Docker 構(gòu)建過程中,如果某層無法應(yīng)用緩存,則依賴此步的后續(xù)層都不能從緩存加載。例如下面這個例子:

          COPY . .RUN npm install

          此時如果我們更改了倉庫的任意一個文件,此時因為 npm install層的上層依賴變更了,哪怕依賴沒有進行任何變動,緩存也不會被復(fù)用。

          因此,若想盡可能的利用上 npm install層緩存,我們可以把 Dockerfile 改成這樣:

          COPY package*.json .RUN npm installCOPY src .

          這樣在僅變更源碼時, node_modules的依賴緩存仍然能被利用上了。

          由此,我們得到了優(yōu)化原則:

          1. 最小化處理變更文件,僅變更下一步所需的文件,以盡可能減少構(gòu)建過程中的緩存失效。

          2. 對于處理文件變更的 ADD 命令、COPY 命令,盡量延遲執(zhí)行。

          構(gòu)建體積優(yōu)化

          在保證速度的前提下,體積優(yōu)化也是我們需要去考慮的。這里我們需要考慮的有三點:

          1. Docker 是以層為單位上傳鏡像倉庫的,這樣也能最大化的利用緩存的能力。因此,執(zhí)行結(jié)果很少變化的命令需要抽出來單獨成層,如上面提到的 npm install的例子里,也用到了這方面的思想。

          2. 如果鏡像層數(shù)越少,總上傳體積就越小。因此,在命令處于執(zhí)行鏈尾部,即不會對其他層緩存產(chǎn)生影響的情況下,盡量合并命令,從而減少緩存體積。例如,設(shè)置環(huán)境變量和清理無用文件的指令,它們的輸出都是不會被使用的,因此可以將這些命令合并為一行 RUN 命令。

            RUN set ENV=prod && rm -rf ./trash
          3. Docker cache 的下載也是通過層緩存的方式,因此為了減少鏡像的傳輸下載時間,我們最好使用固定的物理機器來進行構(gòu)建。例如在流水線中指定專用宿主機,能是的鏡像的準(zhǔn)備時間大大減少。

          當(dāng)然,時間和空間的優(yōu)化從來就沒有兩全其美的辦法,這一點需要我們在設(shè)計 Dockerfile 時,對 Docker Layer 層數(shù)做出權(quán)衡。例如為了時間優(yōu)化,需要我們拆分文件的復(fù)制等操作,而這一點會導(dǎo)致層數(shù)增多,略微增加空間。

          這里我的建議是,優(yōu)先保證構(gòu)建時間,其次在不影響時間的情況下,盡可能的縮小構(gòu)建緩存體積。

          以 Docker 的思維管理服務(wù)

          避免使用進程守護

          我們編寫傳統(tǒng)的后臺服務(wù)時,總是會使用例如 pm2、forever 等等進程守護程序,以保證服務(wù)在意外崩潰時能被監(jiān)測到并自動重啟。但這一點在 Docker 下非但沒有益處,還帶來了額外的不穩(wěn)定因素。

          首先,Docker 本身就是一個流程管理器,因此,進程守護程序提供的崩潰重啟,日志記錄等等工作 Docker 本身或是基于 Docker 的編排程序(如 kubernetes)就能提供了,無需使用額外應(yīng)用實現(xiàn)。除此之外,由于守護進程的特性,將不可避免的對于以下的情況產(chǎn)生影響:

          1. 增加進程守護程序會使得占用的內(nèi)存增多,鏡像體積也會相應(yīng)增大。

          2. 由于守護進程一直能正常運行,服務(wù)發(fā)生故障時,Docker 自身的重啟策略將不會生效,Docker 日志里將不會記錄崩潰信息,排障溯源困難。

          3. 由于多了個進程的加入,Docker 提供的 CPU、內(nèi)存等監(jiān)控指標(biāo)將變得不準(zhǔn)確。

          因此,盡管 pm2 這樣的進程守護程序提供了能夠適配 Docker 的版本:pm2-runtime,但我仍然不推薦大家使用進程守護程序。

          其實這一點其實是源自于我們的固有思想而犯下的錯誤。在服務(wù)上云的過程中,難點其實不僅僅在于寫法與架構(gòu)上的調(diào)整,開發(fā)思路的轉(zhuǎn)變才是最重要的,我們會在上云的過程中更加深刻體會到這一點。

          日志的持久化存儲

          無論是為了排障還是審計的需要,后臺服務(wù)總是需要日志能力。按照以往的思路,我們將日志分好類后,統(tǒng)一寫入某個目錄下的日志文件即可。但是在 Docker 中,任何本地文件都不是持久化的,會隨著容器的生命周期結(jié)束而銷毀。因此,我們需要將日志的存儲跳出容器之外。

          最簡單的做法是利用 DockerManagerVolume,這個特性能繞過容器自身的文件系統(tǒng),直接將數(shù)據(jù)寫到宿主物理機器上。具體用法如下:

          docker run -d -it --name=app -v /app/log:/usr/share/log app

          運行 docker 時,通過-v 參數(shù)為容器綁定 volumes,將宿主機上的 /app/log 目錄(如果沒有會自動創(chuàng)建)掛載到容器的 /usr/share/log 中。這樣服務(wù)在將日志寫入該文件夾時,就能持久化存儲在宿主機上,不隨著 docker 的銷毀而丟失了。

          當(dāng)然,當(dāng)部署集群變多后,物理宿主機上的日志也會變得難以管理。此時就需要一個服務(wù)編排系統(tǒng)來統(tǒng)一管理了。從單純管理日志的角度出發(fā),我們可以進行網(wǎng)絡(luò)上報,給到云日志服務(wù)(如騰訊云 CLS)托管?;蛘吒纱鄬⑷萜鬟M行批量管理,例如 Kubernetes這樣的容器編排系統(tǒng),這樣日志作為其中的一個模塊自然也能得到妥善保管了。這樣的方法很多,就不多加贅述了。

          k8s 服務(wù)控制器的選擇

          鏡像優(yōu)化之外,服務(wù)編排以及控制部署的負載形式對性能的影響也很大。這里以最流行的 Kubernetes的兩種控制器(Controller):DeploymentStatefulSet 為例,簡要比較一下這兩類組織形式,幫助選擇出最適合服務(wù)的 Controller。

          StatefulSet是 K8S 在 1.5 版本后引入的 Controller,主要特點為:能夠?qū)崿F(xiàn) pod 間的有序部署、更新和銷毀。那么我們的制品是否需要使用 StatefulSet 做 pod 管理呢?官方簡要概括為一句話:

          Deployment 用于部署無狀態(tài)服務(wù),StatefulSet 用來部署有狀態(tài)服務(wù)。

          這句話十分精確,但不易于理解。那么,什么是無狀態(tài)呢?在我看來, StatefulSet的特點可以從如下幾個步驟進行理解:

          1. StatefulSet管理的多個 pod 之間進行部署,更新,刪除操作時能夠按照固定順序依次進行。適用于多服務(wù)之間有依賴的情況,如先啟動數(shù)據(jù)庫服務(wù)再開啟查詢服務(wù)。

          2. 由于 pod 之間有依賴關(guān)系,因此每個 pod 提供的服務(wù)必定不同,所以 StatefulSet 管理的 pod 之間沒有負載均衡的能力。

          3. 又因為 pod 提供的服務(wù)不同,所以每個 pod 都會有自己獨立的存儲空間,pod 間不共享。

          4. 為了保證 pod 部署更新時順序,必須固定 pod 的名稱,因此不像 Deployment 那樣生成的 pod 名稱后會帶一串隨機數(shù)。

          5. 而由于 pod 名稱固定,因此跟 StatefulSet 對接的 Service 中可以直接以 pod 名稱作為訪問域名,而不需要提供 ClusterIP,因此跟 StatefulSet 對接的 Service 被稱為 HeadlessService。

          通過這里我們就應(yīng)該明白,如果在 k8s 上部署的是單個服務(wù),或是多服務(wù)間沒有依賴關(guān)系,那么 Deployment 一定是簡單而又效果最佳的選擇,自動調(diào)度,自動負載均衡。而如果服務(wù)的啟停必須滿足一定順序,或者每一個 pod 所掛載的數(shù)據(jù) volume 需要在銷毀后依然存在,那么建議選擇 StatefulSet。

          本著如無必要,勿增實體的原則,強烈建議所有運行單個服務(wù)工作負載采用 Deployment 作為 Controller。

          寫在結(jié)尾

          一通研究下來,差點把一開始的目標(biāo)忘了,趕緊將 Docker 重新構(gòu)建一遍,看看優(yōu)化成果。

          可以看到,對于鏡像體積的優(yōu)化效果還是不錯的,達到了 10 倍左右。當(dāng)然,如果項目中不需要如此高版本的 node 支持,還能進一步縮小大約一半的鏡像體積。

          之后鏡像倉庫會對存放的鏡像文件做一次壓縮,以 node14 打包的鏡像版本最終被壓縮到了 50M 以內(nèi)。

          當(dāng)然,除了看得到的體積數(shù)據(jù)之外,更重要的優(yōu)化其實在于,從面向物理機的服務(wù)向容器化云服務(wù)在架構(gòu)設(shè)計層面上的轉(zhuǎn)變。

          容器化已經(jīng)是看得見的未來,作為一名開發(fā)人員,要時刻保持對前沿技術(shù)的敏感,積極實踐,才能將技術(shù)轉(zhuǎn)化為生產(chǎn)力,為項目的  進化做出貢獻。

          參考資料:

          1. 《Kubernetes in action》--Marko Luk?a

          2. Optimizing Docker Images



          往期推薦


          大廠面試過程復(fù)盤(微信/阿里/頭條,附答案篇)
          面試題:說說事件循環(huán)機制(滿分答案來了)
          專心工作只想搞錢的前端女程序員的2020



          內(nèi)推社群


          我組建了一個氛圍特別好的騰訊內(nèi)推社群,如果你對加入騰訊感興趣的話(后續(xù)有計劃也可以),我們可以一起進行面試相關(guān)的答疑、聊聊面試的故事、并且在你準(zhǔn)備好的時候隨時幫你內(nèi)推。下方加 winty 好友回復(fù)「面試」即可。


          瀏覽 31
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  老妇人一区二区三区 | 中国性爱在线观看 | 成人18禁免费精品网站 | 爱爱免费网站 | 日本三级aaa久久久久久久 |