在上一篇提到安装docker需要至少需要linux内核版本3.10以上,且需要支持cgroups和namespace功能。这是因为docker的容器实现本质还是 host上的进程。

Docker通过namespace实施了资源隔离,且通过了cgroups实施了资源约束,通过写时复制(copy-on-write)机制实现了高效的文件操作。

下面将详细介绍一下这三者。

 

一、写时复制(copy-on-write,cow)机制:

参考wiki:https://en.wikipedia.org/wiki/Copy-on-write以及http://www.jianshu.com/p/b5b95a710fec

总结起来就是,通过浅拷贝(shallow copy)复制引用而避免复制值,当需要进行写入操作时,首先进行值拷贝,再对拷贝后的值执行写入操作,这样减少了无谓的复制耗时。

特点有

  • 读取安全(但是不保证缓存一致性),写入安全(代价是加了锁,而且需要全量复制)
  • 不建议用于频繁读写场景下,全量复制很容易造成GC停顿,因此建议使用平时的ConcurrentXX包来实现。
  • 适用于对象空间占用大,修改次数少,而且对数据实效性要求不高的场景。

它适用于

并发读取场景,即多个线程/进程可以通过对一份相同快照,去处理实效性要求不是很高但是仍然要做的业务(比如实现FS\DB备份、日志、分布式路由)

 

二、namespace实施资源隔离

首先了解一下namespace能提供哪些资源隔离:

Namespace

系统调用参数

隔离内容

UTS

CLONE_NEWUTS

主机名与域名

IPC

CLONE_NEWIPC

信号量、消息队列和共享内存

PID

CLONE_NEWPID

进程编号

Network

CLONE_NEWNET

网络设备、网络栈、端口等

Mount

CLONE_NEWNS

挂载点(文件系统)

User

CLONE_NEWUSER

用户和用户组

 

 

 

 

 

 

 

 

 

 

 

 

 

 

来分别理解一下这些资源隔离主要产生什么作用,然后怎样实施。概括地讲,容器作为一个独立的存在,它需要一些基本属性来标识自己,所以一个主机名、域名是必不可少的,

除此之外容器不能作为一个孤儿独立存在,需要提供一个连接点,那么自然而然的网络相关如IP,端口,路由等也需要添加进来,容器实施隔离后容器可以作为一个独立的os在其上跑各种进程,线程,这个时候用于标识PID的隔离就出现了,进程线程之间的通信要用到的信号量、消息队列、管道、共享内存等等也需要隔离出来,除此之外像文件系统也可以独立出来。那么这个容器就可以视作一个独立的os。

我说了一大堆隔离措施和效果其实并没有什么鸟用,它应该由需求出发来进行驱动理解。首先就是为什么要实施隔离?个人感觉还是和开篇讲的一样,隔离技术的诞生不是由于资源的富余,而是资源的受限导致了开发者想尽办法用来规划各个进程的使用空间,但是只规划宿主机上的资源实际上还是不够安全,举个例子,端口冲突或者僵尸进程等等,都会导致宿主机上的其他进程受到影响,所以在这个基础上,开发者考虑搞一个完全小型的os,它不依赖于操作系统发行版,还能统一调度资源,安全性能也有足够保障,大大的提高了资源的使用效率和灵活性。现在这六项隔离措施事实上和一个os所需的几个基本要素比较贴近,随着技术的发展,这些隔离措施也越来越成熟安全,需要注意的是用户和群组这个隔离目前实施起来还有点问题,此处先mark一下。

 

那么接下来就介绍一下怎样通过namespace的API进行资源隔离,namespace的API包括clone()、setns()以及unshare(),还有/proc下的部分文件:

1)clone()

它主要用来创建一个新的独立的namespace进程,也是最常见的做法。它实际上是Linux调用fork()的一种实现方式,通过表格里的系统调用参数来控制实现哪些隔离功能。

2)/proc/<pid>/ns文件

可以在ns文件目录下看到指定pid使用的哪些隔离功能,这些文件在内核3.8版本之后以软连接的方式存在,如果pid下使用的隔离功能的namespace号相同,则意味着他们具有相同的命名空间。那么可以通过这个特性观察到docker进程下的若干容器使用的ns。此外,/proc/<pid>/ns软连接文件的另一个作用就是,一旦这个link文件被打开,只要打开的文件描述符fd存在,即使该ns下的所有进程以及全部结束,该ns也会一直存在。该特性常被docker用于绑定已有的网络和卷操作,即通过文件描述符加入一个已有的ns。

