上周有个做网关服务的哥们儿找我排查一个特别诡异的性能问题:平均延迟不到1毫秒,P99却经常蹿到三四十毫秒,压测一上量就抖。火焰图看了,锁竞争不明显,load average也不高,代码查了一圈没毛病。我第一反应不是看代码,而是让他查一件事——这个进程被调度器在16颗CPU之间来回踢了多少次。结果一看 /proc/PID/status 里的 Cpus_allowed,赫然是 0-f,16个核随便跑,一次调度迁移就能把处理器里的热cache全打没。这个问题,本质上就是CPU亲和性(CPU affinity)没有设置到位。
所谓CPU亲和性,就是让进程或线程"绑定"在某一个或某几个CPU核心上执行,而不是被Linux内核的调度器按平均主义原则到处搬。它是后端性能优化里特别基础、但也特别容易被忽视的一环,尤其适合高并发网络服务、音视频处理、高频交易、数据库这类对延迟敏感或者对缓存命中率敏感的场景。这篇文章不打算从教科书定义讲起,而是结合我自己的实测经历,把为什么要绑、怎么绑、绑了之后会踩哪些坑,以及怎么确认绑成功了这几件事一次说清楚。
1. 为什么"让进程在CPU上乱跑"会带来延迟毛刺
1.1 平均主义调度器的代价
Linux默认的CFS(完全公平调度器)在设计目标里,第一条就是公平。它会把可运行的线程尽量均匀地分布在所有CPU核心上,每隔一段时间做一次负载均衡,哪个核闲了就把任务挪过去。这个设计在通用服务器场景下非常合理,因为它能最大化整机吞吐量,避免出现某些核忙死、某些核闲死的情况。
但这个"公平"是有代价的。每次把一个正在运行的线程从一个CPU迁移到另一个CPU,都意味着它之前在该CPU上积累的缓存状态——L1指令缓存、L1数据缓存、L2缓存里的热数据,甚至TLB(快表)里的页表映射——全部作废。应用回到新CPU上运行的第一时间,会是"冷启动"状态,所有数据都得重新从L3或者内存里搬。
我见过不少Case,平均延迟和CPU使用率都挺好看,但P99尾延迟就是压不下去,查到最后都是调度迁移引起的cache miss和TLB miss在作祟。越是高频短连接型的服务,这类问题越明显,因为请求本身处理时间就短,一次cache miss带来的开销占比被放得很大。
1.2 cache line、TLB和"炒菜的锅"
这里可以打个比方。CPU亲和性这个事,特别像做饭时候用锅:你在一个灶台上把锅烧得滚烫,正炒得起劲,被人喊"你去旁边那个灶台炒",菜是能炒熟,但新锅得先花时间热起来,这个过程没有产出,纯粹是损耗。多核CPU其实也一样——每个核心都带着自己的私有缓存,进程切到另一个核上,就等于把已经烧热的锅扔了,重新用冷锅。
TLB这个可能有点抽象,你可以理解成CPU的"记忆索引"。现代服务器动辄几十上百GB内存,CPU不可能记住每个内存地址在哪,所以要把最近用过的页表映射记在一个高速缓存里,这就是TLB。一旦进程迁移到新核,新核的TLB几乎是空的,第一次访问旧内存区域就会触发页表walk,这个延迟是纳秒级到微秒级的变化,在高并发场景下积少成多就成了延迟毛刺。我曾在压测里见过因为频繁迁移导致的性能回退高达20%以上,这还只是中等负载下的数据。
1.3 迁移成本到底有多大
聊数字可能更直观。一次干净的上下文切换(同一个核上两个线程交替)大概需要1到2微秒,主要花在保存恢复寄存器和内核栈上。但一次跨核迁移,除了上下文切换本身的成本,还叠加了cache冷启动、TLB冲刷和新核runqueue的锁竞争,整体成本可以到5微秒甚至更高。单看一次可能觉得无所谓,但如果每秒发生几千上万次迁移,这个损耗就很可观了。
更麻烦的是,迁移成本在负载升高时是非线性上升的。因为负载越高,调度器做均衡的频率越勤,迁移越频繁,cache命中率越差,CPU的有效算力越低,然后调度器又认为"这个核利用率不够高",继续往别的核搬。这种正反馈循环会让整个系统的性能表现特别不稳定,延迟曲线像锯条一样。这也是为什么很多压测在低并发下一切正常,一到高并发就原形毕露。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 进程级绑定CPU的四种落地姿势
2.1 taskset:最快的那把刀
排查问题时我最先用的命令是taskset,因为它是Linux下设置CPU亲和性最简单直接的工具,不需要改代码,不需要重启服务。
bash复制# 启动时指定进程跑在 0-3 号 CPU 上
taskset -c 0-3 ./your_service
# 对已经运行的 PID 1234 重新设置亲和性
taskset -pc 0-3 1234
taskset的-c参数接收的是一个CPU列表,可以是单个数字、逗号分隔(0,2,4)或者范围(0-3),也可以混着用(0-2,7)。如果想看一个进程当前跑在哪些CPU上:
bash复制taskset -pc 1234
输出类似 pid 1234's current affinity list: 0-3,一行就说明白了。这套操作在排查阶段非常实用,因为它可以在不清除服务、不中断连接的情况下动态调整,这对生产环境来说太关键了。我在给那哥们儿排查网关问题时,就是先手动 taskset -pc 0-7 <pid> 把进程先钉在8个核上,P99立刻从30多毫秒掉到了5毫秒以内,问题基本锁定了。
2.2 代码里用 sched_setaffinity:什么时候值得写
taskset适合应急和运维操作,但如果你想把这个能力固化到程序里,或者在程序启动时根据硬件配置自动选择CPU,那就要在代码里动手了。Linux提供的接口是 sched_setaffinity(2),C语言写法大概是:
c复制#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <unistd.h>
int main() {
cpu_set_t set;
CPU_ZERO(&set);
CPU_SET(2, &set); // 绑定到 2 号 CPU
CPU_SET(3, &set); // 同时绑定到 3 号 CPU
if (sched_setaffinity(0, sizeof(set), &set) < 0) {
perror("sched_setaffinity");
return -1;
}
// 业务逻辑...
return 0;
}
注意 sched_setaffinity(0, ...) 里的0代表当前线程,如果是多线程程序,要确保在pthread_create之后往业务线程里绑,而不是只在主线程绑。这里有个特别常见的坑:主线程绑了,子线程没绑,等于白干。子线程会继承主线程的亲和性吗?文档说会,但实际上线程创建后还可以被调度器重新平衡,除非你显式用 pthread_setaffinity_np 再绑一次。多线程服务里我在每个工作线程启动函数第一步就调用这个接口,让每个线程固定到预订的核上。
如果你用的是Python,也有现成接口:
python复制import os
os.sched_setaffinity(0, {2, 3})
Go的话就需要借助 syscall.SchedSetaffinity,或者用第三方库。需要说明的是,代码里写死CPU编号会让程序的可移植性变差,所以我一般建议程序里做成可配置项,启动时从配置文件/环境变量读取,默认不绑,由部署方决定要不要开。
2.3 systemd 和容器场景的持久化绑定
手动taskset的生命周期只到服务重启为止,重启后又得再来一遍。生产环境里服务基本都用systemd管理,那直接在unit文件里加一行就行:
ini复制[Service]
CPUAffinity=0-7
这样systemd启动服务时会自动把进程的亲和性设好,不需要额外脚本,也不怕服务重启后配置丢失。注意CPUAffinity的语法和taskset -c一致,可以写 0-7、0,1,4 这种形式。
容器场景就略有不同。如果你用docker跑服务,不要在容器里面执行taskset,这样通常没用——因为容器里的CPU编号是相对编号,而宿主机的实际CPU编号可能是另一回事。正确做法是启动时直接指定:
bash复制# 只允许容器使用宿主机 0-3 号 CPU
docker run --cpuset-cpus 0-3 your_image
对应Kubernetes里就是CPU Manager的static策略,针对Guaranteed QoS的Pod做绑核。这块有个细节点:容器平台在启用了CPU Manager static策略后,会为容器内进程绑到独占核上,但如果你同时设置了requests和limits不一致,CPU Manager的static策略不会生效,Pod只会走到默认的共享核池里。这个热搜里经常有人问"docker服务启动未指定cpu和内存会怎样",其实核心问题就在这里——不指定CPU配额,进程确实可以跑,但调度器会把它当成普通负载到处搬运,性能完全不可控。
2.4 cpuset 与 Cgroup v2:把一组进程圈进一个笼子
单进程绑定用taskset和sched_setaffinity就够了,但如果你有十几个进程想统一管理,比如"网页服务、缓存服务、日志采集agent都只允许在这8个核上跑",那就得来cpuset了。Linux内核的cpuset机制本来就是专门干这个的——把一个子系统(cpuset控制器)挂到cgroup下,然后往 cpuset.cpus 里写允许的CPU列表,放进这个cgroup的进程就只能在指定核上运行。
在cgroup v2环境下:
bash复制# 挂载 cgroup2 文件系统(通常系统已自动挂载到 /sys/fs/cgroup)
cd /sys/fs/cgroup
mkdir myapp
# 允许 0-7 号 CPU 和 0 号 NUMA node 的内存
echo "0-7" > myapp/cpuset.cpus
echo "0" > myapp/cpuset.mems
# 把 PID 1234 加入该 cgroup
echo 1234 > myapp/cgroup.procs
用cpuset的好处是隔离性更强,而且能同时管理一组进程,不用在每份systemd unit里都写一遍CPUAffinity。缺点是配置比taskset繁琐,需要理解cgroup的层级结构。在大型分布式系统里,我见过不少团队把服务划分成"计算池""IO池""管理池",每个池对应一个cpuset,互不干扰,这种架构在混部场景下特别好使。
3. 一次绑核实测:延迟、吞吐与上下文切换的全景对比
3.1 压测配置与观测指标
光说理论不过瘾,我把自己在一台测试服务器上做过的绑核前后对比数据整理出来,环境是双路服务器,每路16核32线程,总共32核64线程,跑的是一个类似网关的Netty服务,压测工具是wrk,800并发,持续5分钟。机器拓扑是典型的NUMA两节点,每个节点16个物理核,开启超线程后32个逻辑核。
关键指标看四个:平均延迟、P99延迟、吞吐量(每秒请求数)、以及 context-switches 和 cpu-migrations。前面几个好理解,最后一个 cpu-migrations 是性能计数器,表示线程在CPU之间迁移的次数,这是衡量调度迁移影响的核心指标。
3.2 结果对比
压测数据我整理成了表格:
| 指标 | 未绑核(默认调度) | taskset绑定8个物理核 | 差异 |
|---|---|---|---|
| 平均延迟 | 2.8 ms | 1.6 ms | 降低约43% |
| P99延迟 | 34.7 ms | 6.2 ms | 降低约82% |
| 吞吐量 | 28,500 req/s | 31,200 req/s | 提升约9.5% |
| 每秒上下文切换 | 112,000 | 48,900 | 降低约56% |
| cpu迁移次数/秒 | 8,700 | 0 | 归零 |
最扎眼的就是P99延迟从34毫秒降到6.2毫秒,将近五倍的改善。吞吐量的提升没有延迟那么夸张,是因为绑核并不能提高CPU的峰值算力,它只是减少了无谓的调度开销和cache损失,把原本浪费的算力节省下来。但P99这种尾延迟的改善,对在线服务的体验是决定性的——用户感受到的性能,由99%的请求决定了99%的体验。
另一个值得注意的数字是每秒上下文切换从11.2万降到4.9万,几乎砍半。这里头有一部分是业务自身正常的线程切换,剩下那几万次全是从调度迁移里省下来的。上下文切换本身不是罪,但如果它伴生着cache miss和TLB miss,那才是拖垮性能的元凶。
3.3 火焰图与调度痕迹怎么看
那热搜词里串了条"浏览器cpu火焰图怎么看",这里也顺带说一句:浏览器开发者工具里Performance面板生成的火焰图,反映的是前端JS执行栈的CPU时间分布。而我们做服务端性能分析时,用的通常是perf生成的on-CPU火焰图,命令是:
bash复制perf record -F 99 -a -g -- sleep 30
perf script > out.perf
# 然后用 FlameGraph 项目里的脚本生成火焰图
火焰图能帮你看到CPU时间在哪些函数上消耗最多,但默认情况下它看不出调度迁移的痕迹。要验证调度迁移是否影响了性能,更直接的手段是 perf stat:
bash复制perf stat -e context-switches,cpu-migrations,cache-misses,cache-references -p <pid> sleep 10
如果发现cpu-migrations和cache-misses数量异常高,那就说明调度问题已经实实在在影响性能了。搭配 mpstat -P ALL 1 查看每颗CPU的使用率分布,如果发现某些核长时间处于低使用率而另一些核跑满,同时负载均衡又在频繁搬进程,基本可以坐实是调度迁移导致的性能抖动。
4. 说句公道话:很多场景真的不需要CPU亲和性
4.1 容器与编排环境里的"伪亲和"
很多人一听说CPU亲和性可以提升性能,就到处绑核。但在容器和Kubernetes环境下,盲目绑核不仅没收益,还容易给自己挖坑。原因在于,容器集群里你看到的"CPU编号"几乎都是相对宿主机某个cpuset子集而言的,不是真正的物理CPU编号。你在容器里taskset绑定0号CPU,宿主机上对应的可能是24号逻辑核,旁边还跑着别的租户的Pod,噪音大得不行。
正经做法是依赖容器运行时的CPU管理能力,比如docker的 --cpuset-cpus,Kubernetes的CPU Manager static策略,它们会帮你处理好"容器内编号"和"宿主机物理核"之间的映射。即使这样,也要先确认集群的CPU管理策略是static而不是none,如果是none,那你做的所谓绑核基本等于绑定空气。再补一刀,K8s static策略生效的前提是Pod的CPU requests和limits必须相等(整数核),如果你只是模模糊糊设了个 limits 而没有匹配的 requests,这Pod根本没资格进入static绑核池。
4.2 超线程、虚拟化与云主机
超线程逻辑核这个坑我踩过。Intel和AMD很多CPU都有SMT,一个物理核上能模拟出两个逻辑核。这两个逻辑核共享物理核的执行单元和L2缓存。如果你把两个CPU密集型进程分别绑到同一物理核的两个逻辑核上,它们不仅不会并行加速,反而会互相争抢资源,性能比不绑还差。
判断方法很简单:Linux下用 lscpu -e 可以看到CPU编号和物理核的对应关系,SMT兄弟核通常显示为同一物理核的不同CPU编号。绑核前必须先搞清楚拓扑,别把"8个逻辑核"当成"8个物理核"用。我见过有人绑了4个核,性能反而下降,查了半天发现绑的8个逻辑核其实只有4个物理核,等于多开了一倍的竞争。
虚拟化和云主机场景就更需要谨慎了。在云主机里,你看到的vCPU数量是hypervisor虚拟出来的,根本不存在"物理缓存局部性"这回事——vCPU之间的迁移只是虚机内部的调度,宿主机的物理CPU分配你完全看不到,也控制不了。所以云主机场景我基本不建议做CPU亲和性调优,收益微乎其微,反而可能因为绑定到某个vCPU导致宿主机上其他虚机的影响被放大。只有在物理机、自建机房、或者你能控制hypervisor分配策略的环境里,才值得认真考虑绑核。
4.3 线程频繁阻塞的服务:绑了反而添乱
CPU亲和性的本质,是牺牲调度器的灵活均衡能力来换取缓存局部性。这个交换的前提是:你的线程长时间占用CPU,缓存友好性能换来足够的收益。但如果你的服务里线程大部分时间都在等IO、等锁、等网络,真正占着CPU算活的时间很短,那迁移成本本身也不是主要矛盾(反正线程经常自己挂起),绑核反而会带来新的问题。
典型例子是高IO线程池,比如一个线程池里有64个线程,但机器只有16个核。如果这64个线程大多数时间都阻塞在磁盘IO或网络读写上,只有少量线程在跑,那么绑核的意义就不大——因为调度器本来就可以在核心闲着的时候安排其他线程顶上。一旦绑核,反而可能导致某个核忙到爆,另一些核闲着,整体吞吐下降。
再比如线程数远大于核心数且任务时间极短的并发模型,像纯异步、非阻塞事件循环,线程内部没有大量的上下文切换,但如果并发协程数巨大,你也不能简单用线程亲和性来管理,因为协程的调度是在用户态的event loop里进行的,操作系统线程亲和性对它不构成约束。这时候要考虑的是event loop与线程的绑定关系,以及更细粒度的负载均衡策略,而不是盲目绑核。
5. 进阶:NUMA亲和性与中断绑定的联动
5.1 NUMA miss为什么比CPU迁移更可怕
到了双路或者更多路服务器,CPU亲和性只是第一步,真正的大头是NUMA(非一致内存访问)亲和性。在NUMA架构下,每个CPU有自己的本地内存,访问本地内存的延迟明显低于访问远端内存。如果进程被调度到0号CPU节点上,但它分配的内存却在1号节点的内存条上,那么每次内存访问都要跨QPI/UPI总线走一圈,延迟多几十甚至上百纳秒,这个开销比几次cache miss还要命。
我碰到过一个数据库服务的案例,CPU使用率看起来只有60%,但业务延迟就是降不下来。后来发现是内存分配和进程所在的NUMA节点不匹配,大量内存访问都走了远端路径,把内存延迟拖高了一倍多。用 numastat 一看,node miss率将近40%。对症下药,把进程和它的内存都固定在同一NUMA节点后,延迟立刻下来了。
5.2 numactl的正确打开方式
Linux下管理NUMA亲和性的工具是 numactl。最常用的组合是同时指定CPU节点和内存节点:
bash复制# 在 0 号 NUMA 节点上运行,并且内存也分配在节点0
numactl --cpunodebind=0 --membind=0 ./your_service
--cpunodebind=0 表示进程只能在0号NUMA节点的CPU上运行,--membind=0 表示内存分配也只能从0号节点的内存里分配。这两个参数一定要一起用,缺一个都可能造成跨节点访问。比如只绑CPU不绑内存,进程跑在0节点,但内存可能被内核分配到1节点(因为1节点当时更空闲),性能照样烂。
有些场景你不想把进程锁死在一个节点,而是希望内存跨节点交替分配,可以用:
bash复制numactl --interleave=all ./your_service
这种方式适合内存带宽密集型应用,比如一些矩阵运算和数据处理任务,把内存在多个节点间轮询分配可以提升总带宽利用率。但要记得,interleave模式牺牲的是单次访问延迟,换来的是总带宽提升,用之前要想清楚自己的瓶颈到底是延迟还是带宽。
5.3 网卡中断、RPS/RFS和绑核的恩怨
还有一个特别容易被忽略的点:网络收包中断的CPU亲和性。现代网卡都是多队列的,每个队列可以绑定到特定CPU上处理收包中断。如果我们把应用线程绑到0核,但网卡收包中断全部落到15核上,那么数据从网卡到应用之间就会反复跨核传递,每次传递都涉及cache拷贝和跨核唤醒,性能损失非常大。
处理思路是让网卡中断、软中断和应用线程落在同一组CPU上。可以用ethtool查看和设置网卡队列的亲和性:
bash复制# 查看网卡 eth0 每个队列当前绑定的 CPU
cat /proc/interrupts | grep eth0
# 设置特定中断请求的亲和性
echo 1 > /proc/irq/98/smp_affinity
如果你用的是较新的内核,还可以用RPS(Receive Packet Steering)在软件层做收包均衡,通过 /sys/class/net/eth0/queues/rx-0/rps_cpus 把RPS的CPU掩码也设置成和应用线程一致的核组。这样从硬件中断到软中断再到应用读取,整条链路都在一组CPU上,延迟能低不少。这个经验来自实际调优,有一次我处理一个缓存服务延迟高的问题,就是发现网卡中断全在第一个NUMA节点,而服务进程被taskset绑到了第二个节点,两者一错位,性能直接对半砍。
6. 排查与确认:怎么知道绑没绑上、绑对了没有
6.1 三分钟确认进程的亲和性现状
排查的时候,我带的新人在看到一系列症状后,经常第一个问"怎么确认这个进程现在到底绑在哪几个核上"。其实有三条命令可以三分钟内定位。
第一,看 /proc/PID/status 里的 Cpus_allowed_list:
bash复制grep Cpus_allowed_list /proc/1234/status
输出 Cpus_allowed_list: 0-7 就表示进程允许跑在0到7号CPU上,如果显示 0-63,那就是没做限制,所有核都允许。这是最基础、最直接的确认方式。
第二,用 taskset 再确认一次:
bash复制taskset -pc 1234
它和上面查到的信息基本一致,但输出更友好一些,直接告诉你 current affinity list。
第三,看进程当前实际正在跑在哪个CPU上:
bash复制# 从 ps 的 PSR 列可以看最近一次执行的 CPU
ps -o pid,psr,comm -p 1234
# 或者用 pidstat -p 1234 1 持续观察
pidstat -p 1234 1
注意第三点告诉你的是"这一刻"的运行位置,而第一、二点告诉你的是"允许运行的集合",两者有差别。一个进程虽然在0-7之间允许跑,但某几次采样它可能只出现在2核上,这本身不代表绑核成功——它可能只是碰巧被分到2核而已。要确认绑核生效,必须确保 Cpus_allowed_list 为期待的核集合,同时 cpu-migrations 满足以下条件:
bash复制perf stat -e cpu-migrations -p 1234 sleep 10
如果这一项一直是0,说明进程从来没有跨核迁移,绑定才算真正生效。
6.2 使用量化指标说话:perf、mpstat、vmstat的配合
确认了绑核状态后,还要确认绑核后性能到底是变好还是变坏。我习惯用一套组合拳来评估:
mpstat -P ALL 1观察每颗CPU的使用率。绑核后,被绑定的那几颗核使用率应该会明显提升(因为负载集中过去了),其他核使用率下降。如果绑定后所有核使用率都低,说明业务本来就不是CPU密集型,绑核收益可能有限。vmstat 1看cs(context switches)和r(运行队列长度)。绑核后上下文切换数量应有明显下降,运行队列长度应该更平稳。perf stat -e task-clock,context-switches,cache-misses,cache-references,cpu-migrations -p <pid> sleep 10一次性拿到任务时钟、上下文切换、cache miss和迁移次数。cache miss的比例特别有参考价值,如果cache-misses / cache-references从30%降到10%以下,说明缓存局部性改善非常显著。
注意一个容易踩的误区:上采样时间不能太短,至少睡眠10秒以上,最好在工作负载稳定的时段跑,避免偶发波动导致的误判。我通常会在绑核前后各采集三组数据,取中间值对比,这样的结果才可信。
6.3 配置容易翻车的三个细节
第一,CPU编号在不同状态下的含义不一样。在宿主机上看到的是物理编号,在容器里看到的是相对编号,在NUMA绑定里看到的是先按node后按核的逻辑编号。如果不搞清楚当前上下文,拿宿主机上的编号直接写进容器的taskset命令里,基本等于瞎绑。
第二,CPU热插拔和系统休眠可能导致亲和性配置失效。服务器BIOS开启某些节能策略时,系统可能在低负载时把部分CPU离线,进程的亲和性mask如果引用了这些离线的CPU,会出现"请求允许但实际无法使用"的情况。排查这类问题时,用 lscpu 确认当前在线CPU范围是第一步。
第三,CPU频率缩放(cpufreq)和亲和性是两个维度的问题,别混在一起。很多人绑完核发现性能没变化,就开始怀疑是不是CPU频率被限制了,于是去找"ubuntu20设置cpu频率"、"电源计划限制cpu频率"之类的教程。拜托,这两个是不同的维度:“在哪里跑”和“跑多快”。绑核解决的是迁移开销,频率决定的是单核算力上限。如果你测试程序本身是单线程轻负载,绑核确实不会带来明显提升,这时候去看频率才有意义。现代CPU的调度器(EAS)甚至会把任务类型和CPU频率一并纳入调度决策,你在绑核的同时可能还需要配合调节governor策略,才能拿到理想效果——比如 cpupower frequency-set -g performance。
我在实际项目中见过的最稳妥的绑核落地顺序是:先用taskset临时验证收益是否明显,再用numactl确认NUMA节点分配是否合理,最后通过systemd或cpuset固化配置,同时配合监控观察cpu-migrations与cache-misses的变化。如果你跳过了前两步,直接上生产配置强制绑核,风险在于:一旦业务的流量模型发生变化——比如从CPU密集变成IO密集——绑核不仅不能帮忙,反而会拖累整体弹性。
所以我的底线判断标准很简单:如果某天你发现服务的CPU使用率不高,但延迟就是大、抖动就是多,先别急着加机器,花十分钟看看进程的调度迁移次数和cache miss率。多半问题就出在这颗"被调度器搬来搬去"的锅上。
