从Linux perf的数据采集说起

挺长时间了,不太情愿地做了一个给蝙蝠集团之一的consultant,即给所谓“精细化资源管理”做支持,该系统通过一个Linux perf为接口的守护工具,不断向控制节点发送当前系统中每一个应用的细粒度资源使用情况,方便从全局入手为应用程序求得最优分配方案。在这个过程中,A-B测试的结果反映这个守护工具会影响到业务的性能——这是意料之中的,对方的认可的心理底线是性能下降3%以内。可问题是对于某个核心应用的特定场景,这个工具居然导致了30%左右的性能下降,这就尴尬了!

首先,简单看了看守护工具的代码,这家头部字母企业即便是测试的工具,代码质量还是非常不错的:include perf_event.h,中规中矩的定义event什么的,完全无懈可击。查看了多种其他业务的性能影响,几乎都控制到了3%一下,这就让这个关键应用显得更加刺眼。

linux的perf_event.h头文件事实上是调用了内核中的perf模块。作为一个内核模块,Linux的perf事实上是一个涵盖多种功能的大而全工具,而在这个案例中对方仅仅只是用到了perf的counter功能的hardware event的计数器访问而已。也就是下图中右侧PMC(performance monitor counter)部分的event。这块的功能映射到普通的perf命令那就是一句最简单不过的 “perf stat -e <event>,<…> -G <cgroup> -I 1000 ” 。

回来说底层真正用于性能计数的hardware event counter。Perf中这几个硬件计数器的访问需要经由一系列的过程:

  1. 寄存器编程
  2. 读取初始值
  3. 触发事件计数
  4. 读取变化后值
  5. 获得delta

这是一个缺一不可的逻辑链,如果需要监控线程级别的事件就必然遭遇到内核级别的上下文切换。这个5大步操作就会显的非常啰嗦。主要对系统性能的影响来自几个方面:

首先是perf一旦被起用,系统会在每次上下文切换的时候在需要perfile的线程执行之前需要额外执行寄存器编程和读取初始值两个操作;在线程切出之后要执行“读取变化后的值”和“获得delta”部分的操作。这两个过程需要耗费额外的时间开销。

还有软中断,考虑到守护工具有自己固定的统计频率,某些时刻需要通过定时器发送软中断强制打断现有的线程强行进行上下文切换。其实对于高性能、大并发的应用来说特别是已经需要“24小时重症监护”的关键应用来说, 处理上下文切换和各种中断已经称得上是系统最重的额外开销了。

最后,由于硬件计数器的数量有限(尽管Skylake这一代每个HT有4个,而在默认情况下Linux kernel仅允许使用3个),如果需要计数超过4种以上的事件,perf一样还是会采用分时复用的方式轮流监控几个事件——当然这会增加上下文的切换时间和切换次数。下图展示的是不同的event数量带来的上下文切换数量的变化,额外说一句,该主机打通了任督二脉,可以使用4个event counter。而计数4个以上的事件时,上下文切换数量直线上升。

不仅如此,同时由于硬件计数器数量的限制,还会带来额外的误差。下图的实验是同步取样5个相同的事件,理论上应该拿到相同的5个数值,但由于该主机仅支持同时使用3个event counter,那结果就是其中2个事件的返回结果和其他3个完全不一样。

相比之下,对于原本相对消耗不小的数值计算部分的资源消耗反而会显得微不足道且无法避免了。

经过这么一分析,原因差不多定位了。但由于我们始终无法得知该应用的特性,多次询问,只能简单得到几个线索:每个应用实例包含多个Java进程/线程数量远大于CPU的核心数(线程超饱和)、有网络接口的微服务(网络开销和系统调用频繁)、大并发应用(业务简洁但碎片化)、时延敏感型……最后就是该应用以container为运行环境。而守护工具是以container为基本监控单位的。

多进/线程,意味着更多的上下文切换,每次切换都要翻来覆去的执行那“5大步”,不用解释肯定会带来更长的应用程序时延。container的封装又让系统不得不花费额外的时间做多级数据合并统计。加上延敏感型业务意味着应用的性能评价会更多的放大上下文切换时“5大步”和“统计合并”带来的影响。

