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