当前位置: 首页 > Linux

理解容器技术的基石:cgroup

时间:2023-04-07 00:09:01 Linux

大家好,我是张锦涛。至此我们所提到的容器技术和虚拟化技术(无论任何抽象层次的虚拟化技术)都可以实现资源层面的隔离和限制。对于容器技术,它实现了资源级别的限制和隔离,依赖于Linux内核提供的cgroup和namespace技术。我们先总结一下这两种技术的功能:cgroup的主要功能:管理资源的分配和限制;命名空间的主要作用:封装抽象、限制、隔离,让命名空间中的进程好像拥有了自己的全局资源;在本文中,我们重点关注cgroup。为什么关注cgroup&namespace云原生/容器技术的井喷式增长从1979年Unixversion7在开发过程中引入了ChrootJail和Chroot系统调用,直到2013年Docker开源,2014年Kubernetes开源直到现在,云原生生态火热。容器技术逐渐成为主流基础技术之一。在越来越多的公司和个人选择云服务/容器技术后,资源分配、隔离、安全成为人们关注和讨论的热点话题。其实容器技术并不难用,但是要真正用好它,在大规模生产环境中使用它,还是需要掌握它的核心。以下是容器技术&云原生生态的大致发展史:图1、容器技术的发展史从图中可以看出容器技术与云原生生态的发展轨迹。容器技术其实很早就出现了,但是为什么在Docker出现之后才开始有了长足的发展呢?早期的chroot和LinuxVServer有什么问题?Chroot带来的安全问题图2,chroot示例Chroot可以将一个进程及其子进程与操作系统的其余部分隔离开来。但是,对于root进程,可以随意退出chroot。packagemainimport("log""os""syscall")funcgetWd()(pathstring){path,err:=os.Getwd()iferr!=nil{log.Println(err)}log.Println(path).)return}funcmain(){RealRoot,err:=os.Open("/")deferRealRoot.Close()iferr!=nil{log.Fatalf("[错误]-/:%v\n",错误)}路径:=getWd()错误=系统调用。Chroot(路径)如果错误!=nil{日志。Fatalf("[Error]-chroot:%v\n",err)}getWd()err=RealRoot。Chdir()iferr!=nil{log.Fatalf("[Error]-chdir():%v",err)}getWd()err=syscall.Chroot(".")iferr!=nil{日志。Fatalf("[Error]-chrootback:%v",err)}getWd()}getWd()}退出sudo数据库:?chrootgorunmain。go2021/11/1800:46:21/tmp/chroot2021/11/1800:46:21[Error]-chroot:operationnotpermittedexitstatus1?chrootsudowillrunmain:25/2021/11/1800:46:25(无法访问)/2021/11/1800:46:25/可以看出,如果使用sudo运行程序,程序会在当前目录和原来的系统根目录之间切换,普通用户无权限操作.LinuxVServer的安全漏洞Linux-VServer是一种基于SecurityContexts的软分区技术,可以隔离虚拟服务器并共享相同的硬件资源。主要问题是VServer应用程序没有很好地抵御“chroot-again”类型的攻击,攻击者可以利用此漏洞逃离受限环境并访问受限目录外的任意文件。(自2004年起,国家信息安全漏洞库登记相关漏洞问题)现代容器技术带来的优势是轻量级,基于Linux内核提供的cgroup和namespace能力,创建容器的成本非常低;一定的隔离;标准化,通过使用容器镜像来打包分发应用,可以屏蔽很多环境不一致带来的问题;DevOps支持(可以方便地在不同环境之间分布,如开发、测试、生产环境迁移应用,同时保留应用的所有功能);为基础设施增加保护以提高可靠性、可扩展性和可靠性;DevOps/GitOps支持(可实现快速有效的持续发布、管理版本和配置);团队成员可有效简化、加速和编排应用程序的开发和部署;了解了为什么要关注cgroup、namespace等技术之后,接下来我们就进入本文的重点,一起来了解一下cgroup。什么是cgroupcgroup是linux内核对一个进程组的资源(如CPU、内存、磁盘输入输出等)进行限制、控制和隔离的功能。它由谷歌的两名工程师开发,自2018年1月正式发布的Linux内核v2.6.24开始可用。cgroup到目前为止,有两个主要版本,cgroupv1和v2。以下内容基于cgroupv2版本,下面将详细介绍两个版本的区别。cgroups限制的主要资源有:CPU内存网络磁盘I/O当我们将可用的系统资源按一定比例分配给cgroups后,剩下的资源就可以被其他cgroups或系统上的其他进程使用。图4、cgroup资源分配和剩余可用资源示例cgroup组成cgroup代表“controlgroup”,不会使用大写。cgroup是一种分层组织进程的机制,以受控方式沿层次结构分配系统资源。我们通常使用单数形式来指定整个功能,也作为限定词,如“cgroupcontroller”。一个cgroup有两个主要组件:core-负责分层组织进程;控制器-通常负责沿层次结构分配特定类型的系统资源。每个cgroup都有一个cgroup.controllers文件,其中列出了cgroup可以启用的所有控制器。当cgroup.subtree_control中指定了多个controller时,要么全部成功,要么全部失败。如果在同一个控制器上指定了多个动作,则只有最后一个动作生效。每个cgroup的controller销毁是异步的,在引用的时候也存在延迟引用的问题;所有cgroup核心接口文件都以cgroup为前缀。每个控制器的接口文件都以控制器名称和一个点为前缀。控制器名称由小写字母和“_”组成,但绝不能以“_”开头。cgroup的核心文件cgroup.type-(单值)存在于非根cgroup上的读写文件。通过在文件中写入“threaded”可以将一个cgroup转换为线程cgroup,可以选择4个值,如下:1)domain——一个普通的有效域cgroup2)domainthreaded——根的线程域cgroup线程子树的3)域无效-无效的cgroup4)线程-线程cgroup,线程子树cgroup.procs-(换行符分隔)所有cgroup具有的可读和可写文件。每行列出属于cgroup的进程的PID。PID没有顺序,如果进程移动到另一个cgroup,相同的PID可能会出现不止一次;cgroup.controllers-(空格分隔)所有cgroup共享的只读文件。显示cgroup可用的所有控制器;cgroup.subtree_control-(空格分隔)所有cgroup的可读和可写文件,最初为空。如果一个控制器在列表中出现多次,则最后一个生效。当指定多个启用和禁用操作时,要么全部成功,要么全部失败。1)以“+”为前缀的控制器名称表示启用的控制器2)以“-”为前缀的控制器名称表示禁用的控制器cgroup.events-存在于非根cgroup上的只读文件。1)populated——cgroup及其子节点包含活跃进程,值为1;noactiveprocesses,valueis0.2)frozen-cgroup是否被冻结,frozen值为1;未冻结的值为0.cgroup.threads-(新行分隔)所有cgroup共享的可读和可写文件。每行列出属于cgroup的线程的TID。TID没有顺序,如果线程移动到另一个cgroup,相同的TID可能会出现不止一次。cgroup.max.descendants-(单值)可读可写文件。cgroup允许的最大子级数。cgroup.max.depth-(单值)可读可写文件。低于当前节点允许的最大树深度。cgroup.stat-只读文件。1)nr_descendants-cgroup可见后代的数量。2)nr_dying_descendants——被用户删除并即将被系统销毁的cgroup数量。cgroup.freeze-(单值)存在于非根cgroup上的读写文件。默认值为0,当值为1时,cgroup及其所有子节点cgroup将被冻结,相关进程将被关闭不再运行。冻结cgroup需要一定的时间。当动作完成后,cgroup.events控制文件中的“frozen”值会更新为“1”,并发出相应的通知。cgroup的冻结状态不会影响任何cgroup树操作(删除、创建等);cgroup.kill-(单值)存在于非根cgroups上的读写文件。唯一允许的值为1,当值为1时,cgroup中的cgroup及其所有子节点将被杀死(进程将被SIGKILL杀死)。一般用于杀掉一个cgroup树,防止叶节点迁移;cgroup所有权和迁移系统中的每个进程都属于一个cgroup,一个进程的所有线程都属于同一个cgroup。进程可以从一个cgroup迁移到另一个。进程迁移不会影响现有后代进程所属的cgroups。图5,进程及其子进程的cgroup分配;跨cgroup迁移示例跨cgroup迁移进程是一项代价高昂的操作,并且有状态资源约束(例如,内存)不会动态应用于迁移。因此,跨cgroup迁移进程通常只是作为一种手段。不鼓励直接应用不同的资源限制。如何实现跨cgroup迁移每个cgroup都有一个可读写的接口文件“cgroup.procs”。每行一个PID记录cgroup限制管理的所有进程。进程可以通过将其PID写入另一个cgroup的“cgroup.procs”文件来迁移。但是这样只能迁移一个进程对单个write(2)的调用(如果一个进程有多个线程,所有线程会同时迁移,还要参考线程子树,是否有线程进程放入不同的cgroup记录)。当一个进程fork出一个子进程时,该进程就诞生在其父进程所属的cgroup中。一个没有任何子进程或活动进程的cgroup可以通过删除目录来销毁(即使有关联的僵尸进程,也被认为是被删除)。什么是cgroups复数形式“cgroups”仅在明确指代多个单独的控制组时使用。cgroups形成一个树结构。(一个给定的cgroup可能有多个子cgroup形成树结构。)每个非根cgroup都有一个cgroup.events文件,其中包含一个填充字段,指示cgroup的子层次结构是否有活动进程。所有非根cgroup.subtree_control文件只能包含在父级中启用的控制器。图6、cgroups示例如图所示,在cgroup1中限制cpu和内存资源的使用,会控制子节点的CPU周期和内存分配(即限制cpu和内存资源的分配在cgroup1中)cgroup2、cgroup3和cgroup4)。cgroup2中启用了内存限制,但没有启用cpu资源限制,导致cgroup3和cgroup4的内存资源受cgroup2中mem设置内容的限制;cgroup3和cgroup4会在cgroup1cpu资源的cpu资源限制内自由竞争。从这里也可以很明显的看出cgroup资源是从上到下分布和受限的。只有当资源从上游cgroup节点分配到下游后,下游cgroup才能进一步分配受限资源。所有非根cgroup.subtree_control文件只能包含在父节点的cgroup.subtree_control文件中启用的控制器内容。那么,子节点cgroup和父节点cgroup之间会不会存在内部进程竞争呢?当然不是。在cgroupv2中,设置了非rootcgroup在没有进程时只能将域资源分配给子节点cgroup。简而言之,只有不包含任何进程的cgroup才能在其cgroup.subtree_control文件中启用域控制器,从而确保进程始终位于叶节点上。挂载委托cgroupsmemory_recursiveprot的挂载方式——递归地对整个子树应用memory.min和memory.low保护,不显式向下传播到叶子节点的cgroup,子树中的叶子节点可以自由竞争;memory_localevents-只能在挂载时设置或通过从init命名空间重新挂载来更改,这是一个系统范围的选项。只用当前cgroup的数据填充memory.events,没有这个选项,默认统计所有子树;nsdelegate-只能在挂载时设置或通过从init命名空间重新挂载进行修改,这也是一个系统范围的选项。它将cgroup命名空间视为委托边界,这是委托给cgroup的两种方式之一;委托cgroup的方式设置挂载选项nsdelegate;授权用户访问该目录及其cgroup.procs、cgroup.threads和cgroup.subtree_control文件写访问两种方式的结果相同。委托后,用户可以在目录下建立子层级,所有的资源分配都受父节点的约束。目前,cgroups对委托子层级中cgroup的数量或嵌套深度没有任何限制(未来可能会有明确的限制)。前面提到了跨cgroup迁移。从委托中我们可以清楚的知道,跨cgroup迁移对于普通用户是有限制的。即,是否对当前cgroup的“cgroup.procs”文件以及源和目标cgroup的共同祖先的“cgroup.procs”文件具有写入权限。DelegationandMigration图7,图中是一个委托权限的例子,普通用户User0拥有cgroup[1-5]的委托权限。为什么User0无法将进程从cgroup3迁移到cgroup5?这是因为User0的权限只到cgroup1和cgroup2层,没有cgroup0的权限。委托中的授权用户明确需要对共同祖先的“cgroup.procs”文件的写入权限!(即需要图中cgroup0的权限才能实现)资源分配模型及作用下面是cgroups的资源分配模型:权重-(例如cpu.weight)所有的权重都在[1,10000],默认值为100。按权重比例分配资源。limit-在[0,max]范围内,默认为'max',即noop(例如io.max)。限制可能会被过度使用(子节点限制的总和可能超过父节点可用的资源量)。protection-在[0,max]范围内,默认为0,即noop(例如,io.low)。保护可以是最大努力的硬保证或软边界,保护也可能被过度使用。allocation-在[0,max]范围内,默认为0,即没有资源。分配不能过度使用(子节点分配的总和不能超过父节点可用的资源量)。Cgroups提供了以下功能:资源限制——上面的cgroup部分已经举例,cgroups可以嵌套在树状结构中限制资源。优先级——当发生资源争用时,优先分配给哪个进程的资源。审计-监控和报告资源限制和使用情况。控制-控制进程的状态(启动、停止、挂起)。cgroupv1和cgroupv2弃用的核心功能与cgroupv2和cgroupv1有很大不同。我们来看看cgroupv1有哪些功能在cgroupv2中被弃用了:不支持multipleHierarchy;并非所有v1挂载选项都受支持;“任务”文件已删除,“cgroup.procs”未排序cgroupv1中的线程组ID列表。无法保证此列表已排序或没有重复的TGID,如果需要此属性,用户空间应该对列表进行排序/统一。将线程组ID写入此文件会将该组中的所有线程移动到此cgroup中;cgroup.clone_children被删除。clone_children只影响cpuset控制器。如果cgroup中开启了clone_children(设置:1),新的cpusetcgroup会在初始化时从父节点的cgroup中复制配置;/proc/cgroups对v2没有意义。改用根目录中的“cgroup.controllers”文件;cgroupv1问题cgroupv2和v1之间最显着的区别是cgroupv1允许任意数量的层次结构,但这会导致问题。让我们详细谈谈它。挂载cgroup层次结构时,您可以指定以逗号分隔的子系统列表作为文件系统挂载选项进行挂载。默认情况下,挂载cgroup文件系统会尝试挂载包含所有已注册子系统的层次结构。如果已经存在具有完全相同的子系统集的活动层次结构,它将被重新用于新安装。如果现有层次结构不匹配,并且任何请求的子系统正在现有层次结构中使用,则挂载将失败并返回-EBUSY。否则,将激活与请求的子系统关联的新层次结构。目前无法将新子系统绑定到活动cgroup层次结构或从中取消绑定子系统。卸载cgroup文件系统时,如果在顶级cgroup下创建了任何子cgroup,即使在卸载后层次结构仍将保持活动状态;如果没有子cgroup,层次结构将被停用。这是cgroupv1中的问题,在cgroupv2中很好的解决了。cgroup与容器的连接这里我们以Docker为例。创建一个容器并限制它可以使用的CPU和内存:?~dockerrun--rm-d--cpus=2--memory=2g--name=2c2gredis:alpinee420a97835d9692df5b90b47e7951bc3fad48269eb2c8b1fa782527e0ae91c8e?~cat/sys/fs/cgroup/system.slice/docker-`dockerps-lq--no-trunc`.scope/cpu.max200000100000?~cat/sys/fs/cgroup/system.slice/docker-`dockerps-lq--no-trunc`.scope/memory.max2147483648??~dockerrun--rm-d--cpus=0.5--memory=0.5g--name=0.5c0.5gredis:alpine8b82790fe0da9d00ab07aac7d6e4ef2f5871d5f3d7d06a5cdb56daaf9f5/catfs./docker-`dockerps-lq--no-trunc`.scope/cpu.max50000100000?~cat/sys/fs/cgroup/system.slice/docker-`dockerps-lq--no-trunc`。scope/memory.max536870912从上面的例子我们可以看出,当我们使用Docker创建一个新的容器并为其指定CPU和内存限制时,对应的cgroup配置文件memory.max的cpu.max和cpu.max设置为相应的值。如果要查看一些已经运行的容器的资源配额,也可以直接查看对应配置文件中的内容。综上所述,以上就是对容器技术基石之一的cgroup的详细介绍。接下来我会写关于命名空间等容器技术相关的内容,敬请期待!欢迎订阅我的文章公众号【MoeLove】