资讯动态

Python epoll高并发网络编程:原理、代码与生产实践全解

发布时间:2026/10/14 14:56:42 来源:尧图企业网站定制
开头直接抛结论epoll是Linux上高并发网络编程绕不开的东西Python要做高并发网络服务九成场景最后都会落到它身上。这套东西我前后折腾过不少次从最早的select模型一路改写过来踩过很多坑也实测验证过不少东西。这篇文章把核心原理、完整代码、生产维护经验一次讲透适合刚接触网络编程的人也适合已经会用但想搞明白内部机制的人。1. 高并发场景下select和poll卡在了哪里1.1 从连接数增长说起网上很多教程一上来就贴epoll代码但很少讲清楚一个关键问题好端端的select模型为什么在高并发下扛不住我先说结论select和poll的问题不在于它们慢而在于它们的工作方式——每次调用都要把全部文件描述符从头到尾检查一遍用户态和内核态之间还要来回拷贝这份fd列表。我最早用Python写一个简单的聊天室服务端连接数到三百左右还行往一千往上走就不对劲了。CPU占用居高不下大量时间不是在处理业务而是在反复遍历fd、检查状态。你用strace去看会发现select调用本身消耗极小但调用前后那一大堆FD_SET、FD_ZERO的拷贝和遍历开销才是吞掉性能的黑洞。这里面有个很反直觉的点select的开销跟活跃连接数无关跟总连接数强相关。哪怕一秒钟只有十个连接有数据只要总连接数是一万select每次还是要遍历这一万个fd。1.2 select的三座大山把select的瓶颈拆开看主要是三个层面。第一层是fd数量限制。FD_SETSIZE默认1024虽然可以改宏重编内核但生产环境通常不允许你为了一个应用去改内核参数。更麻烦的是Python的select模块底层逻辑其实也是包装系统调用数量上去了照样受约束。第二层是O(n)扫描。fd集合被当作一个位图内核不知道哪些fd可读可写只能一个一个去轮询设备、检查状态。连接数越大这个O(n)的n越大时间线性增长。第三层是用户态与内核态之间反复拷贝。select/poll每次调用前你得把要监视的fd集合拷进内核返回后又要从内核拷出来再在用户态遍历一遍找出就绪的fd。一来一回内存拷贝和上下文切换的次数随调用频率上涨。poll解决了fd数量限制从位图变成了链表但O(n)扫描和来回拷贝的毛病一个没改。说白了poll只是把1024的上限拿掉了性能模型没有任何本质变化。1.3 为什么越多连接就越慢的模型不可接受一台普通服务器单进程能接受的连接数通常在几万级以上。要支撑这个量级每次调用都做全量扫描的做法注定走不远。有一个实际场景可以说明问题我做某推送网关时连接数到了两万左右用poll写的业务进程流量低谷时还好一到上行消息密集的时候CPU直接冲到百分之八十以上磁盘和网络都没瓶颈就是CPU在硬扛遍历。反观epoll就绪检测的复杂度直接从O(n)变成了O(1)级别的通知机制——不是你去问内核谁有数据了而是内核主动告诉你谁有数据了。这个差异在高连接数场景下是数量级的差距。2. 拆开epoll看内部机制红黑树、就绪链表和回调2.1 epoll的三个系统调用对应了什么数据结构epoll在Linux内核里对应三样东西一个eventpoll对象、一棵红黑树、一个就绪链表。epoll_create创建一个eventpoll实例内核里分配一个文件对象这个对象里就带着红黑树的根节点和就绪链表的头节点。红黑树用来存你注册的所有fd就绪链表用来放当前有事件发生的fd。epoll_ctl是往红黑树上增删改节点。ADD操作就是在红黑树里插入一个新的节点DEL就是删除MOD就是更新节点上挂的事件类型。操作完成的同时内核还会为这个fd建立一个回调——当fd对应的设备有事件发生时自动把这个fd塞进就绪链表。epoll_wait做的事情简单粗暴检查就绪链表如果非空就把链表里的元素拷到用户态然后清空链表。如果链表是空的当前进程就睡过去一直到某个fd的事件发生唤醒进程来取。这里我多说一句为啥要红黑树而不是哈希表或普通链表。epoll经常有动态增删fd的需求连接建立要ADD连接断开要DEL红黑树在动态插入、删除、查找场景下都能维持对数级别的稳定性能不会像哈希表那样要处理冲突也不会像普通链表那样查找退化成O(n)。2.2 事件通知机制避免了无效轮询select/poll是你问我答epoll是你等我喊。内核在设备驱动层维护了一个等待队列当你调用epoll_ctl注册fd时内核给这个fd挂上了一个epoll回调函数。一旦网卡收到数据、socket缓冲区状态变化驱动层就会调用这个回调把fd塞进就绪链表并唤醒正在epoll_wait上睡眠的进程。这套机制的直接效果是无论总连接数是多少每次epoll_wait返回时你拿到的就是一批真正有事可做的fd不用再遍历无意义的连接。连接数从一万涨到十万、二十万只要同时就绪的fd数量不变单次事件循环的开销基本就不变。2.3 为什么说epoll突破了两万个fd的限制很多人知道select有1024限制但不知道为什么。select的fd集合用固定大小的位图表示位图大小由FD_SETSIZE决定默认1024。poll改成了链表没有这个硬限制但每次要全量拷贝。epoll则完全没有集合上限的概念。红黑树能存多少节点取决于内存大小而不是编译期常量。所以在Linux上单进程打开的文件描述符上限只受两个因素约束ulimit -n的普通进程限制和fs.file-max的系统全局限制。我实际调参的经验是单进程支撑十万连接时把ulimit -n调到1048576就足够了不需要动内核参数。用Python跑epoll时只要注意文件描述符不被其他地方耗尽epoll本身不是瓶颈。3. Python里用epoll的正确姿势3.1 标准库select模块下的epollPython没有独立的epoll模块它内置在标准库select里。select.epoll提供了和Linux系统调用几乎一一对应的接口epoll.create()创建epoll实例对应epoll_createepoll.register()注册fd和事件掩码对应epoll_ctl(ADD)epoll.modify()修改fd的事件掩码对应epoll_ctl(MOD)epoll.unregister()注销fd对应epoll_ctl(DEL)epoll.poll(timeout)等待事件对应epoll_wait用的时候注意一点register一个已经在epoll实例里的fd会报FileExistsErrormodify一个不存在的fd会报FileNotFoundError。处理连接关闭的时候要记得在合适时机unregister否则fd重用后可能触发幽灵事件。一个更隐蔽的问题epoll实例本身也占用一个fd用完了一定要close()否则每次创建epoll实例就会漏一个fd。别小看这个泄漏长期运行的服务早晚被它坑到。3.2 事件掩码EPOLLIN、EPOLLOUT和其他epoll的事件掩码常用的有这几个事件含义EPOLLIN对端有数据到达可以读取EPOLLOUTsocket发送缓冲区可写EPOLLERRfd发生错误无需手动注册epoll自动监听EPOLLHUP对端挂断无需手动注册epoll自动监听EPOLLET设置为边缘触发模式默认是水平触发EPOLLONESHOT事件最多触发一次之后自动注销新手最容易搞混的就是EPOLLOUT的语义。EPOLLOUT不只是对端可写它的本质是本地socket发送缓冲区有空间。如果对方一直不收数据发送缓冲区满了EPOLLOUT就不会再触发。所以生产代码里千万不要长期注册EPOLLOUT事件否则一个慢速客户端能把你的CPU烧干——缓冲区永远满不了但也不空状态频繁变化事件风暴就来了。通常的做法是需要写数据时临时注册EPOLLOUT写完就注销或者干脆只在写缓冲区满的时候才注册。EPOLLHUP和EPOLLERR也不用手动加内核会自动登记。判断连接异常不能光看EPOLLIN要检查revents里有没有EPOLLHUP或EPOLLERR配合getsockopt(SO_ERROR)才能拿到具体的错误码。3.3 水平触发与边缘触发的选择epoll默认是水平触发LT需要主动声明EPOLLET才是边缘触发ET。这两种模式区别从名字上就看得出来LT只要fd还有数据没读完epoll_wait每次都会返回这个fd。ET只有fd状态发生变化的那一刻会通知一次。数据没读完不管了下次不通知了除非有新数据到达。从业务角度看LT的优点是不容易漏事件缺点是可能对同一个fd反复唤醒。ET的优点是事件通知次数少但你必须把当前缓冲区里的数据一次性读完读到EAGAIN为止。读不干净就等着丢数据。我在自己的项目里绝大多数场景都用LT。很多人网上吹ET说性能高但ET对代码的要求是必须读到EAGAIN一旦处理不当轻则数据积压重则触发饥饿。ET适合那种每个fd都有单独线程或协程在不停读数据的场景配合非阻塞IO才能真正榨干性能。在Python这种GIL环境下用LT是最稳的。有些框架比如早期的eventlet直接用LT也是一种务实选择。有个细节必须说ET模式下如果某个连接一直不读数据发送端把缓冲区写满你的EPOLLOUT可能一直不触发然后发送端等你的假事件等死。这种问题排查特别费时间。我在某次压测时遇到过最后发现是ET模式下注册了EPOLLOUT但没处理EAGAIN导致的。4. 手写一个高并发echo服务器4.1 事件循环的整体结构先放一个完整的、可以直接运行的代码框架。这个例子实现了一个echo服务端客户端发什么服务端原样返回什么只不过用epoll来管理所有客户端连接。import socket import select def set_nonblocking(sock): sock.setblocking(False) def main(): # 创建监听socket server socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind((0.0.0.0, 9000)) server.listen(4096) set_nonblocking(server) # 创建epoll实例 epoll select.epoll() epoll.register(server.fileno(), select.EPOLLIN) # fd - 对应的socket对象 connections {} # fd - 待发送数据的缓冲区 write_backlog {} try: while True: # 阻塞等待事件最多等待5秒 events epoll.poll(timeout5) for fd, event in events: if fd server.fileno(): # 新连接到达 while True: try: conn, addr server.accept() except BlockingIOError: break set_nonblocking(conn) epoll.register(conn.fileno(), select.EPOLLIN) connections[conn.fileno()] conn write_backlog[conn.fileno()] b else: conn connections.get(fd) if conn is None: continue # 异常或挂断 if event (select.EPOLLHUP | select.EPOLLERR): close_conn(epoll, connections, write_backlog, fd) continue # 可读事件 if event select.EPOLLIN: data safe_recv(conn) if data is None: close_conn(epoll, connections, write_backlog, fd) continue if data: # 这里先把数据放到写缓冲区不直接发送 write_backlog[fd] data # 可写事件 if event select.EPOLLOUT: buf write_backlog.get(fd, b) if buf: try: sent conn.send(buf) except (BlockingIOError, InterruptedError): continue except OSError: close_conn(epoll, connections, write_backlog, fd) continue remaining buf[sent:] if remaining: write_backlog[fd] remaining else: # 数据全部发完不再监听EPOLLOUT write_backlog[fd] b epoll.modify(fd, select.EPOLLIN) else: epoll.modify(fd, select.EPOLLIN) finally: epoll.close() server.close() def safe_recv(conn): try: data conn.recv(65536) if not data: return None return data except (BlockingIOError, InterruptedError): return b except OSError: return None def close_conn(epoll, connections, write_backlog, fd): epoll.unregister(fd) conn connections.pop(fd, None) write_backlog.pop(fd, None) if conn: try: conn.close() except OSError: pass if __name__ __main__: main()这段代码不是玩具核心的事件循环结构可以直接演进成生产框架。有几个地方是反复琢磨后定的第一个是accept循环用了while True配合BlockingIOError跳出。监听socket在非阻塞模式下多路复用事件到达时可能同时有多个连接等着accept。一次只accept一个其他连接就留在内核队列里等下一次事件这种情况在高并发下会拖慢新连接建立速度。第二个是recv长度我用了65536这是recv在Linux上比较合适的缓冲区大小。设太大会浪费用户态内存设太小会导致syscall次数过多。如果单次recv可能收到多个应用层报文解析协议时需要自己处理粘包。第三个是写缓冲区。这里我故意没有直接send而是先把数据攒到内存里等EPOLLOUT事件再真正发送。原因是非阻塞socket的send在缓冲区满时会立刻返回EAGAIN如果此时恰好又有新数据要发很容易出现发送顺序错乱。统一走写缓冲区数据顺序就有保证。4.2 为什么监听socket要单独注册注意上面代码里监听的server fd和客户端fd在同一个epoll实例里但处理逻辑分开了。if fd server.fileno()这个判断是整个事件循环的分流点。这样做的好处是新连接和已有连接的事件在同一个循环里统一处理不用开第二个线程去accept。多线程accept还需要处理惊群问题虽然Linux 2.6之后有SO_REUSEPORT可以缓解但单线程epoll循环天然就没有这个问题。如果连接数特别大比如十万级以上单线程的accept也能扛得住瓶颈通常不在这里。实测下来accept一秒几万次完全没问题真正的瓶颈在协议处理和业务逻辑。4.3 用wrk或telnet快速验证写完代码先不急着上wrk最简单的验证方式是用Python自带方式import socket s socket.create_connection((127.0.0.1, 9000)) s.sendall(bhello epoll\n) print(s.recv(1024)) s.close()能收到hello epoll就说明基础功能正常。压测用wrk这种工具注意wrk默认是HTTP协议需要写个简单的Lua脚本改成裸TCP回显测试。用ab也行但ab的并发模型比较老压TCP服务不太合适。更常用的还是自己写多线程客户端脚本模拟。我看很多人在这一步偷懒觉得能跑通就行了。真到了线上出问题压测数据和理论值能帮你快速定位瓶颈在哪儿所以有条件最好还是提前跑一轮# 服务端跑起来之后观察进程状态、CPU占用 top -H -p $(pgrep -f echo_server.py)看一下单进程能支撑多少并发连接、事件循环在空闲时的CPU占用率心里有个基准线。我的经验是纯echo服务单进程Python epoll随便就能到几万并发CPU占用主要花在Python字节码执行上。瓶颈从来不是epoll而是业务代码本身。5. 生产环境里真正要踩平的坑5.1 EAGAIN不是错误是你的退出条件新手从阻塞IO转过来最不适应的就是处理EAGAIN。在非阻塞模式下一个接一个的send、recv动不动就抛BlockingIOError。很多人一看到异常就慌了直接断开连接。别把EAGAIN当错误它是非阻塞IO的正常反馈。读数据读到BlockingIOError说明缓冲区已经空了读完收工写数据写到BlockingIOError说明缓冲区满了等下一次EPOLLOUT。真正的错误是ECONNRESET、ETIMEDOUT这些。判断异常有个简单办法在网络IO的异常处理里凡是BlockingIOError和InterruptedError一律continue其他OSError子类再走关闭分支。5.2 数据到达不代表读一次就够水平触发模式下每次EPOLLIN事件最多读一次还是循环读到EAGAIN这里存在选择。每次读一次然后重新进入事件循环水平触发会立刻再次返回EPOLLIN直到数据读完。这样对CPU有额外消耗但代码结构简单。循环读到EAGAIN能减少syscall和事件循环调度次数但如果处理速度跟不上可能出现某个大包长期霸占CPU。我混用的思路是中小数据量场景每次读一次就好简单可靠。大流量、数据包较大的场景循环读到EAGAIN但每轮循环里加一个最大读取次数限制比如读64次就退出把控制权还给事件循环防止单一fd饿死其他fd。# 边缘触发模式必须循环读读到EAGAIN为止 while True: try: data conn.recv(65536) if not data: close_conn() break handle_data(data) except BlockingIOError: break这段代码的执行路径决定了合理调度。在实际部署中如果不加次数限制偶尔出现一个恶意客户端疯狂发数据你的单线程事件循环就全耗在它身上了其他连接都得不到响应。这属于典型的未考虑攻击面问题。5.3 慢客户端跟EPOLLOUT的爱恨情仇前面提过EPOLLOUT要慎用。这里展开讲一下什么时候才需要它。常规的echo逻辑收到数据立刻send通常都能发出去因为发送缓冲区足够大。只有对方不读、把你的发送缓冲区塞满时send才会返回EAGAIN。这时候就得注册EPOLLOUT等缓冲区有空间了再继续发。代码里我维护了一个write_backlog字典专门存待发送的数据。EPOLLOUT事件出现时从这个字典里取数据send。全部发完再modify回只监听EPOLLIN。这里有一个很常见的bug数据发完了忘了把EPOLLOUT注销结果缓冲区一有空闲就触发EPOLLOUT事件而你又没有新数据可发白白忙活一圈。用epoll.modify(fd, EPOLLIN)就能解决不要用register来覆盖register会报文件已存在错误。5.4 fd泄漏这场迟早要还的债跑长连接服务的都懂一段时间后连接数上不去新连接建不起来查了半天发现是fd耗尽报Too many open files。epoll模式下fd泄漏的主要来源是连接关闭时没有正确unregister和close。尤其是读到空数据、对端FIN后连接对象没被清理epoll实例里还留着这个fd。Linux下没关的fd不会自动回收下次accept拿到的fd号只会越来越大直到撑满上限。正确顺序是这样epoll.unregister(fd)在先确保epoll不再监听然后connections字典里删除最后conn.close()释放内核资源。顺序反了会有竞态极端情况下新连接复用了同一个fd旧连接的close反而把新连接给关了。我建议用一个字典来保存fd到对象的映射关闭连接时先pop再unregisterdef close_conn(epoll, connections, write_backlog, fd): epoll.unregister(fd) conn connections.pop(fd, None) write_backlog.pop(fd, None) if conn: try: conn.close() except OSError: pass这套代码我在生产里跑过很久没有出现过fd泄漏。5.5 惊群问题与多进程策略有人会问单进程epoll撑不住怎么办常见答案是多进程epoll。多进程模型下每个进程都有自己的epoll实例各自监听自己的fd。如果几个进程共享同一个监听socketAccept时就会产生惊群——一个连接到达多个进程同时被唤醒最后只有一个accept成功其他进程白醒。Linux 2.6.18之后内核解决了accept惊群但epoll_wait的惊群依然存在。比较好的方案是每个进程建一个自己的监听socket用SO_REUSEPORT让内核做负载均衡。SO_REUSEPORT允许多个socket绑定同一个地址端口内核根据四元组哈希分发连接天然避免惊群。Python的fork在多个进程共享epoll实例时有个隐患父进程创建epoll实例后再fork子进程会继承这个fd不小心在一个子进程里close其他子进程拿到的就是无效fd。稳妥做法是先fork再在每个子进程里单独创建epoll实例不要共享。5.6 GIL和事件循环的关系Python的GIL常被拿来说事。实际上GIL对epoll的影响没有想象中大。epoll.poll在等待事件时是阻塞在系统调用上的不占用GIL。事件循环的主要耗时在Python字节码执行和业务处理上这部分受GIL限制单进程只能用一个核。所以Python做高并发网络服务的标准姿势是多进程每进程独立事件循环或者把CPU密集任务丢到进程池/子进程里跑。我做消息推送服务时用了主进程epoll循环消息派发到worker进程池的架构。主进程只负责网络IO和协议解析业务处理丢给worker。压测下来的数据比纯单进程高了几倍。这个架构在代码层面并不复杂核心就是主循环收到完整消息后通过multiprocessing队列或管道转发给worker。另一个实践是事件循环里不要做任何阻塞操作。文件读写、数据库查询、耗时计算一律异步化或放线程池。事件循环一旦阻塞整个服务的所有连接都会卡住这是异步编程的铁律。有一次我在业务回调里调了一个同步数据库接口数据库慢查询时整台服务器的连接全部积压排查到大半夜才发现是这里的问题。6. 从epoll往上Python生态里的封装层6.1 asyncio底层就是epoll但包装了更多细节Python 3.4引入的asyncio在Linux上默认事件循环就是基于epoll的。它对外暴露的是协程和Future看起来和epoll相去甚远但核心的selector机制就是封装了select.epoll。asyncio的价值在于把事件回调转换成协程调度。用epoll裸写回调函数里处理数据时如果想再次发送只能继续注册事件代码容易变成回调地狱。asyncio的async def把同一个连接的多个操作写在一个协程函数里await的时候挂起数据到了继续执行逻辑上像同步代码实则是异步调度。如果只是在写一个工具或小服务直接上asyncio通常更划算。但如果你想真正理解网络服务背后发生了什么裸写epoll一遍是非常有价值的。我自己写epoll版本时很多以前想不通的异步设计问题——为什么asyncio要求协程内不能有阻塞调用、为什么selector要注册fd而不是注册socket对象——全都豁然开朗了。6.2 轻量级库与重量级框架怎么选Python生态里常见的几个方案方案底层机制适用场景select.epoll裸写直接操作epoll教学、极简服务、需要完全掌控的资源紧张场景asyncioepoll 协程调度标准库推荐方案适合中大型异步服务第三方事件驱动库epoll/kqueue等多后端高性能网络框架、网关、代理裸写epoll适合理解原理和做性能极限压榨asyncio适合大多数业务场景因为内置了超时、信号处理、任务组等大量实用功能重量级框架适合做HTTP服务、RPC服务这类已经有成熟协议的场景。需要说清楚的是框架不等于银弹。无论用哪个方案非阻塞IO、事件循环、fd生命周期管理这些概念是通用的。框架只是把这些概念封装好了出了问题最后还是要回到底层去排查。7. 压测结果与性能预期7.1 一个参考性的性能基准数据用上面的echo代码在某台4核8G的普通云服务器上做一个参考测试连接数两万客户端分批发消息每次16字节纯echo单进程跑结果大概是每秒两万到三万次请求CPU占用百分之七八十。这个数字远不是epoll的上限——瓶颈主要在Python解释器执行字节码的速度。同样的逻辑用C写能达到十万以上。但Python的高并发场景从来不以单请求性能为目标以并发连接数和开发的效率取胜。两万连接对大多数业务场景已经非常充足毕竟同时在线两万的业务本身就不小。需要明确的是压测数字只能作为参考不同内核版本、不同的数据包大小、不同的系统负载表现差距会很大。真正要关注的不是极限数字而是事件循环有没有空转、CPU是否均匀分布、有没有多余的syscall。7.2 用strace看内核调用次数判断是否白忙活调试epoll程序时strace是天然气的好帮手strace -f -e traceepoll_wait,read,write,accept -p pid观察输出如果发现epoll_wait频繁返回大量事件但read和write数量很少说明事件循环可能在重复处理相同的事件需要检查是LT模式反复触发还是逻辑里重复读取。反过来如果epoll_wait一直带着超时返回0说明应用很闲事件通知机制正常。压测时还可以用/proc/pid/fd/目录看fd增长曲线正常应该在连接建立时上涨、断开时回落。如果曲线一路往上不带掉的那就是fd泄漏了顺着代码找unregister漏掉的地方。7.3 关于多路复用的正确心态学习epoll时很容易掉进一个误区以为用了epoll性能就一定比多线程高。其实epoll解决的只是大量连接、少量活跃场景下的等待开销。如果连接数少每个连接都是频繁活跃的多线程模型可能更简单高效。选型时先问自己连接数会不会上万单连接的数据交互是不是很稀疏客户端会不会长时间挂住不发送数据这几个问题的答案是是epoll就有优势否则用多线程或者asyncio就够了。现代架构里epoll通常作为最底层的IO调度器上面盖着协程、业务线程池、任务队列。理解epoll是在打地基地基稳了上面怎么搭都行。8. 我踩过的那些坑和最后想说的话8.1 最容易翻车的几个点回顾整理一个速查表方便大家对照排查自己的代码问题原因解决方案连接建立后收不到数据客户端socket也被setblocking(False)但没有正确处理EAGAIN确认非阻塞模式recv捕获BlockingIOError大量连接连接后CPU飙高EPOLLOUT长期注册或LT模式下反复读取写完数据modify回EPOLLIN设置单次读取上限服务运行几小时后连不上fd泄漏检查unregister/close顺序监控/proc/pid/fd偶发性消息丢失ET模式没读到EAGAIN就停止数据残留在缓冲区改用LT或者ET下循环recv到BlockingIOError多进程accept惊群多个进程共享监听socket用SO_REUSEPORT或每进程独立监听回调里阻塞导致全服务卡住事件循环里做了同步IO或耗时计算业务逻辑丢线程池/进程池主循环只做IO我自己摔得最狠的是满载压测时EPOLLOUT处理不当一度以为是服务器问题换了三台机器都一样最后用strace才发现是事件风暴。从那之后凡是注册EPOLLOUT的代码我都默认加一个数据为空就立即改回EPOLLIN的逻辑宁可多几次modify调用也不让事件风暴有机会出现。8.2 新手学习路径建议如果你想彻底吃透epoll我给一个我验证过有效的学习顺序先把select版本的服务写出来跑起来看监控数据感受它的瓶颈。再看epoll版本对比两段代码的能力差异。写一个简单协议比如换行符分包跑通粘包拆包处理。加一个定时器功能比如心跳检测实现epoll定时器。最后把你的服务器扔到wrk/AutoHTTP下压测用strace定位性能瓶颈。这一个流程走完你对epoll的理解就算真正落地了。以后面试也好、写框架也好、排查线上问题也好都会有底气。8.3 最后分享一个实用小技巧写epoll服务时给每个客户端连接关联一个便携状态是很有用的。创建一个包含socket、读缓冲区、写缓冲区、上次活跃时间等字段的对象保存在fd到对象的映射里。这样一来超时踢线、半包处理、写缓冲管理就都有地方放了。我用的是一个简单的字典加闭包来实现class Connection: __slots__ (sock, recv_buf, send_buf, last_active) def __init__(self, sock): self.sock sock self.recv_buf b self.send_buf b self.last_active time.monotonic()这个设计看起来简单但在长时间运行的服务里它把每个fd的状态管理从一团散乱的全局变量收拢到一个清晰的结构里调试和维护都舒服很多。用__slots__省内存两万连接时差距非常明显——一个带__dict__的普通对象和带__slots__的对象之间能差出好几MB内存。希望这篇东西能帮你少走一些弯路。epoll本身不难难的是真正理解它背后的模型以及在生产环境里踩平那些只有踩过才会知道的坑。把上面的代码和思路跑通一遍你就已经超过大半停留在会用框架阶段的人了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