Docker 映象製作教程:針對不同語言的精簡策略
簡介:
本文主要講解針對不同的語言來選擇適當的精簡策略,其中主要討論 Go,同時也涉及到了 Java,Node,Python,Ruby和 Rust。同時也會詳細介紹 Alpine 映象的避坑指南。
一、Go 語言映象精簡
Go 語言程式編譯時會將所有必須的依賴編譯到二進位制檔案中,但也不能完全肯定它使用的是靜態連結,因為
Go 的某些包是依賴系統標準庫的,例如使用到 DNS 解析的包。只要程式碼中匯入了這些包,編譯的二進位制檔案就需要呼叫到某些系統庫,為了這個需求,Go 實現了一種機制叫
cgo,以允許 Go 呼叫 C 程式碼,這樣編譯好的二進位制檔案就可以呼叫系統庫。
也就是說,如果 Go 程式使用了
net 包,就會生成一個動態的二進位制檔案,如果想讓映象能夠正常工作,必須將需要的庫檔案複製到映象中,或者直接使用
busybox:glibc 映象。
當然,你也可以禁止
cgo
,這樣 Go 就不會使用系統庫,使用內建的實現來替代系統庫(例如使用內建的 DNS 解析器),這種情況下生成的二進位制檔案就是靜態的。可以透過設定環境變數
CGO_ENABLED=0
來禁用 cgo,例如:
FROM
golang
COPY
whatsmyip.go .
ENV
CGO_ENABLED=
0
RUN
go build whatsmyip.go
FROM
scratch
COPY
--from=0 /go/whatsmyip .
CMD
[
"./whatsmyip"
]
由於編譯生成的是靜態二進位制檔案,因此可以直接跑在
scratch 映象中 ?
當然,也可以不用完全禁用
cgo
,可以透過
-tags
引數指定需要使用的內建庫,例如
-tags netgo
就表示使用內建的 net 包,不依賴系統庫:
$
go
build -
tags
netgo whatsmyip.
go
這樣指定之後,如果匯入的其他包都沒有用到系統庫,那麼編譯得到的就是靜態二進位制檔案。也就是說,只要還有一個包用到了系統庫,都會開啟
cgo,最後得到的就是動態二進位制檔案。要想一勞永逸,還是設定環境變數
CGO_ENABLED=0 吧。
二、Alpine 映象探秘
上篇文章已經對
Alpine 映象作了簡要的介紹,並保證會在後面的文章中花很大的篇幅來討論
Alpine 映象,現在時候到了!
Alpine 是眾多 Linux 發行版中的一員,和
CentOS、
Ubuntu、
Archlinux 之類一樣,只是一個發行版的名字,號稱小巧安全,有自己的包管理工具
apk。
與 CentOS 和 Ubuntu 不同,Alpine 並沒有像
Red Hat 或
Canonical 之類的大公司為其提供維護支援,軟體包的數量也比這些發行版少很多(如果只看開箱即用的預設軟體倉庫,Alpine 只有
10000 個軟體包,而 Ubuntu、Debian 和 Fedora 的軟體包數量均大於
50000。)
容器崛起之前,
Alpine 還是個無名之輩,可能是因為大家並不是很關心作業系統本身的大小,畢竟大家只關心業務資料和文件,程式、庫檔案和系統本身的大小通常可以忽略不計。
容器技術席捲整個軟體產業之後,大家都注意到了一個問題,那就是容器的映象太大了,浪費磁碟空間,拉取映象的時間也很長。於是,人們開始尋求適用於容器的更小的映象。對於那些耳熟能詳的發行版(例如 Ubuntu、Debian、Fedora)來說,只能透過刪除某些工具(例如
ifconfig 和
netstat)將映象體積控制在
100M 以下。而對於 Alpine 而言,什麼都不用刪除,映象大小也就只有
5M 而已。
Alpine 映象的另一個優勢是包管理工具的執行速度非常快,安裝軟體體驗非常順滑。誠然,在傳統的虛擬機器上不需要太關心軟體包的安裝速度,同一個包只需要裝一次即可,無需不停重複安裝。容器就不一樣了,你可能會定期構建新映象,也可能會在執行的容器中臨時安裝某些除錯工具,如果軟體包的安裝速度很慢,會很快消磨掉我們的耐心。
為了更直觀,我們來做個簡單的對比測試,看看不同的發行版安裝
tcpdump
需要多長時間,測試命令如下:
? →
time
docker
run
<image> <packagemanager> install tcpdump
測試結果如下:
Base image Size Time to install tcpdump
---------------------------------------------------------
alpine:
3.11
5.6
MB
1
-2
s
archlinux:
20200106
409
MB
7
-9
s
centos:
8
237
MB
5
-6
s
debian:
10
114
MB
5
-7
s
fedora:
31
194
MB
35
-60
s
ubuntu:
18.04
64
MB
6
-8
s
如果你想了解更多關於 Alpine 的內幕,可以看看
。
好吧,既然 Alpine 這麼棒,為什麼不用它作為所有映象的基礎映象呢?別急,先一步一步來,為了趟平所有的坑,需要分兩種情況來考慮:
-
使用 Alpine 作為第二構建階段( run 階段)的基礎映象
-
使用 ALpine 作為所有構建階段( run 階段和 build 階段)的基礎映象
run 階段使用 Alpine
帶著激動的心情,將 Alpine 映象加入了 Dockerfile:
FROM
gcc AS mybuildstage
COPY
hello.c .
RUN
gcc -o hello hello.c
FROM
alpine
COPY
--from=mybuildstage hello .
CMD
[
"./hello"
]
第一個坑來了,啟動容器出現了錯誤:
standard_init_linux.go:211: exec
user
process caused
"no such file or directory"
這個報錯在上篇文章已經見識過了,上篇文章的場景是使用
scratch 映象作為
C 語言程式的基礎映象,錯誤的原因是
scratch 映象中缺少動態庫檔案。可是為什麼使用 Alpine 映象也有報錯,難道它也缺少動態庫檔案?
也不完全是,Alpine 使用的也是動態庫,畢竟它的設計目標之一就是佔用更少的空間。但 Alpine 使用的標準庫與大多數發行版不同,它使用的是
musl libc,這個庫相比於
glibc 更小、更簡單、更安全,但是與大家常用的標準庫
glibc 並不相容。
你可能又要問了:『既然
musl libc 更小、更簡單,還特麼更安全,為啥其他發行版還在用
glibc?』
mmm。。。因為
glibc 有很多額外的擴充套件,並且很多程式都用到了這些擴充套件,而
musl libc 是不包含這些擴充套件的。詳情可以參考
。
也就是說,如果想讓程式跑在 Alpine 映象中,必須在編譯時使用
musl libc
作為動態庫。
所有階段使用 Alpine
為了生成一個與
musl libc
連結的二進位制檔案,有兩條路:
-
某些官方映象提供了 Alpine 版本,可以直接拿來用。
-
還有些官方映象沒有提供 Alpine 版本,我們需要自己構建。
golang 映象就屬於第一種情況,
golang:alpine 提供了基於 Alpine 構建的
Go 工具鏈。
構建 Go 程式可以使用下面的
Dockerfile
:
FROM
golang:alpine
COPY
hello.go .
RUN
go build hello.go
FROM
alpine
COPY
--from=0 /go/hello .
CMD
[
"./hello"
]
生成的映象大小為 7.5M,對於一個只列印 『hello world』的程式來說確實有點大了,但我們可以換個角度:
-
即使程式很複雜,生成的映象也不會很大。
-
包含了很多有用的除錯工具。
-
即使執行時缺少某些特殊的除錯工具,也可以迅速安裝。
Go 語言搞定了,C 語言呢?並沒有
gcc:alpine
這樣的映象啊。只能以 Alpine 映象作為基礎映象,自己安裝 C 編譯器了,Dockerfile 如下:
FROM
alpine
RUN
apk add build-base
COPY
hello.c .
RUN
gcc -o hello hello.c
FROM
alpine
COPY
--from=0 hello .
CMD
[
"./hello"
]
必須安裝 build-base ,如果安裝 gcc ,就只有編譯器,沒有標準庫。build-base 相當於 Ubuntu 的 build-essentials ,引入了編譯器、標準庫和 make 之類的工具。
最後來對比一下不同構建方法得到的 『hello world』映象大小:
-
使用基礎映象 golang 構建:805MB
-
多階段構建,build 階段使用基礎映象 golang ,run 階段使用基礎映象 ubuntu :66.2MB
-
多階段構建,build 階段使用基礎映象 golang:alpine ,run 階段使用基礎映象 alpine :7.6MB
-
多階段構建,build 階段使用基礎映象 golang ,run 階段使用基礎映象 scratch :2MB
最終映象體積減少了
99.75%
,相當驚人了。再來看一個更實際的例子,上一節提到的使用
net
的程式,最終的映象大小對比:
-
使用基礎映象 golang 構建:810MB
-
多階段構建,build 階段使用基礎映象 golang ,run 階段使用基礎映象 ubuntu :71.2MB
-
多階段構建,build 階段使用基礎映象 golang:alpine ,run 階段使用基礎映象 alpine :12.6MB
-
多階段構建,build 階段使用基礎映象 golang ,run 階段使用基礎映象 busybox:glibc :12.2MB
-
多階段構建,build 階段使用基礎映象 golang 並使用引數 CGO_ENABLED=0,run 階段使用基礎映象 ubuntu :7MB
映象體積仍然減少了
99%。
三、Java 語言映象精簡
Java 屬於編譯型語言,但執行時還是要跑在
JVM 中。那麼對於 Java 語言來說,該如何使用多階段構建呢?
靜態還是動態?
從概念上來看,Java 使用的是動態連結,因為 Java 程式碼需要呼叫
JVM 提供的
Java API,這些 API 的程式碼都在可執行檔案之外,通常是
JAR 檔案或
WAR 檔案。
然而這些 Java 庫並不是完全獨立於系統庫的,某些 Java 函式最終還是會呼叫系統庫,例如開啟檔案時需要呼叫
open(),
fopen() 或它們的變體,因此
JVM 本身可能會與系統庫動態連結。
這就意味著理論上可以使用任意的
JVM 來執行 Java 程式,系統標準庫是
musl libc 還是
glibc 都無所謂。因此,也就可以使用任意帶有
JVM 的基礎映象來構建 Java 程式,也可以使用任意帶有
JVM 的映象作為執行 Java 程式的基礎映象。
類檔案格式
Java 類檔案(Java 編譯器生成的位元組碼)的格式會隨著版本而變化,且大部分變化都是
Java API 的變化。還有一部分更改與 Java 語言本身有關,例如
Java 5 中新增了泛型,這種變化就可能會導致類檔案格式的變化,從而破壞與舊版本的相容性。
所以預設情況下,使用給定版本的 Java 編譯器編譯的類不能與更早版本的 JVM 相容,但可以指定編譯器的
-target (Java 8 及其以下版本)引數或者
--release (Java 9 及其以上版本)引數來使用較舊的類檔案格式。
--release 引數還可以指定類檔案的路徑,以確保程式執行在指定的 JVM 版本中(例如 Java 11),不會意外呼叫 Java 12 的 API。
JDK vs JRE
如果你對大多數平臺上的 Java 打包方式很熟悉,那你應該知道
JDK 和
JRE。
JRE 即 Java 執行時環境(
Java Runtime Environment),包含了執行 Java 程式所需要的環境,即
JVM。
JDK 即 Java 開發工具包(
Java Development Kit),既包含了 JRE,也包含了開發 Java 程式所需的工具,即 Java 編譯器。
大多數 Java 映象都提供了 JDK 和 JRE 兩種標籤,因此可以在多階段構建的
build 階段使用
JDK 作為基礎映象,
run 階段使用
JRE 作為基礎映象。
Java vs OpenJDK
推薦使用
openjdk,因為開源啊,更新勤快啊~~
也可以使用
,這是
Amazon fork OpenJDK 後打了補丁的版本,號稱企業級。
開始構建
說了那麼多,到底該用哪個映象呢?這裡給出幾個參考:
-
openjdk:8-jre-alpine (85MB)
-
openjdk:11-jre (267MB)或者 openjdk:11-jre-slim (204MB)
-
openjdk:14-alpine (338MB)
如果你想要更直觀的資料,可以看我的例子,還是搬出屢試不爽的 『hello world』,只不過這次是 Java 版本:
class
hello
{
public
static
void
main
(
String
[] args)
{
System.out.
println
(
"Hello, world!"
);
}
}
不同構建方法得到的映象大小:
-
使用基礎映象 java 構建:643MB
-
使用基礎映象 openjdk 構建:490MB
-
多階段構建,build 階段使用基礎映象 openjdk ,run 階段使用基礎映象 openjdk:jre :479MB
-
使用基礎映象 amazoncorretto 構建:390MB
-
多階段構建,build 階段使用基礎映象 openjdk:11 ,run 階段使用基礎映象 openjdk:11-jre :267MB
-
多階段構建,build 階段使用基礎映象 openjdk:8 ,run 階段使用基礎映象 openjdk:8-jre-alpine :85MB
所有的 Dockerfile 都可以在
這個倉庫找到。
四、解釋型語言映象精簡
對於諸如
Node、
Python、
Rust 之類的解釋型語言來說,情況就比較複雜一點了。先來看看 Alpine 映象。
Alpine 映象
對於解釋型語言來說,如果程式僅用到了標準庫或者依賴項和程式本身使用的是同一種語言,且無需呼叫 C 庫和外部依賴,那麼使用
Alpine
作為基礎映象一般是沒有啥問題的。一旦你的程式需要呼叫外部依賴,情況就複雜了,想繼續使用 Alpine 映象,就得安裝這些依賴。根據難度可以劃分為三個等級:
-
簡單 :依賴庫有針對 Alpine 的安裝說明,一般會說明需要安裝哪些軟體包以及如何建立依賴關係。但這種情況非常罕見,原因前面也提到了,Alpine 的軟體包數量比大多數流行的發行版要少得多。
-
中等 :依賴庫沒有針對 Alpine 的安裝說明,但有針對別的發行版的安裝說明。我們可以透過對比找到與別的發行版的軟體包相匹配的 Alpine 軟體包(假如有的話)。
-
困難 :依賴庫沒有針對 Alpine 的安裝說明,但有針對別的發行版的安裝說明,但是 Alpine 也沒有與之對應的軟體包。這種情況就必須從原始碼開始構建!
最後一種情況最不推薦使用 Alpine 作為基礎映象,不但不能減小體積,可能還會適得其反,因為你需要安裝編譯器、依賴庫、標頭檔案等等。。。更重要的是,構建時間會很長,效率低下。如果非要考慮多階段構建,就更復雜了,
你得搞清楚如何將所有的依賴編譯成二進位制檔案,想想就頭大。因此一般不推薦在解釋型語言中使用多階段構建。
有一種特殊情況會同時遇到 Alpine 的絕大多數問題:
將 Python 用於資料科學。
numpy 和
pandas 之類的包都被預編譯成了
,
wheel 是 Python 新的打包格式,被編譯成了二進位制,用於替代 Python 傳統的
egg 檔案,可以透過
pip 直接安裝。但這些 wheel 都繫結了特定的 C 庫,這就意味著在大多數使用
glibc 的映象中都可以正常安裝,但 Alpine 映象就不行,原因你懂得,前面已經說過了。如果非要在 Alpine 中安裝,你需要安裝很多依賴,重頭構建,耗時又費力,有一篇文章專門解釋了這個問題:
。
既然 Alpine 映象這麼坑,那麼是不是隻要是 Python 寫的程式就不推薦使用 Alpine 映象來構建呢?也不能完全這麼肯定,至少 Python 用於資料科學時不推薦使用 Alpine,其他情況還是要具體情況具體分析,如果有可能,還是可以試一試 Alpine 的。
:slim 映象
如果實在不想折騰,可以選擇一個折衷的映象
xxx:slim。slim 映象一般都基於
Debian 和
glibc,刪除了許多非必需的軟體包,最佳化了體積。如果構建過程中需要編譯器,那麼 slim 映象不適合,除此之外大多數情況下還是可以使用 slim 作為基礎映象的。
下面是主流的解釋型語言的 Alpine 映象和 slim 映象大小對比:
Image Size
---------------------------
node
939
MB
node
:alpine
113
MB
node
:slim
163
MB
python
932
MB
python:alpine
110
MB
python:slim
193
MB
ruby
842
MB
ruby:alpine
54
MB
ruby:slim
149
MB
再來舉個特殊情況的例子,同時安裝
matplotlib
,
numpy
和
pandas
,不同的基礎映象構建的映象大小如下:
Image and technique Size
--------------------------------------
python 1.26 GB
python:slim 407 MB
python:alpine 523 MB
python:alpine multi-stage 517 MB
可以看到這種情況下使用 Alpine 並沒有任何幫助,即使使用多階段構建也無濟於事。
但也不能全盤否定 Alpine,比如下面這種情況:包含大量依賴的
Django
應用。
Image and technique Size
--------------------------------------
python 1.23 GB
python:alpine 636 MB
python:alpine multi-stage 391 MB
最後來總結一下:
到底使用哪個基礎映象並不能蓋棺定論,有時使用 Alpine 效果更好,有時反而使用 slim 效果更好,如果你對映象體積有著極致的追求,可以這兩種映象都嘗試一下。相信隨著時間的推移,我們就會積累足夠的經驗,知道哪種情況該用 Alpine,哪種情況該用 slim,不用再一個一個嘗試。
五、Rust 語言映象精簡
Rust 是最初由
Mozilla 設計的現代程式語言,並且在
Web 和基礎架構領域中越來越受歡迎。Rust 編譯的二進位制檔案動態連結到 C 庫,可以正常執行於
Ubuntu、
Debian 和
Fedora 之類的映象中,但不能執行於
busybox:glibc 中。因為 Rust 二進位制需要呼叫
libdl 庫,
busybox:glibc 中不包含該庫。
還有一個
rust:alpine 映象,Rust 編譯的二進位制也可以正常執行其中。
如果考慮編譯成靜態連結,可以參考
。在 Linux 上需要構建一個特殊版本的 Rust 編譯器,構建的依賴庫就是
musl libc
,你沒有看錯,就是 Alpine 中的那個
musl libc
。如果你想獲得更小的映象,請按照文件中的說明進行操作,最後將生成的二進位制檔案扔進
scratch
映象中就好了。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70011781/viewspace-2848661/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 製作Docker映象Docker
- Docker製作jdk映象DockerJDK
- Docker 映象製作方法Docker
- YashanDB Docker映象製作Docker
- oracle製作docker映象OracleDocker
- Docker二所映象製作Docker
- Docker如何製作映象-Dockerfile的使用Docker
- docker 製作與使用 arcgisserver 映象DockerServer
- 伺服器:如何製作docker映象伺服器Docker
- 使用 Caddy 製作前端 Docker 映象前端Docker
- 製作 Python Docker 映象的最佳實踐PythonDocker
- docker製作自己的映象並上傳dockerhubDocker
- Docker最佳實踐:5個方法精簡映象Docker
- docker容器 如何精簡映象減小體積Docker
- 自編譯製作docker版本的onlyoffice映象編譯Docker
- goldengate針對不同表名及列名的複製Go
- Docker 入門系列四:Dockerfile-映象製作Docker
- 無需依賴Docker環境製作映象Docker
- docker決戰到底(五) 製作自己的Jenkins映象DockerJenkins
- 自己動手製作elasticsearch-head的Docker映象ElasticsearchDocker
- C語言練手專案--C 語言製作簡單計算器C語言
- Dockerfile映象的製作Docker
- 智雲通CRM:針對不同的客戶需求,有哪些溝通策略?
- ROM簡單製作教程
- 使用Docker搭建WordPress部落格(三)nginx映象製作DockerNginx
- Docker 必知必會3----使用自己製作的映象Docker
- Docker 映象加速教程Docker
- 移動應用如何針對不同使用者精準定製應用內容或風格?
- 自己動手製作elasticsearch的ik分詞器的Docker映象Elasticsearch分詞Docker
- proxmox映象製作
- 製作KubeVirt映象
- Docker從零開始製作基礎映象之CentosDockerCentOS
- 製作一個龍芯舊世界的 dotnet sdk docker 映象Docker
- centos製作具備telnet和ping功能的docker映象CentOSDocker
- 製作簡單的個人網頁教程網頁
- Docker介紹下載安裝、製作映象及容器、做目錄對映、做埠對映Docker
- 【實踐】Docker for Windows 製作tomcat 映象並上傳至 docker 倉庫DockerWindowsTomcat
- VSFTP針對不同的使用者限制不同的速度FTP