后来复现实验的结果也带来的另一条“重要信息”证明了我的猜测:同一主机部署的应用数量越多,性能下降的越多。

纯属自己好奇,在系统正常运行时,象征性的让对方执行了下:

perf record -e msr:* -a sleep 10 && perf script | xz -z - > xxx.txt.xz

10秒时间生成了压缩后300M的数据包!捎带秀一把看家的shell操作

[root@localhost perf]# xzcat xxx.txt.xz | awk '{print $5" "$6}' | sort | uniq -c | sort -n -r | head -30 
6879145 msr:write_msr: c0000100, -> FS based 
5430449 msr:write_msr: 38d,  -> write/initial fixed event counter
5430119 msr:read_msr: 38d,   -> read fixed event counter
3268459 msr:write_msr: 38f,  -> write/initial global event counter
2715208 msr:write_msr: 186,  -> write/initial event counter 1 
....
1336535 rdpmc: 40000000      -> fixed controller 0 read by rdpmc
1336514 rdpmc: 1             -> controller 1 read by rdpmc

统计了系统在10秒钟内msr调用的次数,事实上映射回“5大步”都对应了一个或者多个msr调用。居然头部几个读写操作都大几M。
简单的纸面推算是这样的:所有的MSR操作都需要fence操作,一个fence差不多15 cycles加上后续的分支预测逻辑开销40 cycles,以及每次event counter读写消耗的大约70~80 cycles,这一个就是135 cycle。10秒钟时间总共出现了50M+的msr操作,就是6.7G个cycle消耗在监控这件事上,至少等于一个CPU核心的3秒以上时间的工作量。

对于这种情况,我第一次给的建议是对每个container分别隔离,这样的出发点是隔离之后多个应用程序之间的相互CPU资源抢占的概率会减少,上下文切换的概率也会随之降低。不久以后得到的回复是:“性能有所改良,但尚未达到要求。”

继续优化到对方满意为止呗。考虑到CPU已经隔离了,那就直接说服对方放弃进程级别的监控,直接监控CPU级别,且将event控制到4个。此时整个过程就变成了:

  1. 寄存器编程
  2. 读取初始值
  3. 触发事件计数
  4. 读取变化后值1
  5. 获得delta1
  6. 触发事件计数
  7. 读取变化后值2
  8. 获得delta2……

而秒级别的性能监控数据对整个系统的影响就会变得微乎其微了。但这样的话会带来另一个问题:

上图是一个进程级别的监控,TSC是系统的CPU时钟周期数,是系统中最准确的计时器。这是一块2.3GHz的CPU,如果不看结果你会觉得既然sleep了1秒整,那系统肯定会有2.3G个TSC。而实际上对于sleep 1命令,尽管给人的感觉是系统停顿了1秒,但对于Linux kernel来说事实上只需要2.2M个TSC之后就将对应的CPU资源释放出来了。

加了-c参数,perf将直接监控CPU1的TSC而不是之前的sleep进程数据,这个命令已经跟上面一个完全不同了,这才是意义上的1秒钟能有多少个TSC!那结果上2.3G个TSC那是必须的,也自然在两个CPU core参与的时候就成了2.3G*2(当然,在这种粒度下时间是无法做到完全精确的)。这一上一下可是K的数量级。

好在“关键应用”的特点是它几乎没有空闲的时候,即CPU的利用率始终处在高位。加上计算的方法一旦统一之后,事实上用户只是关系数据的微分值而并非数据本身。尽管出乎我的意料,但对方欣然接受了我们的方案。

瞧瞧 所谓“精细化资源管理” 、SLA红线、QoS保障啥的把人都逼成什么样了!

推荐阅读:
事出同事对于某个设备的压力测试
首先是庆祝我们开源小站再次搬家
首先列出本站之前相关的几篇帖子
众所周知的是,CPU的频率和它

发表评论

电子邮件地址不会被公开。 必填项已用*标注

请补全下列算式: *

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据