Docker 學習筆記


Docker 和虛擬機

容器內的進程是直接運行于宿主內核的,這點和宿主進程一致,只是容器的 userland 不同,容器的 userland 由容器鏡像提供,也就是說鏡像提供了 rootfs。

假設宿主是 Ubuntu,容器是 CentOS。CentOS 容器中的進程會直接向 Ubuntu 宿主內核發送 syscall,而不會直接或間接的使用任何 Ubuntuuserland 的庫。

這點和虛擬機有本質的不同,虛擬機是虛擬環境,在現有系統上虛擬一套物理設備,然后在虛擬環境內運行一個虛擬環境的操作系統內核,在內核之上再跑完整系統,并在里面調用進程。

還以上面的例子去考慮,虛擬機中,CentOS 的進程發送 syscall 內核調用,該請求會被虛擬機內的 CentOS 的內核接到,然后 CentOS 內核訪問虛擬硬件時,由虛擬機的服務軟件截獲,并使用宿主系統,也就是 Ubuntu 的內核及 userland 的庫去執行。

而且,Linux 和 Windows 在這點上非常不同。Linux 的進程是直接發 syscall 的,而 Windows 則把 syscall 隱藏于一層層的 DLL 服務之后,因此 Windows 的任何一個進程如果要執行,不僅僅需要 Windows 內核,還需要一群服務來支撐,所以如果 Windows 要實現類似的機制,容器內將不會像 Linux 這樣輕量級,而是非常臃腫??匆幌挛④浺浦驳?Docker 就非常清楚了。

所以不要把 Docker 和虛擬機弄混,Docker 容器只是一個進程而已,只不過利用鏡像提供的 rootfs 提供了調用所需的 userland 庫支持,使得進程可以在受控環境下運行而已,它并沒有虛擬出一個機器出來。

CentOS 7 配置加速器(或其它使用 Systemd 的系統)

Ubuntu 16.04CentOS 7 這類系統都已經開始使用 systemd 進行系統初始化管理了,對于使用 systemd 的系統,應該通過編輯服務配置文件 docker.service 來進行加速器的配置。

在啟用服務后

$ sudo systemctl enable docker

可以直接編輯 /etc/systemd/system/multi-user.target.wants/docker.service 文件來進行配置。

sudo vi /etc/systemd/system/multi-user.target.wants/docker.service

在文件中找到 ExecStart= 這一行,并且在其行尾添加上所需的配置。假設我們的加速器地址為 https://registry.docker-cn.com,那么可以這樣配置:

ExecStart=/usr/bin/dockerd --registry-mirror=https://registry.docker-cn.com

注: Docker 1.12 之前的版本,dockerd 應該換為 docker daemon,更早的版本則是 docker -d

保存退出后,重新加載配置并啟動服務:

sudo systemctl daemon-reload
sudo systemctl restart docker

確認一下配置是否已經生效:

sudo ps -ef | grep dockerd

如果配置成功,生效后就會在這里看到自己所配置的加速器。

1.13 版本以后,可以直接 docker info 查看,如果配置成功,加速器 Registry Mirror 會在最下面列出來。

如果重啟后發現無法啟動 docker 服務,檢查一下服務日志,看看是不是之前執行過那些加速器網站的腳本,如果有做過類似的事情,檢查一下是不是被建立了 /etc/docker/daemon.json 以配置加速器,如果是的話,刪掉這個文件,然后在重啟服務。

使用配置文件是件好事,比如修改配置不必重啟服務,只需發送 SIGHUP 信號即可。但需要注意,目前在 dockerd 中使用配置文件時,無法輸出當前生效配置,并且當 dockerd 的參數和 daemon.json 文件中的配置有所重復時,并不是一個優先級覆蓋另一個,而是會直接導致引擎啟動失敗。很多人發現配了加速器后 Docker 啟動不起來了就是這個原因。解決辦法很簡單,去掉重復項。不過在這些問題解決前,建議使用修改 docker.service 這類做法來實現配置,而不是使用配置文件 daemon.json。方便 ps -ef | grep dockerd 一眼看到實際配置情況。

關于permission denied 沒權限

