简介:io_uring 作为一种新型高性能异步编程框架,代表着 Linux 内核将来的方向,以后仍处于疾速倒退中。阿里云联结 InfoQ 发动《io_uring 介绍及利用实际》的技术公开课,围绕 OpenAnolis 龙蜥社区 Anolis OS 8 全方位解析高性能存储场景。
写在后面
cgroup 作为容器底层技术的半壁江山,很多文章曾经介绍并总结得很好了,对于 cgroup 是什么、有什么用以及一些相干概念,这些内容并不是本文的重点所以也将不再赘述。
情谊揭示:以下内容默认读者曾经初步理解 task、cgroup、subsys、hierarchy 是什么及它们之间的关系。
咱们为啥关注 cgroup 管制立体性能?
云原生目前是云计算畛域的重点倒退方向,其中的函数计算场景中,函数执行的速度是重要的性能指标,要求可能疾速、高并发地创立和销毁实例。在此场景下的隔离个性广泛都会波及到大量 cgroup 的相干操作,而现有的 cgroup 框架设计并发性很差,或者在设计之初并未思考到大规模的管制立体操作(例如:创立和销毁)。而随着技术的演进,大规模的管制立体操作场景逐步增多,也促使咱们开始更加关注管制立体的性能。
本文的论述是基于 4.19 版本的内核源代码,旨在剖析 cgroup 提供给用户的接口背地的实现原理,并基于实现原理给出一些用户态应用 cgroup 的倡议,最初在文章的结尾分享了一些内核态优化的思路。
原理剖析
图一
图二
以上两张图,是 4.19 版本的内核中 cgroup 中最次要的几个数据结构之间的连贯关系和 cgroup 层次结构的连贯关系。
cgroup:字面意思
cgroup_root:hierarchy
cgroup_subsys: 子系统,缩写的变量个别为 ss
cgroup_subsys_state: 当指向某个 subsys 时,代表该 subsys 在某个 cgroup 中一个实体
css_set、cset_cgrp_link:用于建设 task_struct 和 cgroup 之间多对多的关系
这些数据结构形象之后是这张图:
图三
其实也很好了解,实质上 cgroup 框架要解决的是:一个 cgroup 管哪些 task,一个 task 归哪些 cgroup 管的问题,在实现上可通过 cset 作为中介来建设这层关系。相比于 task 和 cgroup 直连,这种做法能够简化简单的关系。这是因为在理论应用的场景中,task 根本都以组为单位进行治理,对某一组 task 的资源管控计划都大概率是统一的。
对于 cgroup 的各类操作围绕着这三类实体开展:
- 创立:在图二所示的树形构造中减少一个叶节点
- 绑定:实质上是迁徙,子过程被 fork 进去时连贯父过程指向的 cset,绑定即是从一个 cset(如果不再有 task 指向则删除)迁徙到了另一个 cset(如果指向的是新的 cgroup 汇合则新创建)
- 删除:在图二所示的树形构造中删除一个不论控任何 task 的叶节点
对于 cgroup 的各类操作的访问控制也围绕这三类实体的开展:
- task: cgroup_threadgroup_rwsem 锁
- cset: css_set_lock 锁
- cgroup: cgroup_mutex 锁
具体的这三类锁有什么作用,将在优化思路里进行剖析。
优化计划
问题出在哪?
问题在于三个锁上:cgroup_mutex、cgroup_threadgroup_rwsem、css_set_lock。
cgroup_mutex 爱护 cgroup 的整个层级构造。cgroup 的层级构造是一个森林,咱们须要用这个一个锁来爱护整个森林。扭转层级构造比方常见的 mount、mkdir、rmdir 就不用多说了,必定是须要持有这个锁的;除此之外对 cgroup 的任何一个其余的操作也须要持有这个锁,比方 attach task、以及其余的读或写 cgroup 提供的接口。同时,因为 rmdir 的操作是随时都有可能产生的,任何操作都须要与 rmdir 都互斥。
css_set_lock 爱护和 css_set 相干的所有操作。任意过程随时都有可能 exit,导致某个 css_set 开释,从而影响 css_set 的哈希表。除此之外,对 cgroup 的绝大多数的操作也会波及到 css_set_lock,这是因为对 cgroup 的绝大多数的操作(创立除外)都会引起 css_set 的变动。
cgroup_threadgroup_rwsem 爱护和 cgroup 相干的线程组操作,事实中随时都有可能的 fork 和 exit 操作导致线程组发生变化。这里用读写锁的起因是,过程本身的行为可能包含扭转线程组的组成和持有读锁,这是能够并行的;当过程 attach 的时候,须要一个稳固的线程组视图,此时如果过程在 fork 或者 exit 的话会导致线程组的扭转,而 attach 又是能够以线程组为单位的,不可并行。这里用读写锁并不是说是真的在读什么或写什么,只是恰好合乎读者并行,写者需与其余写者互斥这个个性而已。也就是说,fork、exec、exit 之间能够并行,相似于读者;attach 与其余的都互斥,相似于写者。
这三个锁会受到过程 fork 和 exit 的影响,并且也会导致对 cgroup 的任何操作之间简直不可并行。笔者在对 cgroup 进行深刻的钻研前,感觉是最开始的设计者偷懒,应用如此大粒度的锁,直到把 cgroup 的框架摸索明确后才发现,临界区就是有这么大,各种会异步产生的事件都须要操作这些数据,所以这些锁被设计成这样也很正当。
这里试着对问题进行形象,思考一下问题的实质在哪。
对于 cgroup_mutex,问题实质是树形(节点是 cgroup)构造的并发拜访。
对于 css_set_lock,问题其实是二部图(一边是 css_set,一边是 cgroup)构造的并发拜访。
对于 cgroup_threadgroup_rwsem,问题其实是汇合(线程组作为汇合的元素)构造的并发拜访。
问题的定义曾经分明了,怎么解决呢?以我目前的能力,我没法解。
是的,剖析了这么多给的论断是此题无解,或者说临时无解,能够有的解法也会对 cgroup 的框架造成刮骨疗毒式的改变。这背地的危险、稳定性的影响、投入产出比的痛能不能接受的住,我给不出一个确定的论断。如果读者有什么想法,欢送在留言区提出,一起交换。
尽管治标难治,但治本还是能够有点想法的。
用户态优化:缩小 cgroup 操作
这个计划很好了解,提前把 cgroup 创立和配置好,等须要用的时候间接取就行。这个计划成果极好,几乎是降维打击。这里贴一下试验数据,这里的测试模仿袋鼠容器启动时的创立与读写——
这个计划达到了 90% 以上的优化率,将原本须要创立配置后 attach 过程最初删除的状况变成了只须要 attach,工作量少了,天然也就变快了。
但这个计划存在一些弊病。一方面,池子里不必的 cgroup 对于零碎来讲仍然是可见的,须要进行治理,因而会存在肯定的负载;另一方面是数据残留问题,并不是所有的 subsys 都提供相似于 clear 的操作接口,如果对监控数据有要求的话 cgroup 就是用一次就废,须要对池子进行补充,减少管制逻辑的同时又呈现了竞争,成果会打折扣。最初便是须要明确 cgroup 的层次结构,毕竟要提前创立和配置,如果对运行时的层次结构无奈掌控的话,池子都没法建设。
缩小 cgroup 数量
systemd 在默认状况下会把大多数 subsys 都挂在独立的一个 hierarchy 下,如果业务的过程都须要受同一些 subsys 管控的话,能够把这些 subsys 都挂载在同一个 hierarchy 下,比方把 cpu、memory、blkio 挂载在一起。
这时候可能有同学要问了,本来在 cpu、memory、blkio 下各创立一个 cgroup,和在 cpu_memory_blkio 下创立一个 cgroup 能有多少区别?该有的逻辑都得有,一个都跑不了,最多就是少了几个 cgroup 本身这个构造体,能有多少区别?
这里要回归到最开始的场景,cgroup 的问题出在场景是高并发,而实质上各类操作却是串行的。咱们晓得,掂量性能有次要的两个维度:吞吐和提早。cgroup 实质的串行无奈间接进步吞吐,各个 subsys 独立在 hierarchy 下等于是被拆解成子工作,反而进步了提早。
上面是测试数据:
内核态优化
对上述三把锁动不了,只能对临界区内的那局部内容下手了。想要放大临界区,那就须要找出临界区内耗时的局部进行优化。
下图是各个子系统创立 cgroup 时各个局部的耗时:
这里简略解释下各个局部做了些什么:
- cgroup:创立和初始化 cgroup 构造体
- kernfs:创立 cgroup 的目录
- populoate:创立 cgroup 管制用的文件接口
- cssalloc:调配 css
- cssonline:css 在各个子系统中的 online 逻辑
- csspopulate:创立子系统管制用的文件接口
从图中能够发现 cpu、cpuacct、memory 的耗时绝对于其余的子系统提早高很多,其中 css alloc 和 css populate 占大头。上面咱们将钻研一下这个“主要矛盾“到底在做些什么。
通过剖析咱们发现,css alloc 上提早高是因为给一些 percpu 的成员分配内存,这一过程比拟耗时。css populate 上是因为局部子系统的接口文件比拟多,须要顺次一个个地创立从而耗费更多的工夫。
剖析过后发现,这些逻辑都是必须没有冗余,怎么优化?做缓存呗。percpu 成员变量记录下地址不开释下次重复使用,子系统接口文件在开释时以文件夹为单位移到一个指定的中央,须要时再移回来,只波及目录文件上一个目录项的读写,开销低且是常数。
通过这两种形式,各个创立 cgroup 的延时优化后果如下:
cpu 子系统 css alloc 局部仍然比拟耗时的起因在于初始化操作比拟多,但相比于原先的 160us,延时曾经降到了 50us。
放大临界区后尽管并不能对并发度有什么影响,但至多提早降下来了,上面是测试数据。
t 个线程并发,每个线程在 cpu、cpuacct、cpuset、memory、blkio 下创立 n 个 cgroup:
一些假想
如果忽视各种限度因素,摈弃现有的框架,不思考向下兼容,实现一个用于管控过程资源且反对高并发的框架,能够怎么设计?
当初 cgroup 的机制提供了相当高的灵活性,子系统之间的关系能够随便绑定,task 能够随便绑在任意一个 cgroup 上,如果就义一下这些灵活性,对问题的解释是不是就能够变得简略点,上面谈谈我的几个想法。
第一,前文提到的为了缩小 cgroup 数量,把所有的子系统都绑定在一起的想法,是否能够固化在内核当中,或者说不提供子系统独立挂载和绑定挂载的个性?这样,过程组与 cgroup 变成了一一对应的关系,cset 就没有了存在的意义,css_set_lock 带来的问题也不攻自破。然而对应的弊病是,一个过程组内的所有过程在每个子系统上资源管制都是统一的。
第二,cgroup 层级构造是否有存在的必要?当初 cgroup 以树形构造组织,的确在逻辑上更加合乎事实。比方,在第一层给业务调配总资源,在第二层给业务的各个组件分配资源。但在操作系统分配资源的视角上,以及业务过程具体取得资源的视角上,第一层的存在并没什么作用,只是给用户提供了逻辑更清晰的运维治理。如果把 cgroup v2 提出的 no internal process 个性也利用上,能够把 cgroup 层级扁平化到只有一层。
cgroup 只有一层的益处是,能够很不便地把 cgroup_mutex 粒度细化,细化到每个 cgroup 一把锁,不会存在好几层的树形构造——改变一个 cgroup 须要从先人开始持锁的问题。锁的粒度细化后,在并发启动容器实例的时候,因为对应不同的 cgroup,也就不会存在竞争的问题。
第三,cgroup 的删除是否加以限度?当初是用户异步手动删除空的 cgroup,如果能够在 cgroup 不再治理过程(exit,move)时暗藏,后续找个机会触发删除,便能够少一个竞争场景。这种办法会造成空的 cgroup 没法再利用,当初有对空 cgroup 再利用的需要吗?
最初,绑定过程是否加以限度?task 绑定 cgroup 的实质是挪动,从一个 cgroup 到另一个 cgroup。cgroup_mutex 粒度细化后会存在 ABBA 的死锁问题。有一个问题是,task 存在绑定到一个 cgroup 后再绑定的需要吗?现实状况是绑定一个后顺利运行而后退出。基于这种假如就能够做一个限度,只容许 task 在绑定时,src 与 dst 内必须蕴含 default cgroup、default cgroup 起一个跳板作用。
下面这些都是我一些不成熟的想法,欢送探讨。
原文链接
本文为阿里云原创内容,未经容许不得转载。