Docker核心原理解读-namespace

1,553 阅读6分钟
1.容器所在的运行环境统称为宿主机,可以是硬件服务器、虚拟机(其实,docker容器也支持在容器中运行容器)

2.Docker容器本质上是宿主机的进程,通过namespace实现隔离,通过cgroups实现了资源限制,通过写时复制机制(copy-on-write)实现了高效的文件操作。 

namespace

3.想要实现一个资源隔离的容器,需要6项隔离,Linux内核提供了这6种namespace隔离的系统调用,在同一个namespace下的进程可以感知彼此的变化,但不了解外界的变化,因此,可以让容器中的进程以为自己在一个独立的系统环境中,达到独立和隔离的目的。


4.四种进行namespace隔离的API操作
  • clone() //在创建新进程的同时创建namespace
  • setns() //加入一个已经存在的namespace
  • unshared() //在原先进程上进行namespace隔离,不需要启动一个新进程
  • /proc下的部分文件 //3.8版本内核开始,可以在/proc/[pid]/ns文件下看到指向不同namespace号的文件
使用时,需要指定以上六个参数中的一个或者多个,来确定隔离的是哪六项namespace,通过|(位或)操作来实现 
  • UTS(UNIX Time-sharing System) namespace
它提供了主机名和域名的隔离,这样每个Docker容器就可以拥有独立的主机名和域名了,在网络上就可以被视作一个独立的节点,而非宿主机上的一个进程。所以,在Docker中,每个镜像基本都以自身所提供的服务名称来命名镜像的hostname,且不会对宿主机产生任何影响。
  • IPC namespace
进程间通信(Inter-Process Communication,IPC)涉及的IPC资源包括常见的信号量、消息队列和共享内存。申请IPC资源就申请了一个全局唯一的32位ID,所以IPC namespace中实际上包含了系统IPC标识符以及实现POSIX消息队列的文件系统。在同一个IPC namespace下的进程彼此可见,不同IPC namespace下的进程则互不可见。
  • PID namespace
它对进程PID重新编号,因此两个不同namespace下的进程可以有相同的PID。每个PID namespace都有自己的计数程序。内核为所有PID namespace维护了一个树状结构,最顶层的是系统初始创建的,即root namespace,它创建的新PID namespace称为child namespace,原来的就称为parent namespace,从而形成一个层级体系。所属的父节点可以看到子节点中的进程,可以通过信号等方式对子节点产生影响,反之则不行。
  • 每个PID namespace中的第一个进程都会像传统Linux中的init进程一样拥有特权
  • 一个namespace中的进程,不可能通过kill或ptrace影响父节点或兄弟节点中的进程,因为其他节点的PID在这个namespace中没有意义
  • 如果在新的PID namespace中重新挂载/proc文件系统,会发现其下只显示同属一个PID namespace中的其他进程
  • root namespace中可以看到所有进程,并且递归包含所有子节点中的进程
因此,想要在外部监控Docker中运行程序,可以监控Docker daemon所在的PID namespace下的所有进程及其子进程,再进行筛选。
  • mount namespace
它通过隔离文件系统挂载点对隔离文件系统提供支持。隔离后,不同mount namespace中的文件结构发生变化也互不影响。进程在创建mount namespace时,会把当前的文件结构复制给新的namespace,新的namespace中的所有mount操作都只影响自身的文件系统,对外界不会产生任何影响。
  • network namespace
主要提供关于网络资源的隔离,包括网络设备、IPv4和IPv6协议栈、IP路由表防火墙、嵌套字等。一个物理的网络设备最多存在于一个network namespace中,可以通过创建虚拟网络设备对在不同的network namespace间创建通道来通信。说到network namespace时,指的未必是真正的网络隔离,而是把网络独立出来,给外部用户一种透明的感觉,彷佛在与一个独立的网络实体进行通信。容器的经典做法就是创建一个weth pair,一端放置在新的namespace中,一端放在原先的namespace中连接物理网络设备,再把多个设备接入网桥或者进行路由转发来通信。
  • user namespace
主要隔离了安全相关的标识符和属性,包括用户ID、用户组ID、root目录、key(密钥)以及特殊权限。即一个普通用户的进程通过clone()创建的新进程在新的user namespace中可以拥有不同的用户和用户组。这意味着一个进程在容器外属于一个没有特权的普通用户,但是它创建的容器却属于拥有所有权限的超级用户,为容器提供了极大的自由。(例子:当用户运行容器,容器内部的root用户并不等于宿主机内的root用户,而是映射到宿主上的普通用户。)
  • 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都有一个父节点user namespace以及零个或多个子节点user namespace,这一点与PID namespace非常相似。