news 2026/10/16 10:20:45

epoll水平触发与边缘触发:LT和ET模式的区别、选型与踩坑实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
epoll水平触发与边缘触发:LT和ET模式的区别、选型与踩坑实践

1. 从一次线上连接数暴涨说起

凌晨两点,监控告警突然响了:某台接入层服务器的并发连接数在十分钟内从八千飙到六万,CPU的sys占用直接顶到90%以上,业务响应时间从20ms劣化到800ms。登录机器用ss -s一看,大量连接卡在ESTABLISHED状态但迟迟不释放,strace跟下去发现主线程在epoll_wait返回后疯狂空转,每次只处理极少量数据就再次陷入循环。

这个场景我遇到过不止一次,排查到最后,根因往往都指向同一个地方:I/O多路复用的事件触发模式选错了。具体来说,就是把本该用边缘触发(Edge Triggered,ET)的场景写成了水平触发(Level Triggered,LT),或者反过来,在ET模式下漏读了数据导致事件再也不来。

这两个概念听起来像是操作系统课本里的名词,但只要你写过基于epoll、kqueue或者select/poll的网络服务,它们就直接决定了你的程序是稳定运行还是半夜把你叫起来。这篇文章不打算复述手册上的定义,而是从实际工程角度,把LT和ET的差异、各自的适用场景、踩坑点和调优经验一次讲透。无论你是刚接触网络编程的新手,还是已经写过几万行服务端代码的老手,都能从中找到可以直接用的东西。

2. 水平触发与边缘触发的本质差异

2.1 用一个生活场景理解两种模式

先抛开代码,用一个日常场景来类比。

假设你家的信箱有一个提示灯。水平触发的逻辑是:只要信箱里有信,灯就一直亮着,你每次路过都能看到灯亮,直到你把信全部取走,灯才熄灭。边缘触发的逻辑是:只有当信箱从"空"变成"有信"的那一瞬间,灯才闪一下,之后不管你取不取信,灯都不再亮了。如果你没注意到那一下闪烁,或者只取了一部分信,剩下的信就永远躺在信箱里,灯也不会再提醒你。

这个类比基本对应了两种模式在操作系统层面的行为:

  • LT模式:只要文件描述符(fd)上还有未读完的数据(读事件)或还有可写的缓冲区空间(写事件),epoll_wait每次都会返回这个fd,通知你继续处理。
  • ET模式:只有当fd的状态发生变化时(比如从无数据变为有数据),epoll_wait才返回一次。之后如果你不把数据一次性读完,即使缓冲区里还有剩余数据,也不会再收到通知。

理解这个差异是后面所有讨论的基础。很多人觉得ET更"高级"、性能更好,就无脑用ET,结果程序行为诡异,数据丢失、连接挂死,排查半天找不到原因。实际上,两种模式没有绝对的优劣,只有适不适合你的场景。

2.2 内核视角下的事件通知机制

从内核实现的角度看,LT和ET的差异体现在事件就绪队列的管理方式上。

在LT模式下,当一个fd被加入epoll实例并注册了读事件后,只要该fd的接收缓冲区非空,内核在每次epoll_wait调用时都会检查并把它放入就绪链表返回给用户态。这意味着即使你上一次没有处理完,下一次调用仍然会被告知"这个fd还有数据"。

在ET模式下,内核只在fd的状态发生跃迁时(比如从不可读变为可读)才把它加入就绪链表。一旦你通过epoll_wait取走了这个事件,除非fd再次发生状态变化(比如你读空了缓冲区,然后对端又发来新数据),否则内核不会再通知你。

这里有一个容易被忽略的细节:ET模式下,如果你没有一次性把数据读完,剩余的数据不会触发新的事件通知,但它们仍然在缓冲区里。这就像信箱里的信你只取了一部分,灯不再亮,但信还在。如果你不主动再去读,这些数据就会一直滞留,直到连接超时或被对端关闭。

2.3 两种模式的行为对比

为了更直观地对比,我把关键行为整理成一张表:

