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


本系列文章將介紹Docker的有關知識:

(1)Docker 安裝及基本用法

(2)Docker 鏡像

(3)Docker 容器的隔離性 - 使用 Linux namespace 隔離容器的運行環境

(4)Docker 容器的隔離性 - 使用 cgroups 限制容器使用的資源

(5)Docker 網絡

 

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 進程.

root@devstack:/home/sammy# ps -ef | grep python
root 2198 2179 0 00:06 ? 00:00:00 python app.py

root@devstack:/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


root@devstack:/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:

root@devstack:/home/sammy# hostname
devstack
root@devstack:/home/sammy# docker exec -it web31 hostname
8b7dd09fbcae

2.3 user namespace

2.3.1 Linux 內核中的 user namespace

老版本中,Linux 內核里面只有一個數據結構負責處理用戶和組。內核從3.8 版本開始實現了 user namespace。通過在 clone() 系統調用中使用 CLONE_NEWUSER 標志,一個單獨的 user namespace 就會被創建出來。在新的 user namespace 中,有一個虛擬的用戶和用戶組的集合。這些用戶和用戶組,從 uid/gid 0 開始,被映射到該 namespace 之外的 非 root 用戶。
 
在現在的linux內核中,管理員可以創建成千上萬的用戶。這些用戶可以被映射到每個 user namespace 中。 通過使用 user namespace 功能,不同的容器可以有完全不同的 uid 和 gid 數字。容器 A 中的 User 500 可能被映射到容器外的 User 1500,而容器 B 中的 user 500 可能被映射到容器外的用戶 2500.
 
為什么需要這么做呢?因為在容器中,提供 root 訪問權限有其特殊用途。想象一下,容器 A 中的 root 用戶 (uid 0) 被映射到宿主機上的 uid 1000,容器B 中的 root 被映射到 uid 2000.類似網絡端口映射,這允許管理員在容器中創建 root 用戶,而不需要在宿主機上創建。 
 
從內核的提交日志上看,user namespace 是 linux 內核 3.8 版本中引入的,而 RedHat 企業版 7 的 linux 內核版本是 3.10,但 7.1版本並不支持 user namespace。這是為什么呢?實際上,在 Fedora 項目中,Redhat 已經在 user namespace 上已經投入了很長時間了,而且認為這是一個非常重要的功能。因此,我們並沒有在 7.1 中啟用 user namespace,直到我們認為它滿足了生產要求為止。而新版本的 Fedora 已經啟用了該功能了。在最新的  RedHat 企業版 Linux 7.4 版本中,已經正式啟用了 user namespace:

(引用自 https://www.redhat.com/cms/managed-files/li-new-in-rhel74-technology-overview-f10498kc-201801-en.pdf

2.3.2 Docker 對 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 目錄做任意修改:
root@devstack:/home/sammy# docker exec -ti web34 id
uid=0(root) gid=0(root) groups=0(root)
root@devstack:/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. 查看進程在容器內外的用戶:
root@devstack:/home/sammy# ps -ef | grep python
231072    1726  1686  0 01:44 ?        00:00:00 python app.py

root@devstack:/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:
root@devstack:/home/sammy# cat /etc/subuid
sammy:100000:65536
stack:165536:65536
dockremap:231072:65536

root@devstack:/home/sammy# cat /etc/subgid
sammy:100000:65536
stack:165536:65536
dockremap:231072:65536

  • 再看文件/proc/1726/uid_map,它表示了容器內外用戶的映射關系,即將host 上的 231072 用戶映射為容器內的 0 (即root)用戶。
root@devstack:/home/sammy# cat /proc/1726/uid_map
         0     231072      65536
  •  現在,我們試圖在容器內修改 host 上的 /bin 文件夾,就會提示權限不足了:
root@80993d821f7b:/host/bin# touch test2
touch: cannot touch 'test2': Permission denied

這說明通過使用 user namespace,使得容器內的進程運行在非 root 用戶,我們就成功地限制了容器內進程的權限。

2.3.3 檢查 linux 操作系統是否啟用了 user namespace

運行下面的命令即可檢查是否啟用了:

 
[root@node1 1573]# uname -a
Linux node1.exampleos.com 3.10.0-514.2.2.el7.x86_64 #1 SMP Tue Dec 6 23:06:41 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux

[root@node1 1573]# cat /boot/config-3.10.0-514.2.2.el7.x86_64 | grep CONFIG_USER_NS
CONFIG_USER_NS=y

如果是 「y」,則啟用了,否則未啟用。同樣地,可以查看其它 namespace:

CONFIG_UTS_NS=y
CONFIG_IPC_NS=y
CONFIG_USER_NS=y
CONFIG_PID_NS=y
CONFIG_NET_NS=y

2.3.4 在 Centos/RedHat Linux 7 中啟用 user namespace

資料來源:https://github.com/procszoo/procszoo/wiki/How-to-enable-%22user%22-namespace-in-RHEL7-and-CentOS7%3F

這兩個版本中,默認 user namespace 是未被啟用的。

運行下面的命令,然后運行 reboot,就可以啟用了:

grubby --args="user_namespace.enable=1" --update-kernel="$(grubby --default-kernel)"

運行下面的命令,然后運行 reboot,就關閉了:

grubby --remove-args="user_namespace.enable=1" --update-kernel="$(grubby --default-kernel)"

2.3.5 OpenShift 對 user namespace 的支持

在 OpenShift 3.11 版本中,應該還不支持 user namespace,下面是 dockerd 進程:

/usr/bin/dockerd-current --add-runtime docker-runc=/usr/libexec/docker/docker-runc-current --default-runtime=docker-runc 
--exec-opt native.cgroupdriver=systemd --userland-proxy-path=/usr/libexec/docker/docker-proxy-current
--init-path=/usr/libexec/docker/docker-init-current --seccomp-profile=/etc/docker/seccomp.json
--signature-verification=False --storage-driver overlay2 --mtu=1400
[root@node1 1573]# ls
attr       cgroup      comm             cwd      fd       io        map_files  mountinfo   net        oom_adj        pagemap      root       sessionid  stack  status   timers
autogroup  clear_refs  coredump_filter  environ  fdinfo   limits    maps       mounts      ns         oom_score      personality  sched      setgroups  stat   syscall  uid_map
auxv       cmdline     cpuset           exe      gid_map  loginuid  mem        mountstats  numa_maps  oom_score_adj  projid_map   schedstat  smaps      statm  task     wchan
[root@node1 1573]# cat uid_map 
         0          0 4294967295
[root@node1 1573]# cat gid_map 
         0          0 4294967295

正是/proc/<pid>/uid_map 和 /proc/<pid>/gid_map 這兩個文件, 把容器中的uid和真實系統的uid給映射在一起。這兩個文件的格式為:

ID-inside-ns ID-outside-ns length 

其中:  

  • 第一個字段ID-inside-ns表示在容器顯示的UID或GID,
  • 第二個字段ID-outside-ns表示容器外映射的真實的UID或GID。
  • 第三個字段表示映射的范圍,一般填1,表示一一對應。

舉個例子, 0 1000 256這條配置就表示父user namespace中的1000~1256映射到新user namespace中的0~256。

 比如,把真實的uid=1000映射成容器內的uid=0:

把namespace內部從0開始的uid映射到外部從0開始的uid,其最大范圍是無符號32位整形:

上面的截圖中正是后面這種情形,也就是容器中的 uid 和宿主機上的 uid 是從0開始一一對應着映射的。 

備注:linux user namespace 非常復雜,應該是所有 namespace 中最復雜的一個。這里只是一個簡單介紹,還進一步理解,還需要閱讀更多材料,比如 https://lwn.net/Articles/532593/ 系列文章。

2.4 network namespace

  默認情況下,當 docker 實例被創建出來后,使用 ip netns  命令無法看到容器實例對應的 network namespace。這是因為 ip netns 命令是從 /var/run/netns 文件夾中讀取內容的。

步驟:
  1. 找到容器的主進程 ID
root@devstack:/home/sammy# docker inspect --format '{{.State.Pid}}' web5
2704
  1. 創建  /var/run/netns 目錄以及符號連接
root@devstack:/home/sammy# mkdir /var/run/netns
root@devstack:/home/sammy# ln -s /proc/2704/ns/net /var/run/netns/web5
  1. 此時可以使用 ip netns 命令了
root@devstack:/home/sammy# ip netns
web5
root@devstack:/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

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

root@devstack:/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] 

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 之中:

root@devstack:/proc/2835# docker run -d -v /bin:/host/bin --name web37 --userns host training/webapp python app.py
9c61e9a233abef7badefa364b683123742420c58d7a06520f14b26a547a9476c
root@devstack:/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。

root@devstack:/proc/2962# docker run -d -v /bin:/host/bin --name web38 --pid host --userns host training/webapp python app.py
f40f6702b61e3028a6708cdd7b167474ddf2a98e95b6793a1326811fc4aa161d
root@devstack:/proc/2962#
root@devstack:/proc/2962# docker exec -it web38 bash
root@f40f6702b61e:/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 是相同的。

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

 

 

參考鏈接


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM