容器核心技术

云原生使微服务、容器技术越来越趋于自动化,以前我一直以为以Docker为首的容器技术是一个新的技术,然而,通过最近的深入学习,发现原来容器技术的本质并非新的技术,而是依赖于两种相对成熟的技术:NameSpace和Cgroup。通过组合这两种技术,对传统物理机时代、虚拟机时代进行了根本的变革。下面就分别介绍下这两种相对成熟的技术。

1、NameSpace

NameSpace(命名空间)提供了一种内核级别的系统资源隔离方式。系统可以为进程分配不同的命名空间,并保证不同命名空间资源独立分配、进程彼此隔离。也就是不同命名空间下的进程互不干扰。Linux支持的NameSpace类型、隔离资源及Kernel版本如下所示:

容器核心技术

在主机的/proc/pid/ns目录中可以查看进程归属的NameSpace

容器核心技术


Linux对NameSpace的操作主要是通过以下三个系统调用函数实现的:

1.clone 在创建新进程的系统调用时,可以通过flags参数指定需要新建命名空间的类型,与其相关的flags及系统调用的代码如下:

//CLONE_NEWCGROUP
//CLONE_NEWIPC
//CLONE_NEWNET
//CLONE_NEWNS
//CLONE_NEWPID
//CLONE_NEWUSER
//CLONE_NEWUTS
int clone(int (*fn)(void *),void *child_stack,int flags,void *arg)

2.setns 该系统调用可以让调用进程加入某个已经存在的命名空间中

int setns(int fd,int nstype)

3.unshare 该系统调用可以将调用进程移动到新的命名空间下

int unshare(int flags)

1.1 IPC

进程间通信(Interprocess Communication)是Linux系统中最常用的通信手段。IPC NameSpace用于隔离IPC资源,包含System V IPC对象和POSIX消息队列。其中System V IPC对象包含信号量、共享内存和消息队列,用于进程间的通信,System V IPC对象具有全局唯一的标识,对在该IPC NameSpace内的进程可见,而对其外的进程不可见,当IPCNameSpace被销毁后。所有的IPC对象也会被自动销毁

Kubernetes允许用户在Pod中使用hostIPC进行定义,通过该属性使授权用户容器共享主机IPC NameSpace,达到进程间通信的目的。

1.2 Network

Network NameSpace提供了关于系统上网络资源的隔离,例如网络设备、IPV4和IPV6协议栈,IP路由表,防火墙规则、/proc/net目录(/proc/pid/net目录的符号链接)、/sys/class/net目录、/proc/sys/net目录下的很多文件、端口号(socket)等。一个物理的网络设备通常会被放到主机的Network NameSpace不同网络的NameSpace由网络虚拟设备(Virtual Ethernet Device即VETH),再基于网桥或者路由实现与物理网络设备的联通。当网络NameSpace被释放后,对应的VETH Pair也会被自动释放。

在Kubernetes中,同一Pod的不同容器共享同一网络的NameSpace,没有例外,这使得Kubernetes能将网络挂载在更轻量、更稳定的sandbox容器上,而用户自定义的容器只需复用已配置好的网络即可、另外,同一Pod的不同容器中运行的进程可以基于localhost彼此通信,这在多容器进程、彼此需要通信的场景下是非常有效的

1.3 PID

PID NameSpace用于进程号隔离,不同PID NameSpace中的进程PID可以相同,容器启动后,Entrypoint进程会作为PID为1的进程㛮,因此是该PID NameSpace的init进程。他是当前NameSpace所有进程的父进程,如果该进程退出,内核会对该PID NameSpace的所有进程发送SIGKILL信号,以便同时结束他们。init进程默认屏蔽系统信号,即除非对该进程对系统信号做特殊处理,否则发往该进程的系统信号默认都会被忽略。不过SIGKILL和SIGSTOP信号比较特殊,init进程无法捕获这两个信号

Kubernetes默认对同一Pod的不同容器构建独立的PID NameSpace,以便将不同容器的进程彼此隔离,同时允许通过ShareProcessNameSpace属性设置不同容器的进程共享PID NameSpace

Kubernetes支持多重容器进程的重启策略,默认行为是用户进程退出后立即重启,Kubernetes用户只需终止其容器的ENTRYPOINT进程,即可实现容器重启。

1.4 Mount

提供了进程能看到的挂载点的隔离,在主机上通过/proc/pid/mounts、/proc/pid/mountinfo、/proc/pid/mountstats等文件来查看挂载点。在容器内,可以通过mount或lsmnt命令查看Mount NameSpace中的有效挂载点。

Linux内核针对Mount NameSpace隔离性开发了共享子树(Shared Subtree)功能,用于在不同Mount NameSpace之间自动可控地传播mount和unmount事件,共享子树引入了对等组的概念。对等组是一组挂载点,其成员之间互相传播mount和unmount事件,此特性使得当主机磁盘发生变更(比如系统导入新磁盘时),只需在一个mount NameSpace中进行挂载,该磁盘即可在所有NameSpace中可见。

