上下文切换:性能屠夫的解剖报告

0. 一次线上毛刺的追查

那天晚上10点,监控突然报警,p99延迟飙到了500ms,平时也就20ms。登录服务器一看,vmstat的cs列——也就是上下文切换次数——每秒15万次。正常?不过2万。那一刻我就知道,CPU这混蛋被切换淹死了。 上下文切换(context switch),教科书定义我就不写了。你只需要知道:当CPU从一个任务(线程/进程)跳到另一个时,它得保存当前任务的运行状态——寄存器、程序计数器、栈指针——再加载新任务的那一坨。这个过程,快的几微秒,慢的几十微秒。听着很快?积少成多,杀人于无形。
Linux vmstat上下文切换监控截图
Linux vmstat上下文切换监控截图
我见过一个夸张的案例:一台96核的机器,就因为没脑子的线程池开了2000个线程,每次请求来了还得切来切去,结果服务的吞吐量还不如单线程干得漂亮。恶心。 本质就是:CPU在穿针引线,但线头太多,针都秃了。

1. 切换的开销,比你想象中的大

你以为就保存几个寄存器?天真。 首先,直接开销。以x86为例,一次完整的进程上下文切换,涉及:保存通用寄存器、浮点寄存器、控制寄存器,更新内核栈、页表,刷新TLB(Translation Lookaside Buffer)。特别是TLB刷新,那是页表缓存啊,一刷,后续的内存访问全变成慢如蜗牛的内存walk。Intel的文档说,一次TLB flush可能引发几百到上千个CPU周期的延迟。实测中,一次进程切换大约3-5微秒,线程切换(同进程)会好一点,大概1-2微秒,因为共享页表。但别忘了,这是理想情况。 有一次我用perf stat去测一个频繁切换的程序,发现context-switches事件占了60%的CPU时间,还夹杂着TLB-load-missescache-misses。切换带来的缓存污染,才是真正的毒药。你切换出去,把L1、L2、L3缓存全弄脏了,再切回来,全得重新load。现代CPU的主频3GHz起,缓存未命中等个100个周期就够喝一壶了。换算一下:如果每微秒执行3000个周期,一次0.1%的缓存未命中率?实际上高太多了。 我们团队做过一个极限压测:两个线程,纯计算任务,在一个核上跑。如果两个线程绑核且不切换,QPS是3800。如果频繁yield让出CPU,QPS直接掉到1100。这还不是I/O密集,纯计算!切换的代价就是70%的性能蒸发。 怕不怕?
操作系统上下文切换步骤图解
操作系统上下文切换步骤图解
不过话说回来,有些场景切换是免不了的,但我们可以聪明地控制。

2. 落地大坑:三个几乎人人都会踩的陷阱

2. 落地大坑:三个几乎人人都会踩的陷阱
2. 落地大坑:三个几乎人人都会踩的陷阱
陷阱一:线程越多越好? 见过太多新手,以为线程数等于CPU核数乘以2就是最优。错。线程数应该等于CPU可用核数,再根据I/O等待时间调整,但绝不是乱开。线程一多,调度器就累,上下文切换暴增。有个经典案例,一个Tomcat应用,500个线程,高峰期CS到20万/秒,十几台机器扛不住。改成50线程+异步处理,一台机器搞定,CPU idle从2%涨到40%。解决方案:用限流线程池,结合异步非阻塞I/O,比如Netty那一套。如果必须大量线程,考虑协程(coroutine),在用户态切换,那是另一个话题了。 陷阱二:CPU亲和性与忙轮询 有些“优化专家”喜欢用tasksetsched_setaffinity把进程绑在固定核上,以为减少缓存miss。思路没错,但经常忽略:你绑了,别的进程也可能绑到同一个核上,然后就是一场惨烈的争抢。更糟的是,中断(IRQ)也喜欢来凑热闹。网卡中断可能默认就洒在所有核上,包括你精心绑定的那个核。结果呢?你的业务线程频繁被网卡中断打断,上下文切换照样多。解决方案:隔离CPU核心(isolcpus),让调度器不往上面放普通任务;同时用irqbalance把中断绑定到其他核。对于那些时延敏感的程序,甚至可以用忙轮询(busy polling),彻底不让出CPU,但得配合DPDK之类的,避免锁竞争。 陷阱三:抢占与锁的连锁反应 这个东西我吃过亏。一个高并发服务,用了大量的读写锁(rwlock),自认为读多写少,性能卓越。但使用perf lock一看,大量spinlock等锁时间极短,却触发了上下文切换。为啥?因为Linux的mutex在无法获取锁时,会进入睡眠,然后被唤醒,两次上下文切换!读写锁的读锁如果写者等待,后续读者也会被阻塞,连锁反应。解决方案:优先使用CAS(compare-and-swap)原子操作,或者用RCU(Read-Copy-Update),读操作完全无锁,写操作Copy一份更新然后切换指针,美极了。这需要仔细分析数据结构的生命周期,不是银弹。 还有个趣事:某天我们发现一个协程框架的切换性能比原生线程还差。细查:它的实现里,每次切换都要保存浮点寄存器,但根本不用浮点!于是去掉这步,切换开销从90ns降到30ns。对症下药,才能药到病除。
Linux perf火焰图上下文切换开销分析
Linux perf火焰图上下文切换开销分析
说实话,上下文切换不是魔鬼,它是多任务操作系统的基石。但你得像外科医生一样,清楚每一刀的代价。把切换控制在最小范围,让CPU大多时间都在跑你的代码,而不是在内核里梦游。那才算真正的工程师。
免责声明:市场有风险,选择需谨慎!此文仅供参考,不作买卖依据。如有侵权请联系删除。
文章名称:上下文切换:性能屠夫的解剖报告
文章链接:https://lfdjt.com/info_23_7877.html