对比维度水平触发(LT)边缘触发(ET)
事件通知时机fd就绪期间每次epoll_wait都通知仅在fd状态变化时通知一次
未读完数据的处理下次仍会通知不再通知,需自行保证读完
编程复杂度低,可按需读取高,必须循环读到EAGAIN
系统调用次数相对较多相对较少
惊群效应影响较明显相对较轻
适用场景连接数适中、逻辑简单高并发、追求极致性能

这张表里的每一行背后都有实际工程中的考量,后面会逐一展开。这里先记住一个核心结论:ET模式要求你在收到通知后必须把数据读到返回EAGAIN为止,否则就会丢事件。这是ET模式所有坑的根源。

3. 为什么ET模式必须配合非阻塞IO

3.1 阻塞IO在ET模式下的致命问题

很多教程会告诉你"ET模式必须配合非阻塞IO",但很少解释清楚为什么。我用一个具体的例子来说明。

假设你有一个阻塞的socket,注册了ET模式的读事件。某次epoll_wait返回,告诉你这个fd可读。你调用read读取数据,假设对端发来了4KB数据,你的缓冲区也是4KB,一次读完了。看起来没问题。

但如果对端发来了8KB数据呢?你第一次read读走4KB,缓冲区里还剩4KB。由于是阻塞IO,你再次调用read时,如果缓冲区里还有数据,它会立即返回;但如果数据刚好被读完了,read就会阻塞在那里,直到有新数据到来。在ET模式下,你为了确保读完数据,通常会写一个循环反复read直到返回EAGAIN。但阻塞IO永远不会返回EAGAIN,它只会阻塞。于是你的整个事件循环就被卡死在这个read调用上,其他所有连接都得不到处理。

这就是为什么ET模式必须配合非阻塞IO:只有非阻塞IO才能在数据读完后返回EAGAIN,让你知道"已经读干净了,可以退出循环了"。

3.2 非阻塞IO下ET模式的正确读取姿势

正确的ET模式读取逻辑应该是这样的:

// 假设fd已设置为非阻塞 while (1) { ssize_t n = read(fd, buf, sizeof(buf)); if (n > 0) { // 处理读到的n字节数据 process_data(buf, n); } else if (n == 0) { // 对端关闭连接 close_connection(fd); break; } else { if (errno == EAGAIN || errno == EWOULDBLOCK) { // 数据已读完,退出循环 break; } else if (errno == EINTR) { // 被信号中断,继续读 continue; } else { // 其他错误,关闭连接 close_connection(fd); break; } } }

这段代码的关键在于EAGAIN的处理。当read返回-1且errno为EAGAIN时,说明当前缓冲区已经没有数据了,可以安全退出循环。此时如果对端再发来数据,内核会再次触发ET事件通知你。

注意:EAGAIN和EWOULDBLOCK在大多数系统上是同一个值,但为了可移植性,建议两个都判断。

3.3 写事件在ET模式下的处理

读事件的处理逻辑相对直观,写事件在ET模式下则更容易出问题。

LT模式下,只要发送缓冲区有空间,epoll_wait就会一直通知你可写。这意味着你可以只在有数据要发的时候才关注写事件,发完就取消关注,逻辑简单。

ET模式下,写事件只在发送缓冲区从"满"变为"不满"的那一刻触发一次。如果你收到通知后只写了一部分数据,缓冲区又满了,那么下次缓冲区有空间时还会触发。但如果你收到通知后没有写数据,或者写的数据量不足以让缓冲区状态发生变化,就可能错过后续的通知。

实际工程中,ET模式下的写事件处理通常遵循这样的模式:

// 当需要发送数据时 if (output_buffer_has_data()) { // 注册写事件 modify_epoll_event(fd, EPOLLOUT | EPOLLET); } // 写事件回调中 while (output_buffer_has_data()) { ssize_t n = write(fd, output_buffer, output_len); if (n > 0) { // 从缓冲区移除已发送的数据 consume_output_buffer(n); } else if (n == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) { // 发送缓冲区满,等待下次可写事件 break; } else { // 错误处理 close_connection(fd); return; } } // 如果数据全部发送完毕,取消写事件关注 if (!output_buffer_has_data()) { modify_epoll_event(fd, EPOLLIN | EPOLLET); }

