1. 程式人生 > >理解Docker(3):Docker 使用 Linux namespace 隔離容器的執行環境

理解Docker(3):Docker 使用 Linux namespace 隔離容器的執行環境

來源:http://www.cnblogs.com/sammyliu/p/5878973.html

1. 基礎知識:Linux namespace 的概念

    Linux 核心從版本 2.4.19 開始陸續引入了 namespace 的概念。其目的是將某個特定的全域性系統資源(global system resource)通過抽象方法使得namespace 中的程序看起來擁有它們自己的隔離的全域性系統資源例項(The purpose of each namespace is to wrap a particular global system resource in an abstraction that makes it appear to the processes within the namespace that they have their own isolated instance of the global resource. )。Linux 核心中實現了六種 namespace,按照引入的先後順序,列表如下:

namespace 引入的相關核心版本 被隔離的全域性系統資源 在容器語境下的隔離效果
Mount namespaces Linux 2.4.19 檔案系統掛接點 每個容器能看到不同的檔案系統層次結構
UTS namespaces Linux 2.6.19 nodename 和 domainname 每個容器可以有自己的 hostname 和 domainame
IPC namespaces
Linux 2.6.19 特定的程序間通訊資源,包括System V IPC 和  POSIX message queues 每個容器有其自己的 System V IPC 和 POSIX 訊息佇列檔案系統,因此,只有在同一個 IPC namespace 的程序之間才能互相通訊
PID namespaces Linux 2.6.24 程序 ID 數字空間 (process ID number space) 每個 PID namespace 中的程序可以有其獨立的 PID; 每個容器可以有其 PID 為 1 的root 程序;也使得容器可以在不同的 host 之間遷移,因為 namespace 中的程序 ID 和 host 無關了。這也使得容器中的每個程序有兩個PID:容器中的 PID 和 host 上的 PID。
Network namespaces 始於Linux 2.6.24 完成於 Linux 2.6.29 網路相關的系統資源 每個容器用有其獨立的網路裝置,IP 地址,IP 路由表,/proc/net 目錄,埠號等等。這也使得一個 host 上多個容器內的同一個應用都繫結到各自容器的 80 埠上。
User namespaces 始於 Linux 2.6.23 完成於 Linux 3.8) 使用者和組 ID 空間  在 user namespace 中的程序的使用者和組 ID 可以和在 host 上不同; 每個 container 可以有不同的 user 和 group id;一個 host 上的非特權使用者可以成為 user namespace 中的特權使用者;

Linux namespace 的概念說簡單也簡單說複雜也複雜。簡單來說,我們只要知道,處於某個 namespace 中的程序,能看到獨立的它自己的隔離的某些特定系統資源;複雜來說,可以去看看 Linux 核心中實現 namespace 的原理,網路上也有大量的文件供參考,這裡不再贅述。

2. Docker 容器使用 linux namespace 做執行環境隔離

當 Docker 建立一個容器時,它會建立新的以上六種 namespace 的例項,然後把容器中的所有程序放到這些 namespace 之中,使得Docker 容器中的程序只能看到隔離的系統資源。 

2.1 PID namespace

我們能看到同一個程序,在容器內外的 PID 是不同的:

  • 在容器內 PID 是 1,PPID 是 0。
  • 在容器外 PID 是 2198, PPID 是 2179 即 docker-containerd-shim 程序.

複製程式碼

[email protected]:/home/sammy# ps -ef | grep python
root 2198 2179 0 00:06 ? 00:00:00 python app.py

[email protected]:/home/sammy# ps -ef | grep 2179
root 2179 765 0 00:06 ? 00:00:00 docker-containerd-shim 8b7dd09fbcae00373207f01e2acde45740871c9e3b98286b5458b4ea09f41b3e /var/run/docker/libcontainerd/8b7dd09fbcae00373207f01e2acde45740871c9e3b98286b5458b4ea09f41b3e docker-runc
root 2198 2179 0 00:06 ? 00:00:00 python app.py
root 2249 1692 0 00:06 pts/0 00:00:00 grep --color=auto 2179


[email protected]:/home/sammy# docker exec -it web31 ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 16:06 ? 00:00:00 python app.py

複製程式碼

