使用Docker構建容器能夠極大的降低運維成本,提高部署效率,同時非常方便對服務的平行擴展。然而在構建容器鏡像過程中的,存在著一個難以避免的問題,就是如果使用常見的發行版本作為程式運行的基礎環境,那麼即使一個服務本身的運行文件非常小,最終構建的鏡像也可能會有會在運行環境的鏡像的基礎上變得更大,動不動 ...
使用Docker構建容器能夠極大的降低運維成本,提高部署效率,同時非常方便對服務的平行擴展。然而在構建容器鏡像過程中的,存在著一個難以避免的問題,就是如果使用常見的發行版本作為程式運行的基礎環境,那麼即使一個服務本身的運行文件非常小,最終構建的鏡像也可能會有會在運行環境的鏡像的基礎上變得更大,動不動就是數百M的體積。
以最常用於微服務開發的golang為例,golang的二進位程式可以一次開發跨平臺編譯,到處運行,因此其本身的程式自洽性其實非常完善,也很少會依賴複雜的外部環境,因此常用的發行版本鏡像碩大的體積其實很沒必要。因此,alpine成為了一個非常好的選擇。最終alpine也不負眾望,將最終的鏡像體積減小到了10M+左右,已經壓縮了非常大的空間了,結果還算理想。
然而,能不能更小呢?
docker自帶的scratch鏡像給了我一個思路:沒有任何鏡像會比空鏡像更小。那使用scratch製作出來的鏡像也必然是最小的。那麼scratch是不是一個好的選擇呢?scratch鏡像是docker自帶的空鏡像,我也曾見過數篇文章推薦使用其作為golang的運行鏡像,然而,在最終實踐的時候,遇到的bug卻會讓人倍感疑惑。
# 1. 使用官方的golang鏡像構建運行的容器
golang程式非常簡單:
package main
import "fmt"
func main(){
fmt.Println("你好,世界!")
}
Dockerfile也不難:
FROM library/golang:alpine as build
MAINTAINER fanxiaoqiang <[email protected]>
ADD . /data/
WORKDIR /data/
RUN export GO111MODULE=on
RUN export GOSUMDB=off
RUN unset GOPATH
RUN go env -w GOPROXY=https://goproxy.cn
RUN go build -o server helloworld.go
# deploy-image
FROM scratch
#FROM alpine
COPY --from=build /data/server /data/server
#COPY --from=build /data/entrypoint.sh /data/entrypoint.sh
WORKDIR /data/
EXPOSE 9090
CMD ["/data/server"]
構建,運行!
docker build -t hello .
&& docker run
-p 9090:9090
hello
容器成功的輸出了helloword,最終的鏡像大小隻有2.068MB,效果非常理想。然後scratch是否真的能夠滿足golang容器化的所有需求呢?下麵我們繼續看。
# 2. 使用scratch構建稍複雜的golang的運行容器
這一次我們構建一個golang實現的http伺服器。在開始之前,首先強調一下scratch是一個空鏡像,意味這Docker內部不存在任何環境和依賴庫。機智的讀者可能已經想到我想說什麼,接下來開始進行實驗。
golang程式:
package main
import (
"log"
"net/http"
"os"
"os/signal"
"time"
)
func main() {
server := http.Server{
Addr: ":9090",
ReadTimeout: 10 * time.Second,
}
//log.Println("start running")
log.Println("start running")
server.ListenAndServe()
//合建chan
c := make(chan os.Signal)
//監聽指定信號 ctrl+c kill
signal.Notify(c, os.Interrupt, os.Kill)
//阻塞直到有信號傳入
//阻塞直至有信號傳入
s := <-c
log.Println("exit!", s)
}
Docker文件第一節相同,這裡就不放了。
現在讓我們嘗試 運行,
docker build -t hello .
&& docker run
-p 9090:9090
hello
構建鏡像的過程依舊輕鬆愉快:
Building image...
Preparing build context archive...
[==================================================>]9/9 files
Done
Sending build context to Docker daemon...
[==================================================>] 2.859kB
Done
Step 1/14 : FROM library/golang:alpine as build
---> dda4232b2bd5
Step 2/14 : MAINTAINER fanxiaoqiang <[email protected]>
---> Using cache
---> 546d5bcb606b
Step 3/14 : ADD . /data/
---> d6bcfc3f9976
Step 4/14 : WORKDIR /data/
---> Running in 4a8f0fa4c9c4
Removing intermediate container 4a8f0fa4c9c4
---> 6f6092bc91a8
Step 5/14 : RUN export GO111MODULE=on
---> Running in 44a83bb9c9a9
Removing intermediate container 44a83bb9c9a9
---> ea199d64e9d9
Step 6/14 : RUN export GOSUMDB=off
---> Running in df368787ddd7
Removing intermediate container df368787ddd7
---> c338c09c4980
Step 7/14 : RUN unset GOPATH
---> Running in c6016dd29cd8
Removing intermediate container c6016dd29cd8
---> 8f7004cb8ed5
Step 8/14 : RUN go env -w GOPROXY=https://goproxy.cn
---> Running in 237a89c7a644
Removing intermediate container 237a89c7a644
---> 5b5b9b8efb43
Step 9/14 : RUN go build -o server http_server.go
---> Running in 27a5afb6b775
Removing intermediate container 27a5afb6b775
---> 8e0771380586
Step 10/14 : FROM scratch
--->
Step 11/14 : COPY --from=build /data/server /data/server
---> 76dc69f34774
Step 12/14 : WORKDIR /data/
---> Running in 8550a1a7b8ee
Removing intermediate container 8550a1a7b8ee
---> 269d3ee7bb29
Step 13/14 : EXPOSE 9090
---> Running in 2a3f21f67f90
Removing intermediate container 2a3f21f67f90
---> 79640d9e743a
Step 14/14 : CMD ["/data/server"]
---> Running in 39581ed1d208
Removing intermediate container 39581ed1d208
---> e30b2238a606
Successfully built e30b2238a606
Successfully tagged hello:latest
Existing container found: 8b31d39f149117566da56be2796418089c47509018857427559600f1ba7c7982, removing...
Creating container...
Container Id: 20d38a265fe3496b5a4b6c3742740c6c517b7d449250ab0be246688973212079
Container name: '/vibrant_hodgkin'
Attaching to container '/vibrant_hodgkin'...
Starting container '/vibrant_hodgkin'
'<unknown> Dockerfile: Dockerfile' has been deployed successfully.
然而查看容器的日誌輸出:
standard_init_linux.go:211: exec user process caused "no such file or directory"
???
是我的二進位程式沒有編譯成功嗎?其實不是。從構建日誌中,我們可以清楚的看到,程式其實是編譯成功的,也成功的COPY到了最終的運行鏡像中,然後啟動的時候就是出錯了。所以首先我們就排除了代碼和Dockerfile的問題。
此處我曾經疑惑了很久,因為容器運行報上述錯誤是讓人非常摸不著頭腦的。我嘗試用搜索引擎進行搜索,確實搜到了結果:
docker啟動報錯:standard_init_linux.go:211: exec user process caused "no such file or directory"
如題所示,根據自己構建的鏡像啟動docker容器,直接退出,查看容器日誌報錯信息,沒有任何別的信息。網上搜索這個問題,發現很多人都遇到過,解決辦法也各不相同,最後發現一篇文章。受到啟發,我的項目是java項目,通過ENTRYPOINT命令啟動腳本docker-entrypoint.sh來構建一個在後臺運行的服務。而我的docker-entrypoint.sh是在windows下編輯的,自然fileformat是dos,這裡需要修改為unix,修改辦法也很簡答,無需再在linux下操作,我們一般機器上安裝了git工具,自帶了git bash命令行工具,進入git bash,找到該文件docker-entrypoint.sh,然後使用vi編輯,修改fileformat=unix,如下所示。
————————————————
原文鏈接:https://blog.csdn.net/feinifi/java/article/details/102910715
然而這個問題卻與我們無關,因為我們根本沒有用到entrypoint的功能。
現在我們回到本節開始的地方:scratch是一個空鏡像,意味這Docker內部不存在任何環境和依賴庫。這就意味著即使是最常見的依賴,在scratch中也是不存在的,那麼我們檢查一下helloworld和httpserver兩個二進位文件的依賴,看看是不是能看出一些端倪。
$ go build http_server.go
$ ldd http_server
linux-vdso.so.1 (0x00007fffc4eaf000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fecea090000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fece9c90000)
/lib64/ld-linux-x86-64.so.2 (0x00007fecea400000)
真相大白,即使golang宣傳了二進位文件不需要依賴任何外部文件,但是即使是程式運行最基礎的libc,scratch也是不包含的。這直接導致了編譯完成的httpserver無法運行,但是容器的報錯卻是找不到文件,報錯讓人摸不著頭腦,希望這篇文章能提供一些小小的幫助。
理論上來說,如果在scratch中添加需要的動態庫,最終是可以讓程式正常運行的,但這違背了簡化開發流程的原則,同時會在代碼中增加不必要的負擔。因此,常見的golang程式使用alpine作為最終的運行環境的基礎鏡像已經是一個非常折衷和合適的方案,不建議再去scratch上折騰。