1. kworker 不是“进程”,它是内核线程的统称——先破一个普遍误解
很多人第一次在top或htop里看到一堆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去慢慢干。
这和我们熟悉的systemd、sshd、nginx有本质区别:后者是用户空间进程,有自己的虚拟内存空间、文件描述符表、信号处理机制;而kworker共享内核地址空间,运行在ring 0,它的“栈”是内核栈(通常 16KB),它的“生命周期”由内核调度器完全掌控。你用ps -ef | grep kworker看到的 PID,只是内核给这个线程分配的一个标识号,不是传统意义上的进程 ID。/proc/<pid>/status里会明确写着State: S (sleeping)或R (running),但PPid永远是 2(即kthreadd进程,所有内核线程的父进程),Uid和Gid都是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高负载的根源,top和htop只能告诉你“谁在忙”,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.2tps(每秒传输次数)高达 12345!这才是真相:它每秒发出 12345 个小于 4KB 的随机写请求(wkB/s小,tps大,必然小 IO)。这些请求涌向块层,触发大量blk_mq_sched_insert_request,进而生成海量work item提交给kworker去做bio合并、request排队、queue唤醒——kworker就成了永动机。
iostat的tps字段,就是r/s + w/s的精确值,但它默认不显示。而r/s和w/s的单位是“请求次数”,不是“字节数”。一个 512B 的请求和一个 1MB 的请求,在r/s里都算作 1。所以,当业务产生大量小 IO(如日志刷盘、元数据更新、小文件同步),iostat的吞吐量指标(kB/s)会严重失真,而tps和await才是黄金指标。
更进一步,iostat -x的aqu-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这里PID和TID相同,说明是主线程。记下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_rq在drivers/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_restart会schedule_work(&q->run_work)—— 这就是kworker被唤醒的源头!至此,整个链路闭环:硬件中断 →nvme_irq_handler→nvme_process_cq→nvme_complete_rq→nvme_end_request→blk_mq_end_request→schedule_work→kworker执行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_readpage和swap_writepage。更糟的是,swapon启用后,kswapd0(内存回收内核线程)会频繁唤醒kworker去执行try_to_unmap和shrink_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调用会频繁触发kworker的workqueue执行。
验证方法: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_alloc和sock_release流程,这些流程内部会提交work item给kworker清理资源。kworker就成了垃圾回收员,永不停歇。
现象:top里kworker/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-headers | awk '{sum += $1} END {print sum}'` | < 5% | 内核后台任务整体过载 |
| 单个 kworker 最高 CPU | `ps -eo %cpu,comm --sort=-%cpu | grep kworker | head -1 | awk '{print $1}'` |
| tps (IOPS) | `iostat -dx 1 1 | awk '/nvme/ {print $4+$5}'` | < 10000 (NVMe) < 2000 (SATA) | 小 IO 请求洪峰 |
| w_await (ms) | `iostat -dx 1 1 | awk '/nvme/ {print $11}'` | < 1.0 (NVMe) < 10.0 (SATA) | 写请求排队严重 |
| jbd2 线程 CPU | `ps -C jbd2 -o %cpu --no-headers | awk '{sum += $1} END {print sum}'` | < 2% | ext4 journal 压力大 |
| /proc/interrupts 中 NVMe 中断次数 | `grep nvme /proc/interrupts | awk '{sum += $2} END {print sum}'` | 24h 增量 < 1e8 | 硬件或驱动频繁中断 |
| /proc/sys/kernel/sched_latency_ns | cat /proc/sys/kernel/sched_latency_ns | 10000000 (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,就是那扇通往内核心脏的门。