做者:個推Node.js 開發工程師 之諾html
因爲工程數量的快速增加,個推在實踐基於 Node.js 的微服務開發的過程當中,遇到了以下問題:node
1. 每次新建項目都須要安裝一次依賴,這些依賴之間基本類似卻又有微妙的區別;web
2. 每次新建項目都要配置一遍類似的配置(好比 tsconfig、lint 規則等);redis
3. 本地 Mac 環境與線上 Docker 內的 Linux 環境不一致(尤爲是有 C++ 依賴的狀況)。docker
爲了解決上述問題,個推內部開發了一個命令行小工具來標準化項目初始化流程、簡化配置甚至是零配置,提供基於 Docker 的一致構建、運行環境。npm
新建一個 Node.js 項目的時候,咱們通常會:json
1. 安裝許多開發依賴:TypeScript、Jest、TSLint、benchmark、typedoc 等;緩存
2. 配置 tsconfig、lint 規則、.prettierrc 等;安全
3. 安裝衆多項目依賴:koa、lodash、sequelize、ioredis、zipkin、node-fetch 等;bash
4. 初始化目錄結構;
5. 配置CI 腳本。
一般,咱們會選擇複製一個現成的項目進行修改,致使出現衆多看似類似卻又不徹底相同的項目,好比十個項目可能會對應十種配置組合。對於同時跨多個工程的開發人員來講,衆多配置組合會增長他們的工做難度。並且,當安全審計發現某些 npm package 出現安全隱患時,開發人員則須要對每一個引用這些包的項目逐一檢查和修正。
在肯定的開發場景下,幾乎全部項目的開發依賴都差很少,開發配置也很是類似,所以咱們基於 commander.js 寫了一個 init 工具,它會開個命令行的嚮導,自動安裝依賴、初始化項目目錄結構和配置。從而建立項目,並按照場景將全部配置收縮爲特定幾種模板,進行統一處理。
隨後,咱們有了 build、test、pack 命令,託管了 tsconfig、jest 配置、打包配置,自動調用 tsc 編譯,構建測試環境,而後調用 Jest 進行測試,進行標準化打包, CI 腳本基本能夠簡化爲幾行標準腳本。
在介紹這個命令前須要先簡單瞭解一下個推的鏡像體系:
前面提到咱們將大部分依賴封裝到了一個 npm 包,這一層封裝也反映在個推的 Docker 鏡像體系內,能夠簡單表述爲下面的 Dockerfile:
# 公共依賴層的 Dockerfile
FROM node:10
RUN mkdir -p /usr/local/lib/webnode/node_modules \
&& cd /usr/local/lib/webnode \
&& npm install webnode
ENV NODE_PATH /usr/local/lib/webnode/node_modules
# 項目的 Dockerfile
FROM getui/webnode:1.2.3
COPY package*.json ./
RUN npm install
COPY . .
複製代碼
當把這層依賴直接作進 Docker 鏡像時,雖然每一個鏡像的 SIZE 仍是 1G 多,可是每一個鏡像的 UNIQUE SIZE 都是極小的,僅有數M的差分層。
一個簡單的對比,好比有 800M 公共系統依賴 + 每一個服務平均 200M 的 npm 依賴 + 1M 的服務代碼,那麼因爲原先每一個服務都會 npm install 大量重複依賴,20 個服務,就會有 800M + 200M * 20 + 1M * 20 = 4.82G 的總 UNIQUE SIZE。而採用依賴分層共享,則僅有 800M + 200M + 1M * 20 = 1.02G 的總 UNIQUE SIZE。在考慮應用的多版本以後,依賴分層共享帶來在存儲上的優點會更加明顯。
咱們以必定的依賴鎖定週期和控制爲代價,換取了:
webnode docker build 命令能夠幫助簡化 Docker image 的構建過程,它內置了一個 Dockerfile 和dockerignore,該命令運行時,會基於這兩個文件和當前的 Context,自動構建docker 鏡像。其中 Dockerfile 內含一些優化和咱們的最佳實踐,開發人員只須要專一 Node.js 的項目的開發,這個命令則能夠負責配置文件權限等操做以及生成標準化的、優化的 Docker 鏡像。
其設計目標是:
以 node_modules 依賴優化爲例,下面兩種 Dockerfile 其實會有很大的區別:
FROM getui/webnode:1.2.3
COPY . .
RUN npm install
FROM getui/webnode:1.2.3
COPY package*.json ./
RUN npm install
COPY . .
複製代碼
前者,每次 docker build 時,只要項目內任何代碼變了,npm install 的緩存都會失效,須要從新安裝,然後者僅當 package*.json 發生改變之時纔會觸發從新 npm install。另外,咱們還會對 package.json 進行預編譯,僅保留依賴相關的字段,避免出現修改 package.json 的版本號就從新 npm install的狀況。
webnode docker build 不只能夠幫助開發者進行統一化的鏡像構建、統一實踐最佳優化,節約資源,還能避免全部開發人員都須要接觸優化細節,省時省力。
在本地調試開發的過程當中,咱們遇到了一些環境差別引發的問題:
與本地直接啓動 Node.js 程序有所不一樣,這個命令會優先基於當前項目利用上面的 webnode docker build 命令構建 Docker 鏡像,而後啓動鏡像。
Docker 能夠幫助消解環境差別:
容器化的Node.js調試方法有些許變化,須要暴露Node.js的Inspector端口,而後配一下Visual Studio Code的localRoot和remoteRoot:
WEBNODE_HOST=${WEBNODE_HOST:-127.0.0.1}
WEBNODE_PORT=${WEBNODE_PORT:-3000}
DOCKER_RUN_OPTIONS="$DOCKER_RUN_OPTIONS \ -it \ --rm \ --network=\"getui-dev\" -p $WEBNODE_HOST:$WEBNODE_PORT:3000 \ -p 127.0.0.1:9229:9229 \ -e NODE_FLAGS=--inspect=0.0.0.0:9229 \ --name $CONTAINER"
docker run \
$DOCKER_RUN_OPTIONS \
$DOCKER_IMAGE_TAG
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "attach",
"name": "Attach Local WebNode",
"address": "127.0.0.1",
"port": 9229,
"restart": true,
"protocol": "inspector",
"localRoot": "${workspaceFolder}",
"remoteRoot": "YOUR_REMOTE_ROOT",
"sourceMaps": true
},
]
}
複製代碼
基於容器的開發能夠帶來諸多好處。一是便於分發,基於 Docker 的 Tag,開發者能夠很方便地作基於小版本、大版本、分支的分發,能夠像 nvm 同樣去切換版本。
二是CLI 腳本不用到處考慮跨平臺兼容的問題,好比:
全部的依賴經過容器帶進來,簡潔而高效。
在基於 Docker 的工具開發的過程當中,咱們也遇到一些問題:
一是容器內外 UID/GID 不一致,若是是以非 ROOT 用戶運行 docker run,會致使容器內程序在掛載的目錄產生的文件權限與當前用戶不一致。
Docker for Mac對於文件權限有一些特別的行爲,具體能夠參見:docs.docker.com/docker-for-…
對於 Host 是 Linux 的狀況,尤爲在 CI 時,須要考慮 UID/GID 的問題。對於這種狀況,咱們選擇覆蓋掉了 entrypoint ,而後用 gosu 去作降權來處理。
CLI_EXEC_UID=${CLI_EXEC_UID:-0}
CLI_EXEC_GID=${CLI_EXEC_GID:-0}
exec gosu $CLI_EXEC_UID:$CLI_EXEC_GID env "$@"
複製代碼
其實RedHat 旗下用於設計container runtime 的daemonless (例如 podman),就很適合作CLI工具,能夠 rootless 運行,又尊重系統的權限配置。然而其目前還沒有成熟,業界採用率也不高,仍須要繼續觀望。
二是有時候 docker run 速度較慢,個推的解決方案是在首次啓動時啓動一個 docker run --detach,而後後續的 CLI 執行徹底經過 docker exec 來進行,這樣避免掉了每次執行命令時啓動的開銷,速度提高明顯。
以上即是個推 Node.js 微服務開發實踐中關於 CLI 工具的實踐,個推試圖標準化、優化項目結構以及鏡像構建,減小組合的可能性,有效下降了存儲、傳輸、構建的成本,讓開發人員更加省時省力。
後續咱們還會繼續爲你們介紹個推的 Docker 鏡像體系設計以及Node.js 微服務開發框架,敬請期待。