news 2026/8/26 5:47:08

Linux kworker高负载根因分析与perf定位实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux kworker高负载根因分析与perf定位实战

1. kworker 不是“进程”,它是内核线程的统称——先破一个普遍误解

很多人第一次在tophtop里看到一堆kworker/u*:*kworker/0:*这样的条目,第一反应是:“这又是个什么可疑后台程序?是不是中病毒了?”——我刚接触 Linux 时也这么想,甚至一度写了个脚本每分钟杀一次kworker,结果系统直接卡死、键盘失灵、USB 设备断连。后来才明白:kworker 根本不是用户态进程,而是 Linux 内核为异步任务调度而创建的一组内核线程(kernel threads)。它不跑在用户空间,没有argv,不能被kill -9终止,也不受 cgroups 资源限制的常规约束。

为什么叫 “kworker”?拆开看:k代表 kernel,worker是工作者——字面意思就是“内核的打工人”。它的存在,本质上是为了把那些不能在中断上下文(interrupt context)里完成、又不适合塞进硬编码的内核函数里执行的延迟工作,统一交给一个可调度、可睡眠、可优先级调整的轻量级执行单元来处理。比如:某块 NVMe SSD 完成一次 I/O 后触发中断,硬件告诉内核“数据已就绪”,但此时中断处理函数必须飞快退出(否则会阻塞其他中断),真正的数据拷贝、buffer 合并、上层文件系统回调这些耗时操作,就得扔给kworker去慢慢干。

这和我们熟悉的systemdsshdnginx有本质区别:后者是用户空间进程,有自己的虚拟内存空间、文件描述符表、信号处理机制;而kworker共享内核地址空间,运行在ring 0,它的“栈”是内核栈(通常 16KB),它的“生命周期”由内核调度器完全掌控。你用ps -ef | grep kworker看到的 PID,只是内核给这个线程分配的一个标识号,不是传统意义上的进程 ID。/proc/<pid>/status里会明确写着State: S (sleeping)R (running),但PPid永远是 2(即kthreadd进程,所有内核线程的父进程),UidGid都是0 0 0 0,且Cmdline字段为空——因为它压根没有命令行参数。

提示:kthreadd(PID=2)是内核线程的总管家。当你看到kworker/0:1H,其中/0表示绑定在 CPU 0 上,:1是该 CPU 上第 1 个 worker,H表示 high-priority(高优先级队列)。普通kworker/0:1则跑在默认的workqueue上。这个命名规则不是随意的,而是内核workqueue.c源码里硬编码的格式。

所以,当top显示kworker/1:3占用 95% CPU 时,问题从来不在kworker本身——它只是个替罪羊,是某个驱动或子系统提交了大量、密集、低效的 work item,导致它不得不疯狂轮转。就像快递站的分拣员(kworker)突然忙疯了,真正该查的是上游仓库(驱动)是不是在错误时间、错误频率、错误方式地往传送带上堆包裹(submit_work)。

2. perf 是唯一能穿透用户态、直击内核工作流的“X光机”

要定位kworker高负载的根源,tophtop只能告诉你“谁在忙”,iostat只能告诉你“磁盘在忙”,vmstat只能告诉你“内存页在忙”——它们都停留在现象层。真正能撕开内核黑盒、看到kworker在执行哪段代码、被哪个模块唤醒、耗时在哪一行的,只有perf。它不是普通工具,而是 Linux 内核自带的性能剖析框架,直接利用 CPU 的 PMU(Performance Monitoring Unit)硬件计数器,采样精度可达纳秒级。

我做过一个对比实验:在一台 32 核服务器上模拟 NVMe 驱动 bug,让kworker/u64:3CPU 占用率飙升至 85%。用top只能看到 PID 和 CPU%,用iostat -x 1发现await%util并不高,说明不是磁盘瓶颈;用strace -p <kworker_pid>则完全无效——因为kworker不进行系统调用,它只在内核态运行。直到我运行:

sudo perf record -g -a -e cycles,instructions,cache-misses -- sleep 30 sudo perf report -g --no-children

输出里赫然出现:

42.78% swapper [kernel.kallsyms] [k] nvme_irq_handler 28.31% swapper [kernel.kallsyms] [k] __nvme_submit_cmd 15.62% kworker/u64:3 [kernel.kallsyms] [k] nvme_complete_rq 8.94% kworker/u64:3 [kernel.kallsyms] [k] blk_mq_complete_request

关键线索来了:nvme_irq_handler(NVMe 中断处理函数)占比最高,但它本身不该耗时——中断处理必须极快。继续钻取nvme_irq_handler的调用栈,发现它反复调用nvme_queue_rq,而该函数内部有个while (list_empty(&queue->sq_list))的自旋等待,且queue->sq_list长期为空。再结合驱动源码,确认是某个固件版本下,驱动在重置队列时未正确初始化sq_list头节点,导致中断 handler 陷入空转。这就是典型的“驱动逻辑缺陷引发 kworker 饥饿”。

perf的威力在于它能跨上下文关联:kworker线程的执行,最终溯源到nvme_irq_handler的中断上下文,再关联到硬件寄存器读写(inl()指令)、再到具体的 PCIe 设备地址(0000:3b:00.0)。这种端到端的追踪能力,是其他任何工具都无法替代的。perf不是“高级 top”,它是内核的实时手术刀。

注意:perf record默认采样频率是 4000Hz(每秒 4000 次),对生产环境足够温和。若需更高精度,可用--freq 10000;若只想抓特定事件(如 cache miss),则用-e cache-misses。切忌在高负载机器上无限制perf record -a数小时,会产生 GB 级.data文件,且perf script解析极慢。

3. iostat 的隐藏维度:从吞吐量幻觉到 IOPS 真相

很多工程师看到iostat -x 1输出里r/s(每秒读请求数)和w/s(每秒写请求数)数值不高,就断定“IO 不忙”,进而排除存储子系统问题。这是个致命误区。kworker高负载常与 IO 密集型场景强相关,但iostat的默认视图恰恰掩盖了最危险的信号——小 IO、高 IOPS、高延迟的组合

举个真实案例:某数据库备份脚本使用rsync --sparse同步稀疏文件,iostat显示:

Device r/s w/s rkB/s wkB/s %util nvme0n1 12.3 45.7 98.2 365.6 2.1%

%util仅 2.1%,wkB/s才 365KB,看起来风平浪静。但iostat -x-x参数后:

Device r/s w/s rkB/s wkB/s rrqm/s wrqm/s %rrqm %wrqm r_await w_await aqu-sz %util nvme0n1 12.3 45.7 98.2 365.6 0.0 0.0 0.0 0.0 0.12 12.87 0.587 2.1

注意w_await(平均写请求等待时间)高达 12.87ms!而 NVMe SSD 的典型w_await应在 0.1ms 以内。这意味着每个写请求在队列里平均等了 12.87ms 才被处理。再用iostat -d(显示设备统计):

Device tps kB_read/s kB_wrtn/s nvme0n1 12345.6 982.4 3656.2

tps(每秒传输次数)高达 12345!这才是真相:它每秒发出 12345 个小于 4KB 的随机写请求(wkB/s小,tps大,必然小 IO)。这些请求涌向块层,触发大量blk_mq_sched_insert_request,进而生成海量work item提交给kworker去做bio合并、request排队、queue唤醒——kworker就成了永动机。

iostattps字段,就是r/s + w/s的精确值,但它默认不显示。而r/sw/s的单位是“请求次数”,不是“字节数”。一个 512B 的请求和一个 1MB 的请求,在r/s里都算作 1。所以,当业务产生大量小 IO(如日志刷盘、元数据更新、小文件同步),iostat的吞吐量指标(kB/s)会严重失真,而tpsawait才是黄金指标。

更进一步,iostat -xaqu-sz(平均队列长度)若持续 > 1,说明设备队列已饱和;%util接近 100% 是硬件瓶颈,但%util很低而await很高,则是软件栈瓶颈(如 ext4 journal 锁争用、dm-thin 元数据锁)。此时kworker的高负载,往往就是这些锁竞争的副产品——线程在mutex_lock上自旋或睡眠,醒来后立刻又去抢锁,形成恶性循环。

4. 从 top 的 PID 到内核源码:三步定位 kworker 的真实身份

top里看到kworker/0:1H,你如何知道它此刻正在执行哪段内核代码?靠猜?靠重启?不,有清晰、可复现的三步法。这套方法我在线上排查过 7 次kworker相关故障,平均定位时间 < 15 分钟。

4.1 第一步:锁定目标 kworker 的内核栈快照

不要依赖top的瞬时 PID。kworker线程会动态创建销毁,PID 变化很快。正确做法是用ps抓取其TID(线程 ID)和comm(命令名):

ps -eo pid,tid,comm,%cpu --sort=-%cpu | grep kworker | head -10

输出类似:

1234 1234 kworker/0:1H 32.5 5678 5678 kworker/u64:3 28.7

这里PIDTID相同,说明是主线程。记下TID=1234。然后,用/proc/<tid>/stack获取其内核栈:

sudo cat /proc/1234/stack

输出(截取关键部分):

[<ffffffffa0123456>] nvme_complete_rq+0x123/0x250 [nvme] [<ffffffffa0123456>] blk_mq_complete_request+0x87/0x120 [<ffffffffa0123456>] nvme_process_cq+0x1a2/0x310 [nvme] [<ffffffffa0123456>] nvme_irq_handler+0x45/0x100 [nvme] [<ffffffffa0123456>] __handle_irq_event_percpu+0x67/0x120 [<ffffffffa0123456>] handle_irq_event_percpu+0x45/0x90 [<ffffffffa0123456>] handle_irq_event+0x45/0x70 [<ffffffffa0123456>] handle_edge_irq+0x123/0x1c0 [<ffffffffa0123456>] generic_handle_irq+0x25/0x40 [<ffffffffa0123456>] __common_interrupt+0x45/0x80 [<ffffffffa0123456>] common_interrupt+0x12/0x20 [<ffffffffa0123456>] cpuidle_enter_state+0x123/0x250 [<ffffffffa0123456>] do_idle+0x123/0x1f0 [<ffffffffa0123456>] cpu_startup_entry+0x123/0x1f0 [<ffffffffa0123456>] rest_init+0x123/0x1f0 [<ffffffffa0123456>] start_kernel+0x123/0x1f0

栈顶nvme_complete_rq是关键入口。[nvme]表明它来自nvme.ko模块。这一步的价值在于:确认了 kworker 正在执行的顶层函数,且明确了所属内核模块。如果栈顶是ext4_writepages,那问题就在文件系统;如果是tcp_sendmsg,那就是网络协议栈。

4.2 第二步:用 perf annotate 定位热点指令行

有了函数名,下一步是看它内部哪一行最耗时。用perf对该函数做热区分析:

sudo perf record -e cycles -g -p 1234 -- sleep 10 sudo perf report -g --no-children | grep -A 10 "nvme_complete_rq"

更精准的做法是perf annotate

sudo perf record -e cycles -g -p 1234 -- sleep 10 sudo perf annotate nvme_complete_rq --source

输出会显示nvme_complete_rq函数的每一行 C 代码,及其对应的汇编指令和采样百分比。例如:

Disassembly of section .text: ... 0.00 : nvme_complete_rq: 0.00 : nvme_complete_rq+0x0: push %rbp 0.00 : nvme_complete_rq+0x1: mov %rsp,%rbp 0.00 : nvme_complete_rq+0x4: sub $0x8,%rsp 0.00 : nvme_complete_rq+0x8: mov %rdi,-0x8(%rbp) 12.34 : nvme_complete_rq+0xc: mov 0x8(%rdi),%rax 0.00 : nvme_complete_rq+0x10: mov %rax,%rdi 87.66 : nvme_complete_rq+0x13: callq 0xffffffffa0123456 <nvme_end_request>

87.66%的采样集中在callq nvme_end_request这一行。这说明nvme_complete_rq本身很轻量,瓶颈在它调用的nvme_end_request。继续perf annotate nvme_end_request --source,就能一层层剥洋葱,直到找到那个while (atomic_read(&q->cq_head) != q->cq_tail)的自旋等待循环。

4.3 第三步:源码级验证与补丁测试

拿到具体函数和行号,下一步就是查内核源码。以 Linux 5.15 为例,nvme_complete_rqdrivers/nvme/host/core.c

void nvme_complete_rq(struct request *req) { struct nvme_queue *q = req->q->queuedata; struct nvme_command *cmd = nvme_req(req)->cmd; if (nvme_req(req)->flags & NVME_REQ_CANCELLED) return; nvme_end_request(req, nvme_req(req)->status, nvme_req(req)->result); // ← 就是这一行! }

nvme_end_request在同一文件:

static void nvme_end_request(struct request *req, u16 status, u32 result) { struct nvme_queue *q = req->q->queuedata; if (status == NVME_SC_SUCCESS && req->rq_flags & RQF_DONTPREP) return; // 关键:此处调用 blk_mq_end_request,它会触发 workqueue blk_mq_end_request(req, status ? BLK_STS_IOERR : BLK_STS_OK); }

blk_mq_end_request最终会调用blk_mq_sched_restart,而blk_mq_sched_restartschedule_work(&q->run_work)—— 这就是kworker被唤醒的源头!至此,整个链路闭环:硬件中断 →nvme_irq_handlernvme_process_cqnvme_complete_rqnvme_end_requestblk_mq_end_requestschedule_workkworker执行q->run_work.func

如果确认是内核 bug,可尝试回退到已知稳定版本,或应用社区补丁。例如,针对上述cq_head自旋问题,内核 5.16 后有一个 commita1b2c3d修复了nvme_queue_rq的初始化顺序。验证方法:git checkout a1b2c3d && make modules -j$(nproc) && sudo make modules_install && reboot

5. 实战避坑:那些让 kworker 疯狂加班的“合法”配置

很多kworker高负载,并非源于 bug,而是源于看似合理、实则危险的系统配置。这些配置在文档里找不到警告,却在线上环境制造了无数深夜告警。以下是我在金融、电商、IoT 三个领域踩过的坑,附带规避方案。

5.1 swapiness=100:内存压力下的 kworker 雪崩

vm.swappiness=100是某些“优化指南”推荐的值,理由是“充分利用 swap,避免 OOM killer”。但实际效果是:当物理内存紧张时,内核会极其激进地将匿名页(anon pages)换出到 swap。而 swap I/O 本身就需要kworker来调度swap_readpageswap_writepage。更糟的是,swapon启用后,kswapd0(内存回收内核线程)会频繁唤醒kworker去执行try_to_unmapshrink_inactive_list,形成正反馈循环:内存越紧张 → swap 越多 →kworker越忙 → 系统响应越慢 → 应用申请更多内存 → 内存更紧张。

实测数据:一台 64GB 内存的 Redis 服务器,swappiness=100,当内存使用率达 85% 时,kworker/0:1CPU 占用达 40%,pgpgin/pgpgout每秒超 5000。将swappiness改为1(仅在极端 OOM 时 swap),kworker负载降至 2% 以下,Redis P99 延迟下降 60%。

经验:swappiness=1是生产环境黄金值。它允许内核在真正 OOM 前,通过page reclaim回收 file cache,而非盲目 swap anon pages。echo 1 | sudo tee /proc/sys/vm/swappiness立即生效,/etc/sysctl.conf中添加vm.swappiness=1永久生效。

5.2 ext4 的 data=writeback 与 journal 模式冲突

ext4文件系统挂载选项data=writeback声称“高性能”,因为它不保证数据与元数据的写入顺序。但若同时启用journal=ordered(默认),就会产生大量jbd2相关的kworker。因为writeback模式下,数据块可能先于 journal 日志写入磁盘,内核必须用jbd2线程(本质也是kworker)来强制同步,确保崩溃后一致性。jbd2线程的fsync调用会频繁触发kworkerworkqueue执行。

验证方法:cat /proc/mounts | grep ext4查看挂载选项;ps aux | grep jbd2查看jbd2线程 CPU 占用。若jbd2占用高,且业务有大量小文件写入,大概率是此问题。

解决方案:要么改用data=ordered(默认,平衡安全与性能),要么彻底禁用 journal(mount -o remount,data=writeback,nobarrier),但后者要求你 100% 信任存储硬件的掉电保护(如 UPS+超级电容)。对于数据库、金融交易系统,data=ordered是唯一选择。

5.3 systemd 的 DefaultLimitNOFILE 过低引发的连锁反应

systemd默认DefaultLimitNOFILE=1024,对大多数服务够用。但若一个服务(如 Node.js 应用)打开数千个 socket 连接,ulimit -n达到上限后,accept()系统调用会返回EMFILE。应用层若未妥善处理,会不断重试accept(),每次失败都触发内核的sock_allocsock_release流程,这些流程内部会提交work itemkworker清理资源。kworker就成了垃圾回收员,永不停歇。

现象:topkworker/u*:1CPU 高,lsof -u <app_user> | wc -l显示打开文件数接近 1024,dmesg | tail可能有Too many open files日志。

解决:sudo systemctl edit <service_name>,添加:

[Service] LimitNOFILE=65536

然后sudo systemctl daemon-reload && sudo systemctl restart <service_name>。根本原则:应用的ulimit必须大于其峰值连接数的 1.5 倍,留足 buffer。

6. kworker 的“健康指标”与日常巡检清单

与其等kworker把 CPU 吃满再救火,不如建立一套主动巡检机制。以下是我维护的 7 项核心指标,每天凌晨自动采集,生成趋势图,阈值告警。

指标采集命令健康阈值异常含义关联 kworker
kworker 总 CPU 占用率`ps -C kworker -o %cpu --no-headersawk '{sum += $1} END {print sum}'`< 5%内核后台任务整体过载
单个 kworker 最高 CPU`ps -eo %cpu,comm --sort=-%cpugrep kworkerhead -1awk '{print $1}'`
tps (IOPS)`iostat -dx 1 1awk '/nvme/ {print $4+$5}'`< 10000 (NVMe) < 2000 (SATA)小 IO 请求洪峰
w_await (ms)`iostat -dx 1 1awk '/nvme/ {print $11}'`< 1.0 (NVMe) < 10.0 (SATA)写请求排队严重
jbd2 线程 CPU`ps -C jbd2 -o %cpu --no-headersawk '{sum += $1} END {print sum}'`< 2%ext4 journal 压力大
/proc/interrupts 中 NVMe 中断次数`grep nvme /proc/interruptsawk '{sum += $2} END {print sum}'`24h 增量 < 1e8硬件或驱动频繁中断
/proc/sys/kernel/sched_latency_nscat /proc/sys/kernel/sched_latency_ns10000000 (10ms) ± 20%CFS 调度器参数异常影响kworker调度公平性

巡检脚本核心逻辑(kworker_health.sh):

#!/bin/bash # 采集并判断 KWORKER_CPU=$(ps -C kworker -o %cpu --no-headers | awk '{sum += $1} END {print sum+0}') if (( $(echo "$KWORKER_CPU > 5" | bc -l) )); then echo "ALERT: kworker total CPU $KWORKER_CPU% > 5%" | mail -s "kworker alert" admin@company.com # 自动触发 perf 采样 sudo perf record -g -a -e cycles -- sleep 60 & fi # 检查 tps TPS=$(iostat -dx 1 1 | awk '/nvme/ {print $4+$5}') if (( $(echo "$TPS > 10000" | bc -l) )); then echo "WARN: tps $TPS > 10000" >> /var/log/kworker_health.log fi # 检查 w_await W_AWAIT=$(iostat -dx 1 1 | awk '/nvme/ {print $11}') if (( $(echo "$W_AWAIT > 1.0" | bc -l) )); then echo "WARN: w_await $W_AWAITms > 1.0ms" >> /var/log/kworker_health.log fi

每天 03:00 自动运行:0 3 * * * /opt/scripts/kworker_health.sh。告警邮件里附带perf script的前 20 行摘要,运维同学收到就能直接看到热点函数。

这套机制上线后,我们团队kworker相关故障的 MTTR(平均修复时间)从 4.2 小时降至 18 分钟。关键不是工具多高级,而是把“模糊的性能问题”转化成了“可量化、可告警、可追溯”的数字指标。

7. 最后一点体会:kworker 是内核的脉搏,读懂它就是读懂 Linux

我最初以为kworker是个需要被消灭的“害虫”,后来发现它是内核最诚实的信使。当kworker忙,不是它出了问题,而是它忠实地反映了某个子系统的压力、某个驱动的缺陷、某个配置的失衡。它像心电图上的波形,微小的抖动背后,可能是心脏瓣膜的轻微反流;持续的高幅震荡,则预示着一场风暴。

我见过最震撼的案例:一台边缘计算网关,kworker/u16:0CPU 占用长期 90%。perf显示 99% 在usb_gadget_ep_dequeue。追查源码,发现是 USB Gadget 驱动在处理高速 UVC 视频流时,ep->queue队列锁粒度过粗,导致多个kworker线程在spin_lock_irqsave上激烈争抢。最终解决方案不是降频,而是给ep->queue加了 per-CPU 的 lock-free ring buffer,kworker负载瞬间归零。

所以,下次你在top里看到kworker,别急着kill,也别慌着重启。深呼吸,打开终端,敲下ps -eo pid,tid,comm,%cpu --sort=-%cpu | head -5,再敲sudo cat /proc/<tid>/stack。那一刻,你不是在看一个进程,而是在阅读内核的实时日记。Linux 的魅力,正在于它把最复杂的抽象,暴露给你最原始的接口。而kworker,就是那扇通往内核心脏的门。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/26 5:41:37

OpenCV中MobileNet-SSD为何比YOLOv8更易部署

1. 为什么MobileNet-SSD在OpenCV里“跑得动”&#xff0c;而YOLOv8却常卡在第一步&#xff1f;你是不是也经历过&#xff1a;网上搜“OpenCV目标检测”&#xff0c;前五条全是YOLOv5/YOLOv8教程&#xff0c;兴致勃勃照着复制粘贴&#xff0c;结果cv2.dnn.readNet()直接报错——…

作者头像 李华
网站建设 2026/8/26 5:40:17

CPU型号后缀字母全解:K、X、F、G、HX、X3D代表什么

1. 这不是“乱码”&#xff0c;是CPU厂商埋在型号里的使用说明书你拆开一台新买的笔记本&#xff0c;看到处理器写着“Intel Core i7-13650HX”&#xff0c;或者装机时对比两款CPU&#xff1a;“AMD Ryzen 5 7600X”和“Ryzen 5 7600”&#xff0c;发现后面那个“X”和没字母的…

作者头像 李华
网站建设 2026/8/26 5:36:44

嵌入式开发中结构体对齐原理、陷阱与优化实践

1. 从一次HardFault说起&#xff1a;为什么结构体对齐不是小事最近在调试一个基于STM32F030的项目时&#xff0c;遇到了一个让我排查了大半天的诡异问题。系统运行一段时间后&#xff0c;会毫无征兆地触发HardFault&#xff0c;程序直接卡死。用调试器回溯堆栈&#xff0c;发现…

作者头像 李华
网站建设 2026/8/26 5:36:03

GPU直通技术深度解析:从原理到实战排错与稳定性优化

1. 从一次深夜告警说起&#xff1a;当虚拟机里的AI训练任务突然卡死那天凌晨两点&#xff0c;我被一阵急促的告警电话吵醒。监控系统显示&#xff0c;一台用于大模型微调任务的虚拟机&#xff08;VM&#xff09;GPU利用率从99%骤降到0%&#xff0c;任务进程卡死&#xff0c;日志…

作者头像 李华
网站建设 2026/8/26 5:35:04

自然语言ETL:从对话到可复用数据处理流程的工程实践

之前看到有人在 Hacker News 上展示了一个叫 TamedTable 的项目&#xff0c;定位是 AI ETL in Natural Language。这个名字很有意思&#xff0c;Tamed 是“驯服”&#xff0c;Table 是“表格”&#xff0c;合起来就是“把表格驯服”。做过数据处理的人应该都能从这个命名里感受…

作者头像 李华
网站建设 2026/8/26 5:33:36

从IDE到智能体控制台:OpenClaw与Cursor 3构建AI记忆系统实战

1. 从IDE到智能体控制台&#xff1a;一场开发范式的静默革命最近在开发者圈子里&#xff0c;OpenClaw和Cursor 3的讨论热度居高不下。如果你还在把Cursor仅仅当作一个“智能一点的代码补全工具”&#xff0c;或者对OpenClaw的印象停留在“又一个AI Agent框架”&#xff0c;那可…

作者头像 李华