除了文件描述符,将ns目录下的文件挂载起来也能达到同样的效果。

3)sentns()

这个方法主要用于加入一个已有的ns。

上面提到,将一个已经结束全部结束进程的ns挂载起来,也可以在后续将其它进程加入进来。它最常用在docker exec命令,这个命令会进入容器内部,并可在容器内部执行新的命令。当然了,它需要和bash或sh结合起来,然后在新加入的ns中运行shell并执行命令。

4)unshare()和fork()(fork属于系统调用函数)

它的作用是在原先的进程上隔离出一个ns出来,而不需要额外起一个新的进程,由于docker没有直接使用这个玩意,这边不再介绍,深入了解可以参考链接:

http://www.man7.org/linux/man-pages/man2/unshare.2.html

https://en.wikipedia.org/wiki/Fork_(system_call)

 

接下来将分别介绍六大ns以及它们在docker常用的使用模式,由于网上已经有一些代码描述了实施这样隔离功能的效果,这边不再贴代码描述结果了。我这里主要提及一些它的使用经验和注意事项,还有表征效果。

 

1)UTS

Unix Time-sharing System为容器提供了主机名和域名的隔离,一般说来,容器的主机名等同于容器ID的短号(据说是全球唯一),此外它的域名解析服务文件resolv.conf一般等同于宿主机的域名解析,如果域名解析服务在容器启动时未特别指定,则按照默认策略实施域名解析。

但需要注意的是由于容器本身的一些特性,容器在进行域名解析的时候常常遇到一些困难,以glibc主导的linux则可以按照nsswitch.conf里面hosts字段配置的域名解析优先级进行解析,它在解析resolv.conf的nameserver时是按照从上往下依次解析的,如果解析失败则会耗费非常多的时间,而非glibc主导的linux,如musl-libc主导的alpine linux则无法使用nsswitch.conf文件进行配置域名解析的优先级,且在解析resolv.conf时是并行解析的,只返回最先解析好的结果。

另外,如果是python,并使用了eventlet某些特殊版本,它会在绿化后优先使用resolv.conf进行域名解析继而使用/etc/hosts文件进行域名解析,在socket连接时的进行的域名解析则会被eventlet的greendns接管,如果域名解析失败,则会导致eventlet的hub sleep 60s继而采用 hosts解析,对整个消息处理流程是极为不利的,如果要搞定这个问题,需要eventlet某些版本,在编写此文档时,最好使用0.21.0版本中EVENTLET_NO_GREENDNS选项配置为yes,就能避免域名解析时间过长的bug。

 2) IPC

Inter Process Communication,进程间通信,一般包括信号量、消息队列和共享内存。

可以在shell中使用ipcmk -Q创建一个消息队列,然后在容器里面也创建这样的一个队列,来观察它们的序号ipcs -q。

不得不提的是,除了docker之外,PG也使用了IPC ns。

3) PID

 PID的ns十分实用,主要体现在不同的ns下可以使用相同的PID号。每个PID ns都有自己的计数程序,且内核为所有PID ns维护了一个树状结构,最顶层的是系统初始化时创建的,被称作 root ns。它新创建的PID ns被称作子 ns,而它作为子ns的父ns,从而了解到,父ns中可以看到所有子ns中的进程,而反过来,子ns则无法看到父ns中的内容。由此可以得出下面的结论:

  • 每个ns中的第一个进程 PID 为 1,都像linux的init进程一样具有特权
  • 子ns中的进程无法影响父ns或其它兄弟ns中的进程,因为他们是相互隔离的,不可见的
  • 上文提到过/proc文件夹,里面描述了各个pid下面的各种ns,如果在ns中挂载这个文件系统,那么只能看到同一个ns下的其他进程
  • root ns可以看到所有进程

由此可以推断,docker daemon所在的PIDns可以看到所有由其衍生出来的容器进程及容器内部进程。

另外还有一些补充,

  • PID为1的init进程:它在ns中是所有进程的父进程,维护进程表,如果有父进程出错导致子进程成为孤儿,init进程则负责回收子进程资源。可以理解为,第一个运行的进程应该具有资源监控和回收能力,比如bash
  • init进程可以防止被同一个ns的进程误杀,此外,父ns除了发送SIGKILL或SIGSTOP给子ns,其他信号也不会被init进程接受。如果子ns的init被父ns强制终止,子ns中的资源会被回收

4)mount ns