这个模式的核心是:只在有数据要发的时候才关注写事件,发完立即取消关注。这样可以避免不必要的epoll_wait唤醒,也是ET模式性能优势的来源之一。

4. LT与ET在实际项目中的选型考量

4.1 性能差异到底有多大

很多人选择ET模式的理由是"性能更好",但这个"更好"到底体现在哪里,需要量化来看。

ET模式相比LT模式,主要的性能优势来自两个方面:

第一,减少了epoll_wait的返回次数。在LT模式下,如果一个连接持续有数据到达,每次epoll_wait都会返回这个fd,即使你上次已经处理过了。在ET模式下,只有状态变化时才返回,减少了用户态和内核态之间的切换开销。

第二,减少了不必要的系统调用。LT模式下,如果你没有一次性读完数据,下次epoll_wait还会通知你,你可能需要多次read调用。ET模式下,你被强制要求一次读完,虽然单次处理的数据量可能更大,但总的系统调用次数可能更少。

但这个优势在高并发场景下才比较明显。根据我在实际项目中的测试,在连接数低于一千、数据吞吐量不大的场景下,LT和ET的性能差异通常在5%以内,甚至因为ET模式需要更复杂的循环读取逻辑,实际性能可能还不如LT。只有当连接数上万、事件通知非常频繁时,ET模式的优势才会显现出来,差距可能达到20%到30%。

4.2 什么场景适合LT

LT模式最大的优势是编程简单、不易出错。你不需要担心数据没读完的问题,因为内核会反复提醒你。这使得LT模式非常适合以下场景:

  • 连接数不多、逻辑复杂的业务服务:比如内部RPC服务、管理后台等,连接数可能就几百个,性能不是瓶颈,开发效率和代码可维护性更重要。
  • 快速原型开发:用LT模式可以先把功能跑通,后续如果需要再优化为ET。
  • 读写逻辑不对称的场景:比如某些协议需要先读一部分数据、解析后再决定读多少,LT模式下可以分多次读取,逻辑更自然。

我个人的经验是,大部分业务服务用LT模式完全够用。真正需要ET模式的,是那些连接数极高、对延迟极其敏感的接入层或网关服务。

4.3 什么场景必须用ET

ET模式在以下场景中有明显优势,甚至可以说是必须的:

  • 高并发接入层:比如单机需要支撑十万以上连接的网关,ET模式减少的事件通知次数能显著降低CPU占用。
  • 需要精确控制读取时机的场景:比如某些协议要求一次性读取完整报文,ET模式强制你循环读取,反而简化了逻辑。
  • 与特定框架或库集成时:有些高性能网络库(如某些基于epoll的C++网络库)默认使用ET模式,如果你要基于它们开发,就必须理解ET的行为。

但即使在这些场景下,我也建议先用LT模式把功能跑通,确认性能瓶颈确实在事件通知上之后,再考虑切换到ET。过早优化是万恶之源,这句话在网络编程里同样适用。

4.4 混合使用的可能性

实际上,epoll允许你对不同的fd使用不同的触发模式。你可以把监听socket设置为LT模式,把连接socket设置为ET模式,或者反过来。这种混合使用在某些场景下能兼顾两者的优势。

比如,一个常见的做法是:监听socket用LT模式,因为新连接到达的频率相对较低,LT模式的额外开销可以忽略,而且LT模式下处理accept更简单,不用担心漏掉连接。连接socket用ET模式,因为连接上的数据读写更频繁,ET模式能减少事件通知次数。

但混合使用也增加了代码的复杂度,需要你对每个fd的模式有清晰的记录和管理。如果没有足够的理由,我建议统一使用一种模式,减少心智负担。

5. 那些年我踩过的ET模式坑

5.1 漏读导致的连接假死

这是ET模式最经典的坑,没有之一。

现象是:客户端明明发送了数据,服务端却像没收到一样,连接既不关闭也不响应,直到超时。用tcpdump抓包能看到数据已经到了服务端的接收缓冲区,但应用层就是没读。

根因通常是这样的:在ET模式下,某次事件通知后,你的读取循环因为某个条件提前退出了,没有读到EAGAIN。比如:

// 错误示例:只读一次就退出 ssize_t n = read(fd, buf, sizeof(buf)); if (n > 0) { process_data(buf, n); } // 没有循环,没有检查EAGAIN

如果这次read没有把缓冲区读空,剩余的数据就再也不会触发事件通知了。连接看起来还活着,但实际上已经"假死"。

正确的做法是必须循环读到EAGAIN,这一点在3.2节已经给出了标准写法。但即使知道这个原则,实际编码中还是容易因为各种原因漏掉,比如在循环中处理业务逻辑时不小心break了,或者错误处理分支没有正确区分EAGAIN和其他错误。

我的经验:在ET模式的读取函数入口处加一个断言或日志,确保每次退出循环时都是因为EAGAIN或连接关闭,而不是其他原因。这个习惯帮我定位过好几次类似问题。

5.2 写事件注册时机不当引发的CPU空转

另一个常见的坑是写事件的注册时机。

在ET模式下,如果你在连接建立后就一直关注写事件(EPOLLOUT),那么只要发送缓冲区有空间,epoll_wait就会不断返回写事件。但你可能根本没有数据要发,于是每次返回后什么都不做,导致CPU空转。

这个问题的根因是:ET模式下,写事件在缓冲区从满变为不满时触发,但如果你一直关注写事件,且缓冲区一直有空间,内核会在每次状态检查时都认为"可写",从而不断通知你。虽然ET模式理论上只在状态变化时通知,但"可写"这个状态在缓冲区有空间时是持续为真的,所以实际上会退化成类似LT的行为。

正确的做法是按需注册写事件:只有在输出缓冲区有数据待发送时,才注册EPOLLOUT;数据发送完毕后,立即取消注册。这样既避免了CPU空转,也符合ET模式的设计意图。

5.3 多线程环境下的ET事件丢失

在多线程环境中使用ET模式,还有一个容易被忽略的问题:事件被一个线程取走后,其他线程再也看不到了。

epoll的就绪事件在epoll_wait返回时就被从就绪链表中移除了(对于ET模式)。如果你有多个线程同时调用epoll_wait,一个事件可能被线程A取走,但线程A因为某种原因没有处理完(比如处理到一半被调度走了),线程B再调用epoll_wait时就不会再看到这个事件。如果线程A最终没有正确处理完数据,这个事件就"丢失"了。

解决这个问题的常见方案是使用EPOLLONESHOT标志。设置了这个标志后,一个fd的事件被取走后,除非你显式地重新注册,否则不会再触发新的事件。这样可以保证同一时刻只有一个线程在处理这个fd,处理完后重新注册,事件不会丢失。

// 注册时加上EPOLLONESHOT event.events = EPOLLIN | EPOLLET | EPOLLONESHOT; epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event); // 处理完数据后重新注册 event.events = EPOLLIN | EPOLLET | EPOLLONESHOT; epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &event);

这个模式在多线程网络服务中非常实用,但要注意重新注册的时机:必须在数据处理完全结束后再重新注册,否则可能出现数据竞争。

5.4 水平触发下的"惊群"与无效唤醒

LT模式也不是没有问题。最常见的是无效唤醒:如果一个连接上有数据但你没有及时读取,epoll_wait会反复返回这个fd,造成大量无效的系统调用。

在多线程场景下,LT模式还容易引发惊群效应:多个线程同时被唤醒去处理同一个事件,但最终只有一个线程能成功处理,其他线程白白消耗CPU。

对于惊群问题,可以通过EPOLLEXCLUSIVE标志来缓解(Linux 4.5+),它保证一个事件只唤醒一个等待的线程。但EPOLLEXCLUSIVE和EPOLLONESHOT不能同时使用,需要根据场景选择。

6. 从代码层面看两种模式的实现细节

6.1 epoll事件注册的差异

在代码层面,LT和ET的差异主要体现在epoll_event的events字段上:

struct epoll_event event; // LT模式(默认,不需要额外标志) event.events = EPOLLIN; event.data.fd = fd; epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event); // ET模式 event.events = EPOLLIN | EPOLLET; event.data.fd = fd; epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);

