從943MB到6.34kB,容器精簡大挑戰(zhàn)
容器給我們的生活帶來了極大便利,人人都喜歡容器,然而容器也很耗空間,動輒幾百兆,上G的鏡像是普遍現(xiàn)象。本文我們就學(xué)習(xí)容器精簡的案例,通過一系列的騷操作,最終將鏡像的大小從943MB減小到了6.32k。
概述
容器是實踐中用來解決與操作軟件版本和包依賴相關(guān)的所有問題的有效途徑。 人人都喜歡容器,但是用容器就得面對各式各樣龐大和雜亂的鏡像,如果空間有限,則很快就會被充滿,實際上可以通過一些有效的策略來減小鏡像大小。
基本步驟
一個Http應(yīng)用容器,可以通過指定端口提供web服務(wù)。
不進行卷掛載。
原始方案
為了獲得基準(zhǔn)鏡像大小,我們用node.js創(chuàng)建一個簡單只提供index.js訪問的簡單的服務(wù)器:
index.js代碼:
- const fs = require("fs");
- const http = require('http');
- const server = http.createServer((req, res) => {
- res.writeHead(200, { 'content-type': 'text/html' })
- fs.createReadStream('index.html').pipe(res)
- })
- server.listen(port, hostname, () => {
- console.log(`Server: http://0.0.0.0:8080/`);
- });
然后,將該文件內(nèi)置到一個鏡像中,鏡像基于Node官方基本鏡像。
- FROM node:14
- COPY . .
- CMD ["node", "index.js"]
編譯
- docker build -t cchttp:01 ./
鏡像大小為943MB
精簡基礎(chǔ)鏡像
鏡像精簡最常用,最簡單,最明顯的策略之一就是使用較小的基礎(chǔ)圖像。Node鏡像中slim 變體(基于debian,但預(yù)安裝的依賴項較少)和基于Alpine Linux的alpine變體 。
這兩個基礎(chǔ)鏡像分別為node:14-slim 和 node:14-alpine ,其鏡像大小分別減少到167MB 和 116MB 分別。
Docker由于鏡像是分層疊加的,node.js需要依賴很多層的鏡像,除了精簡解決方案目前還沒有其他變小的方法。
更換語言
為了進一步優(yōu)化,需要使用運行時依賴項更少的編譯語言。而這時候肯定會首先想到的是一個靜態(tài)編譯語言Golang,這是個常見而且不錯的選擇。在Golang中一個基本的Web服務(wù)代碼如下:
web.go:
- package main
- import (
- "fmt"
- "log"
- "net/http"
- )
- func main() {
- fileServer := http.FileServer(http.Dir("./"))
- http.Handle("/", fileServer)
- fmt.Printf("Starting server at port 8080\n")
- if err := http.ListenAndServe(":8080", nil); err != nil {
- log.Fatal(err)
- }
- }
然后用golang官方基礎(chǔ)鏡像,將其打包到鏡像:
- FROM golang:1.14
- COPY . .
- RUN go build -o server .
- CMD ["./server"]
基于golang的解決方案,鏡像大小818MB,還是很大。
通過分析發(fā)現(xiàn)是由于golang基本鏡像中安裝了很多依賴包,這些依賴包在構(gòu)建go軟件時很有用,但不是每個運行時都需要的,所以可以從這兒著手優(yōu)化。
多階段構(gòu)建
Docker支持多階段構(gòu)建的機制,可以很輕松在具有所有必要依賴項的環(huán)境中構(gòu)建代碼,然后將生成的可執(zhí)行包直接打包到其他鏡像中使用。這樣就可以解決我們上一步遇到需要編譯時工具和包,但是運行時不需要包,這樣可以極大地減少鏡像大小。
注意:Docker多階段構(gòu)建的機制是Docker 17.05引入的新特性,如果要使用該功能你需要將Docker版本升級到Docker 17.05及更高版本。
到多階段構(gòu)建dockerfile:
- ###編譯###
- FROM golang:1.14-alpine AS builder
- COPY . .
- RUN go build -o server .
- ###運行###
- FROM alpine:3.12
- COPY --from=builder /go/server ./server
- COPY index.html index.html
- CMD ["./server"]
- Docker images
(⊙o⊙)哇,策略生效,這樣生成的鏡像只有13.2MB。
靜態(tài)編譯結(jié)合scratch基礎(chǔ)鏡像
13M的鏡像已經(jīng)很不錯了,但是還有其他優(yōu)化的技巧。在docker世界中還有幾個基礎(chǔ)鏡像scratch ,那就是一個From 0 開始的基礎(chǔ)鏡像,使用該鏡像沒有任何依賴,完全從0開始,所以大小也就從0開始。Linux 有個發(fā)行版LFS,其全稱是Linux From Scratch ,就是從零開始自己動手編譯出一個完整的OS。這個scratch基礎(chǔ)鏡像也是這個意思。
為了讓scratch基礎(chǔ)鏡像支持我們的web.go運行,我們需要在編譯鏡像中添加靜態(tài)編譯的標(biāo)志,確保所有依賴都可以打包到運行鏡像中:
- ### 編譯###
- FROM golang:1.14 as builder
- COPY . .
- RUN go build -o server \
- -ldflags "-linkmode external -extldflags -static" \
- -a web.go
- ###運行###
- FROM scratch
- COPY --from=builder /go/server ./server
- COPY index.html index.html
- CMD ["./server"]
上面構(gòu)建過程中,在代碼鏈接過程中模式設(shè)置為external,-static鏈接外部鏈接器。
優(yōu)化后,鏡像大小為8.65MB。
最終大殺器——匯編語言
用Golang語言編寫的程序,起碼也有大概M級別的大小,10MB鏡像應(yīng)該已經(jīng)到了可以精簡的極限。但是還可以用其他技巧來大幅度精簡大小,但是需要使用要給終極大殺器,那就是匯編語言,最終解決方案是使用一個匯編編寫的全功能http服務(wù)器assmttpd,其源碼托管在GitHub(github/nemasu/asmttpd)。
我們還使用多階段編譯方法,在ubuntu基礎(chǔ)鏡像中先編譯其依賴項,然后在Scratch基礎(chǔ)鏡像中打包并運行。
- ###編譯###
- FROM ubuntu:18.04 as builder
- RUN apt update
- RUN apt install -y make yasm as31 nasm binutils
- COPY . .
- RUN make release
- ###運行###
- FROM scratch
- COPY --from=builder /asmttpd /asmttpd
- COPY /web_root/index.html /web_root/index.html
- CMD ["/asmttpd", "/web_root", "8080"]
產(chǎn)生的圖像大小僅為6.34kB:
然后用該鏡像運行一個容器:
- docker run -it -p 10080:8080 cchttp:07
用curl訪問一下:
- curl -vv 127.0.0.1:10080
總結(jié)
本文我們探索了容器精簡的各種方法和嘗試。當(dāng)然由于容器的功能簡單,這些策略可能不發(fā)直接在實踐中使用,但是可以作為容器調(diào)優(yōu)的思路參考。