在 Linux 環境下,一些新裝了 docker 的用戶,特別是使用了 sudo 命令安裝好了 Docker 后,發現當前用戶一執行 docker 命令,就會報沒權限的錯誤:

dial unix /var/run/docker.sock: permission denied

官方安裝文檔:只需要將操作 docker 的用戶,加入 docker 組,那么該用戶既擁有了操作 docker 的權限。

因此,只需要執行:

sudo usermod -aG docker $USER

就可以把當前用戶加入 docker 組,退出、重新登錄系統后,執行 docker info 看一下,就會發現可以不用 sudo 直接執行 docker 命令了。

如果需要添加別的用戶,將其中的 $USER 換成對應的用戶名即可。

Dockerfile中的EXPOSEdocker run -p

Docker中有兩個概念,一個叫做 EXPOSE ,一個叫做 PUBLISH 。

  • EXPOSE 是鏡像/容器聲明要暴露該端口,可以供其他容器使用。這種聲明,在沒有設定 --icc=false的時候,實際上只是一種標注,并不強制。也就是說,沒有聲明 EXPOSE 的端口,其它容器也可以訪問。但是當強制 --icc=false 的時候,那么只有 EXPOSE 的端口,其它容器才可以訪問。
  • PUBLISH 則是通過映射宿主端口,將容器的端口公開于外界,也就是說宿主之外的機器,可以通過訪問宿主IP及對應的該映射端口,訪問到容器對應端口,從而使用容器服務。

EXPOSE 的端口可以不 PUBLISH,這樣只有容器間可以訪問,宿主之外無法訪問。而 PUBLISH 的端口,可以不事先 EXPOSE,換句話說 PUBLISH 等于同時隱式定義了該端口要 EXPOSE

docker run 命令中的 -p, -P 參數,以及 docker-compose.yml 中的 ports 部分,實際上均是指 PUBLISH。

小寫 -p 是端口映射,格式為 [宿主IP:]<宿主端口>:<容器端口>,其中宿主端口和容器端口,既可以是一個數字,也可以是一個范圍,比如:1000-2000:1000-2000。對于多宿主的機器,可以指定宿主IP,不指定宿主IP時,守護所有接口。

大寫 -P 則是自動映射,將所有定義 EXPOSE 的端口,隨機映射到宿主的某個端口。

如何讓一個容器連接兩個網絡?

如果是使用 docker run,那很不幸,一次只可以連接一個網絡,因為 docker run--network 參數只可以出現一次(如果出現多次,最后的會覆蓋之前的)。不過容器運行后,可以用命令 docker network connect 連接多個網絡。

假設我們創建了兩個網絡:

$ docker network create mynet1
$ docker network create mynet2

然后,我們運行容器,并連接這兩個網絡。

$ docker run -d --name web --network mynet1 nginx
$ docker network connect mynet2 web

但是如果使用 docker-compose 那就沒這個問題了。因為實際上,Docker Remote API 是支持一次性指定多個網絡的,但是估計是命令行上不方便,所以 docker run 限定為只可以一次連一個。docker-compose 直接就可以將服務的容器連入多個網絡,沒有問題。

version: '2'
services:
    web:
        image: nginx
        networks:
            - mynet1
            - mynet2
networks:
    mynet1:
    mynet2:

Docker 多宿主網絡怎么配置?

Docker 跨節點容器網絡互聯,最通用的是使用 overlay 網絡。

使用 Swarm -- Docker Swarm Mode,非常簡單,只要 docker swarm init 建立集群,其它節點 docker swarm join 加入集群后,集群內的服務就自動建立了 overlay 網絡互聯能力。

需要注意的是,如果是多網卡環境,無論是 docker swarm init 還是 docker swarm join,都不要忘記使用參數 --advertise-addr 指定宣告地址,否則自動選擇的地址很可能不是你期望的,從而導致集群互聯失敗。格式為 --advertise-addr <地址>:<端口>,地址可以是 IP 地址,也可以是網卡接口,比如 eth0。端口默認為 2377,如果不改動可以忽略。

此外,這是供服務使用的 overlay,因此所有 docker service create 的服務容器可以使用該網絡,而 docker run 不可以使用該網絡,除非明確該網絡為 --attachable。

