Docker 是怎么實現的?前端怎么用 Docker 做部署?
代碼開發完之后,要經過構建,把產物部署到服務器上跑起來,這樣才能被用戶訪問到。
不同的代碼需要不同的環境,比如 JS 代碼的構建需要 node 環境,Java 代碼 需要 JVM 環境,一般我們會把它們隔離開來單獨部署。
現在一臺物理主機的性能是很高的,完全可以同時跑很多個服務,而我們又有環境隔離的需求,所以會用虛擬化技術把一臺物理主機變為多臺虛擬主機來用。
現在主流的虛擬化技術就是 docker 了,它是基于容器的虛擬化技術。
它可以在一臺機器上跑多個容器,每個容器都有獨立的操作系統環境,比如文件系統、網絡端口等。
這也是為什么它的 logo 是這樣的:
那它是怎么實現的這種隔離的容器呢?
這就依賴操作系統的機制了:
linix 提供了一種叫 namespace 的機制,可以給進程、用戶、網絡等分配一個命名空間,這個命名空間下的資源都是獨立命名的。
比如 PID namespace,也就是進程的命名空間,它會使命名空間內的這個進程 id 變為 1,而 linux 的初始進程的 id 就是 1,所以這個命名空間內它就是所有進程的父進程了。
而 IPC namespace 能限制只有這個 namespace 內的進程可以相互通信,不能和 namespace 外的進程通信。
Mount namespace 會創建一個新的文件系統,namespace 內的文件訪問都是在這個文件系統之上。
類似這樣的 namespace 一共有 6 種:
- PID namespace:進程 id 的命名空間
- IPC namespace:進程通信的命名空間
- Mount namespace:文件系統掛載的命名空間
- Network namespace:網絡的命名空間
- User namespace:用戶和用戶組的命名空間
- UTS namespace:主機名和域名的命名空間
通過這 6 種命名空間,Docker 就實現了資源的隔離。
但是只有命名空間的隔離還不夠,這樣還是有問題的,比如如果一個容器占用了太多的資源,那就會導致別的容器受影響。
怎么能限制容器的資源訪問呢?
這就需要 linux 操作系統的另一種機制:Control Group。
創建一個 Control Group 可以給它指定參數,比如 cpu 用多少、內存用多少、磁盤用多少,然后加到這個組里的進程就會受到這個限制。
這樣,創建容器的時候先創建一個 Control Group,指定資源的限制,然后把容器進程加到這個 Control Group 里,就不會有容器占用過多資源的問題了。
那這樣就完美了么?
其實還有一個問題:每個容器都是獨立的文件系統,相互獨立,而這些文件系統之間可能很大部分都是一樣的,同樣的內容占據了很大的磁盤空間,會導致浪費。
那怎么解決這個問題呢?
Docker 設計了一種分層機制:
每一層都是不可修改的,也叫做鏡像。那要修改怎么辦呢?
會創建一個新的層,在這一層做修改
然后通過一種叫做 UnionFS 的機制把這些層合并起來,變成一個文件系統:
這樣如果有多個容器內做了文件修改,只要創建不同的層即可,底層的基礎鏡像是一樣的。
Docker 通過這種分層的鏡像存儲,寫時復制的機制,極大的減少了文件系統的磁盤占用。
而且這種鏡像是可以復用的,上傳到鏡像倉庫,別人拉下來也可以直接用。
比如下面這張 Docker 架構圖:
docker 文件系統的內容是通過鏡像的方式存儲的,可以上傳到 registry 倉庫。docker pull 拉下來之后經過 docker run 就可以跑起來。
回顧一下 Docker 實現原理的三大基礎技術:
- Namespace:實現各種資源的隔離
- Control Group:實現容器進程的資源訪問限制
- UnionFS:實現容器文件系統的分層存儲,寫時復制,鏡像合并
都是缺一不可的。
上圖中還有個 docker build 是干啥的呢?
一般我們生成鏡像都是通過 dockerfile 來描述的。
比如這樣:
FROM node:10
WORKDIR /app
COPY . /app
EXPOSE 8080
RUN npm install http-server -g
RUN npm install && npm run build
CMD http-server ./dist
Dokcer 是分層存儲的,修改的時候會創建一個新的層,所以這里的每一行都會創建一個新的層。
這些指令的含義如下:
- FROM:基于一個基礎鏡像來修改
- WORKDIR:指定當前工作目錄
- COPY:把容器外的內容復制到容器內
- EXPOSE:聲明當前容器要訪問的網絡端口,比如這里起服務會用到 8080
- RUN:在容器內執行命令
- CMD:容器啟動的時候執行的命令
上面這個 dockerfile 的作用不難看出來,就是在 node 環境下,把項目復制過去,執行依賴安裝和構建。
我們通過 docker build 就可以根據這個 dockerfile 來生成鏡像。
然后執行 docker run 把這個鏡像跑起來,這時候就會執行 http-server ./dist 來啟動服務。
這個就是一個 docker 跑 node 靜態服務的例子。
但其實這個例子不是很好,從上面流程的描述我們可以看出來,構建的過程只是為了拿到產物,容器運行的時候就不再需要了。
那能不能把構建分到一個鏡像里,然后把產物賦值到另一個鏡像,這樣單獨跑產物呢?
確實可以,而且這也是推薦的用法。
那豈不是要 build 寫一個 dockerfile,run 寫一個 dockerfile 嗎?
也不用,docker 支持多階段構建,比如這樣:
# build stage
FROM node:10 AS build_image
WORKDIR /app
COPY . /app
EXPOSE 8080
RUN npm install && npm run build
# production stage
FROM node:10
WORKDIR /app
COPY --from=build_image /app/dist ./dist
RUN npm i -g http-server
CMD http-server ./dist
我們把兩個鏡像的生成過程寫到了一個 dockerfile 里,這是 docker 支持的多階段構建。
第一個 FROM 里我們寫了 as build_image,這是把第一個鏡像命名為 build_image。
后面第二個鏡像 COPY 的時候就可以指定 --from=build_image 來從那個鏡像復制內容了。
這樣,最終只會留下第二個鏡像,這個鏡像里只有生產環境需要的依賴,體積更小。傳輸速度、運行速度也會更快。
構建鏡像和運行鏡像分離,這個算是一種最佳實踐了。
一般我們都是在 jenkins 里跑,push 代碼的時候,通過 web hooks 觸發 jenkins 構建,最終產生運行時的鏡像,上傳到 registry。
部署的時候把這個鏡像 docker pull 下來,然后 docker run 就完成了部署。
node 項目的 dockerfile 大概怎么寫我們知道了,那前端項目呢?
大概是這樣的:
# build stage
FROM node:14.15.0 as build-stage
WORKDIR /app
COPY package.json ./
RUN npm install
COPY . .
RUN npm run build
# production stage
FROM nginx:stable-perl as production-stage
COPY --from=build-stage /app/dist /usr/share/nginx/html
COPY --from=build-stage /app/default.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
也是 build 階段通過一個鏡像做構建,然后再制作一個鏡像把產物復制過去,然后用 nginx 跑一個靜態服務。
一般公司內部署前端項目都是這樣的。
不過也不一定。
因為公司部署前端代碼的服務是作為 CDN 的源站服務器的,CDN 會從這里取文件,然后在各地區的緩存服務器緩存下來。
而阿里云這種云服務廠商都提供了對象存儲服務,可以直接把靜態文件上傳到 oss,根本不用自己部署:
但是,如果是內部的網站,或者私有部署之類的,還是要用 docker 部署的。
總結
Docker 是一種虛擬化技術,通過容器的方式,它的實現原理依賴 linux 的 Namespace、Control Group、UnionFS 這三種機制。
Namespace 做資源隔離,Control Group 做容器的資源限制,UnionFS 做文件系統的鏡像存儲、寫時復制、鏡像合并。
一般我們是通過 dockerfile 描述鏡像構建的過程,然后通過 docker build 構建出鏡像,上傳到 registry。
鏡像通過 docker run 就可以跑起來,對外提供服務。
用 dockerfile 做部署的最佳實踐是分階段構建,build 階段單獨生成一個鏡像,然后把產物復制到另一個鏡像,把這個鏡像上傳 registry。
這樣鏡像是最小的,傳輸速度、運行速度都比較快。
前端、node 的代碼都可以用 docker 部署,前端代碼的靜態服務還要作為 CDN 的源站服務器,不過我們也不一定要自己部署,很可能直接用阿里云的 OSS 對象存儲服務了。
理解了 Docker 的實現原理,知道了怎么寫 dockerfile 還有 dockerfile 的分階段構建,就可以應付大多數前端部署需求了。