史上第一个linux ns。2006引入了挂载传播,它定义了挂载对象的关系,包括共享关系和从属关系。一个挂载状态可以为:

  • 共享挂载
  • 从属挂载
  • 共享/从属挂载
  • 私有挂载
  • 不可绑定挂载

传播事件的挂载对象称作共享挂载,接收传播事件的挂载对象称作从属挂载,兼具前两者特征的是共享/从属挂载,而两者特征都没有的则称作私有挂载,最后不可绑定挂载和私有挂载很像,但不允许执行绑定挂载。

5) network ns

network ns主要是为了防止port被占用的尴尬,它主要提供了网络资源的隔离,包括网络设备,IPv4和IPv6协议栈,路由表,防火墙,/proc/net /sys/class/net 套接字等。

一个物流网络设备只能存在在一个ns中,如果需要两个ns通信,则需要在两个ns中分别建立veth。需要了解一下,在两个ns的物理网络设备建立通信之前,两个ns通过pipe进行通信。以docker创建的容器为例,init在管道的外侧等待另一端的反馈,如果另一端的docker daemon反馈了信息,则关闭管道。

6) user ns

主要实现隔离了安全相关的标识符,属性,包括用户的ID,用户群组ID,root目录,key等。在用户启动docker daemon的时候指定 --userns-remap,那么启动后的容器的root用户与host上的root用户并不等同。

它有以下特点:

  • user ns创建之后第一个进程赋予了该ns中的全部权限,这样init就可以完成所有必要初始化工作
  • ns内部的UID和GID已经不同,默认显示为65534则意味着未与外部ns用户映射。
  • 新的ns中所具有的各种权限,在其他ns中没有效果
  • 它和PID一样,也是一个树状结构

一点补充说明:ns的六项隔离只能初步实现了一个容器生命周期内所需的资源,但仍未达到一个独立os的要求,除了上述描述的之外,还需要SElinux,cgroups,/sys,/proc/sys,/dev/sd*等资源。

 

三、cgroups

参考文献:http://www.infoq.com/cn/articles/docker-kernel-knowledge-cgroups-resource-isolation

cgroups, control groups,是linux 内核提供的一种机制,这种机制可以根据需求把一系列系统任务及其子任务整合或分隔到按资源划分等级的不同组内,从而为系统资源管理提供一个统一的框架。它可以限制被ns隔离后的资源,可以为资源设置权重、计算使用量、操控进程或线程启停等。

它的特点:

  • 它的API以一个伪文件系统的方式实现,用户态的程序可以通过文件操作实现cgroups的组织管理
  • 组织管理操作单元的粒度细到线程级别,可以通过创建销毁cgroups来实施资源的再分配
  • 所有的资源管理功能都以子系统的方式实现,接口统一
  • 子任务在初建立时与父任务在同一个cgroups

它的作用:

  • 资源限制
  • 优先级分配
  • 资源统计
  • 任务控制

它的相关术语,在此之前首先了解一下它的结构,事实上它和PID ns比较相似,也是一个树状结构,子 cgroups 从父 cgroups里面继承属性。

它的术语包括:

  • task,可以直接理解为一个进程或线程
  • cgroup,cgroups里面的资源控制都以cgroup为单位,它实际上是一个task组,包含一个或者多个subsystem,task可以在cgroup之间迁移
  • subsystem, 资源调度控制器,比如 CPU subsystem可以控制cpu时间分配,内存subsystem可以控制内存使用
  • hierarchy,由一系列cgroup以树状结构排列而来

它的组织架构和基本规则:

  • 规则1: 同一个hierarchy可以附加一个或多个subsystem。如下图1,cpu和memory的subsystem附加到了一个hierarchy。

  • 规则2: 一个subsystem可以附加到多个hierarchy,当且仅当这些hierarchy只有这唯一一个subsystem。

  • 规则3: 系统每次新建一个hierarchy时,该系统上的所有task默认构成了这个新建的hierarchy的初始化cgroup,这个cgroup也称为root cgroup。对于你创建的每个hierarchy,task只能存在于其中一个cgroup中,即一个task不能存在于同一个hierarchy的不同cgroup中,但是一个task可以存在在不同hierarchy中的多个cgroup中。如果操作时把一个task添加到同一个hierarchy中的另一个cgroup中,则会从第一个cgroup中移除。

  • 规则4: 进程(task)在fork自身时创建的子任务(child task)默认与原task在同一个cgroup中,但是child task允许被移动到不同的cgroup中。即fork完成后,父子进程间是完全独立的。