容器精簡大挑戰:從 943MB 到 6-34kB

容器給我們的生活帶來了極大便利,人人都喜歡容器,然而容器也很耗空間,動輒幾百兆,上 G 的鏡像是普遍現象。本文我們就學習容器精簡的案例,通過一系列的騷操作,最終講鏡像的大小從 943MB 減小到了 6.32k。

概述

容器是實踐中用來解決與操作軟件版本和包依賴相關的所有問題的有效途徑。人人都喜歡容器,但是用容器就得面對各式各樣龐大和雜亂的鏡像,如果空間有限,則很快就會被充滿,實際上可以通過一些有效的策略來減小鏡像大小。

基本步驟

一個 HTTP 應用容器,可以通過指定端口提供 web 服務。

不進行卷掛載。

原始方案

爲了獲得基準鏡像大小,我們用 node.js 創建一個簡單隻提供 index.js 訪問的簡單的服務器:

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/`);
});

然後,將該文件內置到一個鏡像中,鏡像基於 Node 官方基本鏡像。

FROM node:14
COPY . .
CMD ["node", "index.js"]

編譯

docker build -t cchttp:01 ./

鏡像大小爲 943MB

精簡基礎鏡像

鏡像精簡最常用,最簡單,最明顯的策略之一就是使用較小的基礎鏡像。Node 鏡像中 slim 變體(基於 debian,但預安裝的依賴項較少)和基於 Alpine Linux 的 alpine 變體 。

這兩個基礎鏡像分別爲 node:14-slim 和 node:14-alpine ,其鏡像大小分別減少到 167MB 和 116MB 分別。

Docker 由於鏡像是分層疊加的,node.js 需要依賴很多層的鏡像,除了精簡解決方案目前還沒有其他變小的方法。

更換語言

爲了進一步優化,需要使用運行時依賴項更少的編譯語言。而這時候肯定會首先想到的是一個靜態編譯語言 Golang,這是個常見而且不錯的選擇。在 Golang 中一個基本的 Web 服務代碼如下:

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 官方基礎鏡像,將其打包到鏡像:

FROM golang:1.14
COPY . .
RUN go build -o server .
CMD ["./server"]

基於 golang 的解決方案,鏡像大小 818MB,還是很大。

通過分析發現是由於 golang 基本鏡像中安裝了很多依賴包,這些依賴包在構建 go 軟件時很有用,但不是每個運行時都需要的,所以可以從這兒着手優化。

多階段構建

Docker 支持多階段構建的機制,可以很輕鬆在具有所有必要依賴項的環境中構建代碼,然後將生成的可執行包直接打包到其他鏡像中使用。這樣就可以解決我們上一步遇到需要編譯時工具和包,但是運行時不需要包,這樣可以極大的減少鏡像大小。

注意:Docker 多階段構建的機制是 Docker 17.05 引入的新特性, 如果要使用該功能你需要將 Docker 版本升級到 Docker 17.05 及更高版本。

到多階段構建 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。

靜態編譯結合 scratch 基礎鏡像

13M 的鏡像已經很不錯了,但是還有其他優化的技巧。在 docker 世界中還有幾個基礎鏡像 scratch ,那就是一個 From 0 開始的基礎鏡像,使用該鏡像沒有任何依賴,完全從 0 開始,所以大小也就從 0 開始。Linux 有個發行版 LFS,其全稱是 Linux From Scratch ,就是從零開始自己動手編譯出一個完整的 OS。這個 scratch 基礎鏡像也是這個意思。

爲了讓 scratch 基礎鏡像支持我們的 web.go 運行,我們需要在編譯鏡像中添加靜態編譯的標誌,確保所有依賴都可以打包到運行鏡像中:

編譯

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"]

上面構建過程中,在代碼鏈接過程中模式設置爲 external,-static 鏈接外部鏈接器。

優化後,鏡像大小爲 8.65MB。

最終大殺器——彙編語言

用 Golang 語言編寫的程序,起碼也有大概 M 級別的大小,10MB 鏡像應該已經到了可以精簡的極限。但是還可以用其他技巧來大幅度精簡大小,但是需要使用要給終極大殺器,那就是彙編語言,最終解決方案是使用一個彙編編寫的全功能 http 服務器 assmttpd,其源碼託管在 GitHub(github/nemasu/asmttpd)。

我們還使用多階段編譯方法,在 ubuntu 基礎鏡像中先編譯其依賴項,然後在 Scratch 基礎鏡像中打包並運行。

###編譯###
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"]

產生的鏡像大小僅爲 6.34kB:

然後用該鏡像運行一個容器:

docker run -it -p 10080:8080 cchttp:07

用 curl 訪問一下:

curl -vv http://127.0.0.1:10080

總結

本文我們探索了容器精簡的各種方法和嘗試。當然由於容器的功能簡單,這些策略可能不發直接在實踐中使用,但是可以作爲容器調優的思路參考。

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