简介: 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起一个跳板作用。
下面这些都是我一些不成熟的想法,欢送探讨。
原文链接
本文为阿里云原创内容,未经容许不得转载。