關於 containerd,containerd-shim 和 container 的關係,文章 中的下圖可以說明:

  • Docker 引擎管理著映象,然後移交給 containerd 執行,containerd 再使用 runC 執行容器。
  • Containerd 是一個簡單的守護程序,它可以使用 runC 管理容器,使用 gRPC 暴露容器的其他功能。它管理容器的開始,停止,暫停和銷燬。由於容器執行時是孤立的引擎,引擎最終能夠啟動和升級而無需重新啟動容器。
  • runC是一個輕量級的工具,它是用來執行容器的,只用來做這一件事,並且這一件事要做好。runC基本上是一個小命令列工具且它可以不用通過Docker引擎,直接就可以使用容器。

因此,容器中的主應用在 host 上的父程序是 containerd-shim,是它通過工具 runC 來啟動這些程序的。

這也能看出來,pid namespace 通過將 host 上 PID 對映為容器內的 PID, 使得容器內的程序看起來有個獨立的 PID 空間。

2.2 UTS namespace

類似地,容器可以有自己的 hostname 和 domainname:

[email protected]:/home/sammy# hostname
devstack
[email protected]:/home/sammy# docker exec -it web31 hostname
8b7dd09fbcae

2.3 user namespace

在 Docker 1.10 版本之前,Docker 是不支援 user namespace。也就是說,預設地,容器內的程序的執行使用者就是 host 上的 root 使用者,這樣的話,當 host 上的檔案或者目錄作為 volume 被對映到容器以後,容器內的程序其實是有 root 的幾乎所有許可權去修改這些 host 上的目錄的,這會有很大的安全問題。

舉例:

  • 啟動一個容器: docker run -d -v /bin:/host/bin --name web34 training/webapp python app.py
  • 此時程序的使用者在容器內和外都是root,它在容器內可以對 host 上的 /bin 目錄做任意修改:
[email protected]:/home/sammy# docker exec -ti web34 id
uid=0(root) gid=0(root) groups=0(root)
[email protected]:/home/sammy# id
uid=0(root) gid=0(root) groups=0(root)

而 Docker 1.10 中引入的 user namespace 就可以讓容器有一個 “假”的  root 使用者,它在容器內是 root,在容器外是一個非 root 使用者。也就是說,user namespace 實現了 host users 和 container users 之間的對映。

啟用步驟:

  1. 修改 /etc/default/docker 檔案,新增行  DOCKER_OPTS="--userns-remap=default"
  2. 重啟 docker 服務,此時 dockerd 程序為 /usr/bin/dockerd --userns-remap=default --raw-logs
  3. 然後建立一個容器:docker run -d -v /bin:/host/bin --name web35 training/webapp python app.py
  4. 檢視程序在容器內外的使用者:
[email protected]:/home/sammy# ps -ef | grep python
231072    1726  1686  0 01:44 ?        00:00:00 python app.py

[email protected]:/home/sammy# docker exec web35 ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 17:44 ?        00:00:00 python app.py
  • 檢視檔案/etc/subuid 和 /etc/subgid,可以看到 dockermap 使用者在host 上的 uid 和 gid 都是 231072:

複製程式碼

[email protected]:/home/sammy# cat /etc/subuid
sammy:100000:65536
stack:165536:65536
dockremap:231072:65536

[email protected]:/home/sammy# cat /etc/subgid
sammy:100000:65536
stack:165536:65536
dockremap:231072:65536

複製程式碼

  • 再看檔案/proc/1726/uid_map,它表示了容器內外使用者的對映關係,即將host 上的 231072 使用者對映為容器內的 0 (即root)使用者。
[email protected]:/home/sammy# cat /proc/1726/uid_map
         0     231072      65536
  •  現在,我們試圖在容器內修改 host 上的 /bin 資料夾,就會提示許可權不足了:
[email protected]:/host/bin# touch test2
touch: cannot touch 'test2': Permission denied

這說明通過使用 user namespace,使得容器內的程序執行在非 root 使用者,我們就成功地限制了容器內程序的許可權。

其他的幾個 namespace,比如 network,mnt 等,比較簡單,這裡就不多說了。總之,Docker 守護程序為每個容器都建立了六種namespace 的例項,使得容器中的程序都處於一種隔離的執行環境之中:

複製程式碼

