這是一篇關於鏡像與容器的基礎篇,雖然有些內容與18年寫的文章邁入Docker、Kubernetes容器世界的大門有重疊,但隨着這幾年對容器的熟悉,我想將一些認識分享出來,並做爲我後續將要寫的文章一些技術鋪墊。python
在描述什麼是鏡像前,先來看一下以下示例程序,其爲基於flask框架寫的一個簡單的python程序。linux
# 文件app.py from flask import Flask app = Flask(__name__) @app.route('/') def hello(): return "Hello, World" if __name__ == "__main__": app.run(host='::', port=9080, threaded=True) # 文件requirements.txt flask
爲了運行此程序,咱們首先須要一個操做系統(如ubuntu),然後將程序上傳到主機某個目錄(如/app),接着安裝python 3與pip程序,然後使用pip安裝flask模塊,最後使用python運行此程序,其過程涉及命令以下所示:git
apt-get update apt-get install python3 python3-pip pip install -r /app/requirements.txt python3 /app/app.py
假設另外一款程序只能運行在某特定版本(如0.8)的flask模塊上,那麼此時運行pip install flask=0.8
將會與上面安裝的flask版本相沖突,爲了解決此問題,咱們可以使用容器技術將程序運行環境與程序自己打包起來,而打包後的東西咱們稱之爲Image鏡像。github
爲了製做鏡像,咱們需選擇一款工具,如docker、,而本文選擇一款名爲podman的工具,功能可用alias docker=podman
來描述。在centos 7.6以上操做系統,執行以下命令安裝:docker
yum -y install podman
一般,咱們將製做鏡像的過程或邏輯編寫在一個名爲Dockerfile的文件中,對於示例程序,咱們在主機源碼目錄下添加一個Dockerfile,其包含的構建邏輯以下所示:shell
# 1. 選擇ubuntu操做系統,版本爲bionic(18.04),咱們後續將使用apt-get安裝python與pip FROM docker.io/library/ubuntu:bionic # 2. 指定工做目錄,等價於命令:mkdir /app && cd /app WORKDIR /app # 3. 使用ubuntu操做系統包管理軟件apt-get安裝python RUN apt-get update && apt-get install -y \ python3 \ python3-pip \ && rm -rf /var/lib/apt/lists/* # 4. 將python模塊依賴文件拷貝到工做目錄並執行pip從阿里雲pypi源安裝python模塊 COPY requirements.txt ./ ENV INDEX_URL https://mirrors.aliyun.com/pypi/simple RUN pip3 install -i $INDEX_URL --no-cache-dir -r requirements.txt # 5. 將主程序拷貝到工做目錄 COPY app.py ./ # 6. 指定使用此鏡像運行容器時的命令 CMD python3 /app/app.py
接着,咱們執行以下命令將應用打包成鏡像,也就是說,下述命令執行Dockerfile文件內的指令從而生成應用鏡像(名爲hello-flask),其包含python運行環境與源碼。flask
podman build -t hello-flask -f Dockerfile .
生成的鏡像此時保存到咱們的宿主機上,此時其是靜態的,以下所示,這個鏡像共460MB大小。ubuntu
$ podman images hello-flask REPOSITORY TAG IMAGE ID CREATED SIZE localhost/hello-flask latest ffe9ef09e05d 6 minutes ago 460 MB
鏡像(Image)將咱們的程序運行環境與程序自己打包爲一個總體,其是靜止的,而當咱們基於鏡像運行一個實例時,此時則將所運行的實例描述爲容器(container)。segmentfault
由於製做好的鏡像已包含程序運行時環境,如示例鏡像包含了python與python flask模塊,故運行容器時,容器所在的宿主機無需再爲程序準備運行時環境,咱們僅需在宿主機上安裝一個容器運行時引擎便可運行容器,如本文選擇podman。centos
以下所示,咱們基於鏡像hello-flask運行一個容器(名爲hello-1),其可經過宿主機的9080端口可訪問此容器。
# 啓動容器 $ podman run --rm --name hello-1 -p 9080:9080 hello-flask * Serving Flask app "app" (lazy loading) * Environment: production WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. * Debug mode: off * Running on http://[::]:9080/ (Press CTRL+C to quit) # 訪問容器 $ curl localhost:9080 Hello, World
咱們可基於相同的鏡像運行多個容器,以下所示,再次基於鏡像hello-flask運行一個容器(名爲hello-2),其可經過主機的9081端口訪問。
$ podman run --rm --name hello-2 -p 9081:9080 hello-flask
主機運行了哪些容器咱們可經過以下命令查看:
$ podman ps CONTAINER ID IMAGE ... PORTS NAMES 7687848eb0b5 hello-flask:latest ... 0.0.0.0:9081->9080/tcp hello-2 aab353fb7008 hello-flask:latest ... 0.0.0.0:9080->9080/tcp hello-1
各容器經過Linux Namespace作隔離,也就是說hello-1容器與hello-2容器是互相看不見的。以下所示,咱們執行以下命令登錄到容器hello-1中,然後執行ps -ef
可發現僅含幾個命令:
$ podman exec -it hello-1 /bin/sh # ps -ef UID PID PPID C STIME TTY TIME CMD root 1 0 0 09:01 ? 00:00:00 /bin/sh -c python3 /app/app.py root 7 1 0 09:01 ? 00:00:00 python3 /app/app.py root 10 0 40 09:21 pts/0 00:00:00 /bin/sh root 16 10 0 09:21 pts/0 00:00:00 ps -ef
如上所示,咱們可發覺容器不含操做系統內核,經過ps
可發現容器運行的幾個命令,而在上章構建鏡像時,我有提到在Dockerfile中經過FROM ubuntu:bionic
指令選擇了ubuntu系統,此說法不是很正確,而正確的說法應是選擇了一個沒有內核的ubuntu操做系統鏡像。
因容器不是虛擬機,虛擬機是一個完整的操做系統,而容器倒是沒有操做系統內核的,全部的容器仍然共享宿主機的內核,咱們可在宿主機上經過ps -ef
發現容器執行的命令。
$ ps -ef|grep app.py root 3133 3120 0 17:01 ? 00:00:00 /bin/sh -c python3 /app/app.py root 3146 3133 0 17:01 ? 00:00:00 python3 /app/app.py root 14041 14029 0 17:15 ? 00:00:00 /bin/sh -c python3 /app/app.py root 14057 14041 0 17:15 ? 00:00:00 python3 /app/app.py
爲了分發鏡像,咱們將製做好的鏡像經過網絡上傳到鏡像倉庫中,然後只要主機可訪問鏡像倉庫,則其就可經過鏡像倉庫下載鏡像並快速部署容器,其相似於github,在github咱們存儲源碼,而鏡像倉庫則存儲鏡像而已。
在構建鏡像時Dockerfile中有以下From指令,此鏡像咱們指定從docker hub中獲取,此爲docker公司製做的public鏡像,從https://hub.docker.com上咱們...。
FROM docker.io/library/ubuntu:bionic
對於企業來講一般會搭建本身的私有鏡像倉庫,如habor、quay,但對於我的測試用途來講,咱們可基於registry鏡像搭建一個簡單的私有鏡像倉庫,以下所示:
mkdir /app/registry cat > /etc/systemd/system/poc-registry.service <<EOF [Unit] Description=Local Docker Mirror registry cache After=network.target [Service] ExecStartPre=-/usr/bin/podman rm -f %p ExecStart=/usr/bin/podman run --name %p \ -v /app/registry:/var/lib/registry:z \ -e REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY=/var/lib/registry \ -p 5000:5000 registry:2 ExecStop=-/usr/bin/podman stop -t 2 %p Restart=on-failure RestartSec=10 [Install] WantedBy=multi-user.target EOF systemctl daemon-reload systemctl enable poc-registry systemctl restart poc-registry
假設爲部署的鏡像服務咱們配置主機名稱爲registry.zyl.io,因其未使用SSL加密,對於podman容器引擎,咱們需在以下文件中添加以下信息,後續訪問此鏡像倉庫時將不驗證HTTPS證書:
# vi /etc/containers/registries.conf ... [[registry]] location = "registry.zyl.io:5000" insecure = true ...
接着,咱們將鏡像推送到此倉庫中,但在此以前咱們先執行podman tag
鏡像名稱。
podman tag localhost/hello-flask:latest registry.zyl.io:5000/hello-flask:latest podman push registry.zyl.io:5000/hello-flask:latest
然後,咱們先刪除鏡像,再使用pull
命令下載鏡像。
podman rmi registry.zyl.io:5000/hello-flask:latest podman pull registry.zyl.io:5000/hello-flask:latest
參考docker官方文檔About storage drivers可知鏡像由只讀層(layer)堆疊而成,而上一層又是對下一層的引用,而基於鏡像運行的容器,其又會在鏡像層(Image layers)上生成一個可讀寫的容器層(Container layer),咱們對容器的寫操做均發生在容器層上,而至於各層如何交互則由不一樣的存儲驅動(storage drivers)負責。
一般Dockerfile中的每條指令均會生成只讀鏡像層,如官方示例所示,其總共含4個指令:
FROM ubuntu:15.04 COPY . /app RUN make /app CMD python /app/app.py
下圖截取自docker官方文檔,其顯示上面的Dockfile構建了4個鏡像層,從上往下,第1層由cmd指令生成,第2層由run指令生成,第3層爲copy指令生成,而第4層爲from指令生成,但下圖的第4層爲一個籠統的歸納,其包含基礎鏡像的全部層。
下面咱們經過命令來觀察鏡像ubuntu所包含的層,其顯示有5個鏡像層:
$ podman history ubuntu:bionic ID CREATED CREATED BY SIZE ... c3c304cb4f22 7 weeks ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B <missing> 7 weeks ago /bin/sh -c mkdir -p /run/systemd && echo '... 161B <missing> 7 weeks ago /bin/sh -c set -xe && echo '#!/bin/sh' > /... 847B <missing> 7 weeks ago /bin/sh -c [ -z "$(apt-get indextargets)" ] 35.37kB <missing> 7 weeks ago /bin/sh -c #(nop) ADD file:c3e6bb316dfa6b8... 26.69MB
上面構建的hello-flask鏡像基於ubuntu鏡像,其總共包含12層:
$ podman history hello-flask ID CREATED CREATED BY SIZE # CMD python3 /app/app.py ffe9ef09e05d 2 hours ago /bin/sh -c #(nop) CMD python3 /app/app.py 0B # COPY app.py ./ <missing> 2 hours ago /bin/sh -c #(nop) COPY file:e007c2b54ecd4c... 294B # RUN pip3 install -i $INDEX_URL --no-cache-dir -r requirements.txt <missing> 2 hours ago /bin/sh -c pip3 install -i $INDEX_URL --no... 1.291MB # ENV INDEX_URL https://mirrors.aliyun.com/pypi/simple <missing> 2 hours ago /bin/sh -c #(nop) ENV INDEX_URL https://mi... 1.291MB # COPY requirements.txt ./ <missing> 2 hours ago /bin/sh -c #(nop) COPY file:774347764755ea... 179B # RUN apt-get update && ... <missing> 2 hours ago /bin/sh -c apt-get update && apt-get insta... 165.4MB # WORKDIR /app <missing> 2 hours ago /bin/sh -c #(nop) WORKDIR /app 322B # FROM docker.io/library/ubuntu:bionic <missing> 7 weeks ago /bin/sh -c #(nop) CMD ["/bin/bash"] 322B <missing> 7 weeks ago /bin/sh -c mkdir -p /run/systemd && echo '... 185B <missing> 7 weeks ago /bin/sh -c set -xe && echo '#!/bin/sh' > /... 965B <missing> 7 weeks ago /bin/sh -c [ -z "$(apt-get indextargets)" ] 38.94kB <missing> 7 weeks ago /bin/sh -c #(nop) ADD file:c3e6bb316dfa6b8... 27.76MB
知曉鏡像由只讀層堆疊而成對於構建優雅的鏡像很是有用,下面我將使用一個簡單的例子來說解原因,但若想獲取更詳細信息則可參考官方文檔Best practices for writing Dockerfiles。
考慮這樣的場景:有一個臨時文件,咱們對其處理後就刪除以免佔用空間。若在操做系統執行下述示例,則所涉及過程與結果符合咱們的指望:磁盤空間被釋放了。
# 1. 生成一個50m的文件用於測試 dd if=/dev/zero of=test.txt bs=1M count=50 # 2. 處理臨時文件,這裏咱們使用ls命令 ls -lh test.txt -rw-r--r-- 1 root root 50M Jun 12 18:49 test.txt # 3. 刪除臨時文件,避免佔用磁盤空間 rm -f test.txt
咱們按照上面處理過程原封不動的平移到Dockerfile中,上述每條命令咱們將其單獨放在一個RUN
指令中:
$ podman build -t test -f - . <<EOF FROM docker.io/library/ubuntu:bionic RUN dd if=/dev/zero of=test.txt bs=1M count=50 RUN ls -lh test.txt RUN rm -f test.txt EOF
咱們指望構建後的鏡像應與基礎鏡像ubuntu:bionic大小差很少,由於咱們最終將文件刪除了嘛,但實際結果卻與咱們預期相差太多,最終生成的鏡像要比基礎鏡像大50M左右。
$ podman images | grep -w ubuntu docker.io/library/ubuntu bionic ... 66.6 MB $ podman images | grep -w test localhost/test latest ... 119 MB $ podman history localhost/test ID CREATED CREATED BY SIZE 719f3ed7b57c 5 minutes ago /bin/sh -c rm -f test.txt 1.536kB <missing> 5 minutes ago /bin/sh -c ls -lh test.txt 1.024kB # RUN dd if=/dev/zero of=test.txt bs=1M count=50生成了50m的只讀鏡像層 <missing> 5 minutes ago /bin/sh -c dd if=/dev/zero of=test.txt bs=...52.43MB ...
當咱們瞭解到鏡像由只讀層堆疊而成,那麼對於此結果能接受,那麼,對於相似問題,咱們則可調整鏡像構建邏輯,將其置於相同的層上以優化鏡像大小。
$ podman build -t test -f - . <<EOF FROM docker.io/library/ubuntu:bionic RUN dd if=/dev/zero of=test.txt bs=1M count=50 && \ ls -lh test.txt && \ rm -f test.txt EOF
此時可發現鏡像大小與咱們預期相符合了。
$ podman images | grep -w test localhost/test latest d57331d89d86 9 seconds ago 66.6 MB $ podman history test ID CREATED CREATED BY SIZE d57331d89d86 20 seconds ago /bin/sh -c dd if=/dev/zero of=test.txt bs=... 167B ...
若咱們重複運行相同的構建過程,可發現後續構建會比以前快速不少,在構建輸出中咱們可發現有--> Using cache ...
的提示,此提示代表新的構建生成的鏡像重用了原有鏡像的層,故加快了構建速度,但一樣會所以形成問題。
以下述構建邏輯貌似並無任何問題,在咱們安裝curl
工具前先執行apt-get update
更新系統源,但後續咱們的構建可能因緩存緣由重用了RUN apt-get update
這一層,從而致使後續安裝的curl
工具可能不是最新的,這與咱們的預期有差異。
FROM ubuntu:18.04 RUN apt-get update RUN apt-get install -y curl
官方文檔Best practices for writing Dockerfiles有說使用RUN apt-get update && apt-get install -y
可確保安裝了最新的軟件包,這樣會致使清除此層緩存(cache busting)或失效,但測試發現依舊重用緩存,解決此問題最終的辦法也許是在構建時傳遞--no-cache
參數明確告知構建過程不重用任何緩存,但又致使構建時間過長。
鏡像由層堆疊而成,而上層是對下層的引用,而構建過程又能夠重用緩存加快速度。那麼考慮以下構建邏輯,咱們首先將源碼拷貝到鏡像中,然後安裝python與python模塊。
FROM ubuntu COPY app.py ./ COPY requirements.txt ./ RUN apt-get update && apt-get install -y \ python3 \ python3-pip \ && rm -rf /var/lib/apt/lists/* RUN pip3 install -r requirements.txt CMD python3 /app/app.py
上面構建邏輯會致使這樣一個問題,假設咱們修改了app.py源碼,這樣會致使COPY app.py ./
層的緩存失效,故而此層須要從新構建,而下層失效會致使全部依賴於此層的上層緩存均失效,故而下述全部指令均沒法利用緩存層,鑑於此,咱們調整構建邏輯爲這樣儘可能減小修改代碼形成緩存層失效問題。
FROM ubuntu RUN apt-get update && apt-get install -y \ python3 \ python3-pip \ && rm -rf /var/lib/apt/lists/* COPY requirements.txt ./ RUN pip3 install -r requirements.txt COPY app.py ./ CMD python3 /app/app.py
在介紹多段構建鏡像前,咱們先來考慮如何將下述示例構建爲鏡像,其是一個使用c語言編寫的hello world程序:
$ mkdir hello-c && cd hello-c $ cat > hello.c <<EOF #include <stdio.h> int main(void) { printf("hello world\n"); } EOF $ cat > Makefile <<EOF all: gcc --static hello.c -o hello EOF
咱們須要gcc與make命令來編譯此程序,故咱們編寫以下Dockerfile構建鏡像:
$ cat > Dockerfile <<'EOF' FROM ubuntu:bionic WORKDIR /app RUN apt-get update && apt-get install -y \ build-essential \ libc-dev \ && rm -rf /var/lib/apt/lists/* COPY Makefile ./ COPY hello.c ./ RUN make all CMD ["./hello"] EOF
執行podman build -t test -f Dockerfile .
構建鏡像後,其最終大小近300M。
$ podman images|grep test localhost/test latest ... 281 MB
上面生成的應用鏡像包含了編譯環境,這些工具只在編譯C程序時起做用,而程序運行卻不依賴於編譯環境,也就是說,最終生成的應用鏡像咱們可去除這些編譯環境,鑑於此,咱們可採用多階段構建方式構建鏡像。
以下所示,咱們調整構建邏輯,在一個Dockerfile中咱們嵌套了兩個FROM
指令,第1個From塊咱們安裝編譯環境並編譯代碼,由於採用gcc --static
靜態編譯程序,故最終生成的二進制程序不依賴於主機上任何動態庫,故而咱們將其拷貝到最終的鏡像中,而最終的鏡像咱們使用了一個系統保留的鏡像名scratch,此鏡像不存在於任何鏡像倉庫中,但使用此鏡像會告知構建進程生成最小的鏡像結構。
cat > Dockerfile <<'EOF' FROM ubuntu:bionic AS builder WORKDIR /app COPY files/sources.list /etc/apt/sources.list RUN apt-get update && apt-get install -y \ build-essential \ libc-dev \ && rm -rf /var/lib/apt/lists/* COPY Makefile ./ COPY hello.c ./ RUN make all FROM scratch WORKDIR /app COPY --from=builder /app/hello . CMD ["./hello"] EOF
執行podman build -t test -f Dockerfile .
構建鏡像後,其最終大小不到1M,且此鏡像是可被運行的。
$ podman images|grep test localhost/test latest ... 848 kB $ podman run --rm test hello world
咱們的容器大多數部署在k8s集羣中,故此處我將講解如何在k8s集羣環境調試pod的方法。
當前常見的調試pod的方法是查看其日誌、登錄容器內部等方法,以下所示:
kubectl logs <pod_name> kubectl exec <pod_name> -- /bin/sh
可是,如上節所示,咱們爲了容器的大小,不少調試工具咱們並無包含到最終鏡像中,甚至於連/bin/sh
都沒有,亦或者容器是異常狀態,此時咱們無法登錄容器調試。
對於這種狀況,在K8S 1.18集羣中,官方在kubectl
工具中內置了一個調試功能,咱們可啓動一個臨時的調試容器以附加到需調試的pod上,但當前處於alpha狀態,咱們須要啓用此特性。編輯以下文件在其中的command
處添加--feature-gates=EphemeralContainers=true
,等待kubelet
自動重啓kube-apiserver與kube-scheduler。
爲測試用途,咱們以pause鏡像啓動一個pod。注意:這裏咱們指定--restart=Never
避免有問題的pod被不斷自動重啓。
$ podman images|grep pause k8s.gcr.io/pause 3.2 ... 686 kB $ kubectl run ephemeral-demo --image=k8s.gcr.io/pause:3.2 --restart=Never pod/ephemeral-demo created $ kubectl get pod NAME READY STATUS RESTARTS AGE ephemeral-demo 1/1 Running 0 23s
pause鏡像如同咱們上面構建的鏡像同樣其沒有shell,故咱們沒法登錄容器:
$ kubectl exec ephemeral-demo -- /bin/sh ... exec failed: container_linux.go:346: starting container process caused "exec: \"/bin/sh\": stat /bin/sh: no such file or directory" command terminated with exit code 1
啓動一個debug容器附加到被調試的pod上,此時咱們將獲取到一個shell外殼,此時咱們則可作調試任務了。
$ kubectl alpha debug -it ephemeral-demo --image=ubuntu:bionic --target=ephemeral-demo Defaulting debug container name to debugger-rzwl2. If you don't see a command prompt, try pressing enter. / #
可是,本人環境採用crio容器運行時,上面kubectl alpha debug
命令沒法啓動debug容器,或許如同官方文檔所示此容器運行時也許不支持--target參數,就算按照Ephemeral Containers — the future of Kubernetes workload debugging此文章所示能啓動臨時pod,但卻處於獨立的Pid命名空間中,這確定有問題。最後,咱們可嘗試使用kubectl-debug工具調試容器,本文再也不描述。
Note: The
--target
parameter must be supported by the
Container Runtime. When not supported, the Ephemeral Container may not be started, or it may be started with an isolated process namespace.
本文做者介紹了鏡像的一些基礎知識與構建鏡像的技巧,咱們知道鏡像由只讀的layers堆疊而成,從而在構建鏡像時考慮其層結構而調整構建邏輯來優化生成的鏡像大小,一樣,咱們使用多階段構建來利用不一樣鏡像提供的能力並優化鏡像大小。
本章咱們均經過Dockerfile構建鏡像,其提供了足夠的能力來使咱們掌控全部構建細節,但其實在過於底層,用戶需掌握太多的知識,如對於研發來講,咱們不須要他們耗費在如何構建鏡像的過程當中,鑑於此,是否有足夠友好的方法來生成鏡像呢,答案是確定的,如s2i、cnb,這些方法做者將在下面的文章中予以講解。