雖然默認使用的是 overlay 網絡,但這并不是唯一的多宿主互聯方案。Docker 內置了一些其它的互聯方案,比如效率比較高的 macvlan。如果在局域網絡環境下,對 overlay 的額外開銷不滿意,那么可以考慮 macvlan 以及 ipvlan,這是比較好的方案。
https://docs.docker.com/engine/userguide/networking/get-started-macvlan/

此外,還有很多第三方的網絡可以用來進行跨宿主互聯,可以訪問官網對應文檔進一步查看:https://docs.docker.com/engine/extend/legacy_plugins/#/network-plugins

容器無狀態

容器存儲層的無狀態
服務層面的無狀態

容器存儲層的無狀態

這里提到的存儲層是指用于存儲鏡像、容器各個層的存儲,一般是 Union FS,如 AUFS,或者是使用塊設備的一些機制(如 snapshot )進行模擬,如 devicemapper。

Union FS 這類存儲系統,相當于是在現有存儲上,再加一層或多層存儲,這類存儲的讀寫性能并不好。并且對于 CentOS 這類只能使用 devicemapper 的系統而言,存儲層的讀寫還經常出 bug。因此,在 Docker 使用過程中,要避免存儲層的讀寫。頻繁讀寫的部分,應該使用。需要持久化的部分,可以使用命名卷進行持久化。由于命名卷的生存周期和容器不同,容器消亡重建,卷不會跟隨消亡。所以容器可以隨便刪了重新run,而其掛載的則會保持之前的數據。

服務層面的無狀態

使用卷持久化容器狀態,雖然從存儲層的角度看,是無狀態的,但是從服務層面看,這個服務是有狀態的。

從服務層面上說,也存在無狀態服務。就是說服務本身不需要寫入任何文件。比如前端 nginx,它不需要寫入任何文件(日志走Docker日志驅動),中間的 php, node.js 等服務,可能也不需要本地存儲,它們所需的數據都在 redis, mysql, mongodb 中了。這類服務,由于不需要卷,也不發生本地寫操作,刪除、重啟、不保存自身狀態,并不影響服務運行,它們都是無狀態服務。這類服務由于不需要狀態遷移,不需要分布式存儲,因此它們的集群調度更方便。

之前沒有 docker volume 的時候,有些人說 Docker 只可以支持無狀態服務,原因就是只看到了存儲層需求無狀態,而沒有 docker volume 的持久化解決方案。

現在這個說法已經不成立,服務可以有狀態,狀態持久化用 docker volume。

當服務可以有狀態后,如果使用默認的 local 卷驅動,并且使用本地存儲進行狀態持久化的情況,單機服務、容器的再調度運行沒有問題。但是顧名思義,使用本地存儲的卷,只可以為當前主機提供持久化的存儲,而無法跨主機。

但這只是使用默認的 local 驅動,并且使用 本地存儲 而已。使用分布式/共享存儲就可以解決跨主機的問題。docker volume 自然支持很多分布式存儲的驅動,比如 flocker、glusterfscephipfs 等等。常用的插件列表可以參考官方文檔:https://docs.docker.com/engine/extend/legacy_plugins/#/volume-plugins

在鏡像的 Dockerfile 制作中,加入初始化部分

官方鏡像 mysql 中可以使用 Dockerfile 來添加初始化腳本,并且會在運行時判斷是否為第一次運行,如果確實需要初始化,則執行定制的初始化腳本。

假設我們使用這種方法將 hello.txt 在初始化的時候加入到 mydata 卷中去。

首先我們需要寫一個進入點的腳本,用以確保在容器執行的時候都會運行,而這個腳本將判斷是否需要數據初始化,并且進行初始化操作。

#!/bin/bash
# entrypoint.sh

if [ ! -f "/data/hello.txt" ]; then
    cp /source/hello.txt /data/
fi

exec "$@"

名為 entrypoint.sh 的這個腳本很簡單,判斷一下 /data/hello.txt 是否存在,如果不存在就需要初始化。初始化行為也很簡單,將實現準備好的 /source/hello.txt 復制到 /data/ 目錄中去,以完成初始化。程序的最后,將執行送入的命令。

我們可以這樣寫 Dockerfile

