1
调度类问题定位步步通
调度问题可以抽象描述为:关键进程的调度延迟”,也就是“关键任务不能及时得到cpu。
我们需要解决的问题是:“定位调度问题,并协调多个关键任务,使得业务充分发挥cpu性能。
在KVM虚拟化环境中HostOS采用了Linux系统,虚拟机的vcpu线程以及虚拟化后端的线程一般都是普通进程,通过Linux的CFS(完全公平调度)调度器调度运行,CFS调度器的基本原理:
设定一个调度周期(sched_lantency_ns),使得每个进程在这个周期内至少有机会运行一次,即每个进程等待cpu的时间最长不超过这个调度周期;然后根据进程的数量,平分这个调度周期内的CPU使用权,由于进程的优先级即nice值不同,分割调度周期的时候会加权;每个进程的累积运行时间保存在进程的vruntime字段里,哪个进程的vruntime最小就获得本轮运行的权利(优先级高的进程vruntime增长得慢)。
1.1
定位调度问题
从cpu的角度,关键任务不能及时得到cpu的情况分为两种:
1)
关键任务自愿放弃cpu,需要等待某种资源的就绪才可以继续运行(如等待信号量、等待获取mutex锁、等待IO文件描述符就绪、等待定时器到期等)。这种情况下,关键任务会进入睡眠状态,自愿放弃cpu并从cpu的运行队列退出,直到等待的资源就绪后唤醒它。这里的调度延迟指从被唤醒到真正获取到cpu的时间段(不考虑等待资源的时间)。
2)
关键任务非自愿放弃cpu。因同一个物理cpu上同时可能有多个任务运行,调度器安排其他任务抢占了当前的关键任务。这种情况下,关键任务不会进入睡眠状态,继续留在cpu的运行队列中,等待调度器安排下次运行机会,等待过程中也会产生延迟。
根据上述两种情况,调度延迟可分为:唤醒延迟和抢占延迟。
1.1.1
抢占延迟
进程自愿与非自愿切换发生的次数记录在/proc/$pid/status中,可以通过watch命令观察它们每秒的增长情况:
watch -d -n1
'cat /proc/$pid/status |grep ctxt'
进程号29549的任务被抢占次数记录在nonvoluntary_ctxt_switches字段,voluntary_ctxt_switches字段表示进程自愿放弃cpu的次数,通过watch命令可以观察每秒的增长数量。
关键任务非自愿被抢占会使得下次运行可能需要多等待一个调度周期以及进程上下文切换的时间,过多的抢占会影响关键任务的性能。
通常一个cpu上不会只运行一个任务,而如果关键任务属于cpu消耗型的任务(常常占用cpu运行)并且需要和其他任务共享cpu,那么一些非自愿抢占往往是不可避免的,因为常常占用cpu运行的任务vruntime会持续增长高于不常占cpu的任务。
1.1.1.1 抢占发生的时机
1.1.1.1.1
tick时机抢占
在每个cpu上,内核每隔1/HZ秒会执行一次检查,判断是否该抢占当前任务。
触发抢占的条件如下:
1)
nr_running>1,显然只有运行不止一个任务的情况下才会发生抢占。
2)
当前任务的时间片已经用尽,这种情况将触发重新调度,当前任务被抢占。
3)
如果当前任务的实际运行时间超过了sysctl_sched_min_granularity的保护,运行队列队首任务的vruntime小于当前任务的vruntime,且差值超过了当前任务的时间片大小,则可以抢占。
1.1.1.1.2
唤醒抢占
如果HostOS开启了唤醒抢占开关,关键任务被抢占的概率则会增大:
echo WAKEUP_PREEMPTION >
/sys/kernel/debug/sched_features
被唤醒任务抢占当前任务的条件:
1) 抢占开关打开
2) 被唤醒任务的vruntime小于当前任务,并且差值超过sysctl_sched_wakeup_granularity,该sysctl值是实际时间,带入计算时会按照被唤醒任务的优先级换算为虚拟运行时间。
如果有其他任务在关键任务的cpu上被唤醒,并且开启了唤醒抢占开关,那么较大概率会抢占当前正在运行的关键进程,因为被唤醒的任务的vruntime一般都会比正在运行的任务小。
1.1.2
唤醒延迟
有时关键任务是延时敏感型的,一次或少量几次的唤醒延迟都会导致任务失败。
1.1.2.1 为什么会有唤醒延迟
有时任务会因为等待资源或某个事件而进入睡眠状态从而退出运行队列,当事件到来时会唤醒该任务,给任务找到合适的cpu并将任务放到cpu的运行队列。唤醒动作结束,等待合适的调度时机被唤醒任务才能再次获得运行。如果开启了唤醒抢占则再唤醒动作中根据抢占条件可能会立刻给予被唤醒进程一次重新调度的机会。
1.1.2.2 如何观察任务的唤醒延迟
通过perf sched可以查看某段时间内进程的调度时延:
例如:搜集系统中1秒内的所有调度信息
1秒内perf搜集了31.156MB的调度事件信息,具体的大小取决于负载和cpu数(这里是一个48核的cpu)。需要注意的是执行perf的当前目录需要有写权限。
分析搜集信息:
1)
观察调度延迟 perf sched latency --sort=max
可以看到vcpu7在2秒的时间内的调度时延信息:
最大调度延迟为 2.420毫秒,平均调度时延0.005毫秒;
最大时延发生的时刻为:6767.303953。
任务在这1秒内运行了39.753ms,总共发生了1227次上下文切换,因此也可以推算出vcpu7每次获得cpu后平均运行约32us。
2) 分析最大时延产生的原因: perf sched map
如果需要分析最大时延发生时刻附近,cpu上都有哪些调度事件,可以通过perf
script输出的trace事件进行分析。
1.1.3
排查虚拟化场景下的调度性能问题
如果业务运行在GuestOS中,那么涉及双重调度:host上的调度器调度vcpu,vcpu还需按照GuestOS内部调度器的逻辑调度GuestOS内部的任务。两者的调度器行为依赖于各自的OS实现,因此我们需要确定是host调度的问题还是guest内部调度的问题。
1.1.3.1 查看虚拟化层的开销
如果Guest为Linux,在guest内部执行top,观察st%(steal时间占比),如果st%较高则表示vcpu经常得不到执行。
造成st%较高的直接原因是当vcpu需要执行的时候host(hypervisor)把pcpu安排跑其他任务。通常是因为host上的cpu压力较大,可以在host端调整绑核策略,使得vcpu运行在相对空闲的pcpu上,不被其他任务干扰。
1.1.3.1.1
在Host端查看当前vcpu的可用pcpu资源
virsh list //查询虚拟机的domain_name
virsh vcpupin domain_name //查看vcpu的绑核策略
不重启虚拟机的情况下动态修改绑定策略(重启虚拟机后修改无效):
virsh vcpupin xxx --vcpu=0 1-2 ( 将虚拟机xxx的vcpu0绑定在pcpu1和pcpu2上)
排查问题时可以通过动态绑定vcpu的方式将关心的vcpu任务绑定到比较空闲的pcpu上。
修改host端其他任务的cpu绑定
taskset -pc 0 $pid (将pid进程绑定到cpu0上)
如果host端启用了cpuset cgroup配置,在修改绑定关系时可能会遇到失败,需要先检查当前cgroup的cpuset控制组配置。
注意: 对vcpu的绑核调整最好通过
virsh vcpupin的方式调整,而不要通过taskset或修改任务的cpuset cgroup方式,这样可以与libvirt端保持一致。
如果guest上无法观察st%,或者不方便登陆到guest上操作,那么也可以在host上参考3.1节所述的定位调度问题的方法,查看vcpu进程是否有较大的调度延迟或频繁被抢占。
在Host端,对于qemu后端线程也可用同样的办法查看。
1.1.3.2 案例:分析调度产生的性能问题
1.1.3.2.1
问题现象
问题来源于性能基线测试用例: cmt_Pi_pfm_001
测试在对比UVP与H3C性能时发现问题(关联DTS号:DTS2017072811005)
【用例描述】
利用y-cruncher在虚拟机内部计算圆周率,测试单核、多核场景下的CPU浮点型计算能力,关注计算固定位数下的计算时间和CPU利用率
【前置条件】
UVP:测试版本KVM2.3 RC8B030 (FC环境)、kvm-4.4.11
H3C:测试版本4.1.0-generic SMP
GuestOS:centos7.2 3.10.0-327.el7.x86_64
UVP与H3C分别部署在同型号、同配置的物理单板上( E5 2690 v4)
UVP,H3C通用配置:
1)虚拟机规格:4U8G,1网卡,1系统盘。
2)开启内存预占;
3)使虚拟机内部的L1-L3cache呈现HOST能力保持与H3C一致;
4)4个vcpu分别绑定在4,5,6,7这4个物理core上。
UVP环境做了I层隔离,I层任务都放0-3上。
【测试方法】
在虚拟机内部执行./y-cruncher bench 100m,计算圆周率到1E位。
【问题】
多核场景下(4个vcpu),比较UVP与H3C的测试结果。
UVP:
H3C:
UVP计算时间比H3C多,CPU利用效率低3%
1.1.3.2.2
问题分析
一般看到这个问题的第一反应是UVP的虚拟化层有开销,使得guest不能充分利用cpu。接下来就在host上抓内核虚拟化事件以及内核的调度事件开始分析,事实上我们也是这样去做了,因为GuestOS是同一款,倾向于认为问题出在host或者虚拟化实现上。结果却是面对一堆trace数据分析了很久发现这些事件都是正常的,并没有什么异常事件产生,仅从事件的次数上也分析不出什么。
由于GuestOS中运行的测试程序是闭源的,只能黑盒分析,期间怀疑的点非常多,比如怀疑vcpu之前的通信效率低、浮点计算性能差、host上调度性开销大、甚至怀疑两款同型号的cpu计算性能有差距。
后来也试图用其他计算性能的评测工具做比较,结果发现UVP的计算性能也没有比H3C低,于是陷入了困惑中。
事实上,直接从虚拟化层面切入分析调度问题往往容易使我们陷入困惑,比较好的步骤应该是这样:
1.
首先排查物理环境的差异,确保环境一致(虽然本例并不是硬件有差距)。
2.
再排查BIOS硬件配置一致,比如通过turbostat观察cpu频率是否差不多,有没有开启cpu节能等特性。
3.
排查HostOS的配置,HostOS上有一些与调度以及节能相关的配置,通过sysctl –a |grep
sched 以及 cat
/proc/cmdline可以进行初步的排查,这一步需要关注的配置项很多,尽量保证一致。
调度器相关的特性是否一致 cat
/sys/kernel/debug/sched_features
Cgroup的配置是否一致 system-cgls(或者通过/sys/kernel/fs/cgroup/cpu文件系统观察)
实际上,为了简化比对过程,我们直接使用了H3C版本的HostOS内核安装在UVP上做比较,并且把所有sysctl的配置设置成一样,发现现象依旧是UVP环境的Guest计算利用率上不去。
4.
排查虚拟机的配置是否一致,由于UVP与H3C的虚拟化实现不完全一致,有些选项的含义会有区别,但是最终定位到了测试用来对比的虚拟机配置文件有一处区别比较可疑。两个环境的虚拟机cpu上限不一样,虽然它们实际分配的cpu数量是相同的。主观判断非常容易忽略这一点,因为理论上虚拟化层配置cpu上限对于计算性能并没有太大作用,其实是有区别的,至少需要考虑cpu拓扑结构可能不一致的影响。经过试验把cpu上限设置成一样后,测试结果就几乎没差别了。
5.
排查GuestOS的区别,比如观察%st,这里的测试环境虚拟机镜像是一样的,因此无需排查。
6.
最后分析应用程序的行为,如果测试程序是黑盒的,可以通过strace跟踪测试过程中的系统调用进行分析。在这个问题中,测试程序将自己设置为实时任务,而该版本GuestOS的内核有bug,如果最大cpu数量超过64个,那么实时任务默认的带宽限制是失效的(这是导致问题的根因)。
7.
如果到这里还没找到问题,可以抓取虚拟化事件,进行具体分析。即使到了这一步也可以进行一些简单的排查工作,比如将GuestOS的cmdline参数设置为idle=poll,这样就可以排除虚拟化层vcpu之间的通信性能区别以及Host的唤醒延迟区别,
以上步骤是一个基本的排查调度问题流程,其中排查环境的区别尤为重要,第4步中我们虽然还没找到根因,但是通过排查的步骤就已经找到了问题的关键,对于后续的分析非常有帮助。因此,在遇到怀疑与调度有关的性能问题时,如果条件允许的话,尽量按照上述顺序排查。
关于这个问题的分析过程详细步骤见下面的附件:
1.2
合理分配cpu资源
根据关键任务请求cpu资源的特点,可把关键任务分类:
1)
高时延敏感,低cpu占用率,如外部设备发中断通知cpu处理数据,并立刻反馈处理结果。
2)
非时延敏感,高cpu占用率,一般为批处理型任务,比如编译内核。
3)
高时延敏感,高cpu占用率,这种情况通常需要独占cpu运行不与其他任务共享cpu。
管理员在做cpu资源划分时,必须要清楚当前部署的应用的特点,合理分配cpu资源。
1.2.1
观察Host上的整体负载
了解系统的平均负载后,有助于辅助管理员进行资源的合理划分。通过top命令的输出可以知道当前Host
OS上的整体负载。
top 输出中的 average :
load average后面的三组数据分别表示
最近 1分钟,5分钟,15分钟的系统平均负载,这里的负载指系统所有cpu的运行队列中的任务总数。
从上图可以看出:最近1分钟内top采样的运行任务平均为53.11个,最近5分钟平均为41.88个,最近15分钟平均为34.19个,说明最近15分钟系统的负载在上升;上图系统一共48个cpu,说明最近1分钟平均每个cpu上需要运行不止一个任务。
在top的交互界面按下数字键"1",可以观察每个cpu的利用率以及cpu被何种类型的任务开销所消耗。
us : 用户态进程
sys: 内核态(通过系统调用)
ni : cpu上的nice值不为0的任务所消耗的cpu
id: cpu的空闲率
wa: cpu在等待IO
hi : cpu处理硬件中断
si: cpu处理软中断(通常也是硬中断触发的软中断)
st: 如果有值表示当前系统处于虚拟化环境,被hypervisor偷走的占用率
上图可以看出: cpu0上有大量中断,因为si值较高; cpu1~cpu7运行用户态任务,并且比较繁忙;cpu8~cpu10几乎没有任务运行,接近百分百空闲。
这些cpu上运行的都是cpu消耗型任务,几乎没有等待IO的情况。
st都是0,表示当前系统可能并没有处于虚拟化环境中,也有可能虽然在虚拟化环境中但并没有其他虚拟机竞争cpu。
类似的工具还有vmstat或mpstat。
1.2.2
调度器load_balance
1.2.2.1.1
Load_balance简述
load_balance所需要做的事情是:在一定的时机,通过将进程从一个运行队列迁移到另一个运行队列,来保持CPU之间的负载均衡。
普通进程的负载均衡在以下情况下会被触发:
1) 当前进程进入睡眠而对应的运行队列中已经没有需要被调度的进程。这时触发idle_balance,试图从其他cpu的运行队列中pull一部分任务过来运行。
2) 每隔1/HZ秒,即tick的时机触发rebalance_domains,试图发现并解决系统中的不均衡。
另外对于调用exec的进程,它的地址空间已经被完全重建,当前cpu上已经不再缓存对它有用的信息。这时内核也会考虑负载均衡,为它们找到一个合适的cpu。
Linux内核中使用Load来表示任务的负载,每个普通任务根据自身的nice值(即优先级)对应固定的load,如ncie值为0的任务的load为1024。简单的理解,一个ncie值为0的任务在运行时对当前cpu造成的负载就是1024,但是内核为了更准确的衡量cpu的负载,在load的统计中加入了时间衰减的概念,一个load为1024的任务运行时造成1024的load,但是它不一定时刻都处于运行状态,有可能在过去的1毫秒内只运行了500微秒,因此每个任务对cpu造成的负载会根据时间段衰减并取平均值。每个cpu有一个运行队列rq,该cpu上的所有任务造成的负载累积到rq的load中。
load_balance过程简述:从最繁忙(load最高)的run_queue中pull几个进程到当前run_queue中(只pull,不push),使得当前run_queue与最繁忙的run_queue得到均衡(使它们的load接近于所有run_queue的平均load),仅此而已。load_balance并不需要考虑所有run_queue全局的均衡,但是当load_balance在各个CPU上分别得到运行之后,全局的均衡也就实现了。这样的实现极大程度减小了负载均衡的开销。
load_balance的过程大致如下:
1、找出最繁忙的一个rq;
2、如果找到的rq比本地run_queue繁忙,且本地rq的繁忙程度低于平均水平,那么迁移几个进程过来,使两个rq的load接近平均水平。反之则什么都不做;
有些进程可能频繁地在TASK_RUNNING和非TASK_RUNNING状态之间变换,导致run_queue的load值不断抖动。仅看某一时刻的load值,是体会不到rq的负载情况的,必须将一段时间内的load值综合起来看才行。于是,rq结构中维护了一个保存load值的数组:
unsigned long cpu_load[CPU_LOAD_IDX_MAX] (目前CPU_LOAD_IDX_MAX值为5)
每个CPU上,每个tick的时钟中断会调用到update_cpu_load函数,来更新该CPU所对应的rq的cpu_load值。
1.2.2.1.2
动态查看各cpu上负载状态
|
linux-aKNDzC:~
# cat /proc/sched_debug |grep -A20
cpu#1 cpu#1,
2199.999 MHz .nr_running : 1 .load : 40 .nr_switches : 39148465 .nr_load_updates : 10955731 .nr_uninterruptible : -12 .next_balance : 4305.623520 .curr->pid : 36301 .clock : 10946592.682860 .clock_task : 10946592.682860 .cpu_load[0] : 36 .cpu_load[1] : 34 .cpu_load[2] : 31 .cpu_load[3] : 29 .cpu_load[4] : 32 |
通过cat
/proc/sched_debug可以看到cpu1上的负载为40,cpu_load[x]则表示不同衰减程度计算出的负载。
当前内核支持组调度,组内任务产生的load会累加到每个cpu的组上,最终计入cpu的load。通过/proc/sched_debug可以观察每个组产生的load。(如果某个组在cpu上没有运行,则不显示)
1.2.2.1.3
调度域
在复杂的SMP系统中,为了描述CPU与CPU之间的亲缘关系,引入了调度域。
两个CPU之间的亲缘关系主要有以下几种:
1)超线程。超线程CPU是一个可以“同时”执行几个线程的CPU。就像操作系统通过进程调度能够让多个进程“同时”在一个CPU上运行一样,超线程CPU也是通过这样的分时复用技术来实现几个线程的“同时”执行的。这样做之所以能够提高执行效率,是因为CPU的速度比内存速度快很多(一个数量级以上)。如果cache不能命中,CPU在等待内存的时间内将无事可做,可以切换到其他线程去执行。这样的多个线程对于操作系统来说就相当于多个CPU,它们共享着大部分的cache,非常之亲近;
2)同一物理CPU上的不同核心。现在的多核CPU大多属于这种情况,每个CPU核心都有独立执行程序的能力,而它们之间也会共享着一些cache;
3)同一NUMA结点上的CPU;
4)不同NUMA结点上的CPU;
进程在两个很亲近的CPU之间迁移,代价较小,因为还有一部分cache可以继续使用;在属于同一NUMA结点上的两个CPU之间迁移,虽然cache会全部丢失,但是内存访问的速度是相同的;如果进程在属于不同NUMA结点的两个CPU之间迁移,那么这个进程将在新NUMA结点的CPU上被执行,却还是要访问旧NUMA结点的内存(进程可以迁移,内存却没法迁移),速度就要慢很多了。
通过调度域的描述,内核就可以知道CPU与CPU的亲缘关系。对于关系远的CPU,尽量少在它们之间迁移进程;而对于关系近的CPU,则可以容忍较多一些的进程迁移。
在开启numa的系统中,一般可以会创建三层调度域:
1)
第一层:同一个物理core的两个超线程 domain0
2)
第二层:同一个numa node上的core domain1
3)
第三层:不同的numa节点 domain2
每个domain包含的cpu数不同,一般domain0包含两个超线程cpu,domain1包含同numa节点上的cpu,domain2则包含所有cpu。当某个cpu在执行load_balance时,按照循序从domain0到domain2一次执行负载均衡操作。
1.2.2.1.3.1 调度域的可配参数
调度域的可配置项在/proc/sys/kernel/sched_domain/cpux/domainx中,每个cpu处于不同的调度域一般在开启了numa和超线程的机器上可以看到3层调度域。可针对每层调度域单独设置。
|
name |
调度域名称 |
|
busy_factor |
用于调整load_balance间隔 |
|
cache_nice_tries |
如果因为cach_hot失败次数超过本调度域的cache_nice_tries, |
|
imbalance_pct |
阀值,超过此值表示存在不均衡 |
|
max_interval |
最大load_balance间隔 (取2*sd_weight) |
|
min_interval |
最小load_balance间隔 |
|
busy_idx |
这些参数中的 busy、forkexec、idle、newidle、wake分别表示5种不同的load_balance时机。 |
|
flags |
调度域的特征(见下面的描述) |
下面四个标志表示某个调度域是否开启对应的load_balance时机
SD_LOAD_BALANCE
SD_BALANCE_NEWIDLE
SD_BALANCE_EXEC
SD_BALANCE_FORK
这三个标志表示调度域的层次
SD_SHARE_CPUPOWER
SD_SHARE_PKG_RESOURCES
SD_NUMA
SD_BALANCE_WAKE、SD_WAKE_AFFINE
在try_to_wake_up中select_task_rq中作为参数传入,配合SD_WAKE_AFFINE,使得挑选cpu时尽量从低层次的sd中选一个idle cpu。
SD_SERIALIZE
在rebalance_domains中参考,这个调度域内只有一个cpu可以做load_balance。
1.2.2.2 Cpuset划分调度域
cgroup cpuset可以动态影响sched_domain的建立。规则如下:
1) 如果有一个cpuset包含了所有cpu,并且该级的cpuset开启了load_balance,那么sched_domain认为所有cpu参与sched_domain的创建。内核会按照ht,core,numa三个层次创建调度域。
2) 如果关闭了包含所有cpu的cpuset的load_balance,只对子集的cpuset开启sched_domain,那么每个子集拥有独立的调度域。
3) 如果两个cpuset子集有重叠的cpu,并且都开启了load_balance,那么系统创建调度域时将把这两个cpuset子集合并对待。
一般在大部分场景中都不会启用cpuset划分调度域的能力。
1.3
调度器sched_features配置
cat /proc/sys/kernel/sched_features可以查看当前调度器开启了哪些特性:
|
GENTLE_FAIR_SLEEPERS |
平滑补偿睡眠进程,睡眠进程唤醒时如果它的vruntime比 |
|
START_DEBIT |
新进程第一次运行时要有延迟;子进程在创建时,vruntime初值首先被设置为min_vruntime; |
|
WAKEUP_PREEMPT |
唤醒进程时检查是否可以去抢占当前运行的进程 |
|
NEXT_BUDDY |
唤醒进程时,是否将被唤醒进程放到调度队列中的下一个 |
|
LAST_BUDDY |
如果发生唤醒抢占,并且当前队列的运行任务超过sched_nr_latency, |
|
CACHE_HOT_BUDDY |
考虑cache热的因素,尽量少的迁移cache热的任务 |
|
ARCH_POWER |
使用arch独立的power功能 |
|
HRTICK |
除了scheduler_tick之外的另一种触发task_tick的方式。 |
|
DOUBLE_TICK |
除了scheduler_tick之外,hrtick是否可以触发check_preempt_tick |
|
LB_BIAS |
影响load_balance时对src_cpu及target_cpu的负载判断, |
|
TTWU_QUEUE |
唤醒远端任务时,将任务插入到远端cpu上的rq(numa) |
|
LB_MIN |
move_task中有效,load_balance是否考虑任务load的下限 |
|
CACHE_HOT_BUDDY |
影响task_hot的判断,cache_hot只针对cfs任务,如果使能了CACHE_HOT_BUDDY,则会去判断,当前cfs_rq中就剩这一个任务,如果就剩这一个那么就是cache_hot;另外,cache_hot还受sysctl_sched_migration_cost有关,如果为-1,则一定cache_hot; |
|
NUMA_FAVOUR_HIGHER |
影响can_migrate_task的判断,如果使能,倾向于不迁移。 |