挂载点可设置的传播类型有:

  • MS_SHARED:此挂载点与同一对等组里其他挂载点共享mount和unmount事件,在此挂载点下添加或删除挂载点时,事件会传播到对等组内的其他NameSpace中,事件在NameSpace中也会自动进行相同的时间。同样的,对等组中其他的挂载和卸载时间也会传播到此挂载点。
  • MS_PRIVATE:此挂载点是私有的,没有对等组,挂载和卸载时间不会和其他的命名空间共享。
  • MS_SLAVE:此挂载点可以从主对等组接收挂载和卸载时间,但是在本挂载点下的mount和unmount时间不会传播到其他任何的Mount 命名空间下
  • MS_UNBINDABLE:与MS_PRIVATE不同的是,该挂载点不可以执行bind mount操作。

在目录/proc/pid/mountinfo下,可以看到该PID所属进程的mount Namespace下的挂载点的传播类型及所属的对等组。

在Kubernetes中,挂载点通常是private类型,如果需要设置挂载点类型,可以在Pod的spec中填写相应的挂载配置。

1.5 UTS

UTS(UNIX Time-Sharing System)NameSpace允许不同容器拥有独立的hostname和domain name。该NameSpace中的一个进程可以看做一个在网络上独立存在的节点,也就是说,除了IP外,还能通过主机名进行访问。

1.6 USR

User NameSpace主要隔离了安全相关的标识符和树形,比如用户ID、用户组ID、root目录、秘钥等,一个进程的用户ID和组ID在User NameSpace内外可以有所不同,在该UserNameSpace外,他是一个非特权的用户ID,而在此命名空间内,进程可以使用0作为用户ID,且具有完全的特殊权限。

User NameSpace允许不同容器拥有独立的用户和用户组,它主要提供两种职能:权限隔离和用户身份标识隔离。我们协议通过在容器镜像中创建和切换用户,来为文件目录设置不同的用户权限,从而实现容器内的权限管理,而无需影响主机配置。

1.7 CGroup

内核从4.6版本开始支持CGroup NameSpace。如果容器启动时没有开启该命名空间,那么在容器内部查询CGroup时,返回整个系统信息,而开启CGroup后,可以看到当前容器从当前容器以根形式展示的单独的CGroup信息

CGroup视图的改变使容器更加安全,而且在容器内也可以有自己的CGroup结构。

2、CGroup

CGroups(Control Groups)是Linux 下用于对一个或一组进程进行资源控制和监控的机制。利用CGroups 可以对诸如CPU 使用时间、内存、磁盘I/O 等进程所需的资源进行限制。Kubernetes 允许用户为Pod 的容器申请资源,当容器在计算节点上运行起来时,可通过CGroups 来完成资源的分配和限制。

在CGroups 中,对资源的控制都是以CGroup 为单位的。目前CGroups 可以控制多种资源,不同资源的具体管理工作由相应的CGroup 子系统(Subsystem)来实现。因此,针对不同类型的资源限制,只要将限制策略在不同的的子系统上进行关联即可。CGroups 由谷歌的工程师引入2.6.24 版本的内核中,最初只对CPU 进行了资源限制,然而随着对其他资源控制需求的增多,它们的CGroup 子系统不断地被引入内核。在容器时代,特别是Kubernetes 中,为了提高对节点资源的利用率,一个节点上会运行尽可能多的容器,这就对 资源隔离的多样性和精确性提出了越来越高的要求,因此CGroups 也发挥着越来越重要的作用。

对于CGroups 的组织管理,用户可以通过文件操作来实现,对资源的控制可以细化到线程级别。CGroups 在不同的系统资源管理子系统中以层级树(Hierarchy)的方式来组织管理:每个CGroup 都可以包含其他的子CGroup,因此子CGroup 能使用的资源,除了受本CGroup 配置的资源参数限制,还受到父CGroup 设置的资源限制。

2.1 CPU

CPU子系统用于限制进程的CPU使用时间,在CPU子系统中,对每个CGroup下的非实时任务,CPU使用的时间可以通过cpu.shares、cpu.cfs_period_us和cpu.cfs_quoat_us参数来进行控制,而系统的CFS(Completely Fair Scheduler)调度器则根据CGroup下进程的优先级、权重和cpu.shares等配置来给该进程分配相应的CPU时间。

CPU CGroup的主要配置参数如下:

  1. cpu.shares 是在该CGroup能获得CPU使用时间的相对值,最小为2,如果两个CGroup的cpu.shares都为100,那么他们可以得到相同的CPU时间。但是如果一个CGroup的cpu.shares是200,那么他可以得到两倍于cpu.shares=100的CGroup获取的cpu时间,但是,如果一个CGroup中的任务处于空闲状态,不适用任何的CPU时间,则该CPU时间就可被其他CGroup所借用,简言之,cpu.shares主要用于表示当前系统cpu繁忙时,给该CGroup分配的CPU时间额。
  2. Cpu_cfs_period_us和cpu_cfs_quota_us 前者用于配置时间周期长度,单位为微秒(us).后者用来配置当前CGroup在cfs_period_us时间内最多能使用的CPU时间数,单位为us(微秒),这两个参数被用来设置CGroup鞥使用的CPU的时间上限。如果不想对进程使用的CPU设置限制,可以将cpu_cfs_quota_us设置为-1
  3. cpu.stat CGroup内的进程使用的CPU的时间统计、
  4. nr_period
  5. 经过cpu_cfs_period_us的时间周期数量
  6. nr_throttled 在经过周期内,有多少次因为进程在指定的时间周期内用光了配额时间而受到限制。
  7. Throttled_time CGroup中进程被限制使用cpu的总用时。