FROM nginx
COPY hello.txt /source/
COPY entrypoint.sh /
VOLUME /data
ENTRYPOINT ["/entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]

當我們構建鏡像、啟動容器后,就會發現 /data 目錄下已經存在了 hello.txt 文件了,初始化成功了。

關于Docker 容器里運行數據庫

Docker Volume 可以解決持久化問題,從本地目錄綁定、受控存儲空間、塊設備、網絡存儲到分布式存儲,Docker Volume 都支持

Docker 不是虛擬機,使用數據卷是直接向宿主寫入文件,不存在性能損耗。而且卷的生存周期獨立于容器,容器消亡卷不消亡,重新運行容器可以掛載指定命名卷,數據依然存在,也不存在無法持久化的問題。

Dockerfile 與鏡像

docker commit

Docker 提供了很好的 Dockerfile 的機制來幫助定制鏡像,可以直接使用 Shell 命令,非常方便。而且,這樣制作的鏡像更加透明,也容易維護,在基礎鏡像升級后,可以簡單地重新構建一下,就可以繼承基礎鏡像的安全維護操作。

使用 docker commit 制作的鏡像被稱為黑箱鏡像,換句話說,就是里面進行的是黑箱操作,除本人外無人知曉。即使這個制作鏡像的人,過一段時間后也不會完整的記起里面的操作。那么當有些東西需要改變時,或者因基礎鏡像更新而需要重新制作鏡像時,會讓一切變得異常困難,就如同重新安裝調試配置服務器一樣,失去了 Docker 的優勢了。

使用 commit 的場合是一些特殊環境,比如入侵后保存現場等等,這個命令不應該成為定制鏡像的標準做法。

shell 腳本

Dockerfile 不等于 .sh 腳本

Dockerfile 確實是描述如何構建鏡像的,其中也提供了 RUN 這樣的命令,可以運行 shell 命令。但是和普通 shell 腳本還有很大的不同。

Dockerfile 描述的實際上是鏡像的每一層要如何構建,所以每一個RUN是一個獨立的一層。所以一定要理解“分層存儲”的概念。上一層的東西不會被物理刪除,而是會保留給下一層,下一層中可以指定刪除這部分內容,但實際上只是這一層做的某個標記,說這個路徑的東西刪了。但實際上并不會去修改上一層的東西。每一層都是靜態的,這也是容器本身的 immutable 特性,要保持自身的靜態特性。

Dockerfile 確的寫法應該是把同一個任務的命令放到一個 RUN 下,多條命令應該用 && 連接,并且在最后要打掃干凈所使用的環境。比如下面這段摘自官方 redis 鏡像 Dockerfile 的部分:

RUN buildDeps='gcc libc6-dev make' \
    && set -x \
    && apt-get update && apt-get install -y $buildDeps --no-install-recommends \
    && rm -rf /var/lib/apt/lists/* \
    && wget -O redis.tar.gz "$REDIS_DOWNLOAD_URL" \
    && echo "$REDIS_DOWNLOAD_SHA1 *redis.tar.gz" | sha1sum -c - \
    && mkdir -p /usr/src/redis \
    && tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 \
    && rm redis.tar.gz \
    && make -C /usr/src/redis \
    && make -C /usr/src/redis install \
    && rm -r /usr/src/redis \
    && apt-get purge -y --auto-remove $buildDeps

context

context,上下文,是 docker build 中很重要的一個概念。構建鏡像必須指定 context

docker build -t xxx <context路徑>

或者 docker-compose.yml 中的

    app:
        build:
            context: <context路徑>
            dockerfile: dockerfile

這里都需要指定 context

context 是工作目錄,但不要和構建鏡像的Dockerfile 中的 WORKDIR 弄混,contextdocker build 命令的工作目錄。

docker build 命令實際上是客戶端,真正構建鏡像并非由該命令直接完成。docker build 命令將 context 的目錄上傳給 Docker 引擎,由它負責制作鏡像。

在 Dockerfile 中如果寫 COPY ./package.json /app/ 這種命令,實際的意思并不是指執行 docker build 所在的目錄下的 package.json,也不是指 Dockerfile 所在目錄下的 package.json,而是指 context 目錄下的 package.json。

這就是為什么有人發現 COPY ../package.json /app 或者 COPY /opt/xxxx /app 無法工作的原因,因為它們都在 context 之外,如果真正需要,應該將它們復制到 context 目錄下再操作。

docker build -t xxx . 中的這個.,實際上就是在指定 Context 的目錄,而并非是指定 Dockerfile 所在目錄。

默認情況下,如果不額外指定 Dockerfile 的話,會將 Context 下的名為 Dockerfile 的文件作為 Dockerfile。所以很多人會混淆,認為這個 . 是在說 Dockerfile 的位置,其實不然。

一般項目中,Dockerfile 可能被放置于兩個位置。

  • 一個可能是放置于項目頂級目錄,這樣的好處是在頂級目錄構建時,項目所有內容都在上下文內,方便構建;
  • 另一個做法是,將所有 Docker 相關的內容集中于某個目錄,比如 docker 目錄,里面包含所有不同分支的 Dockerfile,以及 docker-compose.yml 類的文件、entrypoint 的腳本等等。這種情況的上下文所在目錄不再是 Dockerfile 所在目錄了,因此需要注意指定上下文的位置。

此外,項目中可能會包含一些構建不需要的文件,這些文件不應該被發送給 dockerd 引擎,但是它們處于上下文目錄下,這種情況,我們需要使用 .dockerignore 文件來過濾不必要的內容。.dockerignore 文件應該放置于上下文頂級目錄下,內容格式和 .gitignore 一樣。

tmp
db

這樣就過濾了 tmpdb 目錄,它們不會被作為上下文的一部分發給 dockerd 引擎。

如果你發現你的 docker build 需要發送龐大的 Context 的時候,就需要來檢查是不是 .dockerignore 忘了撰寫,或者忘了過濾某些東西了。

ENTRYPOINTCMD 的不同

Dockerfile 的目的是制作鏡像,換句話說,實際上是準備的是主進程運行環境。那么準備好后,需要執行一個程序才可以啟動主進程,而啟動的辦法就是調用 ENTRYPOINT,并且把 CMD 作為參數傳進去運行。也就是下面的概念:

ENTRYPOINT "CMD"

假設有個 myubuntu 鏡像 ENTRYPOINTsh -c,而我們 docker run -it myubuntu uname -a。那么 uname -a 就是運行時指定的 CMD,那么 Docker 實際運行的就是結合起來的結果:

sh -c "uname -a"
  • 如果沒有指定 ENTRYPOINT,那么就只執行 CMD;
  • 如果指定了 ENTRYPOINT 而沒有指定 CMD,自然執行 ENTRYPOINT;
  • 如果 ENTRYPOINTCMD 都指定了,那么就如同上面所述,執行 ENTRYPOINT "CMD"
  • 如果沒有指定 ENTRYPOINT,而 CMD 用的是上述那種 shell 命令的形式,則自動使用 sh -c 作為 ENTRYPOINT。

注意最后一點的區別,這個區別導致了同樣的命令放到 CMDENTRYPOINT 下效果不同,因此有可能放在 ENTRYPOINT 下的同樣的命令,由于需要 tty 而運行時忘記了給(比如忘記了docker-compose.ymltty:true)導致運行失敗。

這種用法可以很靈活,比如我們做個 git 鏡像,可以把 git 命令指定為 ENTRYPOINT,這樣我們在 docker run 的時候,直接跟子命令即可。比如 docker run git log 就是顯示日志。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 我是一個懶憊的人,閑坐看云起云落就好。 生活總是有起起落落,平靜了30年的人生戛然而止,生活變得翻天覆地起來。畢業...
    子軍LH閱讀 230評論 0 0
  • 無心賞雪 雪花空中舞,平添幾分愁。 關山路途遠,把九寒天路。 夫差回家中,幾時把家還。 釋放 電話鈴聲響,夫報近家...
    學會知足真正快樂閱讀 215評論 0 3
  • 精神分析進行得越迅速,越有效,患者就會越“配合”,這幾乎是每位精神分析師都有的共識。我在這里所說的“配合”,意思并...
    暖陽_1332閱讀 1,339評論 0 1