k8s 深入篇———— 一些容器操作的原理[三]

前言

簡單介紹一下一些容器的操作原理。

正文

docker exec 是怎麼做到進入容器裏的呢。

比如說:

這裏有一個容器,我們可以exec 進去:

 docker exec -it b265 /bin/sh

我們爲什麼能看到和容器內部一樣的場景呢?

首先我們知道了爲什麼容器進程只能看到規定的namespace了,那麼如果我們能拿到這個namespace的信息,那麼我們就能看到容器進程一樣的場景了。

docker inspect --format '{{ .State.Pid }}' b26585ee826b

上面可以查看容器進程。

可以查看namespace信息。

#define _GNU_SOURCE
#include <fcntl.h>
#include <sched.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE);} while (0)

int main(int argc, char *argv[]) {
int fd;

fd = open(argv[1], O_RDONLY);
if (setns(fd, 0) == -1) {
errExit("setns");
}
execvp(argv[2], &argv[2]);
errExit("execvp");
}

寫入到文件中,然後編譯:

tee exectest.c <<- 'EOF' 
> #define _GNU_SOURCE
> #include <fcntl.h>
> #include <sched.h>
> #include <unistd.h>
> #include <stdlib.h>
> #include <stdio.h>
> 
> #define errExit(msg) do { perror(msg); exit(EXIT_FAILURE);} while (0)
> 
> int main(int argc, char *argv[]) {
> int fd;
> 
> fd = open(argv[1], O_RDONLY);
> if (setns(fd, 0) == -1) {
> errExit("setns");
> }
> execvp(argv[2], &argv[2]);
> errExit("execvp");
> }
> EOF
gcc -o exectest exectest.c

然後運行./exectest:

./exectest /proc/17240/ns/mnt /bin/sh

那麼看下這個進程看到的信息是啥:

./exectest /proc/17240/ns/mnt /bin/sh

看到的信息:

這段代碼的的核心操作,則是通過 open() 系統調用打開了指定的 Namespace 文件,並把
這個文件的描述符 fd 交給 setns() 使用。在 setns() 執行後,當前進程就加入了這個文件
對應的 Linux Namespace 當中了

你也可以將網絡帶進去:

正如上所示,當我們執行 ifconfig 命令查看網絡設備時,我會發現能看到的網卡“變
少”了:只有兩個。而我的宿主機則至少有四個網卡。這是怎麼回事呢?
實際上,在 setns() 之後我看到的這兩個網卡,正是我在前面啓動的 Docker 容器裏的網
卡。也就是說,我新創建的這個 /bin/bash 進程,由於加入了該容器進程(PID=25686)
的 Network Namepace,它看到的網絡設備與這個容器裏是一樣的,即:/bin/bash 進程
的網絡設備視圖,也被修改了。
而一旦一個進程加入到了另一個 Namespace 當中,在宿主機的 Namespace 文件上,也
會有所體現。

其實就是將自己的namespace 指向了原來容器進程的namespace。

查看進程號:

發現和原來的17240進程的net一樣。

此外,Docker 還專門提供了一個參數,可以讓你啓動一個容器並“加入”到另一個容器的
Network Namespace 裏,這個參數就是 -net,比如:

docker run -it --net container:4ddf4638572d busybox ifconfig

而如果我指定–net=host,就意味着這個容器不會爲進程啓用 Network Namespace。這
就意味着,這個容器拆除了 Network Namespace 的“隔離牆”,所以,它會和宿主機上
的其他普通進程一樣,直接共享宿主機的網絡棧。這就爲容器直接操作和使用宿主機網絡提
供了一個渠道。

docker commit 幹了啥? 比如我們提交一個容器。

docker commit,實際上就是在容器運行起來後,把最上層的“可讀寫層”,加上原先容
器鏡像的只讀層,打包組成了一個新的鏡像。當然,下面這些只讀層在宿主機上是共享的,
不會佔用額外的空間。
而由於使用了聯合文件系統,你在容器裏對鏡像 rootfs 所做的任何修改,都會被操作系統
先複製到這個可讀寫層,然後再修改。這就是所謂的:Copy-on-Write。
而正如前所說,Init 層的存在,就是爲了避免你執行 docker commit 時,把 Docker 自己
對 /etc/hosts 等文件做的修改,也一起提交掉。

前面我已經介紹過,容器技術使用了 rootfs 機制和 Mount Namespace,構建出了一個同
宿主機完全隔離開的文件系統環境。這時候,我們就需要考慮這樣兩個問題:

  1. 容器裏進程新建的文件,怎麼才能讓宿主機獲取到?
  2. 宿主機上的文件和目錄,怎麼才能讓容器裏的進程訪問到?
    這正是 Docker Volume 要解決的問題:Volume 機制,允許你將宿主機上指定的目錄或
    者文件,掛載到容器裏面進行讀取和修改操作。