看起来只是多了一个EPOLLET标志,但行为差异巨大。需要注意的是,EPOLLET是一个文件描述符级别的属性,一旦设置,对该fd的所有事件(读、写、异常)都生效。你不能只对读事件用ET、对写事件用LT。

另外,EPOLLET标志在EPOLL_CTL_MOD时也需要重新设置,否则可能被清除。这一点在动态修改事件关注时容易出错。

6.2 读取循环的边界条件处理

ET模式的读取循环看似简单,但边界条件很多。除了前面提到的EAGAIN,还需要处理:

  • EINTR:系统调用被信号中断,应该继续读取而不是退出。
  • 读取到0字节:表示对端关闭了连接,应该关闭本地fd并清理资源。
  • 部分读取:如果一次read没有读满缓冲区,说明数据可能还没到齐,但在ET模式下你仍然应该继续读直到EAGAIN,因为可能还有后续数据。
  • 缓冲区大小选择:缓冲区太小会导致循环次数增多,太大则浪费内存。通常选择4KB到64KB之间,根据业务特点调整。
#define READ_BUFFER_SIZE 16384 void handle_read(int fd) { char buf[READ_BUFFER_SIZE]; while (1) { ssize_t n = read(fd, buf, sizeof(buf)); if (n > 0) { process_data(fd, buf, n); } else if (n == 0) { close_connection(fd); return; } else { if (errno == EAGAIN || errno == EWOULDBLOCK) { return; // 正常退出 } else if (errno == EINTR) { continue; // 被中断,继续 } else { close_connection(fd); return; } } } }

这段代码看起来简单,但每一行都有其存在的理由。特别是EINTR的处理,在高负载系统中信号可能频繁到来,忽略这个分支会导致连接被错误关闭。

6.3 写缓冲区的管理策略

ET模式下的写操作需要一个用户态的发送缓冲区来配合。基本策略是:

  1. 应用层调用send时,数据先写入用户态缓冲区。
  2. 如果当前没有注册写事件,注册EPOLLOUT。
  3. 写事件触发时,尽可能多地从用户态缓冲区写入内核发送缓冲区。
  4. 如果内核发送缓冲区满(EAGAIN),保留剩余数据,等待下次写事件。
  5. 用户态缓冲区清空后,取消EPOLLOUT关注。

这个策略的关键在于用户态缓冲区的管理。缓冲区需要有动态扩容能力,否则当应用层发送速度超过网络发送速度时,缓冲区会溢出。常见的做法是使用链表或环形缓冲区,并设置一个上限,超过上限时采取背压策略(比如暂停读取对端数据,或者直接关闭连接)。

6.4 错误处理与连接生命周期

ET模式下的错误处理需要格外小心,因为一个未处理的错误可能导致事件永久丢失。

常见的错误码和处理方式:

错误码含义处理方式
EAGAIN/EWOULDBLOCK数据读完/缓冲区满正常退出循环
EINTR被信号中断继续循环
ECONNRESET对端重置连接关闭连接
EPIPE对端已关闭关闭连接,忽略SIGPIPE
ETIMEDOUT连接超时关闭连接
EMFILE/ENFILE文件描述符耗尽紧急处理,关闭空闲连接

特别要注意EPIPE:当向一个已关闭的连接写入数据时,内核会发送SIGPIPE信号,默认行为是终止进程。网络服务必须忽略这个信号(signal(SIGPIPE, SIG_IGN)),否则会因为一个客户端的异常断开而导致整个服务崩溃。

7. 性能调优与监控实践

7.1 如何判断当前模式是否合适

判断LT还是ET是否合适,最直接的方法是监控epoll_wait的返回频率和每次返回的事件数。

如果epoll_wait返回非常频繁,但每次返回的事件数很少(比如平均不到1个),说明存在大量无效唤醒,可能是LT模式下没有及时读取数据,或者ET模式下写事件注册不当。

如果epoll_wait返回频率低,但每次返回的事件数很多,说明事件处理效率高,当前模式比较合适。

我通常会在服务中埋点统计以下指标:

  • epoll_wait调用次数/秒
  • 每次返回的平均事件数
  • 读事件和写事件的触发比例
  • 因EAGAIN退出的读取循环占比

