机器大概:这台机器是跑 PHP 的 4C 统计机器,同时可能会有多个进程在进行。
使用 Top 观察如下
%Cpu(s): 46.7 us, 35.6 sy, 0.0 ni, 17.7 id, 0.0 wa, 0.0 hi, 0.1 si, 0.0 st
有一个很明显的现象 sy 占用很高,通常这个值要比较小才好。
Sy 定义: system : time running kernel processes, 表示CPU在执行内核代码占用的cpu时间。
查了下相关的文章,主要都是说上下文切换导致的。原文如下:
如果可运行的线程数大于CPU的数量,那么OS最终会强行换出正在执行的线程,从而使其他线程能够使用CPU。这会引起上下文切换,它会保存当前运行线程的执行上下文,并重建新调入线程的执行上下文。
切换上下文是要付出代价的;线程的调度需要操控OS和JVM中共享的数据结构。你的程序与OS、JVM使用相同的CPU;CPU在JVM和OS的代码花费越多时间,意味着用于你的程序的时间就越少。
但是JVM和OS活动的花费并不是切换上下文开销的唯一来源。当一个新的线程被换入后,它所需要的数据可能不在当前处理器本地的缓存中,所以切换上下文会引起缓存缺失的小恐慌,因此线程在第一次调度的时候会运行得稍慢一些。
即使有很多其他正在等待的线程,调度程序也会为每一个可运行的线程分配一个最小执行时间的定额。就是因为这个原因:它分期偿付切换上下文的开销,获得更多不中断的执行时间,从整体上提高了吞吐量 (以损失响应性为代价)
从上面的回答中提炼了两点:频繁上下文切换和缓存。
频繁上下文切换
如何证明这些进程进行频繁的上下午切换?这些进程导致 CPU sy高时间很短,所以我想怎么看这种瞬间的任务。
然后想到一个工具 sysrq ,它可以观察当前的内存快照、任务快照,可以构造 vmcore 把系统的所有信息都保存下来,甚至还可以在内存紧张的时候用它杀掉内存开销最大的那个进程。
具体使用:
- 设置系统变量 sysctl -w kernel.sysrq=1
- 输出到内核缓冲区 echo t > /proc/sysrq-trigger
- 使用 dmesg 查看快照信息
在 cpu sy高的时候执行步骤 2 即可,但是有时候太快了。单身二十几年的手速都来不及,所以写了一个shell 采集。
#!/bin/sh
while [ 1 ]; do
top -bn2 grep "Cpu(s)" tail -1 awk '{
# $2 is usr, $4 is sys.
# 这个规则可以看自己系统情况调节
if ($2 < 10.0 && $4 > 10.0) {
# 日志文件名生成规则
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("/work/tmp/%s_info.highsys", prefix);
print $2 > sys_usr_file;
print $4 >> sys_usr_file;
# run sysrq
system("echo t > /proc/sysrq-trigger");
}
}'
# 这个时间也可以自己调节
sleep 5s
done
使用 demsg 查看上面输出的快照数据,有些特别的发现。每次执行PHP时候一定会发生下面调用信息:
[73304.409408] php W ffff88023252fba4 0 12374 12369 0x00000080
[73304.409410] ffff88023252fa30 0000000000000086 ffff880232a49700 ffff88023252ffd8
[73304.409412] ffff88023252ffd8 ffff88023252ffd8 ffff880232a49700 ffff88023252fb88
[73304.409414] 00000000039386ff 0000000000000000 0000000000000000 ffff88023252fba4
[73304.409416] Call Trace:
[73304.409418] [<ffffffff8163a909>] schedule+0x29/0x70
[73304.409420] [<ffffffff81639a22>] schedule_hrtimeout_range_clock+0xb2/0x150
[73304.409422] [<ffffffff810a95c0>] ? hrtimer_get_res+0x50/0x50
[73304.409424] [<ffffffff81639a16>] ? schedule_hrtimeout_range_clock+0xa6/0x150
[73304.409426] [<ffffffff81639ad3>] schedule_hrtimeout_range+0x13/0x20
[73304.409428] [<ffffffff811f2bb5>] poll_schedule_timeout+0x55/0xb0
[73304.409430] [<ffffffff811f413d>] do_sys_poll+0x4cd/0x580
[73304.409433] [<ffffffff8163cb5b>] ? _raw_spin_unlock_bh+0x1b/0x40
[73304.409435] [<ffffffff815154a8>] ? release_sock+0x118/0x170
[73304.409437] [<ffffffff815769e6>] ? tcp_sendmsg+0xd6/0xc20
[73304.409439] [<ffffffff811f2ad0>] ? poll_select_copy_remaining+0x150/0x150
[73304.409450] [<ffffffffa01eed4c>] ? xfs_iunlock+0x11c/0x130 [xfs]
[73304.409452] [<ffffffff8116902b>] ? unlock_page+0x2b/0x30
[73304.409454] [<ffffffff81192eb1>] ? __do_fault+0x401/0x510
[73304.409457] [<ffffffff81197088>] ? handle_mm_fault+0x5b8/0xf50
[73304.409459] [<ffffffff81511281>] ? SYSC_sendto+0x121/0x1c0
[73304.409460] [<ffffffff81640ec9>] ? __do_page_fault+0x1f9/0x420
[73304.409463] [<ffffffff81058aaf>] ? kvm_clock_get_cycles+0x1f/0x30
[73304.409465] [<ffffffff810d814c>] ? ktime_get_ts64+0x4c/0xf0
[73304.409467] [<ffffffff811f42f4>] SyS_poll+0x74/0x110
[73304.409469] [<ffffffff81645909>] system_call_fastpath+0x16/0x1b
出现的关键信息解释:
schedule_hrtimeout 使得当前进程休眠指定的时间,使用CLOCK_MONOTONIC计时系统;
schedule_hrtimeout_range 使得当前进程休眠指定的时间范围,使用CLOCK_MONOTONIC计时系统;
schedule_hrtimeout_range_clock 使得当前进程休眠指定的时间范围,可以自行指定计时系统;
…
证明了上面的猜想 频繁上下文切换。
缓存
为了验证 “数据可能不在当前处理器本地的缓存中” 导致频繁的页复制,导致 cpu 瞬间飙升。所以要证明这一点还得继续看上面的日志,果不其然发现了一些可用的信息。
[73304.409471] php R running task 0 12375 12371 0x00000080
[73304.409473] ffff8802328e7f58 0000000000000082 ffff880232a4e780 ffff8802328e7fd8
[73304.409475] ffff8802328e7fd8 ffff8802328e7fd8 ffff880232a4e780 0000000000000000
[73304.409477] 00007ffdf3ab7c98 00000000000000c8 00007ffdf3ab7aa2 0000000000000093
[73304.409479] Call Trace:
[73304.409481] [<ffffffff8163b931>] schedule_user+0x31/0xc0
[73304.409483] [<ffffffff8163d196>] retint_careful+0x14/0x32
[73304.409485] php R running task 0 12376 12373 0x00000080
[73304.409486] 0000000000000001 0000000000000000 ffff8802339aff68 ffffffff8119cc61
[73304.409488] ffff88008ec91800 ffff88008ec91a48 ffff88008ec91800 ffff8802339aff48
[73304.409490] ffffffff816408d9 0000000000000000 0000000000000001 ffffffff8163d438
[73304.409492] Call Trace:
[73304.409494] [<ffffffff816411b3>] ? trace_do_page_fault+0x43/0x110
[73304.409496] [<ffffffff816408d9>] do_async_page_fault+0x29/0xe0
[73304.409498] [<ffffffff8163d438>] async_page_fault+0x28/0x30
程序运行时候频繁的出现缺页异常(虽然是正常的),同时机智想到一个问题。如果内存页越大,那瞬间 cpu 的负载一定会越大。那开启了内存大页机制不就更加凉凉???
果然这台机器是开启的!!!
$ cat /sys/kernel/mm/transparent_hugepage/enabled
always madvise [always]
或者也可以使用 grep -i HugePages_Total /proc/meminfo 查看。
总结
频繁上下文切换很大部分是程序开启的进程或线程远远超过机器 CPU 本身的核心数,检查了业务代码的确是用了 PHP Fock多个子进程。机器本身也开启了内存大页机制,导致瞬间的cpu负载过高从而不断触发报警。
这种情况我的处理办法, 第一 优化代码逻辑。 第二 关闭内存大页机制,内存大页虽好但是不当使用反而是一个负优化。
特别注意:redis 机器最好关闭透明大页,因为写时复制的特性。