蒋彪,腾讯云高级工程师,10+年专注于操作系统相关技术,Linux内核资深发烧友。目前负责腾讯云原生OS的研发,以及OS/虚拟化的性能优化工作。
## 导语
云原生场景,相比于传统的IDC场景,业务更加复杂多样,而原生 Linux kernel 在面对云原生的各种复杂场景时,时常显得有些力不从心。本文基于一个腾讯云原生场景中的一个实际案例,展现针对类似问题的一些排查思路,并希望借此透视Linux kernel的相关底层逻辑以及可能的优化方向。
## 背景
腾讯云客户某关键业务容器所在节点,偶发CPU sys(内核态CPU占用)冲高的问题,导致业务抖动,复现无规律。节点使用内核为upstream 3.x版本。
## 现象
在业务负载正常的情况下,监控可见明显的CPU占用率毛刺,最高可达100%,同时节点load飙升,此时业务会随之出现抖动。
![](https://main.qcloudimg.com/raw/3b77e017c01469f86363f1883f00dcdb.png)
## 捕获数据
### 思路
故障现象为CPU sys冲高,即CPU在内核态持续运行导致,分析思路很简单,需要确认sys冲高时,具体的执行上下文信息,可以是堆栈,也可以是热点。
**难点:**
由于故障出现随机,持续时间比较短(秒级),而且由于是内核态CPU冲高,当故障复现时,常规排查工具无法得到调度运行,登录终端也会hung住(由于无法正常调度),所以常规监控(通常粒度为分钟级)和排查工具均无法及时抓到现场数据。
### 具体操作
#### 秒级监控
通过部署秒级监控(基于atop),在故障复现时能抓到故障发生时的系统级别的上下文信息,示例如下:
![](https://main.qcloudimg.com/raw/ff5d0054f633b2ce910c7fafe601db12.jpg)
从图中我们可以看到如下现象:
1. sys很高,usr比较低
2. 触发了页面回收(PAG行),且非常频繁
3. 比如ps之类的进程普遍内核态CPU使用率较高,而用户态CPU使用率较低,且处于退出状态
至此,抓到了系统级别的上下文信息,可以看到故障当时,系统中正在运行的、CPU占用较高的进程和状态,也有一些系统级别的统计信息,但仍无从知晓故障当时,sys具体消耗在了什么地方,需要通过其他方法/工具继续抓现场。
#### 故障现场
如前面所说,这里说的**现场**,可以是故障当时的瞬时堆栈信息,也可以是热点信息。
对于堆栈的采集,直接能想到的简单方式:
1. pstack
2. cat /proc/<pid>/stack
当然这两种方式都依赖:
1. 故障当时CPU占用高的进程的pid
2. 故障时采集进程能及时执行,并得到及时调度、处理
显然这些对于当前的问题来说,都是难以操作的。
对于热点的采集,最直接的方式就是perf工具,简单、直接、易用。但也存在问题:
1. 开销较大,难以常态化部署;如果常态化部署,采集数据量巨大,解析困难
2. 故障时不能保证能及时触发执行
perf本质上是通过pmu硬件进行周期性采样,实现时采用NMI(x86)进行采样,所以,一旦触发采集,就不会受到调度、中断、软中断等因素的干扰。但由于执行perf命令的动作本身必须是在进程上下文中触发(通过命令行、程序等),所以在故障发生时,由于内核态CPU使用率较高,并不能保证perf命令执行的进程能得到正常调度,从而及时采样。
因此针对此问题的热点采集,必须提前部署(常态化部署)。通过两种方式可解决(缓解)前面提到的开销大和数据解析困难的问题:
1. 降低perf采样频率,通常降低到99次/s,实测对真实业务影响可控
2. Perf数据切片。通过对perf采集的数据按时间段进行切片,结合云监控中的故障时间点(段),可以准确定位到相应的数据片,然后做针对性的统计分析。
具体方法:
采集:
```
`.``/perf` `record -F99 -g -a`
```
分析:
```
#查看header里面的captured on时间,应该表示结束时间,time of last sample最后采集时间戳,单位是秒,可往前追溯现场时间
./perf report --header-only
#根据时间戳索引
./perf report --time start_tsc,end_tsc
```
按此思路,通过提前部署perf工具采集到了一个**现场**,热点分析如下:
![](https://main.qcloudimg.com/raw/1b0d43937b70cf3058b596ffa6b3e8bc.png)
可以看到,主要的热点在于 shrink_dentry_list 中的一把 spinlock。
## 分析
### 现场分析
根据 perf 的结果,我们找到内核中的热点函数 dentry_lru_del,简单看下代码:
```
// dentry_lru_del()函数:
static void dentry_lru_del(struct dentry *dentry) {
if (!list_empty(&dentry->d_lru)) {
spin_lock(&dcache_lru_lock);
__dentry_lru_del(dentry);
spin_unlock(&dcache_lru_lock);
}
}
```
函数中使用到的 spinlock 为 dentry_lru_lock,在3.x内核代码中,这是一把超大锁(全局锁