️This article has been over 1 years since the last update.
本文从应用侧调度软件开始介绍调度器,并主要以单机版OS为例,介绍了CGroups的隔离。
面向读者:已经有Docker等使用经验,但是希望更深入了解的人。不适用于Java初学者,也不适用于内核级专家。注意本文较长。
在阅读本文前,我个人建议直接去阅读RedHat的文档,专职团队的文档质量肯定比很多博客写的更加好。
调度器的介绍
什么是调度
所谓调度,就是将资源在一定时间内对任务(Workload)的分配与隔离,比如最简单的切蛋糕,到复杂的PaaS容器编排,再到现实生活的敏捷迭代、打车、外卖都是调度。不过本文主要介绍的是单机版CGroup调度。
我们可以分类
- 简单调度:上下文无关,用贪心算法即可,满足当前最优
- 复杂调度:是对复杂系统的处理,或者叫做NP问题,在短期很难有最优解,而且最优情况不同,比如亲和性(NUMA),借用/预占资源,队列优先级等HPC领域问题
调度软件的介绍
常见的有LXC, Docker(Runc), Nomad(新但是部署复杂), Mesos, Platform LSF, Kubernetes等。它们既可以用于HPC作科学计算,也可以用作PaaS服务Web业务。
我在日常开发中, 搭建并使用过多种调度器
- 私有云:缺点非常明显,维保异常昂贵。如果只是部署Web服务等单功能业务,不推荐上云。
- Kubernetes:这个用起来方便,配合各种方法学实施,大部分Web应用都能很好地部署、扩容与滚动发布。缺点是它本身的运维/插件/网络部署太重了,YAML(特别是annotaion/CRD)学习与方言比较陡峭,中小团队投入的运维成本可能比纯Jar包部署还高,所以用k8s的前提是有人帮你运维或者买现成的。
- Nomad:一款新的调度框架,内部采用mvcc锁,对Docker/Raw都有支持,它的安装部署很简单(单个Go文件即可),功能也够用,而且支持广域网调度(基于Consul转发),商用产品有CircleCI、fly.io等产品。我在项目中用它来做Jenkins的动态扩容,可以管理至少600U的虚拟/物理机。
- HPC类:比如IBM LSF/Slurm,其中LSF费用很贵而且受到USA出口管制,但是功能强大,主要基于核数/MPI框架进行调度,可以管理上千台机器(基本上都是28核以上每台),千万级HPC任务。它有叫做OpenLava的开源实现版,但是被IBM的律师给整残了。更多可以参考这位的博文
- iSulad/volcano/karmada:由HW的各种实验室开发,符合HW各自为政的内卷作风
此外有
- 多云编排调度,比如Terraform,这里强调的是状态而不是动作
- 数据中心/HPC超算的硬件调度方案,比如Atlas/TaiShan的ARM算力集群等。
- 应用层的Workflow调度,比如Jenkins的CPS编排,Flink的管道计算,ARM的Lava等
但是它们不再本文介绍范围内。
唠叨这么多,主要就是想告诉大家,调度是一个广阔的领域,虽然读者与我可能是IT/互联网行业,但是不要限制自己的视野止于“应用”经验。更广阔的底层调度软硬件,编译器,国内极少数企业才能烧钱研发的。
总体调度策略
大部分调度器总体策略基本如下
- 在OS外: 通过BinPack等算法让正确的资源分给需要的任务进行规划调度,一般需要维护状态机/队列。它的难点在于约束,目前主流的调度方法均是求出当前最优解,而不考虑未来。这个玩意太烧钱了,是一般人永远也达不到的天花板,大部分产品都是西方高校或者实验室才能搞出来。
- 在OS内: 主要通过CGroups(由谷歌开源)进行调度,如果OS不支持CGroup或者非Root模式,那么可能会需要全表扫描
/proc
定位PID(比如Jenkins实现了ps
读取/proc
),但是性能较弱。我个人认为CGroups本质上就是PID的倒排索引,实现了避免全表扫描。在Docker/Nomad中,Runc是底层容器框架,它的规格与实现由多家厂商(比如RedHat/HW等)制定规范,是2016年后容器的事实标准。它将CGroups/chroot等命令封装为Go的SDK(libcontainer),而不用命令行去管理CGroups。在KVM虚拟机中,也是用CGgroup进行资源限制。
有哪些资源可以被隔离调度?
更多可以看libcontainer的参数配置
关于CGroups(Control Groups),网上有手动命令行的介绍,比如Redhat的文档就很详细。但是值得注意的是,根据阿姆达尔定律,能够并行的计算是有限的,需要通过PERT方法进行拆解。
CPU调度
CFS调度介绍
CFS通过vruntime维护了每个进程的总虚拟运行时间(实际运行时间*加权),并用红黑树实现优先级的排序,总时间越少的排在前面。
1 | # 进程结构体的如下信息将在 cfs_rq 树中进行排序 |
在CGroup中,通过干预权重,使vruntime更低,更加容易被优先调度到
它在内核开关内部如下
1 |
|
这个宏表示
This option allows you to create arbitrary task groups using the “cgroup” pseudo filesystem and control the cpu bandwidth allocated to each such task group
更多参考:https://developer.ibm.com/tutorials/l-completely-fair-scheduler/?mhsrc=ibmlearning_l&mhq=cfs
CFS硬分配(Docker主导)
此方案是按照CPS时钟进行划分,也是Docker/Kubernetes的默认实现,不管当前机器是否空闲,它都只能限制到固定的算力,这样应用可能会缺少爆发力(burst)。此方案与主频是无关的。如下是以Docker为例,分别分配4核/1核/0.5核的场景,注意Linux文档中,并没有说100000
就一定是一个核心,这里只是一个常见的默认值
1 | # one second, the default value in containers |
我在CI构建中,也显示配置了这些最大总硬限制,防止某些不可控的应用把整个OS给搞挂。
cpu.shares软限制(Nomad主导)
此参数为相对值,与CFS中的vruntime
的权重成反比,强调是占用了相对值/SUM(相对值)
,而不是固定的频率/核数,使用它的潜规则是你的OS中只能基于一个容器平台维护,否则每个平台映射的含义可能不同。
在 Nomad框架中,将主频与此一一映射对应,假如定义了1234Mhz的CPU资源,那么在cpu.shares
也将配置为1234。我认为这个是最好的思路,它将相对值转为了主频总和的绝对值分配问题。但是一旦集群中加入了不同型号的CPU,那么可能性能不完全相同。底层透传给了runc
库以启动容器。
在Docker中,默认一核为1024,但是这个开关默认没有打开。
具体如何影响内核,可以参考cpu.sharesの内部動作
假如使用Nomad部署业务,千万不要为了省钱让Server节点承载任务,否则压力上来后,整个调度节点因为shares的均分调度而卡死。
高级使用(混合配置)
假设你想对某个任务软性限制 300MHz主频,硬性限制一个CPU,可以如下配置,比如在nomad的raw_exec中使用。
1 | cpu.shares = 300 |
再次强调这里的值都是相对值,只有机器内所有CGroup全部遵循这个要求才能说shares约等于主频。
cpuset(绑核,LSF主导)
当启动任务时,指定Dedicated CPU的核数,bsub -n 8
表示使用了8个物理CPU。此类场景主要是涉及到License或者对Context/cache affinity的任务,比如某些工业软件/EDA/科学计算通过CPU个数进行授权,运行费用甚至比CPU本身还贵。而且这类机器基本都是NUMA+关闭超线程,因为超线程/超售CPU在这种场景下软件授权是赔本的。这种一般需要租赁bare metal machine云主机算力,甚至自己搭建RMDA存储网络。
此外,很多中间件也需要绑核,比如SDN、Cache、Queue等,否则会抖动。
Facts: 大部分工业软件只能单核运行。
Memory控制
程序内存的组成
在一个程序启动后。有如下内存将被分配,假如我启动了一个bash,它将进行如下分配
1 | # 启动一个Bash程序 |
值得注意
- CGroup对RAM的统计比较激进,只统计RSS用量,共享库会重复统计。
- 在纯运算调度器中(比如LSF调度器),内存占用都特指物理内存PSS(自己占用+共享库均分)的统计。比如你启动多个JVM,可以发现平均占用还降低了。或者OS将你的内存扔到分页里面了,导致内存统计维度占用显得很低。
使用CGroups进行RSS内存限制
如果需要限制内存总量,主要有如下配置进行限制
1 | # 进程的用户内存(包括文件缓存),比如2G,这里是k8s等调度器的首选项 |
至于是否配置虚拟内存比例,有如下场景
- 如果是WebService/K8S类应用,一般默认会配置
swappiness
为0(The kubelet right now lacks the smarts to provide the right amount of predictable behavior here across pods.),这样基于物理内存的统计调度更加简单准确,当全部内存(RAM)耗尽时,取决于node-oom-behavior,通常是Kill掉,这时你要引入Oberservity工具,分析为啥挂了 - 如果是高性能计算,原则上不限制SWAP,需要配置昂贵的NVMe SSD进行swap配置,一般是物理RAM的2倍。
测试代码
1 | mkdir -p /sys/fs/cgroup/memory/test/test-1 |
Nomad对CGroup资源的管理
在Nomad中,假如我希望配置某个任务的CPU/内存(含超售)
1 | # Mebibyte |
它在内部将在如下位置生成libcontainer的CGroups
1 | drivers/shared/executor/executor_linux.go:configureCgroups |
转化如下
1 | // runc容器的默认配置项 |
K8S对内存的管理
默认场景下优先以limit为准,而软限制是无限。建议不要配置过远。
RDMA技术
基于RDMA协议跨过Infiniband/RoCE网线访问内存,场景主要还是GPU加速卡/存储资源的直通,比如GPU Direct,但是与CGroup的关系不大。
除了极其昂贵以外,没有太多缺点。
总结
当前CGroup调度器主要就是一个内核系统的外壳,Docker等组件并不是非常复杂。如果需要了解内部实现,需要去学习内核。如果对CGroup有兴趣,可以研究HPC开源平台OpenLava代码,它是纯CGroup而没有namespace,分析更简单。