Image鏡像與Container容器基礎篇

前言

這是一篇關於鏡像與容器的基礎篇,雖然有些內容與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 3pip程序,然後使用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)將咱們的程序運行環境程序自己打包爲一個總體,其是靜止的,而當咱們基於鏡像運行一個實例時,此時則將所運行的實例描述爲容器(containersegmentfault

由於製做好的鏡像已包含程序運行時環境,如示例鏡像包含了pythonpython flask模塊,故運行容器時,容器所在的宿主機無需再爲程序準備運行時環境,咱們僅需在宿主機上安裝一個容器運行時引擎便可運行容器,如本文選擇podmancentos

以下所示,咱們基於鏡像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

image.png

對於企業來講一般會搭建本身的私有鏡像倉庫,如haborquay,但對於我的測試用途來講,咱們可基於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層爲一個籠統的歸納,其包含基礎鏡像的全部層。

image.png

下面咱們經過命令來觀察鏡像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參數明確告知構建過程不重用任何緩存,但又致使構建時間過長。

鏡像由層堆疊而成,而上層是對下層的引用,而構建過程又能夠重用緩存加快速度。那麼考慮以下構建邏輯,咱們首先將源碼拷貝到鏡像中,然後安裝pythonpython模塊。

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

咱們須要gccmake命令來編譯此程序,故咱們編寫以下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指令,第1From塊咱們安裝編譯環境並編譯代碼,由於採用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

如何調試Pod或容器

咱們的容器大多數部署在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-apiserverkube-scheduler

  • /etc/kubernetes/manifests/kube-apiserver.yaml
  • /etc/kubernetes/manifests/kube-scheduler.yaml

爲測試用途,咱們以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構建鏡像,其提供了足夠的能力來使咱們掌控全部構建細節,但其實在過於底層,用戶需掌握太多的知識,如對於研發來講,咱們不須要他們耗費在如何構建鏡像的過程當中,鑑於此,是否有足夠友好的方法來生成鏡像呢,答案是確定的,如s2icnb,這些方法做者將在下面的文章中予以講解。

相關文章
相關標籤/搜索