[email protected]:/proc/1726/ns# ls -l
total 0
lrwxrwxrwx 1 231072 231072 0 Sep 18 01:45 ipc -> ipc:[4026532210]
lrwxrwxrwx 1 231072 231072 0 Sep 18 01:45 mnt -> mnt:[4026532208]
lrwxrwxrwx 1 231072 231072 0 Sep 18 01:44 net -> net:[4026532213]
lrwxrwxrwx 1 231072 231072 0 Sep 18 01:45 pid -> pid:[4026532211]
lrwxrwxrwx 1 231072 231072 0 Sep 18 01:45 user -> user:[4026532207]
lrwxrwxrwx 1 231072 231072 0 Sep 18 01:45 uts -> uts:[4026532209]

複製程式碼

2.4 network namespace

  預設情況下,當 docker 例項被創建出來後,使用 ip netns  命令無法看到容器例項對應的 network namespace。這是因為 ip netns 命令是從 /var/run/netns 資料夾中讀取內容的。

步驟:

  1. 找到容器的主程序 ID
[email protected]:/home/sammy# docker inspect --format '{{.State.Pid}}' web5
2704
  1. 建立  /var/run/netns 目錄以及符號連線
[email protected]:/home/sammy# mkdir /var/run/netns
[email protected]:/home/sammy# ln -s /proc/2704/ns/net /var/run/netns/web5
  1. 此時可以使用 ip netns 命令了

複製程式碼

[email protected]:/home/sammy# ip netns
web5
[email protected]:/home/sammy# ip netns exec web5 ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default
  link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
  inet 127.0.0.1/8 scope host lo
  valid_lft forever preferred_lft forever
  inet6 ::1/128 scope host
  valid_lft forever preferred_lft forever
15: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
  link/ether 02:42:ac:11:00:03 brd ff:ff:ff:ff:ff:ff
  inet 172.17.0.3/16 scope global eth0
  valid_lft forever preferred_lft forever
  inet6 fe80::42:acff:fe11:3/64 scope link
  valid_lft forever preferred_lft forever

複製程式碼

3. Docker run 命令中 namespace 中相關引數

Docker run 命令有幾個引數和 namespace 相關:

  • --ipc string IPC namespace to use
  • --pid string PID namespace to use
  • --userns string User namespace to use
  • --uts string UTS namespace to use

3.1 --userns

--userns:指定容器使用的 user namespace

  • 'host': 使用 Docker host user namespace
  • '': 使用由 `--userns-remap‘ 指定的 Docker deamon user namespace

你可以在啟用了 user namespace 的情況下,強制某個容器執行在 host user namespace 之中:

[email protected]:/proc/2835# docker run -d -v /bin:/host/bin --name web37 --userns host training/webapp python app.py
9c61e9a233abef7badefa364b683123742420c58d7a06520f14b26a547a9476c
[email protected]:/proc/2835# ps -ef | grep python
root      2962  2930  1 02:17 ?        00:00:00 python app.py

否則預設的話,就會執行在特定的 user namespace 之中了。

3.2 --pid

同樣的,可以指定容器使用 Docker host pid namespace,這樣,在容器中的程序,可以看到 host 上的所有程序。注意此時不能啟用 user namespace。

複製程式碼

[email protected]:/proc/2962# docker run -d -v /bin:/host/bin --name web38 --pid host --userns host training/webapp python app.py
f40f6702b61e3028a6708cdd7b167474ddf2a98e95b6793a1326811fc4aa161d
[email protected]:/proc/2962#
[email protected]:/proc/2962# docker exec -it web38 bash
[email protected]:/opt/webapp# ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.1  33480  2768 ?        Ss   17:40   0:01 /sbin/init
root         2  0.0  0.0      0     0 ?        S    17:40   0:00 [kthreadd]
root         3  0.0  0.0      0     0 ?        S    17:40   0:00 [ksoftirqd/0]
root         5  0.0  0.0      0     0 ?        S<   17:40   0:00 [kworker/0:0H]
root         6  0.0  0.0      0     0 ?        S    17:40   0:00 [kworker/u2:0]
root         7  0.0  0.0      0     0 ?        S    17:40   0:00 [rcu_sched]
......

複製程式碼

3.3 --uts

同樣地,可以使容器使用 Docker host uts namespace。此時,最明顯的是,容器的 hostname 和 Docker hostname 是相同的。

[email protected]:/proc/2962# docker run -d -v /bin:/host/bin --name web39 --uts host training/webapp python app.py
38e8b812e7020106bf8d3952b88085028fc87f4427af0c3b0a29b6a69c979221
[email protected]:/proc/2962# docker exec -it web39 bash
[email protected]:/opt/webapp# hostname
devstack