docker&kubernets基础篇(五)

Docker背后的内核知识

1. namespace资源隔离

Docker大热之后,热衷技术的开发者就会思考,想要实现一个资源隔离的容器,应该从哪些方面下手?也许第一反应就是chroot命令,这条命令给用户最直观的感受就是在使用后根目录/的挂载点切换了,即文件系统被隔离了。接着,为了在分布式的环境下进行通信和定位,容器必然要有独立的IP、端口、路由等,自然就联想到了网络的隔离。同时,容器还需要一个独立的主机名以便在网络中标识自己。有了网络,自然离不开通信,也就想到了进程间通信需要隔离。开发者可能也已经想到了权限的问题,对用户和用户组的隔离就实现了用户权限的隔离。最后,运行在容器中的应用需要有进程号(PID),自然也需要与宿主机中的PID进行隔离。由此,基本上完成了一个容器所需要做的6项隔离,Linux内核中提供了这6种namespace隔离的系统调用,如表3-1所示。当然,真正的容器还需要处理许多其他工作。
在这里插入图片描述1.1. 进行namespace API操作的4种方式
通过clone()在创建新进程的同时创建namespace;
clone()实际上是Linux系统调用fork()的一种更通用的实现方式,它可以通过flags来控制使用多少功能。一共有20多种CLONE_*的flag(标志位)参数用来控制clone进程的方方面面(如是否与父进程共享虚拟内存等)
1.2 查看/proc/[pid]/ns文件
,用户就可以在/proc/[pid]/ns文件下看到指向不同namespace号的文件,效果如下所示,形如[4026531839]者即为namespace号。

[root@iZbp102fxl8duhse0avs3mZ ~]# ls -l /proc/$$/ns
总用量 0
lrwxrwxrwx 1 root root 0 714 22:26 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0 714 22:26 ipc -> 'ipc:[4026531839]'
lrwxrwxrwx 1 root root 0 714 22:26 mnt -> 'mnt:[4026531840]'
lrwxrwxrwx 1 root root 0 714 22:26 net -> 'net:[4026531992]'
lrwxrwxrwx 1 root root 0 714 22:26 pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 714 22:26 pid_for_children -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 714 22:26 time -> 'time:[4026531834]'
lrwxrwxrwx 1 root root 0 714 22:26 time_for_children -> 'time:[4026531834]'
lrwxrwxrwx 1 root root 0 714 22:26 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 714 22:26 uts -> 'uts:[4026531838]'

1.3 通过setns()加入一个已经存在的namespace
上文提到,在进程都结束的情况下,也可以通过挂载的形式把namespace保留下来,保留namespace的目的是为以后有进程加入做准备。在Docker中,使用docker exec命令在已经运行着的容器中执行一个新的命令,就需要用到该方法。通过setns()系统调用,进程从原先的namespace加入某个已经存在的namespace,使用方法如下。通常为了不影响进程的调用者,也为了使新加入的pid namespace生效,会在setns()函数执行后使用clone()创建子进程继续执行命令,让原先的进程结束运行

1.4 通过unshare()在原先进程上进行namespace隔离
调用unshare()的主要作用就是,不启动新进程就可以起到隔离的效果,相当于跳出原先的namespace进行操作。这样,就可以在原进程进行一些需要隔离的操作。Linux中自带的unshare命令,就是通过unshare()系统调用实现的。Docker目前并没有使用这个系统调用,这里不做展开

2. UTS namespace

UTS(UNIX Time-sharing System)namespace提供了主机名和域名的隔离,这样每个Docker容器就可以拥有独立的主机名和域名了,在网络上可以被视作一个独立的节点,而非宿主机上的一个进程。Docker中,每个镜像基本都以自身所提供的服务名称来命名镜像的hostname,且不会对宿主机产生任何影响,其原理就是利用了UTS namespace。

3. IPC namespace

进程间通信(Inter-Process Communication, IPC)涉及的IPC资源包括常见的信号量、消息队列和共享内存。申请IPC资源就申请了一个全局唯一的32位ID,所以IPC namespace中实际上包含了系统IPC标识符以及实现POSIX消息队列的文件系统。在同一个IPC namespace下的进程彼此可见,不同IPC namespace下的进程则互相不可见。IPC namespace在实现代码上与UTS namespace相似,只是标识位有所变化,需要加上CLONE_NEWIPC参数。主要改动如下,其他部分不变,程序名称改为ipc.c[插图]。

4. PID namespace

PID namespace隔离非常实用,它对进程PID重新标号,即两个不同namespace下的进程可以有相同的PID。每个PID namespace都有自己的计数程序。内核为所有的PID namespace维护了一个树状结构,最顶层的是系统初始时创建的,被称为root namespace。它创建的新PID namespace被称为child namespace(树的子节点),而原先的PID namespace就是新创建的PID namespace的parent namespace(树的父节点)。通过这种方式,不同的PID namespaces会形成一个层级体系。所属的父节点可以看到子节点中的进程,并可以通过信号等方式对子节点中的进程产生影响。反过来,子节点却不能看到父节点PID namespace中的任何内容
❏ 每个PID namespace中的第一个进程“PID 1”,都会像传统Linux中的init进程一样拥有特权,起特殊作用。
❏ 一个namespace中的进程,不可能通过kill或ptrace影响父节点或者兄弟节点中的进程,因为其他节点的PID在这个namespace中没有任何意义。
❏ 如果你在新的PID namespace中重新挂载/proc文件系统,会发现其下只显示同属一个PID namespace中的其他进程。
❏ 在root namespace中可以看到所有的进程,并且递归包含所有子节点中的进程。
到这可能已经联想到一种在外部监控Docker中运行程序的方法了,就是监控Docker daemon所在的PID namespace下的所有进程及其子进程,再进行筛选即可。

5. mount namespace

mount namespace通过隔离文件系统挂载点对隔离文件系统提供支持,它是历史上第一个Linux namespace,所以标识位比较特殊,就是CLONE_NEWNS。隔离后,不同mount namespace中的文件结构发生变化也互不影响。可以通过/proc/[pid]/mounts查看到所有挂载在当前namespace中的文件系统,还可以通过/proc/[pid]/mountstats看到mount namespace中文件设备的统计信息,包括挂载文件的名字、文件系统类型、挂载位置等。进程在创建mount namespace时,会把当前的文件结构复制给新的namespace。新namespace中的所有mount操作都只影响自身的文件系统,对外界不会产生任何影响。这种做法非常严格地实现了隔离,但对某些情况可能并不适用。比如父节点namespace中的进程挂载了一张CD-ROM,这时子节点namespace复制的目录结构是无法自动挂载上这张CD-ROM的,因为这种操作会影响到父节点的文件系统。
2006年引入的挂载传播(mount propagation)解决了这个问题,挂载传播定义了挂载对象(mount object)之间的关系,这样的关系包括共享关系和从属关系,系统用这些关系决定任何挂载对象中的挂载事件如何传播到其他挂载对象[插图]。
❏ 共享关系(share relationship)。如果两个挂载对象具有共享关系,那么一个挂载对象中的挂载事件会传播到另一个挂载对象,反之亦然。
❏ 从属关系(slave relationship)。如果两个挂载对象形成从属关系,那么一个挂载对象中的挂载事件会传播到另一个挂载对象,但是反之不行;在这种关系中,从属对象是事件的接收者。一个挂载状态可能为以下一种:
一个挂载状态可能为以下一种:❏ 共享挂载(share)❏ 从属挂载(slave)❏ 共享/从属挂载(shared and slave)❏ 私有挂载(private)❏ 不可绑定挂载(unbindable)
传播事件的挂载对象称为共享挂载;接收传播事件的挂载对象称为从属挂载;同时兼有前述两者特征的挂载对象称为共享/从属挂载;既不传播也不接收传播事件的挂载对象称为私有挂载;另一种特殊的挂载对象称为不可绑定的挂载,它们与私有挂载相似,但是不允许执行绑定挂载,即创建mount namespace时这块文件对象不可被复制。通过图3-1可以更好地了解它们的状态变化。
在这里插入图片描述
最上层的mount namespace下的/bin目录与child namespace通过master slave方式进行挂载传播,当mount namespace中的/bin目录发生变化时,发生的挂载事件能够自动传播到child namespace中;/lib目录使用完全的共享挂载传播,各namespace之间发生的变化都会互相影响;/proc目录使用私有挂载传播的方式,各mount namespace之间互相隔离;最后的/root目录一般都是管理员所有,不能让其他mount namespace挂载绑定。默认情况下,所有挂载状态都是私有的。设置为共享挂载的命令如下

6. network namespace

当我们了解完各类namespace,兴致勃勃地构建出一个容器,并在容器中启动一个Apache进程时,却出现了“80端口已被占用”的错误,原来主机上已经运行了一个Apache进程,这时就需要借助network namespace技术进行网络隔离。network namespace主要提供了关于网络资源的隔离,包括网络设备、IPv4和IPv6协议栈、IP路由表、防火墙、/proc/net目录、/sys/class/net目录、套接字(socket)等。一个物理的网络设备最多存在于一个network namespace中,可以通过创建veth pair(虚拟网络设备对:有两端,类似管道,如果数据从一端传入另一端也能接收到,反之亦然)在不同的network namespace间创建通道,以达到通信目的。
在建立起veth pair之前,新旧namespace该如何通信呢?答案是pipe(管道)。以Docker daemon启动容器的过程为例,假设容器内初始化的进程称为init。Docker daemon在宿主机上负责创建这个veth pair,把一端绑定到docker0网桥上,另一端接入新建的network namespace进程中。这个过程执行期间,Docker daemon和init就通过pipe进行通信。具体来说,就是在Docker daemon完成veth pair的创建之前,init在管道的另一端循环等待,直到管道另一端传来Docker daemon关于veth设备的信息,并关闭管道。init才结束等待的过程,并把它的“eth0”启动起来。整个结构如图所示
在这里插入图片描述

7. user namespaces

user namespace主要隔离了安全相关的标识符(identifier)和属性(attribute),包括用户ID、用户组ID、root目录、key(指密钥)以及特殊权限。通俗地讲,一个普通用户的进程通过clone()创建的新进程在新user namespace中可以拥有不同的用户和用户组。这意味着一个进程在容器外属于一个没有特权的普通用户,但是它创建的容器进程却属于拥有所有权限的超级用户,这个技术为容器提供了极大的自由。user namespace是目前的6个namespace中最后一个支持的,并且直到Linux内核3.8版本的时候还未完全实现(还有部分文件系统不支持)。user namespace实际上并不算完全成熟,很多发行版担心安全问题,在编译内核的时候并未开启USER_NS。Docker在1.10版本中对user namespace进行了支持。只要用户在启动Docker daemon的时候指定了——userns-remap,那么当用户运行容器时,容器内部的root用户并不等于宿主机内的root用户,而是映射到宿主上的普通用户。在进行接下来的代码实验时,请确保系统的Linux内核

Linux中,特权用户的user ID就是0,演示的最后将看到user ID非0的进程启动user namespace后user ID可以变为0。使用user namespace的方法跟别的namespace相同,即调用clone()或unshare()时加入CLONE_NEWUSER标识位。修改代码并另存为userns.c,为了看到用户权限(Capabilities),还需要安装libcap-dev包。首先包含以下头文件以调用Capabilities包

❏ user namespace被创建后,第一个进程被赋予了该namespace中的全部权限,这样该init进程就可以完成所有必要的初始化工作,而不会因权限不足出现错误。
❏ 从namespace内部观察到的UID和GID已经与外部不同了,默认显示为65534,表示尚未与外部namespace用户映射。此时需要对user namespace内部的这个初始user和它外部namespace的某个用户建立映射,这样可以保证当涉及一些对外部namespace的操作时,系统可以检验其权限(比如发送一个信号量或操作某个文件)。同样用户组也要建立映射。
❏ 还有一点虽然不能从输出中发现,但却值得注意。用户在新namespace中有全部权限,但它在创建它的父namespace中不含任何权限,就算调用和创建它的进程有全部权限也是如此。因此哪怕是root用户调用了clone()在user namespace中创建出的新用户,在外部也没有任何权限。
❏ 最后,user namespace的创建其实是一个层层嵌套的树状结构。最上层的根节点就是root namespace,新创建的每个user namespace都有

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yitian_hm

您的支持是我最大鼓励

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值