減小容器鏡像的三板斧


在構建容器鏡像時,我們總是希望得到尺寸更小的鏡像。比如盡可能的減少鏡像中的層數,因為創建新的層是有代價的,每個層都會產生一些數據上的開銷。常見的手段是通過 && 把多個 RUN 指令合並為一個:

# 兩個 RUN 指令會創建兩個鏡像層
FROM ubuntu
RUN apt-get update
RUN apt-get install vim

# 通過 && 把兩個 RUN 指令合並為一個,
# 這樣只會創建一個鏡像層,
# 從而減小最終鏡像的尺寸
FROM ubuntu
RUN apt-get update && apt-get install vim

而現在,我們有了更多的選擇。我們可以使用 multi-stage 技術(關於 multi-stage 技術,請參考筆者的博文《Dockerfile 中的 multi-stage》)來減小容器鏡像的尺寸,並且還可以使用 multi-stage 技術結合不同的父鏡像來極大的減小最終的鏡像的尺寸。有沒有很心動呢?接下來就讓我們通過 demo 演示筆者砍掉鏡像虛膘的三板斧!

通過 multi-stage 減小鏡像尺寸

讓我們創建一個 host 在容器中的 nodejs 程序,並用它來進行本文的 demo 演示。先創建 index.js 文件,其內容如下:

const express = require('express')
const app = express()

app.get('/', (req, res) => res.send('Hello World!'))

app.listen(3000, () => {
  console.log(`Example app listening on port 3000!`)
})

然后創建 package.json 文件,內容如下:

{
  "name": "hello-world",
  "version": "1.0.0",
  "main": "index.js",
  "dependencies": {
    "express": "^4.16.2"
  },
  "scripts": {
    "start": "node index.js"
  }
}

最后創建的 Dockerfile 文件內容如下:

FROM node:8

EXPOSE 3000
WORKDIR /app
COPY package.json index.js ./
RUN npm install

CMD ["npm", "start"]

我們使用父鏡像 node:8 來構建並運行上面的 nodejs 應用:

$ docker build -t node-demo .
$ docker run -p 3000:3000 -ti --rm --init node-demo

這樣應用程序就運行起來了,然后可以通過 http://localhost:3000/ 訪問它。

現在讓我們把注意力轉移到運行容器的鏡像 node-demo 身上,先看看它的構建歷史:

$ docker history node-demo

我們的構建過程只是在父進行的基礎上增了 3M 多一點的數據。
下面在 Dockerfile 中使用 multi-stage(關於 multi-stage 技術,請參考筆者的博文《Dockerfile 中的 multi-stage》):

FROM node:8 as build

WORKDIR /app
COPY package.json index.js ./
RUN npm install

FROM node:8

COPY --from=build /app /
EXPOSE 3000
CMD ["npm", "start"]

把上面的內容保存到 Dockerfile.multi 文件中,然后構建鏡像 node-demo-multi:

$ docker build --no-cache -t node-demo-multi . -f Dockerfile.multi

構建成功后看看 node-demo-multi 的歷史:

$ docker history node-demo-multi

這次在父鏡像的基礎上增加的數據更少,總共只有 1.63M,應該是 multi-stage 在中間過程中進行了層合並的結果。然后再對比一下兩個鏡像的大小:

$ docker images|grep node-demo

在我們的場景下使用了 multi-stage 產生的鏡像只比沒有使用的情況小了 1M,但是對於鏡像層數比較多的場景效果會更加明顯。關於 multi-stage 的更多內容,請參考筆者的博文《Dockerfile 中的 multi-stage》。

移除鏡像中的非必要內容

以我們使用的父鏡像 node:8 為例:

其 676M 的體積中包含了眾多運行 nodejs 應用並不需要的程序,比如 npm,bash 和其它眾多的文件。如果我們能夠把這些不需要的文件全部移除,鏡像就會縮小很多。但是,具體該怎么做呢?答案是:使用合適的父鏡像!
Google 在 github 上創建了 distroless 項目專門來做這件事!Distroless 項目提供的容器鏡像只包含運行應用程序的最新集合,所以能夠把鏡像壓縮到很小。但是需要為不同的應用程序使用不同的鏡像,下面是當前已經提供的鏡像:

讓我們使用 distroless 提供的 nodejs 鏡像來重新構建 demo 應用的鏡像:

FROM node:8 as build

WORKDIR /app
COPY package.json index.js ./
RUN npm install

FROM gcr.io/distroless/nodejs

COPY --from=build /app /
EXPOSE 3000
CMD ["index.js"]

把上面的內容保存到文件 Dockerfile.less 中,並運行下面的命令:

$ docker build --no-cache -t node-demo-less . -f Dockerfile.less
$ docker run -p 3000:3000 -ti --rm --init node-demo-less

應用程序可以正常的運行,然后讓我們看看剛才構建的容器鏡像 node-demo-less:

只有 76.7M!這確實太不可思議了,在吃驚之於讓我們回歸理性,看看 distroless 究竟是如何把鏡像做的這么小?我想先用 docker exec 命令進入容器內部看看情況,結果是我無法用下面的命令進入到容器內部:

$  docker exec -it <container id> bash

結論是為了減小鏡像的大小,鏡像中沒有 bash 這樣的工具。那么鏡像中有什么?答案是只有 nodejs。你唯一能通過 docker exec 運行的命令就是:

$ docker exec -it <container id> node

這同樣讓人大吃一驚,不能進入容器的話可怎么調查故障?但現實就是這么殘酷,當你享受極小的鏡像時,你也為方便性付出了代價。其實故障調查完全可以通過完善的日志系統來解決。鏡像中只有一個程序帶來的另一個好處是安全性的提升!

使用 Alpine 作為 base 鏡像

除了 Google 的 distroless 項目,其實我們還可以有其他的選擇,它就是 Alpine。使用 Alpine 作為父鏡像同樣也會為你帶來意想不到的驚喜(如果你留意了《Dockerfile 中的 multi-stage》一文中的鏡像大小)!

Alpine Linux 是一個基於 musl libc 和 busybox 的,以安全性為目標的輕量級 Linux 發行版。換句話說就是:它的 size 更小,安全性更高!
接下來讓我們用 Alpine 版的父鏡像構建 demo 程序鏡像:

FROM node:8 as build

WORKDIR /app
COPY package.json index.js ./
RUN npm install

FROM node:8-alpine

COPY --from=build /app /
EXPOSE 3000
CMD ["npm", "start"]

把上面的內容保存到文件 Dockerfile.alpine 中,並運行下面的命令:

$ docker build --no-cache -t node-demo-alpine . -f Dockerfile.alpine
$ docker run -p 3000:3000 -ti --rm --init node-demo-alpine

應用程序可以正常的運行,然后讓我們看看剛才構建的容器鏡像 node-demo-alpine:

只有 69.7M,比使用 distroless 項目創建的鏡像還要小!能用 docker exec 命令進入容器嗎?讓我們運行一個命名的容器試試:

$ docker run -p 3000:3000 -d --name democon --init node-demo-alpine
$ docker exec -it democon sh

這次我們成功的進入了容器,雖然不支持 bash,但能有個 shell 哥們就感覺幸福無邊了!

Alpine Linux 看起來很完美,但是請把接下來的文章讀完!
Alpine Linux 中的 C 庫不是我們常用的 glibc,而是 muslc。也就是說,使用基於 Alpine 的鏡像有可能產生 C 庫不同導致的問題。舉個例子,PhantomJS 就不能在 Alpine 中正常工作。

我們究竟該如何選擇父鏡像

  • 如果是在生產環境中使用,並且有安全性的考慮,建議使用 distroless 鏡像。
  • 如果要保持盡可能小的鏡像,建議使用 alpine 進行。
  • 開發、測試環境中建議使用官方鏡像通過 multi-stage 構建,方便問題調查。

下表展示了使用不同方式構建的鏡像大小:

總結

控制鏡像的大小是一件永遠在路上的事情,使用 multi-stage 技術並選擇合適的父鏡像可以把我們從繁瑣的操作中解放出來,這就是生產力啊!需要注意的是,你需要小心的選擇父鏡像,選擇體積又小又適合你的應用程序的鏡像可是個技術活兒呢!

參考:
3 simple tricks for smaller Docker images


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM