Docker系列文-----Docker的隔离机制(5)
Docker底层的隔离机制
命名空间(namespace):
命名空间(namespace)是Linux内核的一个强大特性,为容器虚拟化带来了极大便利,利用这特性,每个容器都能够拥有自己单独的命名空间,运行在其中的应用都像是在独立的操作系统环境中一样,命名空间机制保证了容器之间彼此互不影响。
在操作系统中,包括内核、文件系统、网络、进程号(Process ID、PID)、用户名(UserID、UID、进行间通信 IPC)等资源,所有的资源都是应用进程直接共享的,要想实现虚拟化,除了要实现对内存、CPU、网络IO、硬盘IO、存储空间等的限制外,还要实现文件系统、网络、PID、UID、IPC等的相互gelling。前者相对容易实现一点,后者则需要宿主主机系统的深入支持。随着Linux系统对于命名空间功能的逐步完善,现在已经可以实现这些需求,让进程在彼此隔离的命名空间中运行,虽然这些进程仍在共用一个内核和某些环境时运行,但是彼此时不可见的,并且认为自己是独占系统的
Docker内部有多少种命名空间?
- Network命名空间
- 进程命名空间
- IPC命名空间
- 挂载命名空间
- UTS命名空间
- 用户命名空间
namespace | 系统调用参数 | 系统调用参数 |
---|---|---|
UTS | CLONE_NEWUTS | 主机名或域名 |
IPC | CLONE_NEWIPC | 信号量、消息队列和共享内存 |
PID | CLONE_NEWPID | 进程编号 |
Network | CLONE_NEWNET | CLONE_NEWNET |
Mount | CLONE_NEWNS | 挂载点(文件系统) |
User | CLONE_NEWUSER | 用户组和用户组 |
1.UTS namespace
UTS(UNIX Time-sharing System)namespace提供了主机名与域名的隔离,这样每个docke容器就可以拥有独立的主机名和域名了,在网络上可以被视为一个独立的节点,而非宿主机上的一个进程。docker中,每个镜像基本都以自身所提供的服务名称来命名镜像的hostname,且不会对宿主机产生任何影响,其原理就是使用了UTS namespace
2.IPC namespace
进程间通信(Inter-Process Communication,IPC)涉及的IPC资源包括常见的信号量、消息队列和共享内存。申请IPC资源就申请了一个全局唯一的32位ID,所以IPC namespace中实际上包含了系统IPC标识符以及实现POSIX消息队列的文件系统。在同一个IPC namespace下的进程彼此可见,不同IPC namespace下的进程则互相不可见。
目前使用IPC namespace机制的系统不多,其中比较有名的有PostgreSQL。Docker当前也使用IPC namespace实现了容器与宿主机、容器与容器之间的IPC隔离。
3.PID namespace
PID namespace隔离非常实用,它对进程PID重新标号,即两个不同namespace下的进程可以有相同的PID。每个PID namespace都有自己的计数程序。内核为所有的PID namespace维护了一个树状结构,最顶层的是系统初始时创建的,被称为root namespace,它创建的新PID namespace被称为child namespace(树的子节点),原先的PID namespace就是新创建的PID namespace的parent namespace(树的父节点)。通过这种方式,不同的PID namespace会形成一个层级体系。所属的父节点可以看到子节点中的进程,并可以通过信号等方式对子节点中的进程产生影响。反过来,子节点却不能看到父节点PID namespace中的任何内容,由此产生如下结论。
1.每个PID namespace中的第一个进程“PID 1”,都会像全通Linux中的init进程一样拥有特权,其特殊作用。
2.一个namespace中的进程,不可能通过kill或ptrace影响父节点或者兄弟节点中的进程,因为其他节点的PID在这个namespace没有任何意义。
3.如果你在新的PID namespace中重新挂载/proc文件系统,会发现其下只显示同属一个PID namespace中的其他进程。
4.在root namespace中看到所有的进程,并且递归包含所有子节点中的进程。到这里,读者可能已经联想到了一种在Docker外部监控运行程序的方法了,就是监控Docker daemon所在的PID namespace下的所有进程及子进程,在进行筛选即可。
4.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的,因为这种操作会影响到父节点的文件系统。
5.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 deamon完成veth pair的创建之前,init在管道的另一端循环等待,直到管道另一端传来Docker daemon关于veth设备的信息,并关闭管道。init才结束等待的过程,并把它的“eth0”启动起来。
与其他namespace类似,对network namespace的使用其实就是在创建的时候添加CLONE_NEWNET标识符位。
6.user namespace
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的时候制定了–user-remap,那么当用户运行容器时,容器内部的root用户并不等于宿主机的root用户,而是映射到宿主机上的普通用户。
Docker不仅使用了user namespace,还使用了在user namespace中涉及的Capability机制。从内核2.2版本开始,Linux把原来和超级用户相关的高级权限分为不同的单元,称为Capability。这样管理员就可以独立的对特定的Capability进行使用或禁止。Docker同时使用namespace和Capability,这很大程度上加强了容器的安全性。
1.一个用户可以在父user namespace中是普通用户,而在子中是超级用户。
2.从Linux 3.8 开始,创建新的user namespace不再需要root权限。