这些指标能帮助快速定位事件处理是否高效。

7.2 从LT迁移到ET的渐进式方案

如果你现有的服务用的是LT模式,想迁移到ET,不建议一次性全量切换。可以采用渐进式方案:

第一步,保持LT模式,但优化读取逻辑,尽量一次性读完数据。这能减少无效唤醒,为后续切换打基础。

第二步,选择非核心连接试点ET模式。比如把某个特定端口的连接改为ET,观察稳定性和性能变化。

第三步,逐步扩大ET模式的范围,同时加强监控和告警,确保出现问题时能快速回滚。

第四步,全量切换后持续观察,重点关注连接异常断开率、数据丢失率和CPU占用变化。

这个过程中,最重要的是保留快速回滚的能力。ET模式的问题往往在特定条件下才暴露,没有充分的观察期就全量切换,风险很大。

7.3 常见监控指标与告警设置

针对I/O多路复用的监控,我建议至少设置以下告警:

  • 连接数突增:短时间内连接数增长超过50%,可能是ET模式漏读导致连接堆积。
  • epoll_wait空转率:如果epoll_wait返回但没有任何事件可处理的比例超过10%,说明事件注册策略有问题。
  • 读取循环异常退出率:如果读取循环因为非EAGAIN原因退出的比例超过1%,可能存在ET模式漏读。
  • 发送缓冲区积压:用户态发送缓冲区持续增长,说明写事件处理不及时。

这些指标不需要很精确,但能帮你快速发现异常。我在实际运维中,就是靠这些指标在问题扩大前及时介入的。

8. 一些容易被忽略的细节

8.1 accept在ET模式下的特殊处理

监听socket在ET模式下有一个特殊之处:新连接到达时,accept可能返回多个连接。如果你只accept一次,剩余的新连接可能不会再次触发事件。

正确的做法是在ET模式下循环accept直到返回EAGAIN:

