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 種:

通過這 6 種命名空間,Docker 就實現了資源的隔離。

但是隻有命名空間的隔離還不夠,這樣還是有問題的,比如如果一個容器佔用了太多的資源,那就會導致別的容器受影響。

怎麼能限制容器的資源訪問呢?

這就需要 linux 操作系統的另一種機制:Control Group。

創建一個 Control Group 可以給它指定參數,比如 cpu 用多少、內存用多少、磁盤用多少,然後加到這個組裏的進程就會受到這個限制。

這樣,創建容器的時候先創建一個 Control Group,指定資源的限制,然後把容器進程加到這個 Control Group 裏,就不會有容器佔用過多資源的問題了。

那這樣就完美了麼?

其實還有一個問題:每個容器都是獨立的文件系統,相互獨立,而這些文件系統之間可能很大部分都是一樣的,同樣的內容佔據了很大的磁盤空間,會導致浪費。

那怎麼解決這個問題呢?

Docker 設計了一種分層機制:

每一層都是不可修改的,也叫做鏡像。那要修改怎麼辦呢?

會創建一個新的層,在這一層做修改

然後通過一種叫做 UnionFS 的機制把這些層合併起來,變成一個文件系統:

這樣如果有多個容器內做了文件修改,只要創建不同的層即可,底層的基礎鏡像是一樣的。

Docker 通過這種分層的鏡像存儲,寫時複製的機制,極大的減少了文件系統的磁盤佔用。

而且這種鏡像是可以複用的,上傳到鏡像倉庫,別人拉下來也可以直接用。

比如下面這張 Docker 架構圖:

docker 文件系統的內容是通過鏡像的方式存儲的,可以上傳到 registry 倉庫。docker pull 拉下來之後經過 docker run 就可以跑起來。

回顧一下 Docker 實現原理的三大基礎技術:

都是缺一不可的。

上圖中還有個 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 是分層存儲的,修改的時候會創建一個新的層,所以這裏的每一行都會創建一個新的層。

這些指令的含義如下:

上面這個 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 的分階段構建,就可以應付大多數前端部署需求了。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/pc12FDV6AiVt8fbC5tK2Gw