2.2 cpuset

cpuset为CGroup的进程分配单独的cpu和内存节点,将进程固定在某个cpu或内存节点上,以达到提高性能的目的。该子系统主要包含如下配置参数:

  1. cpuset.cpus 设置CGroup下进程可以使用的CPU核数,在将进程加入CGroup之前,该参数必须进行设置。
  2. cpuset.mems 指定CGroup下进程可以使用的内存节点,在将进程加入CGroup之前,该参数必须设置。
  3. cpuset.memory_migrate 如果cpuset.mems发生变化,那么该参数用于指示已经申请成功的内存页是否需要迁移到新配置的内存节点上。
  4. cpuset.cpu_exclusive cpu互斥,默认不开启,该参数用于指示该CGroup设置的CPU核是否可以被除父CGroup和子CGroup外的其他CGroup共享。
  5. cpuset.mem_exclusive 内存互斥,默认不开启。该参数用于指示该CGroup设置的内存节点是否可以被除父CGroup和子CGroup外的其他CGroup共享。

2.3 cpuacct

用于统计CGroup及其子CGroup下进程的CPU的使用情况

  1. Cpuacct.usage 包含该CGroup 及其子CGroup 下进程使用CPU 的时间,单位是ns(纳秒)。
  2. Cpuacct.stat 包含该CGroup 及其子CGroup 下进程使用的CPU 时间,以及用户态和内核态的时间

2.4 memory

memory用于限制CGroup下进程的内存使用量,也可以获取到内存的详细使用信息

  1. memory.stat 该文件中包含该CGroup下进程的详细的内存使用信息
  2. memory.usage_in_bytes CGroup 下进程使用的内存,包含CGroup 及其子CGroup 下的进程使用的内存。
  3. memory.max_usage_in_bytes
  4. CGroup 下进程使用内存的最大值,包含子CGroup 的内存使用量。
  5. memory.limit_in_bytes
  6. 设置CGroup 下进程最多能使用的内存。如果设置为-1,则表示对该CGroup 的内存使
  7. 用不做限制。
  8. memory.failcnt CGroup 下的进程达到内存最大使用限制的次数。
  9. memory.force_empty 当没有进程输入该CGroup后,将该值设置为0,系统会尽可能的将该CGroup使用的内存释放掉,对于不能释放的内存,则会将其移动到父CGroup上,在CGroup销毁之前,将能释放的内存释放掉,可以尽量避免将已经不适用的内存移动的父CGroup上,从而避免对运行在父CGroup中的进程造成内存压力。
  10. memory.oom_control 设置是否在CGroup中使用OOM(Out of Memory)Killer,默认为使用。当属于该CGroup的进程使用的内存超过最大的限定值时,会立即被OOM Killer处理。

2.5 blkio

用来实现对块设备访问的I/O 控制,按权重分配目前有两种限制方式:一是限制每秒写入的字节数(Bytes Per Second,即BPS),二是限制每秒的读写次(I/O Per Second,即IOPS)。blkio 子系统按权重分配模式工作于I/O 调度层,依赖于磁盘的CFQ(Completely Fair Queuing,完全公平算法)调度,如果磁盘调度使用deadline 或者none的算法则无法支持。BPS、IOPS 工作于通用设备层,不依赖于磁盘的调度算法,因此有更多的适用场景

2.6 PID

PID子系统用来限制CGroup能够创建的进程数

  • pids.max:允许创建的最大进程树。
  • pid.current:当前的进程数。

2.7 其他

● devices 子系统,控制进程访问某些设备。

● perf_event 子系统,控制perf 监控CGroup 下的进程。

● net_cls 子系统,标记CGroups 中进程的网络数据包,通过TC 模块(Traffic Control )对数据包进行控制。

● net_prio 子系统,针对每个网络设备设置特定的优先级。

● hugetlb 子系统,对hugepage 的使用进行限制。

● freezer 子系统,挂起或者恢复CGroups 中的进程。

● ns 子系统,使不同CGroups 下面的进程使用不同的Namespace。

● rdma 子系统,对RDMA/IB-spe-cific 资源进行限制。

以上就是容器技术的核心技术,虽然现在对于Docker等容器达到了会用的地步,但是对其实现的原理还是知之甚少,如果想要用好容器技术,还需要继续努力去了解这些容器技术背后的实现原理。

发表评论
留言与评论(共有 0 条评论) “”
   
验证码:

相关文章

推荐文章