while (1) { int conn_fd = accept(listen_fd, ...); if (conn_fd == -1) { if (errno == EAGAIN || errno == EWOULDBLOCK) { break; // 所有新连接已处理完 } else if (errno == EINTR) { continue; } else { // 错误处理 break; } } // 处理新连接 setup_connection(conn_fd); }

这个细节在低并发时不容易暴露,但在高并发场景下,如果只accept一次,会导致部分新连接长时间得不到处理,表现为客户端连接超时。

8.2 文件描述符耗尽时的应对

无论是LT还是ET模式,文件描述符耗尽都是一个需要处理的问题。当accept返回EMFILE时,说明进程的fd用完了。

常见的处理策略是预留一个空闲fd:当检测到EMFILE时,关闭预留的fd,腾出一个位置,accept新连接后立即关闭它,然后重新打开预留fd。这样做的目的是给客户端一个明确的连接关闭信号,而不是让它一直等待。

int idle_fd = open("/dev/null", O_RDONLY); // accept返回EMFILE时 if (errno == EMFILE) { close(idle_fd); int conn_fd = accept(listen_fd, ...); if (conn_fd >= 0) { close(conn_fd); // 立即关闭,通知客户端 } idle_fd = open("/dev/null", O_RDONLY); return; }

这个技巧在连接数接近上限时非常有用,能避免服务完全不可用。

8.3 与TCP_NODELAY的配合

ET模式下,由于数据读取更及时,TCP_NODELAY(禁用Nagle算法)的效果会更明显。Nagle算法会合并小包发送,增加延迟,而ET模式本身就是为了降低延迟和减少系统调用,两者配合使用能获得更好的效果。

但TCP_NODELAY也不是万能的。如果你的应用本身就是批量发送数据,禁用Nagle可能导致网络中小包增多,反而降低吞吐量。是否启用需要根据业务特点测试决定。

8.4 跨平台差异

epoll是Linux特有的,其他平台有类似的机制:FreeBSD的kqueue、Windows的IOCP。kqueue也支持ET和LT模式,但API和行为有差异。比如kqueue的EV_CLEAR标志对应ET模式,但事件注册和读取逻辑与epoll不完全相同。

如果你需要跨平台,建议使用成熟的网络库(如libevent、libuv)来屏蔽这些差异,而不是自己直接操作底层API。这些库内部已经处理了各种边界情况,能节省大量调试时间。

9. 写在最后的一些个人体会

关于LT和ET的选择,我这些年最大的体会是:不要为了用ET而用ET。我见过太多项目,连接数不过几千,却非要用ET模式,结果代码复杂度翻倍,bug层出不穷,性能提升却微乎其微。

LT模式就像自动挡汽车,开起来省心,大部分路况都能应付。ET模式像手动挡,操控更精细,但需要你时刻注意转速和档位匹配,稍不留神就熄火。选哪个,取决于你要走的是什么路。

如果你正在写一个连接数在几千以内的业务服务,我建议直接用LT模式,把精力放在业务逻辑上。如果你在写接入层或网关,连接数可能上万,那ET模式值得投入时间去理解和优化。但无论选哪个,充分理解其行为、做好监控、保留回滚能力,这三点比选什么模式更重要。

还有一个实用的小建议:在开发阶段,可以写一个简单的测试工具,模拟大量连接和突发数据,观察你的服务在LT和ET模式下的行为差异。这种实测经验比看多少文档都管用。我自己就是通过反复测试,才真正理解了ET模式下"必须读到EAGAIN"这条规则背后的深意。

最后,如果你在排查ET模式相关的问题,记住一个排查顺序:先确认fd是否非阻塞,再确认读取循环是否读到EAGAIN,然后检查写事件注册时机,最后看多线程环境下是否有事件竞争。按这个顺序走,大部分问题都能定位到。

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

FlinkSQL实时去重:HBase维表+Kafka流的强一致实现

1. 项目概述:为什么FlinkSQL里做HBaseKafka的去重不是“配个DDL”就完事?你是不是也遇到过这种场景:上游业务把用户点击、订单创建、设备上报这些事件源源不断地打到Kafka,每条消息带一个业务主键(比如user_id、order_…

作者头像 李华
网站建设 2026/10/16 10:20:16

MySQL到底能不能存图片?大对象存储的代价与替代方案

做数据库开发这些年,几乎每隔一阵子就会碰到人问:“图片直接存 MySQL 行不行?”问话的有刚入行的新人,也有已经带过几个项目的开发者。理由基本一致:“加个 BLOB 字段就完事,读写都方便,省得再搭…

作者头像 李华
网站建设 2026/10/16 10:19:19

JSP网上书店系统源码解析:从部署到二次开发全指南

简介:这份资源是面向Java Web初学者与课程设计者的网上书店系统完整源码,基于JSP技术构建B/S架构的在线图书查询、购买与管理平台,适合用作毕业设计、课程作业或JSP入门实战参考。压缩包共60个文件,约2.19MB,以asp动态…

作者头像 李华
网站建设 2026/10/16 10:19:17

JSP网上书店源码实战:从跑通到二次开发

简介:这份资源是面向Java Web初学者与课程设计/毕业设计开发者的网上书店系统完整源码,基于JSP技术构建典型B/S三层架构,帮助读者理解从用户浏览、图书查询到购物车、订单管理的全流程实现。压缩包共60个文件,约2.19MB&#xff0c…

作者头像 李华
网站建设 2026/10/16 10:19:16

Deepin圆角自定义全指南:GTK/DDE/Qt三层架构修改实战

1. 项目概述:为什么一个“圆角”值得专门折腾?在桌面环境里,窗口圆角看似只是个微不足道的视觉细节——它不提升性能,不增加功能,甚至不影响交互逻辑。但恰恰是这种“无用之美”,成了用户对操作系统第一印象…

作者头像 李华
网站建设 2026/10/16 10:16:13

C# WinForms TCP通信实战:Socket异步编程与UI线程安全

1. 项目概述:为什么一个“TCP通信窗体应用”值得花两小时认真做一遍C#实现TCP通信——使用VS的Windows窗体应用,这个标题看起来平平无奇,甚至有点老派。但如果你正在带学生做课程设计、接手公司遗留的工业数据采集模块、或者刚从Web后端转岗到…

作者头像 李华