Linux namespace
- Docker 和虚拟机技术一样,从操作系统级上实现了资源的隔离,它本质上是宿主机上的进程(容器进程),所以资源隔离主要就是指进程资源的隔离。实现资源隔离的核心技术就是 Linux namespace。这技术和很多语言的命名空间的设计思想是一致的。
- 隔离意味着可以抽象出多个轻量级的内核(容器进程),这些进程可以充分利用宿主机的资源,宿主机有的资源容器进程都可以享有,但彼此之间是隔离的,同样,不同容器进程之间使用资源也是隔离的,这样,彼此之间进行相同的操作,都不会互相干扰,安全性得到保障。
为了支持这些特性,Linux namespace 实现了 6 项资源隔离,基本上涵盖了一个小型操作系统的运行要素,包括主机名、用户权限、文件系统、网络、进程号、进程间通信。
「查看名称空间 namespace_id」
uname -r 3.10.0-1127.el7.x86_64 ls -l /proc/PID/ns total 0 lrwxrwxrwx 1 0 Jun 25 15:30 ipc -> ipc:[4026531839] lrwxrwxrwx 1 0 Jun 25 15:30 mnt -> mnt:[4026531840] lrwxrwxrwx 1 0 Jun 25 15:30 net -> net:[4026531968] lrwxrwxrwx 1 0 Jun 25 15:30 pid -> pid:[4026531836] lrwxrwxrwx 1 0 Jun 25 15:30 user -> user:[4026531837] lrwxrwxrwx 1 0 Jun 25 15:30 uts -> uts:[4026531838]
Linux cgroups
- Cgroups是control groups的缩写,最初由google的工程师提出,后来被整合进Linux内核。Cgroups是Linux内核提供的一种可以限制、记录、隔离进程组(process groups)所使用的物理资源(如:CPU、内存、IO等)的机制。
- 本质上来说,cgroups 是内核附加在程序上的一系列钩子(hooks),通过程序运行时对资源的调度触发相应的钩子以达到资源追踪和限制的目的。实现 cgroups 的主要目的是为不同用户层面的资源管理,提供一个统一化的接口。从单个进程的资源控制到操作系统层面的虚拟化。
「Cgroups 提供了以下四大功能:」
- 资源限制(Resource Limitation):cgroups 可以对进程组使用的资源总额进行限制。如设定应用运行时使用内存的上限,一旦超过这个配额就发出 OOM(Out of Memory)。
- 优先级分配(Prioritization):通过分配的 CPU 时间片数量及硬盘 IO 带宽大小,实际上就相当于控制了进程运行的优先级。
- 资源统计(Accounting):cgroups 可以统计系统的资源使用量,如 CPU 使用时长、内存用量等等,这个功能非常适用于计费。
- 进程控制(Control):cgroups 可以对进程组执行挂起、恢复等操作。
Docker正是使用cgroup进行资源划分,每个容器都作为一个进程运行起来,每个业务容器都会有一个基础的pause容器也就是POD作为基础容器。pause容器提供了划分namespace的内容,并连通同一POD下的所有容器,共享网络资源。
「查看linux是否启用了linux cgroups」
对应的CGROUP项为“y”代表已经打开linux cgroups功能。
cat /boot/config-3.10.0-1127.el7.x86_64 | grep CGROUP CONFIG_CGROUPS=y # CONFIG_CGROUP_DEBUG is not set CONFIG_CGROUP_FREEZER=y CONFIG_CGROUP_PIDS=y CONFIG_CGROUP_DEVICE=y CONFIG_CGROUP_CPUACCT=y CONFIG_CGROUP_HUGETLB=y CONFIG_CGROUP_PERF=y CONFIG_CGROUP_SCHED=y CONFIG_BLK_CGROUP=y # CONFIG_DEBUG_BLK_CGROUP is not set CONFIG_NETFILTER_XT_MATCH_CGROUP=m CONFIG_NET_CLS_CGROUP=y CONFIG_NETPRIO_CGROUP=y
CGroup 支持的文件种类
- 控制族群(control group)。控制族群就是一组按照某种标准划分的进程。Cgroups中的资源控制都是以控制族群为单位实现。一个进程可以加入到某个控制族群,也从一个进程组迁移到另一个控制族群。一个进程组的进程可以使用。cgroups 以控制族群为单位分配的资源,同时受到 cgroups 以控制族群为单位设定的限制。
- 层级(hierarchy)。控制族群可以组织成 hierarchical的形式,既一颗控制族群树。控制族群树上的子节点控制族群是父节点控制族群的孩子,继承父控制族群的特定的属性。
- 子系统(subsytem)。一个子系统就是一个资源控制器,比如 cpu 子系统就是控制 cpu时间分配的一个控制器。子系统必须附加(attach)到一个层级上才能起作用,一个子系统附加到某个层级以后,这个层级上的所有控制族群都受到这个子系统的控制。
CGroup子系统
- CPU:使用调度程序为cgroup任务提供 CPU 的访问。
- cpuacct:产生cgroup任务的 CPU 资源报告。
- cpuset:如果是多核心的CPU,这个子系统会为cgroup任务分配单的CPU和内存。
- devices:允许或拒绝cgroup任务对设备的访问。
- freezer:暂停和恢复cgroup任务。
- memory:设置每个cgroup 的内存限制以及产生内存资源报告。
- net_cls:标记每个网络包以供 cgroup方便使用。
- ns:命名空间子系统。
- perf event:增加了对每个group的监测跟踪的能力,可以监测属于某个特定的group 的所有线程以及运行在特定CPU上的线程。
CGroup 层级图
如图所示的 CGroup 层级关系显示,CPU 和 Memory 两个子系统有自己独立的层级系统,而又通过 Task Group 取得关联关系。
yarn 实现的资源隔离
资源调度和资源隔离是YARN作为一个资源管理系统,最重要和最基础的两个功能。
- 资源调度:由resourcemanager完成
- 资源隔离:各个nodemanager完成
内存资源隔离
- 「进程监控的方案」
首先可计算当前每个运行的Container使用的内存总量。
但需要注意的是,不能仅凭该内存量是否超过设定的内存最高值来决定是否杀死一个Container。在创建一个子进程时,JVM采用了"fork()+exec()"模型,这意味着进程创建之后、执行之前会复制一份父进程内存空间,进而使得进程树在某一小段时间内存使用量翻倍。
❝Linux中所有的进程都是通过fork()复制来实现的,而为了减少创建进程带来的堆栈消耗和性能影响,Linux使用了写时复制机制来快速创建进程。也就是说,一个子进程刚刚产生时,它的堆栈空间和父进程是完全一致的,那么从一开始它就拥有和父进程同样的ru_maxrss,如果父进程的ru_maxrss比较大,那么由于rusage计算值取最大值,就算在触发写时复制后,子进程使用的实际最大驻留集大小被更新,我们获得的也还是父进程的那个值,也就是说我们永远拿不到子进程真实使用的内存。Java创建子进程时采用了“fork() + exec()”的方案,子进程启动瞬间,它的内存使用量与父进程是一致的,exec系函数,这个系别的函数通过将当前进程的使用权转交给另一个程序,这时候进程原有的所有运行堆栈等数据将全部被销毁,因此ru_maxrss也会被清零计算,然后子进程的内存会恢复正常;也就是说,Container(子进程)的创建过程中可能会出现内存使用量超过预先定义的上限值的情况(取决于父进程,也就是NodeManager的内存使用量);此时,如果使用Cgroup进行内存资源隔离,这个Container就可能会被“kill”
❞ ❝「Linux写时拷贝技术(copy-on-write)」在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,linux中引入了“写时复制“技术,也就是只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。那么子进程的物理空间没有代码,怎么去取指令执行exec系统调用呢?在fork之后exec之前两个进程用的是相同的物理空间(内存区),子进程的代码段、数据段、堆栈都是指向父进程的物理空间,也就是说,两者的虚拟空间不同,但其对应的物理空间是同一个。当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间,如果不是因为exec,内核会给子进程的数据段、堆栈段分配相应的物理空间(至此两者有各自的进程空间,互不影响),而代码段继续共享父进程的物理空间(两者的代码完全相同)。而如果是因为exec,由于两者执行的代码不同,子进程的代码段也会分配单独的物理空间。传统的fork()系统调用直接把所有的资源复制给新创建的进程。这种实现过于简单并且效率低下,因为它拷贝的数据也许并不共享,更糟的情况是,如果新进程打算立即执行一个新的映像,那么所有的拷贝都将前功尽弃。Linux的fork()使用写时拷贝(copy-on-write)页实现。写时拷贝是一种可以推迟甚至免除拷贝数据的技术。内核此时并不复制整个进程地址空间,而是让父进程和子进程共享同一个拷贝。只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝。也就是说,资源的复制只有在需要写入的时候才进行,在此之前,只是以只读方式共享。这种技术使地址空间上的页的拷贝被推迟到实际发生写入的时候。在页根本不会被写入的情况下—举例来说,fork()后立即调用exec()—它们就无需复制了。fork()的实际开销就是复制父进程的页表以及给子进程创建惟一的进程描述符。在一般情况下,进程创建后都会马上运行一个可执行的文件,这种优化可以避免拷贝大量根本就不会被使用的数据(地址空间里常常包含数十兆的数据)。由于Unix强调进程快速执行的能力,所以这个优化是很重要的。
❞
- fork() 和 vfork() 参数是写死的,而 clone() 是可选的,它可以选择当前创建的进程哪些部分是共享的,哪些部分是独立的;
- vfork() 是历史的产物,当调用 fork() 的时候,需要将父进程的线性区和页表都拷贝一份,而调用 exec() 执行新程序后,又要把所有页表删除重置新的页表,建立映射关系,效率很低;
- 所以要有 vfork(),vfork() 的 clone_flags 位置了 CLONE_VM ,表示共享父进程的地址空间,vfork() 中创建的进程没有分配自己的地址空间,而是通过一个 mm_struct 指针指向父进程的地址空间,这个进程是为了在之后调用 exec() 执行新的程序;
- 而在有了 Copy-on-write 技术后,fork() 出的子进程只创建了自己的地址空间,然后用父进程的地址空间初始化,每个页表的项置为父进程的页表项,共享父进程的物理页面,并将所有 私有/可写 页面改为只读;
- 当我们改变父子进程的数据后,cpu在运行过程中会发生一个缺页错误,cpu转交控制权给操作系统,操作系统查找VMA发现该页权限为只读,但所在段又是可写的,产生一个矛盾,这就是识别Copy-on-write的方法,接着 OS 给子进程分配一个新的物理页,并将页表该页的地址修改成新的物理页地址;
- 这样 fork() 后再调用 exec() 就不用那么麻烦了,可以直接将新的物理页与子进程的虚拟空间建立映射
为了避免误杀Container,Hadoop赋予每个进程年龄属性,并规定刚启动进程的年龄是1,且MonitoringThread线程每更新一次,各个进程年龄加一 在此基础上,选择被杀死Container的标准如下:
- 如果一个Container对应的进程树中所有进程(年龄大于0)总内存超过(用户设置的)最大值的两倍。
- 有年龄大于1的进程总内存量超过(用户设置的)最大值。
则认为该Container过量使用内存,则向Container发送ContainerEventType.KILL_CONTAINER事件将其杀死。
- 「基于轻量级资源隔离技术Cgroups的方案」
Cgroup会严格限制应用程序的内存使用上限,一旦使用量超过预先定义的上限值,就会将该应用程序“杀死”,因此无法有效地使用Cgroup进行内存资源隔离。
cpu资源隔离
Yarn 3.0 版本中,在 Linux 系统环境下,ContainerExecutor 有两种实现:
- DefaultContainerExecutor: 简称 DCE , 如其名,是默认的 ContainerExecutor 实现。如果用户未指定 ContainerExecutor 的具体实现,NM 就会使用它。DCE 直接使用 bash 来启动 container 进程,所有 container 都使用 NM 进程用户 (yarn) 启动,安全性低且没有任何CPU资源隔离机制。
- LinuxContainerExecutor: 简称 LCE,相比于 DCE ,它能提供更多有用的功能,如用户权限隔离,支持使用提交任务用户来启动 container;支持使用 cgroup 进行资源限制;支持运行 docker container (合并了2.x 版本中的 DockerContainerExecutor)。LCE 使用可执行的二进制文件 container-executor 来启动 container 进程,container 的用户根据配置可以统一使用默认用户,也可以使用提交任务的用户(需要提前在 NM 上添加所有支持的用户),从而以应用提交者的身份创建文件,运行/销毁 Container,允许用户在启动Container后直接将CPU份额和进程ID写入cgroup路径的方式实现CPU资源隔离。
❝需要注意的是,YARN允许你配置每个节点上可使用的物理cpu个数,以及物理cpu与虚拟cpu个比例,而用户申请资源时,只能申请虚拟cpu。默认情况下,物理cpu和虚拟cpu是1:1的,如果你的集群是异构的,某些节点上的CPU拥有更强的计算能力,则可以调整物理cpu和虚拟cpu的比例。虚拟cpu的概念是借鉴“物理内存和虚拟内存”的,主要目的是消除集群中cpu计算能力的异构性。
❞
课外知识补充
- bootfs(boot file system)主要包含bootloader和kernel,bootloader主要引导加载kernel,linux刚启动的时候会加载bootfs文件系统,在docker镜像最底层是bootfs。这一层和我们典型的linux系统一样,包含bootloader和kernel,当bootloader加载完kernel之后,kernel就在内存中了,此时内存的使用权由bootloader交给内核,此时系统会卸载bootfs。
- rootfs(root file system) 在bootfs之上,包含典型的Linux系统的/dev,/proc,/bin,/etc等标准目录和文件,rootfs就是各种不同操作系统的发行版。
总结
Linux内核提供namespace完成隔离,Cgroup完成资源限制。namespace+Cgroup构成了容器的底层技术(rootfs是容器文件系统层技术)。