gitbook/Linux内核技术实战课/docs/292060.md
2022-09-03 22:05:03 +08:00

194 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 18 案例篇 | 业务是否需要使用透明大页:水可载舟,亦可覆舟?
你好,我是邵亚方。
我们这节课的案例来自于我在多年以前帮助业务团队分析的一个稳定性问题。当时业务团队反映说他们有一些服务器的CPU利用率会异常飙高然后很快就能恢复并且持续的时间不长大概几秒到几分钟从监控图上可以看到它像一些毛刺。
因为这类问题是普遍存在的所以我就把该问题的定位分析过程分享给你希望你以后遇到CPU利用率飙高的问题时知道该如何一步步地分析。
CPU利用率是一个很笼统的概念在遇到CPU利用率飙高的问题时我们需要看看CPU到底在忙哪类事情比如说CPU是在忙着处理中断、等待I/O、执行内核函数还是在执行用户函数这个时候就需要我们细化CPU利用率的监控因为监控这些细化的指标对我们分析问题很有帮助。
## 细化CPU利用率监控
这里我们以常用的top命令为例来看看CPU更加细化的利用率指标不同版本的top命令显示可能会略有不同
%Cpu(s): 12.5 us, 0.0 sy, 0.0 ni, 87.4 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
top命令显示了us、sy、ni、id、wa、hi、si和st这几个指标这几个指标之和为100。那你可能会有疑问细化CPU利用率指标的监控会不会带来明显的额外开销答案是不会的因为CPU利用率监控通常是去解析/proc/stat文件而这些文件中就包含了这些细化的指标。
我们继续来看下上述几个指标的具体含义,这些含义你也可以从[top手册](https://man7.org/linux/man-pages/man1/top.1.html)里来查看:
```
us, user : time running un-niced user processes
sy, system : time running kernel processes
ni, nice : time running niced user processes
id, idle : time spent in the kernel idle handler
wa, IO-wait : time waiting for I/O completion
hi : time spent servicing hardware interrupts
si : time spent servicing software interrupts
st : time stolen from this vm by the hypervisor
```
上述指标的具体含义以及注意事项如下:
![](https://static001.geekbang.org/resource/image/37/a5/3756d973a1f7f350bf600c9438f1a4a5.jpg)
在上面这几项中idle和wait是CPU不工作的时间其余的项都是CPU工作的时间。idle和wait的主要区别是idle是CPU无事可做而wait则是CPU想做事却做不了。你也可以将wait理解为是一类特殊的idle即该CPU上有至少一个线程阻塞在I/O时的idle。
而我们通过对CPU利用率的细化监控发现案例中的CPU利用率飙高是由sys利用率变高导致的也就是说sys利用率会忽然飙高一下比如在usr低于30%的情况下sys会高于15%,持续几秒后又恢复正常。
所以接下来我们就需要抓取sys利用率飙高的现场。
## 抓取sys利用率飙高现场
我们在前面讲到CPU的sys利用率高说明内核函数执行花费了太多的时间所以我们需要采集CPU在sys飙高的瞬间所执行的内核函数。采集内核函数的方法有很多比如
* 通过perf可以采集CPU的热点看看sys利用率高时哪些内核耗时的CPU利用率高
* 通过perf的call-graph功能可以查看具体的调用栈信息也就是线程是从什么路径上执行下来的
* 通过perf的annotate功能可以追踪到线程是在内核函数的哪些语句上比较耗时
* 通过ftrace的function-graph功能可以查看这些内核函数的具体耗时以及在哪个路径上耗时最大。
不过,这些常用的追踪方式在这种瞬间消失的问题上是不太适用的,因为它们更加适合采集一个时间段内的信息。
那么针对这种瞬时的状态我希望有一个系统快照把当前CPU正在做的工作记录下来然后我们就可以结合内核源码分析为什么sys利用率会高了。
有一个工具就可以很好地追踪这种系统瞬时状态即系统快照它就是sysrq。sysrq是我经常用来分析内核问题的工具用它可以观察当前的内存快照、任务快照可以构造vmcore把系统的所有信息都保存下来甚至还可以在内存紧张的时候用它杀掉内存开销最大的那个进程。sysrq可以说是分析很多疑难问题的利器。
要想用sysrq来分析问题首先需要使能sysyrq。我建议你将sysrq的所有功能都使能你无需担心会有什么额外开销而且这也没有什么风险。使能方式如下
> $ sysctl -w kernel.sysrq = 1
sysrq的功能被使能后你可以使用它的-t选项把当前的任务快照保存下来看看系统中都有哪些任务以及这些任务都在干什么。使用方式如下
> $ echo t > /proc/sysrq-trigger
然后任务快照就会被打印到内核缓冲区这些任务快照信息你可以通过dmesg命令来查看
> $ dmesg
当时我为了抓取这种瞬时的状态,写了一个脚本来采集,如下就是一个简单的脚本示例:
```
#!/bin/sh
while [ 1 ]; do
top -bn2 | grep "Cpu(s)" | tail -1 | awk '{
# $2 is usr, $4 is sys.
if ($2 < 30.0 && $4 > 15.0) {
# save the current usr and sys into a tmp file
while ("date" | getline date) {
split(date, str, " ");
prefix=sprintf("%s_%s_%s_%s", str[2],str[3], str[4], str[5]);
}
sys_usr_file=sprintf("/tmp/%s_info.highsys", prefix);
print $2 > sys_usr_file;
print $4 >> sys_usr_file;
# run sysrq
system("echo t > /proc/sysrq-trigger");
}
}'
sleep 1m
done
```
这个脚本会检测sys利用率高于15%同时usr较低的情况也就是说检测CPU是否在内核里花费了太多时间。如果出现这种情况就会运行sysrq来保存当前任务快照。你可以发现这个脚本设置的是1分钟执行一次之所以这么做是因为不想引起很大的性能开销而且当时业务团队里有几台机器差不多是一天出现两三次这种状况有些机器每次可以持续几分钟所以这已经足够了。不过如果你遇到的问题出现的频率更低持续时间更短那就需要更加精确的方法了。
## 透明大页:水可载舟,亦可覆舟?
我们把脚本部署好后就把问题现场抓取出来了。从dmesg输出的信息中我们发现处于R状态的线程都在进行compcation内存规整线程的调用栈如下所示这是一个比较古老的内核版本为2.6.32
```
java R running task 0 144305 144271 0x00000080
ffff88096393d788 0000000000000086 ffff88096393d7b8 ffffffff81060b13
ffff88096393d738 ffffea003968ce50 000000000000000e ffff880caa713040
ffff8801688b0638 ffff88096393dfd8 000000000000fbc8 ffff8801688b0640
Call Trace:
[<ffffffff81060b13>] ? perf_event_task_sched_out+0x33/0x70
[<ffffffff8100bb8e>] ? apic_timer_interrupt+0xe/0x20
[<ffffffff810686da>] __cond_resched+0x2a/0x40
[<ffffffff81528300>] _cond_resched+0x30/0x40
[<ffffffff81169505>] compact_checklock_irqsave+0x65/0xd0
[<ffffffff81169862>] compaction_alloc+0x202/0x460
[<ffffffff811748d8>] ? buffer_migrate_page+0xe8/0x130
[<ffffffff81174b4a>] migrate_pages+0xaa/0x480
[<ffffffff81169660>] ? compaction_alloc+0x0/0x460
[<ffffffff8116a1a1>] compact_zone+0x581/0x950
[<ffffffff8116a81c>] compact_zone_order+0xac/0x100
[<ffffffff8116a951>] try_to_compact_pages+0xe1/0x120
[<ffffffff8112f1ba>] __alloc_pages_direct_compact+0xda/0x1b0
[<ffffffff8112f80b>] __alloc_pages_nodemask+0x57b/0x8d0
[<ffffffff81167b9a>] alloc_pages_vma+0x9a/0x150
[<ffffffff8118337d>] do_huge_pmd_anonymous_page+0x14d/0x3b0
[<ffffffff8152a116>] ? rwsem_down_read_failed+0x26/0x30
[<ffffffff8114b350>] handle_mm_fault+0x2f0/0x300
[<ffffffff810ae950>] ? wake_futex+0x40/0x60
[<ffffffff8104a8d8>] __do_page_fault+0x138/0x480
[<ffffffff810097cc>] ? __switch_to+0x1ac/0x320
[<ffffffff81527910>] ? thread_return+0x4e/0x76e
[<ffffffff8152d45e>] do_page_fault+0x3e/0xa0
[<ffffffff8152a815>] page_fault+0x25/0x30
```
从该调用栈我们可以看出此时这个java线程在申请THPdo\_huge\_pmd\_anonymous\_page。THP就是透明大页它是一个2M的连续物理内存。但是因为这个时候物理内存中已经没有连续2M的内存空间了所以触发了direct compaction直接内存规整内存规整的过程可以用下图来表示
![](https://static001.geekbang.org/resource/image/db/51/db981eb703a88ae85458618355789251.jpg "THP Compaction")
这个过程并不复杂在进行compcation时线程会从前往后扫描已使用的movable page然后从后往前扫描free page扫描结束后会把这些movable page给迁移到free page里最终规整出一个2M的连续物理内存这样THP就可以成功申请内存了。
direct compaction这个过程是很耗时的而且在2.6.32版本的内核上,该过程需要持有粗粒度的锁,所以在运行过程中线程还可能会主动检查(\_cond\_resched是否有其他更高优先级的任务需要执行。如果有的话就会让其他线程先执行这便进一步加剧了它的执行耗时。这也就是sys利用率飙高的原因。关于这些你也都可以从内核源码的注释来看到
```
/*
* Compaction requires the taking of some coarse locks that are potentially
* very heavily contended. Check if the process needs to be scheduled or
* if the lock is contended. For async compaction, back out in the event
* if contention is severe. For sync compaction, schedule.
* ...
*/
```
在我们找到了原因之后为了快速解决生产环境上的这些问题我们就把该业务服务器上的THP关掉了关闭后系统变得很稳定再也没有出现过sys利用率飙高的问题。关闭THP可以使用下面这个命令
> $ echo never > /sys/kernel/mm/transparent\_hugepage/enabled
关闭了生产环境上的THP后我们又在线下测试环境中评估了THP对该业务的性能影响我们发现THP并不能给该业务带来明显的性能提升即使是在内存不紧张、不会触发内存规整的情况下。这也引起了我的思考**THP究竟适合什么样的业务呢**
这就要从THP的目的来说起了。我们长话短说THP的目的是用一个页表项来映射更大的内存大页这样可以减少Page Fault因为需要的页数少了。当然这也会提升TLB命中率因为需要的页表项也少了。如果进程要访问的数据都在这个大页中那么这个大页就会很热会被缓存在Cache中。而大页对应的页表项也会出现在TLB中从上一讲的存储层次我们可以知道这有助于性能提升。但是反过来假设应用程序的数据局部性比较差它在短时间内要访问的数据很随机地位于不同的大页上那么大页的优势就会消失。
因此我们基于大页给业务做性能优化的时候首先要评估业务的数据局部性尽量把业务的热点数据聚合在一起以便于充分享受大页的优势。以我在华为任职期间所做的大页性能优化为例我们将业务的热点数据聚合在一起然后将这些热点数据分配到大页上再与不使用大页的情况相比最终发现这可以带来20%以上的性能提升。对于TLB较小的架构比如MIPS这种架构它可以带来50%以上的性能提升。当然了,我们在这个过程中也对内核的大页代码做了很多优化,这里就不展开说了。
针对THP的使用我在这里给你几点建议
* 不要将/sys/kernel/mm/transparent\_hugepage/enabled配置为always你可以将它配置为madvise。如果你不清楚该如何来配置那就将它配置为never
* 如果你想要用THP优化业务最好可以让业务以madvise的方式来使用大页即通过修改业务代码来指定特定数据使用THP因为业务更熟悉自己的数据流
* 很多时候修改业务代码会很麻烦如果你不想修改业务代码的话那就去优化THP的内核代码吧。
好了,这节课就讲到这里。
## 课堂总结
我们来回顾一下这节课的要点:
* 细化CPU利用率监控在CPU利用率高时你需要查看具体是哪一项指标比较高
* sysrq是分析内核态CPU利用率高的利器也是分析很多内核疑难问题的利器你需要去了解如何使用它
* THP可以给业务带来性能提升但也可能会给业务带来严重的稳定性问题你最好以madvise的方式使用它。如果你不清楚如何使用它那就把它关闭。
## 课后作业
我们这节课的作业有三种,你可以根据自己的情况进行选择:
* 如果你是应用开发者请问如何来观察系统中分配了多少THP
* 如果你是初级内核开发者请问在进行compaction时哪些页可以被迁移哪些不可以被迁移
* 如果你是高级内核开发者假设现在让你来设计让程序的代码段也可以使用hugetlbfs那你觉得应该要做什么
欢迎你在留言区与我讨论。
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。