关于docker:彻底搞懂容器技术的基石-namespace-下

45次阅读

共计 8100 个字符,预计需要花费 21 分钟才能阅读完成。

大家好,我是张晋涛。

目前咱们所提到的容器技术、虚拟化技术(不管何种抽象层次下的虚拟化技术)都能做到资源层面上的隔离和限度。

对于容器技术而言,它实现资源层面上的限度和隔离,依赖于 Linux 内核所提供的 cgroup 和 namespace 技术。

咱们先对这两项技术的作用做个概括:

  • cgroup 的次要作用:治理资源的调配、限度;
  • namespace 的次要作用:封装形象,限度,隔离,使命名空间内的过程看起来领有他们本人的全局资源;

这是一个系列文章,对此系列感兴趣的小伙伴能够查看:

  • 彻底搞懂容器技术的基石:cgroup
  • 彻底搞懂容器技术的基石:namespace(上)

本篇咱们将持续聊 namespace。

Namespace 类型

咱们先来总览一下 namespace 的类型,上篇中曾经为大家介绍过 Cgroup , IPC, NetworkMount 等 4 种类型的 namespace。咱们持续聊残余的局部。

namespace 名称 应用的标识 – Flag 管制内容
Cgroup CLONE_NEWCGROUP Cgroup root directory cgroup 根目录
IPC CLONE_NEWIPC System V IPC, POSIX message queues 信号量,音讯队列
Network CLONE_NEWNET Network devices, stacks, ports, etc. 网络设备,协定栈,端口等等
Mount CLONE_NEWNS Mount points 挂载点
PID CLONE_NEWPID Process IDs 过程号
Time CLONE_NEWTIME Boot and monotonic clocks 启动和枯燥时钟
User CLONE_NEWUSER User and group IDs 用户和用户组
UTS CLONE_NEWUTS Hostname and NIS domain name 主机名与 NIS 域名

PID namespaces

咱们晓得在 Linux 零碎中,每个过程都会有本人的独立的 PID,而 PID namespace 次要是用于隔离过程号。即,在不同的 PID namespace 中能够蕴含雷同的过程号。

每个 PID namespace 中过程号都是从 1 开始的,在此 PID namespace 中可通过调用 fork(2), vfork(2)clone(2) 等零碎调用来创立其余领有独立 PID 的过程。

要应用 PID namespace 须要内核反对 CONFIG_PID_NS 选项。如下:

(MoeLove) ➜ grep CONFIG_PID_NS /boot/config-$(uname -r)
CONFIG_PID_NS=y

init 过程

咱们都晓得在 Linux 零碎中有一个过程比拟非凡,所谓的 init 过程,也就是 PID 为 1 的过程。

后面咱们曾经说了每个 PID namespace 中过程号都是从 1 开始的,那么它有什么特点呢?

首先,PID namespace 中的 1 号过程是所有孤立过程的父过程。

其次,如果这个过程被终止,内核将调用 SIGKILL 收回终止此 namespace 中的所有过程的信号。这部分内容与 Kubernetes 中利用的优雅敞开 / 平滑降级等都有肯定的分割。(对此局部感兴趣的小伙伴能够留言交换,如果对这些内容感兴趣的话,我能够专门写一篇开展来聊)

最初,从 Linux v3.4 内核版本开始,如果在一个 PID namespace 中产生 reboot() 的零碎调用,则 PID namespace 中的 init 过程会立刻退出。这算是一个比拟非凡的技巧,可用于解决高负载机器上容器退出的问题。

PID namespace 的层次结构

PID namespace 反对嵌套,除了初始的 PID namespace 外,其余的 PID namespace 都领有其父节点的 PID namespace。

也就是说 PID namespace 也是树形构造的,此构造内的所有 PID namespace 咱们都能够追踪到先人 PID namespace。当然,这个深度也不是有限的,从 Linux v3.7 内核版本开始,树的最大深度被限度成 32。

如果达到此最大深度,将会抛出 No space left on device的谬误。(我之前尝试嵌套容器的时候遇到过)

在同一个(且同级)PID namespace 中,过程间彼此可见。

但如果某个过程位于子 PID namespace 的话,那么该过程是看不到上一层(即,父 PID namespace)中的过程的。

过程间是否可见,决定了过程间是否存在肯定的关联和调用关系,小伙伴们对这个应该比拟相熟,这里我就不赘述了。

那么,过程是否能够调度到不同层级的 PID namespace 呢?

咱们先来说论断,过程在 PID namespace 中的调度只能是单向调度(从高 -> 低)。即:

  • 过程只能从父 PID namespace 调度到 子 PID namespace 中;
  • 过程不能从子 PID namespace 调度到 父 PID namespace 中;

图 1,通过 setns(2) 调度过程阐明

PID namespace 的层级关系其实是由 ioctl_ns(2) 零碎调用进行发现和保护的(NS_GET_PARENT),这里先不开展。那么,上述内容中的调度是如何实现的呢?

要解答这个问题,就必须先意识到在 PID namespace 创立之初,哪些过程具备该 namespace 的权限就曾经确定了。至于调度,咱们能够简略地将其了解成关系映射或者符号链接。

线程必须在同一个 PID namespace 中,以便保障过程中的线程间能够彼此互传信号。这就导致了CLONE_NEWPID 不能与 CLONE_THREAD 同时应用。但如果散布在不同 PID namespace 的多个过程相互有信号传递的需要要怎么办呢?用共享的信号队列即可解决。

此外,咱们常接触到的 /proc 目录下有很多 /proc/${PID}的目录,在其中可看到 PID namespace 中的过程状况。同时此目录也是可间接通过挂载形式进行操作的。比方:

(MoeLove) ➜ mount |grep proc 
proc on /proc type proc (rw,nosuid,nodev,noexec,relatime)

有没有方法晓得以后最大的 PID 数呢?

这也是能够的,自从 Linux v3.3 版本的内核开始新增了一个 /proc/sys/kernel/ns_last_pid的文件,用于记录最初一个过程的 ID。

当须要调配下一个过程 ID 的时候,内核会去搜寻最大的未应用 ID 进行调配,随后会更新此文件中 PID 的信息。

Time namespaces

在聊 time namespace 之前,咱们须要先聊下 枯燥工夫。首先,咱们通常提到的零碎工夫,指的是 clock realtime,即,机器对以后工夫的展现。它能够向前或者向后调整(联合 NTP 服务来了解)。而 clock monotonic 示意在某一时刻之后的工夫记录,它是单向向后的相对工夫,不受零碎工夫的变动所影响。

应用 time namespace 须要内核反对 CONFIG_TIME_NS 选项。如:

(MoeLove) ➜ grep CONFIG_TIME_NS /boot/config-$(uname -r)
CONFIG_TIME_NS=y

time namespace 不会虚拟化 CLOCK_REALTIME 时钟。你可能会好奇,为什么内核反对 time namespace 呢?次要是为了一些非凡的场景。

time namespace 中的所有过程共享由 time namespace 提供的以下两个参数:

  • CLOCK_MONOTONIC – 枯燥工夫,一个不可设置的时钟;
  • CLOCK_BOOTTIME(可参考 CLOCK_BOOTTIME_ALARM 内核参数)- 不可设置的时钟,包含零碎暂停的工夫。

time namespace 目前只能应用 CLONE_NEWTIME 标识,通过调用 unshare(2) 零碎调用进行创立。创立 time namespace 的过程是独立于新建的 time namespace 之外的,而该过程后续的子过程将会被搁置到新建的 time namespace 之内。同一个 time namespace 中的过程们会共享 CLOCK_MONOTONIC 和 CLOCK_BOOTTIME。

当父过程创立子过程时,子过程的 time namespace 归属将在文件 /proc/[pid]/ns/time_for_children 中显示。

(MoeLove) ➜ ls -al /proc/self/ns/time_for_children 
lrwxrwxrwx. 1 tao tao 0 12 月 14 02:06 /proc/self/ns/time_for_children -> 'time:[4026531834]'

文件 /proc/PID/timens_offsets 定义了初始 time namespace 的枯燥时钟和启动时钟,并记录了偏移量。(如果一个新的 time namespace 还没有过程入驻时,是能够进行批改的。这里暂不开展,感兴趣的小伙伴可讨论区留言交换探讨。)

须要留神的是:在初始的 time namespace 中,/proc/self/timens_offsets 显示的偏移量都为 0。

(MoeLove) ➜ cat /proc/self/timens_offsets 
monotonic           0         0
boottime            0         0

其中第二列和第三列的含意如下:

  • <offset-secs> 能够为负值,单位:秒(s)
  • <offset-nanosecs> 是个无符号值,单位:纳秒(ns)

以下的时钟接口都与此 namespace 有所关联:

  • clock_gettime(2)
  • clock_nanosleep(2)
  • nanosleep(2)
  • timer_settime(2)
  • timerfd_settime(2)

整体而言,time namespace 在一些非凡场景下还是很有用的。

User namespaces

User namespaces 顾名思义是隔离了用户 id、组 id 等。

应用 user namespaces 须要内核反对 CONFIG_USER_NS 选项。如:

➜  local_time grep CONFIG_USER_NS /boot/config-$(uname -r)
CONFIG_USER_NS=y

过程的用户 id 和组 id 在一个 user namespace 内和外有可能是不同的。

比方,一个过程在 user namespace 中的用户和组能够是特权用户(root),但在该 user namespace 之外,可能只是一个一般的非特权用户。这就波及到用户、组映射(uid_map、gid_map)等相干的内容了。

自 Linux v3.5 版本的内核开始,在 /proc/[pid]/uid_map/proc/[pid]/gid_map 文件中,咱们能够查看到映射内容。

(MoeLove) ➜ cat /proc/self/uid_map 
         0          0 4294967295
(MoeLove) ➜ cat /proc/self/gid_map 
         0          0 4294967295

user namespace 也反对嵌套,应用 CLONE_NEWUSER 标识,应用 unshare(2) 或者 clone(2) 等零碎调用来创立,最大的嵌套层级深度也是 32。

如果是通过 fork(2) 或者 clone(2) 创立的子过程没带有 CLONE_NEWUSER 标识,也是一样的,子过程跟父过程同在一个 user namespace 中。树状的关联关系同样通过 ioctl(2) 零碎调用接口保护。

一个单线程过程能够通过 setns(2) 零碎调用来调整其归属的 user namespace。

此外,user namespace 还有个很重要的规定,那就是对于 Linux capability 的继承关系。对于 Linux capability 我就不开展了,这里简略记录一下:

  • 当过程所在的 user namespace 领有 effective capability set 中的 capability 时,该过程具备该 capability。
  • 当过程在该 user namespace 中领有 capability 时,该过程在此 user namespace 的所有子 user namespace 中都领有该 capability。
  • 创立该 user namespace 的用户会被内核记录为 owner,即,领有该 user namespace 中的全副 capabilities。

对于 Docker 而言,它能够原生的反对此能力,进而达到对容器环境的一种爱护。

UTS namespaces

UTS namespaces 隔离了主机名和 NIS 域名。

应用 UTS namespaces 须要内核反对 CONFIG_UTS_NS 选项。如:

(MoeLove) ➜ grep CONFIG_UTS_NS /boot/config-$(uname -r)
CONFIG_UTS_NS=y

在同一个 UTS namespace 中,通过 sethostname(2) 和 and setdomainname(2) 零碎调用进行的设置和批改是所有过程共享查看的,然而对于不同 UTS namespaces 而言,则彼此隔离不可见。

Namespaces 次要的 API

后面内容中提到了很多的零碎调用,这里咱们来挑几个重要的介绍一下。

clone(2)

零碎调用 clone(2) 创立一个新的过程,它会依据参数中的 CLONE_NEW* 设置,一一实现对应的配置性能。当然这个零碎调用也实现了一些与 namespace 无关的性能。对低于 Linux 3.8 版本内核的零碎而言,大多数状况下,须要具备 CAP_SYS_ADMIN 的 capability。

unshare(2)

零碎调用 unshare(2) 将过程调配至新的 namespace,同样,它也会依据参数中的 CLONE_NEW* 设置来调整实现对应的配置性能。对低于 Linux 3.8 的零碎而言,大多数状况,须要具备 CAP_SYS_ADMIN 的 capability。

setns(2)

零碎调用 setns(2) 将过程挪动到某一已存在的 namespace,这会导致 /proc/[pid]/ns 对应的目录中内容的变更。过程创立的子过程能够通过调用 unshare(2) 和 setns(2) 来调整所属的 namespace。这通常是须要具备 CAP_SYS_ADMIN 的 capability 的。

一些要害目录阐明

/proc/[pid]/ns/ 目录

每个过程都有一个 /proc/[pid]/ns/ 子目录,目录中的内容会受到 setns(2) 零碎调用的影响。只有目录中的文件被关上,对应的 namespace 就不能被销毁。零碎能够通过调用 setns(2) 来变更这些文件内容。

  • Linux 3.7 及更晚期的版本 – 文件是以硬链接形式存在的;
  • Linux 3.8 开始 – 文件以软连贯的形式存在;
(MoeLove) ➜ ls -l --time-style='+' /proc/$$/ns  
总用量 0
lrwxrwxrwx. 1 tao tao 0  cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx. 1 tao tao 0  ipc -> 'ipc:[4026531839]'
lrwxrwxrwx. 1 tao tao 0  mnt -> 'mnt:[4026531840]'
lrwxrwxrwx. 1 tao tao 0  net -> 'net:[4026532008]'
lrwxrwxrwx. 1 tao tao 0  pid -> 'pid:[4026531836]'
lrwxrwxrwx. 1 tao tao 0  pid_for_children -> 'pid:[4026531836]'
lrwxrwxrwx. 1 tao tao 0  time -> 'time:[4026531834]'
lrwxrwxrwx. 1 tao tao 0  time_for_children -> 'time:[4026531834]'
lrwxrwxrwx. 1 tao tao 0  user -> 'user:[4026531837]'
lrwxrwxrwx. 1 tao tao 0  uts -> 'uts:[4026531838]'

如果两个过程的 namespace 雷同,那么它们这个目录内的内容应该是一样的。

以下是该目录下文件的具体阐明:

文件名称 起始版本 形容
/proc/[pid]/ns/cgroup Linux 4.6 过程的 cgroup namespace
/proc/[pid]/ns/ipc Linux 3.0 过程的 IPC namespace
/proc/[pid]/ns/mnt Linux 3.8 过程的 mount namespace
/proc/[pid]/ns/net Linux 3.0 过程的 network namespace
/proc/[pid]/ns/pid Linux 3.8 过程的 PID namespace 在过程的整个生命周期里是不变的
/proc/[pid]/ns/pid_for_children Linux 4.12 过程创立子过程的 PID namespace 这个文件与 /proc/[pid]/ns/pid 不肯定统一。
/proc/[pid]/ns/time Linux 5.6 过程的 time namespace
/proc/[pid]/ns/time_for_children Linux 5.6 过程创立子过程的 time namespace
/proc/[pid]/ns/user Linux 3.8 过程的 user namespace
/proc/[pid]/ns/uts Linux 3.0 过程的 UTS namespace

/proc/sys/user 目录

/proc/sys/user 目录下的文件记录了各 namespace 的相干限度。当达到限度,相干调用会报错 error ENOSPC。

文件名称 限度内容阐明
max_cgroup_namespaces 在 user namespace 中的每个用户能够创立的最大 cgroup namespaces 数
max_ipc_namespaces 在 user namespace 中的每个用户能够创立的最大 ipc namespaces 数
max_mnt_namespaces 在 user namespace 中的每个用户能够创立的最大 mount namespaces 数
max_net_namespaces 在 user namespace 中的每个用户能够创立的最大 network namespaces 数
max_pid_namespaces 在 user namespace 中的每个用户能够创立的最大 PID namespaces 数
max_time_namespaces Linux 5.7 在 user namespace 中的每个用户能够创立的最大 time namespaces 数
max_user_namespaces 在 user namespace 中的每个用户能够创立的最大 user namespaces 数
max_uts_namespaces 在 user namespace 中的每个用户能够创立的最大 uts namespaces 数

Namespace 的生命周期

失常的 namespace 的生命周期与最初一个过程的终止和来到相干。

但有一些状况,即便最初一个过程曾经退出了,namespace 仍不能被销毁。这里来略微聊下这些非凡的状况:

  • /proc/[pid]/ns/* 中的文件被关上或者 mount,即便最初一个过程退出,也不能被销毁;
  • namespace 存在分层,子 namespace 仍存在,即便最初一个过程退出,也不能被销毁;
  • 一个 user namespace 领有一些非 user namespace(比方领有 PID namespace 等其余的 namespace 存在),即便最初一个过程退出,也不能被销毁;
  • 对于 PID namespace 而言,如果与 /proc/[pid]/ns/pid_for_children 存在关联关系时,即便最初一个过程退出,也不能被销毁;

当然还有一些其余的状况,有空再补充。

总结

通过之前的一篇,和本篇,次要为大家介绍了 Linux namespace 的倒退历程,根本类型,次要 API 以及一些应用场景和用处。

namespace 对于容器技术而言,是十分外围的局部。后续本系列中还将持续为大家分享对于容器和 Kubernetes 等技术的内容,敬请期待。


欢送订阅我的文章公众号【MoeLove】

正文完
 0