lt模式下epoll_wait持续返回就绪fd导致cpu虚高,et模式仅触发一次但要求非阻塞+循环读写;未设epollet或漏判eagain会使et退化或卡死。

epoll_wait 在 LT 和 ET 模式下返回频率差异直接决定 CPU 占用
LT 模式下,只要 socket 接收缓冲区还有数据,epoll_wait 就会持续返回该 fd 的 EPOLLIN 事件;ET 模式下,仅在数据首次到达、缓冲区从空变非空时触发一次。这意味着:在单次接收 8KB 数据但只 read 1KB 的场景中,LT 可能连续触发 8 次(每次返回同一 fd),而 ET 只触发 1 次——后续必须靠循环 read 直到 EAGAIN 才能清空缓冲区。
压测时若未控制好读写节奏,LT 容易引发“事件抖动”:大量短连接或小包场景下,epoll_wait 返回次数暴增,线程频繁唤醒,CPU 使用率虚高但吞吐未必提升。ET 则把通知压力转嫁给应用层的循环逻辑,系统调用更少,上下文切换更少。
ET 模式必须搭配非阻塞 socket + 循环读写,否则数据会卡在内核缓冲区
这是 ET 最容易踩的坑:设了 EPOLLET 却没把 socket 设为非阻塞,read 到缓冲区末尾时会直接阻塞住,整个事件循环卡死。
- 必须在
accept后立即调用fcntl(fd, F_SETFL, O_NONBLOCK) - 处理
EPOLLIN时,要 while 循环read,直到返回-1且errno == EAGAIN || errno == EWOULDBLOCK - 处理
EPOLLOUT同理:循环write,直到返回-1且错误码匹配 - 忽略
EPOLLRDHUP会导致半关闭连接无法及时发现,建议始终启用
真实压测中 ET 的优势只在高并发+长连接+大包场景下明显
用 wrk 或 go-wrk 对比测试(10K 连接、keep-alive、平均包长 4KB)可观察到:
- LT 模式下,QPS 达到 35K 时,用户态 CPU 占用约 78%,其中约 30% 耗在重复
epoll_wait返回和 fd 遍历上 - ET 模式下,同等条件下 QPS 提升至 42K,用户态 CPU 降至 62%,但代码出错率上升——比如漏掉
EAGAIN判断,就会导致连接 hang 住 - 若压测模型是短连接(HTTP/1.0)或小包(平均 128B),LT 和 ET 的性能差距缩至 5% 以内,ET 反而因循环逻辑增加分支预测失败,L1i 缓存压力略升
epoll_ctl 添加 fd 时忘记 EPOLLET 标志,ET 就退化成 LT
常见错误是只改了 epoll_event.events 字段,却没确认是否真正生效:
-
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event)中event.events必须显式包含EPOLLET,不能依赖默认值 - 如果先用 LT 添加,再用
EPOLL_CTL_MOD改为 ET,部分旧内核(如 3.10 之前)可能不支持运行时切换,需先DEL再ADD - 检查是否生效:用
strace -e epoll_ctl观察系统调用输出,确认events参数含EPOLLET(通常显示为EPOLLIN|EPOLLET) - 误写成
EPOLLLT(多一个 L)这类拼写错误,在编译期不会报错,但运行时等效于没设标志——这就是为什么有些“ET 代码”压测表现和 LT 一模一样
ET 的收益不是白来的:它把内核的“懒通知”转嫁为应用层的“勤处理”。一旦循环逻辑有疏漏,或者忘了设非阻塞,数据就静默滞留在内核 recv 缓冲区里,既不触发新事件,也不报错,排查起来非常隐蔽。











