前言
docker的口號是build,ship,and run any app,anywhere,在我們使用 docker 的大部分時候,的確能感覺到其優(yōu)越性,但是往往在我們 build 一個應(yīng)用的時候,是將我們的源代碼也構(gòu)建進去的,這對于類似于 golang 這樣的編譯型語言肯定是不行的,因為實際運行的時候我只需要把最終構(gòu)建的二進制包給你就行,把源碼也一起打包在鏡像中,需要承擔(dān)很多風(fēng)險,即使是腳本語言,在構(gòu)建的時候也可能需要使用到一些上線的工具,這樣無疑也增大了我們的鏡像體積。
在應(yīng)用了容器技術(shù)的軟件開發(fā)過程中,控制容器鏡像的大小可是一件費時費力的事情。如果我們構(gòu)建的鏡像既是編譯軟件的環(huán)境,又是軟件最終的運行環(huán)境,這是很難控制鏡像大小的。所以常見的配置模式為:分別為軟件的編譯環(huán)境和運行環(huán)境提供不同的容器鏡像。比如為編譯環(huán)境提供一個 dockerfile.build,用它構(gòu)建的鏡像包含了編譯軟件需要的所有內(nèi)容,比如代碼、sdk、工具等等。同時為軟件的運行環(huán)境提供另外一個單獨的 dockerfile,它從 dockerfile.build 中獲得編譯好的軟件,用它構(gòu)建的鏡像只包含運行軟件所必須的內(nèi)容。這種情況被稱為構(gòu)造者模式(builder pattern),本文將介紹如何通過 dockerfile 中的 multi-stage 來解決構(gòu)造者模式帶來的問題。
常見的容器鏡像構(gòu)建過程
比如我們創(chuàng)建了一個 go 語言編寫了一個檢查頁面中超級鏈接的程序 app.go(請從 (sparkdev )獲取本文相關(guān)的代碼):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
|
package main import ( "encoding/json" "fmt" "log" "net/http" "net/url" "os" "strings" "golang.org/x/net/html" ) type scrapedatastore struct { internal int `json: "internal" ` external int `json: "external" ` } func isinternal(parsedlink *url.url, siteurl *url.url, link string) bool { return parsedlink.host == siteurl.host || strings.index(link, "#" ) == 0 || len(parsedlink.host) == 0 } func main() { urlin := os.getenv( "url" ) if len(urlin) == 0 { urlin = "https://www.cnblogs.com/" } resp, err := http.get(urlin) scrapedata := &scrapedatastore{} tokenizer := html.newtokenizer(resp.body) end := false for { tt := tokenizer.next() switch { case tt == html.starttagtoken: token := tokenizer.token() switch token.data { case "a" : for _, attr := range token.attr { if attr.key == "href" { link := attr.val parsedlink, parselinkerr := url.parse(link) if parselinkerr == nil { if isinternal(parsedlink, siteurl, link) { scrapedata.internal++ } else { scrapedata.external++ } } if parselinkerr != nil { fmt .println( "can't parse: " + token.data) } } } break } case tt == html.errortoken: end = true break } if end { break } } data, _ := json.marshal(&scrapedata) fmt .println(string(data)) } |
下面我們通過容器來構(gòu)建它,并把它部署到生產(chǎn)型的容器鏡像中。
首先構(gòu)建編譯應(yīng)用程序的鏡像:
1
2
3
4
5
|
from golang:1.7.3 workdir /go/src/github .com /sparkdevo/href-counter/ run go get -d - v golang.org /x/net/html copy app.go . run cgo_enabled=0 goos=linux go build -a -installsuffix cgo -o app . |
把上面的內(nèi)容保存到 dockerfile.build 文件中。
接著把構(gòu)建好的應(yīng)用程序部署到生產(chǎn)環(huán)境用的鏡像中:
1
2
3
4
5
|
from alpine:latest run apk --no-cache add ca-certificates workdir /root/ copy app . cmd [ "./app" ] |
把上面的內(nèi)容保存到 dockerfile 文件中。
最后需要使用一個腳本把整個構(gòu)建過程整合起來:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
#!/bin/sh echo building sparkdevo /href-counter :build # 構(gòu)建編譯應(yīng)用程序的鏡像 docker build --no-cache -t sparkdevo /href-counter :build . -f dockerfile.build # 創(chuàng)建應(yīng)用程序 docker create --name extract sparkdevo /href-counter :build # 拷貝編譯好的應(yīng)用程序 docker cp extract: /go/src/github .com /sparkdevo/href-counter/app . /app docker rm -f extract echo building sparkdevo /href-counter :latest # 構(gòu)建運行應(yīng)用程序的鏡像 docker build --no-cache -t sparkdevo /href-counter :latest . |
把上面的內(nèi)容保存到 build.sh 文件中。這個腳本會先創(chuàng)建出一個容器來構(gòu)建應(yīng)用程序,然后再創(chuàng)建最終運行應(yīng)用程序的鏡像。
把 app.go、dockerfile.build、dockerfile 和 build.sh 放在同一個目錄下,然后進入這個目錄執(zhí)行 build.sh 腳本進行構(gòu)建。構(gòu)建后的容器鏡像大小:
從上圖中我們可以觀察到,用于編譯應(yīng)用程序的容器鏡像大小接近 700m,而用于生產(chǎn)環(huán)境的容器鏡像只有 10.3 m,這樣的大小在網(wǎng)絡(luò)間傳輸?shù)男适呛芨叩摹?/p>
運行下面的命令可以檢查我們構(gòu)建的容器是否可以正常的工作:
1
2
|
$ docker run -e url=https: //www .cnblogs.com/ sparkdevo /href-counter :latest $ docker run -e url=http: //www .cnblogs.com /sparkdev/ sparkdevo /href-counter :latest |
ok,我們寫的程序正確的統(tǒng)計了博客園首頁和筆者的首頁中超級鏈接的情況。
采用上面的構(gòu)建過程,我們需要維護兩個 dockerfile 文件和一個腳本文件 build.sh。能不能簡化一些呢? 下面我們看看 docker 針對這種情況提供的解決方案:multi-stage。
在 dockerfile 中使用 multi-stage
multi-stage 允許我們在 dockerfile 中完成類似前面 build.sh 腳本中的功能,每個 stage 可以理解為構(gòu)建一個容器鏡像,后面的 stage 可以引用前面 stage 中創(chuàng)建的鏡像。所以我們可以使用下面單個的 dockerfile 文件實現(xiàn)前面的需求:
1
2
3
4
5
6
7
8
9
10
11
|
from golang:1.7.3 workdir /go/src/github .com /sparkdevo/href-counter/ run go get -d - v golang.org /x/net/html copy app.go . run cgo_enabled=0 goos=linux go build -a -installsuffix cgo -o app . from alpine:latest run apk --no-cache add ca-certificates workdir /root/ copy --from=0 /go/src/github .com /sparkdevo/href-counter/app . cmd [ "./app" ] |
把上面的內(nèi)容保存到文件 dockerfile.multi 中。這個 dockerfile 文件的特點是同時存在多個 from 指令,每個 from 指令代表一個 stage 的開始部分。我們可以把一個 stage 的產(chǎn)物拷貝到另一個 stage 中。本例中的第一個 stage 完成了應(yīng)用程序的構(gòu)建,內(nèi)容和前面的 dockerfile.build 是一樣的。第二個 stage 中的 copy 指令通過 --from=0 引用了第一個 stage ,并把應(yīng)用程序拷貝到了當(dāng)前 stage 中。接下來讓我們編譯新的鏡像:
1
|
$ docker build --no-cache -t sparkdevo /href-counter :multi . -f dockerfile.multi |
這次使用 href-counter:multi 鏡像運行應(yīng)用:
1
2
|
$ docker run -e url=https: //www .cnblogs.com/ sparkdevo /href-counter :multi $ docker run -e url=http: //www .cnblogs.com /sparkdev/ sparkdevo /href-counter :multi |
結(jié)果和之前是一樣的。那么新生成的鏡像有沒有特別之處呢:
好吧,從上圖我們可以看到,除了 sparkdevo/href-counter:multi 鏡像,還生成了一個匿名的鏡像。因此,所謂的 multi-stage 不過時多個 dockerfile 的語法糖罷了。但是這個語法糖還好很誘人的,現(xiàn)在我們維護一個結(jié)構(gòu)簡潔的 dockerfile 文件就可以了!
使用命名的 stage
在上面的例子中我們通過 --from=0 引用了 dockerfile 中第一個 stage,這樣的做法會讓 dockerfile 變得不容易閱讀。其實我們是可以為 stage 命名的,然后就可以通過名稱來引用 stage 了。下面是改造后的 dockerfile.mult 文件:
1
2
3
4
5
6
7
8
9
10
11
|
from golang:1.7.3 as builder workdir /go/src/github .com /sparkdevo/href-counter/ run go get -d - v golang.org /x/net/html copy app.go . run cgo_enabled=0 goos=linux go build -a -installsuffix cgo -o app . from alpine:latest run apk --no-cache add ca-certificates workdir /root/ copy --from=builder /go/src/github .com /sparkdevo/href-counter/app . cmd [ "./app" ] |
我們把第一個 stage 使用 as 語法命名為 builder,然后在后面的 stage 中通過名稱 builder 進行引用 --from=builder。通過使用命名的 stage, dockerfile 更容易閱讀了。
總結(jié)
dockerfile 中的 multi-stage 雖然只是些語法糖,但它確實為我們帶來了很多便利。尤其是減輕了 dockerfile 維護者的負(fù)擔(dān)(要知道實際生產(chǎn)中的 dockerfile 可不像 demo 中的這么簡單)。需要注意的是舊版本的 docker 是不支持 multi-stage 的,只有 17.05 以及之后的版本才開始支持。好了,是不是該去升級你的 docker 版本了?
好了,以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,如果有疑問大家可以留言交流,謝謝大家對服務(wù)器之家的支持。
參考:
原文鏈接:http://www.cnblogs.com/sparkdev/p/8508435.html