在 Docker 項目裏,它支持兩種 Volume 聲明方式,可以把宿主機目錄掛載進容器的 /test
目錄當中:

docker run -v /test ...
docker run -v /home:/test ...

而這兩種聲明方式的本質,實際上是相同的:都是把一個宿主機的目錄掛載進了容器的/test 目錄。

只不過,在第一種情況下,由於你並沒有顯示聲明宿主機目錄,那麼 Docker 就會默認在宿
主機上創建一個臨時目錄 /var/lib/docker/volumes/[VOLUME_ID]/_data,然後把它掛載
到容器的 /test 目錄上。而在第二種情況下,Docker 就直接把宿主機的 /home 目錄掛載
到容器的 /test 目錄上

當容器進程被創建之後,儘管開啓了 Mount Namespace,但是在它執行 chroot(或者 pivot_root)之前,容器進程一直可以看到宿主機上的整個文件系統

而宿主機上的文件系統,也自然包括了我們要使用的容器鏡像。這個鏡像的各個層,保存在
/var/lib/docker/aufs/diff 目錄下,在容器進程啓動後,它們會被聯合掛載在
/var/lib/docker/aufs/mnt/ 目錄中,這樣容器所需的 rootfs 就準備好了。
所以,我們只需要在 rootfs 準備好之後,在執行 chroot 之前,把 Volume 指定的宿主機
目錄(比如 /home 目錄),掛載到指定的容器目錄(比如 /test 目錄)在宿主機上對應的
目錄(即 /var/lib/docker/aufs/mnt/[可讀寫層 ID]/test)上,這個 Volume 的掛載工作
就完成了。

更重要的是,由於執行這個掛載操作時,“容器進程”已經創建了,也就意味着此時
Mount Namespace 已經開啓了。所以,這個掛載事件只在這個容器裏可見。你在宿主機
上,是看不見容器內部的這個掛載點的。這就保證了容器的隔離性不會被 Volume 打破。

注意:這裏提到的 " 容器進程 ",是 Docker 創建的一個容器初始化進程
(dockerinit),而不是應用進程 (ENTRYPOINT + CMD)。dockerinit 會負責
完成根目錄的準備、掛載設備和目錄、配置 hostname 等一系列需要在容器
內進行的初始化操作。最後,它通過 execv() 系統調用,讓應用進程取代自
己,成爲容器裏的 PID=1 的進程。

而這裏要使用到的掛載技術,就是 Linux 的綁定掛載(bind mount)機制。它的主要作
用就是,允許你將一個目錄或者文件,而不是整個設備,掛載到一個指定的目錄上。並且,
這時你在該掛載點上進行的任何操作,只是發生在被掛載的目錄或者文件上,而原掛載點的
內容則會被隱藏起來且不受影響

所以,在一個正確的時機,進行一次綁定掛載,Docker 就可以成功地將一個宿主機上的目
錄或文件,不動聲色地掛載到容器中。

這樣,進程在容器裏對這個 /test 目錄進行的所有操作,都實際發生在宿主機的對應目錄
(比如,/home,或者 /var/lib/docker/volumes/[VOLUME_ID]/_data)裏,而不會影響
容器鏡像的內容。
那麼,這個 /test 目錄裏的內容,既然掛載在容器 rootfs 的可讀寫層,它會不會被 docker
commit 提交掉呢?
也不會。

這個原因其實我們前面已經提到過。容器的鏡像操作,比如 docker commit,都是發生在
宿主機空間的。而由於 Mount Namespace 的隔離作用,宿主機並不知道這個綁定掛載的
存在。所以,在宿主機看來,容器中可讀寫層的 /test 目錄
(/var/lib/docker/aufs/mnt/[可讀寫層 ID]/test),始終是空的。

不過,由於 Docker 一開始還是要創建 /test 這個目錄作爲掛載點,所以執行了 docker
commit 之後,你會發現新產生的鏡像裏,會多出來一個空的 /test 目錄。畢竟,新建目錄
操作,又不是掛載操作,Mount Namespace 對它可起不到“障眼法”的作用。

實驗:

docker run -d -v /test nginx

查看掛載信息:

那麼是哪一個呢?

docker inspect a9c79

直接看這個好了。

docker exec -it a9c79 /bin/bash

然後運行一下:

在裏面寫了一下文件.

然後看下這個目錄裏面有不。

然後我們看下讀寫層有不:

有一個test 目錄,但是是空的。

可以確認,容器 Volume 裏的信息,並不會被 docker commit 提交掉;但這個掛載點目錄 /test 本身,則會出現在新的鏡像當中。

如果你執行 docker run -v /home:/test 的時候,容器鏡像裏的 /test 目錄下本來就有內容的話,你會發現,在宿主機的 /home 目錄下,也會出現這些內容。這是怎麼回事?爲什麼它們沒有被綁定掛載隱藏起來呢?(提示:Docker 的“copyData”功能)

下一節k8s的本質。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章