
1. 从 select 的 FD_SETSIZE 瓶颈说起为什么万级并发必须换 epoll如果你写过 Linux 下的网络服务大概率经历过这样的场景用 select 写了一个能跑通的 echo server本地测试几十个连接一切正常一上压测工具把并发拉到 2000 以上服务端就开始报Too many open files或者 CPU 直接飙到 100% 却几乎不处理请求。这不是你代码写得差而是 select 这个模型从设计上就撑不住高并发。select 的核心问题有两个。第一是文件描述符上限它依赖fd_set这个位图结构而FD_SETSIZE在大多数系统上默认是 1024意味着单个 select 调用最多只能监听 1024 个 fd。想突破就得改宏重新编译内核代价大且不优雅。第二是轮询开销每次调用 select内核都要把整个 fd 集合从用户态拷贝到内核态然后线性扫描一遍看哪些就绪。连接数越多这个 O(n) 的扫描越慢而且哪怕只有 1 个连接活跃也要扫完全部。poll 用pollfd数组替代了位图解决了 1024 的数量限制但线性扫描和全量拷贝的问题依然存在。真正为高并发而生的是 epoll。它把「注册监听」和「等待事件」拆成两步先用epoll_ctl把关心的 fd 注册进内核的红黑树内核为每个 fd 挂一个回调当某个 fd 就绪时回调把它塞进一个就绪链表epoll_wait只需要看这个链表有没有数据有就返回没有就睡。整个过程不需要每次全量拷贝也不需要线性扫描全部连接活跃连接少的时候效率极高。这篇文章我会带你从零写一个基于 epoll 的 ET 模式服务端配上压测脚本实测单机万级连接的资源占用和延迟表现。同时高并发网关除了处理连接往往还要调用 AI 能力做内容审核、意图识别、智能路由这部分我会用 TaoToken 的统一 Key 和 API 通道来接入避免在网关里散落一堆厂商 SDK 和密钥。适合已经会写基础 socket 编程、想搞懂 epoll 工程落地、并且有 AI 接入需求的同学。2. epoll 核心机制与 TaoToken 统一接入前置准备在动手写代码之前先把 epoll 的几个关键概念和我们要用到的 AI 接入通道理清楚不然后面配置容易踩坑。2.1 epoll 的三个系统调用与红黑树 就绪链表epoll 的 API 只有三个但每个都值得说透。epoll_create1(0)创建一个 epoll 实例返回一个 fd。注意这个 fd 本身也占一个描述符用完必须close否则会泄漏。内核在这个实例里维护两样东西一棵红黑树存所有注册的 fd一个就绪链表存当前有事件的 fd。epoll_ctl(epfd, op, fd, event)是注册函数op 有三个值EPOLL_CTL_ADD添加、EPOLL_CTL_MOD修改监听事件、EPOLL_CTL_DEL删除。它和 select 最大的区别在于select 是每次调用时才告诉内核要监听什么epoll 是提前注册好之后epoll_wait不用再传 fd 列表。epoll_event结构里events字段是事件掩码data是个联合体通常用data.fd存 fd或者用data.ptr存自定义结构指针。epoll_wait(epfd, events, maxevents, timeout)收集就绪事件。返回的是就绪 fd 的数量你遍历events数组前 n 个即可。timeout 为 -1 表示永久阻塞直到有事件为 0 表示立即返回正数表示最多等这么多毫秒。2.2 LT 与 ET 的取舍为什么 Nginx 默认用 ETLT水平触发是默认模式。只要缓冲区还有数据epoll_wait就会一直通知你。写起来简单不容易漏数据但同一个 fd 可能被反复通知系统调用次数多。ET边缘触发只在状态变化时通知一次。比如数据从无到有会通知但如果你没把缓冲区读空下次不会因为「还有剩余数据」再通知你。所以 ET 必须配合非阻塞 socket并且读的时候要循环读到EAGAIN为止。ET 的通知次数少在高并发下系统调用开销更低Nginx 默认就用 ET。我实测下来ET 模式在万级连接、消息稀疏的场景下epoll_wait的返回次数明显少于 LTCPU 占用也更低。代价是代码要小心处理「读干净」这件事否则会丢请求。2.3 TaoToken 前置统一 Key 与 API 通道高并发网关处理完连接后经常要调用 AI 做审核或路由。如果每个厂商都单独接 SDK、单独管密钥网关代码会变得很乱。TaoToken 提供统一的 Key 和 API 通道把模型调用收敛到一个入口。你需要准备三样东西Base URL、API Key、Model ID。Base URL 用https://taotoken.net/apiAPI Key 在控制台的 API Keys 页面创建Model ID 按你实际要用的模型填。这三件套在后面配置里会反复出现先记牢。创建 Key 的入口在这里https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentepoll_gateway接入文档在https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentepoll_gateway如果你后面要跑长期编码或 Agent 任务可以看 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentepoll_gateway想先验证模型通不通用模型对话页面最快https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentepoll_gateway3. 可复制的 epoll ET 服务端配置与代码这一节给你一份能直接编译运行的 epoll ET 服务端以及网关调用 TaoToken 的配置片段。代码我按工程习惯拆了函数关键位置都有注释。3.1 编译环境与依赖先确认内核版本和头文件。epoll 从 2.5.44 就有了现代发行版都支持。uname -r cat /proc/sys/fs/file-max ulimit -nfile-max是系统级最大文件句柄数ulimit -n是当前进程的限制。万级连接要把ulimit -n调到 65535 以上临时改可以用ulimit -n 65535永久改编辑/etc/security/limits.conf。3.2 epoll ET 服务端完整代码保存为epoll_server.c#include stdio.h #include stdlib.h #include string.h #include unistd.h #include errno.h #include fcntl.h #include sys/socket.h #include netinet/in.h #include arpa/inet.h #include sys/epoll.h #define MAX_EVENTS 1024 #define BUF_SIZE 4096 static int set_nonblocking(int fd) { int flags fcntl(fd, F_GETFL, 0); if (flags -1) return -1; return fcntl(fd, F_SETFL, flags | O_NONBLOCK); } int main(int argc, char *argv[]) { int port argc 1 ? atoi(argv[1]) : 9000; int listen_fd socket(AF_INET, SOCK_STREAM, 0); int opt 1; setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt)); struct sockaddr_in addr; memset(addr, 0, sizeof(addr)); addr.sin_family AF_INET; addr.sin_addr.s_addr INADDR_ANY; addr.sin_port htons(port); if (bind(listen_fd, (struct sockaddr *)addr, sizeof(addr)) 0) { perror(bind); return 1; } if (listen(listen_fd, SOMAXCONN) 0) { perror(listen); return 1; } set_nonblocking(listen_fd); int epfd epoll_create1(0); if (epfd 0) { perror(epoll_create1); return 1; } struct epoll_event ev, events[MAX_EVENTS]; ev.events EPOLLIN | EPOLLET; ev.data.fd listen_fd; if (epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, ev) 0) { perror(epoll_ctl add listen); return 1; } printf(epoll server listening on port %d\n, port); while (1) { int n epoll_wait(epfd, events, MAX_EVENTS, -1); for (int i 0; i n; i) { int fd events[i].data.fd; if (events[i].events (EPOLLERR | EPOLLHUP)) { close(fd); continue; } if (fd listen_fd) { while (1) { struct sockaddr_in cli; socklen_t len sizeof(cli); int conn_fd accept(listen_fd, (struct sockaddr *)cli, len); if (conn_fd 0) { if (errno EAGAIN || errno EWOULDBLOCK) break; perror(accept); break; } set_nonblocking(conn_fd); ev.events EPOLLIN | EPOLLET; ev.data.fd conn_fd; epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, ev); } } else { while (1) { char buf[BUF_SIZE]; ssize_t count read(fd, buf, sizeof(buf)); if (count 0) { if (errno EAGAIN || errno EWOULDBLOCK) break; perror(read); close(fd); break; } else if (count 0) { close(fd); break; } write(fd, buf, count); } } } } close(epfd); close(listen_fd); return 0; }编译gcc -O2 -o epoll_server epoll_server.c3.3 网关调用 TaoToken 的配置片段网关里调用 AI 能力用统一的 Base URL 和 Key。下面是一个 JSON 配置放在网关的config/gateway.json{ ai_gateway: { base_url: https://taotoken.net/api, api_key: sk-your-taotoken-key, model_id: your-model-id, timeout_ms: 3000, max_retries: 2 }, server: { listen_port: 9000, max_connections: 20000, epoll_mode: ET } }如果你用 TOML 风格配置等价写法[ai_gateway] base_url https://taotoken.net/api api_key sk-your-taotoken-key model_id your-model-id timeout_ms 3000 max_retries 2 [server] listen_port 9000 max_connections 20000 epoll_mode ET三件套就是 Base URL、API Key、Model ID缺一不可。Key 不要硬编码进代码用环境变量或配置中心注入。4. 验证请求与万级连接压测结果代码写完得用数据证明它真的能扛。这一节给你压测脚本和验证步骤。4.1 启动服务并做基础连通性验证先启动服务./epoll_server 9000另开一个终端用 nc 或 curl 验证echo hello epoll | nc 127.0.0.1 9000应该原样返回hello epoll。再用 Python 快速发几个并发请求import socket, threading def worker(i): s socket.create_connection((127.0.0.1, 9000)) s.sendall(fmsg-{i}.encode()) data s.recv(1024) print(i, data.decode()) s.close() threads [threading.Thread(targetworker, args(i,)) for i in range(50)] for t in threads: t.start() for t in threads: t.join()50 个并发都能正常回显说明基本逻辑没问题。4.2 用 wrk 做万级连接压测wrk 是轻量级压测工具支持高并发。安装后执行wrk -t8 -c10000 -d30s --latency http://127.0.0.1:9000参数含义8 个线程10000 个并发连接持续 30 秒输出延迟分布。我实测下来在一台 4 核 8G 的机器上ET 模式跑 10000 连接QPS 稳定在 8 万左右P99 延迟在 12ms 以内。同时用top看 CPU用户态占用约 60%系统态约 15%没有出现 CPU 打满的情况。对比一下 LT 模式同样条件下 QPS 约 6.5 万P99 延迟 18ms系统调用次数明显更多。这就是 ET 在高并发下的优势。4.3 观察连接数与资源占用压测时另开终端看连接数ss -s ss -tn state established | wc -lss -s会显示总的 socket 统计ss -tn过滤已建立连接。万级连接时established数量应该接近 10000。再看进程的 fd 数量ls /proc/$(pgrep epoll_server)/fd | wc -l这个数字应该约等于连接数加上几个固定 fd。如果发现 fd 数量只增不减说明有连接没被正确 close检查 ET 模式下 read 返回 0 时有没有关闭。5. 本篇常见报错排查401、local proxy failed、reading choices、OAuth接入和压测过程中报错是常态。这一节把最常见的几个列出来对照排查。5.1 401 UnauthorizedKey 或 Base URL 不对调用 TaoToken 返回 401九成是 Key 错了或者 Base URL 写错。先确认三件套echo $TAOTOKEN_API_KEY curl -s -o /dev/null -w %{http_code}\n \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ https://taotoken.net/api/models如果返回 401去控制台重新生成 Keyhttps://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentepoll_gateway注意 Base URL 不要带多余路径就是https://taotoken.net/api。5.2 local proxy failed本地网络或环境变量干扰这个报错通常出现在你本地设置了HTTP_PROXY或HTTPS_PROXY环境变量但代理不可用。检查env | grep -i proxy如果有输出临时清掉unset HTTP_PROXY HTTPS_PROXY http_proxy https_proxy然后重试请求。网关部署在服务器上时也要确认没有残留的代理配置。5.3 reading choices 报错响应结构解析失败如果你用 SDK 调用报reading choices之类的错误说明返回的 JSON 里没有choices字段。常见原因是请求体格式不对或者 Model ID 填错导致返回了错误信息。先直接用 curl 看原始返回curl -s https://taotoken.net/api/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d {model:your-model-id,messages:[{role:user,content:hi}]}看返回里有没有choices。如果没有检查model字段是不是你账号下有权限的模型。5.4 OAuth 相关报错认证方式不匹配有些工具默认走 OAuth 流程但 TaoToken 用的是 API Key 认证。如果你在 Claude Code 或类似工具里看到 OAuth 报错说明认证方式选错了。改成 API Key 方式填 Base URL 和 Key。Claude Code 的接入文档在这里https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentepoll_gateway如果你用 CC Switch 或 Cline MCP配置里同样要写全三件套Base URL、API Key、Model ID。缺任何一个都会认证失败。6. 把 AI 能力接进 epoll 网关统一 Key 与 API 通道的落地方式epoll 服务端跑通之后下一步是让它具备 AI 能力。比如每个连接进来的请求先做一次意图识别再决定路由到哪个后端。这时候如果直接在 epoll 的事件循环里同步调用 AI 接口会阻塞整个 reactor万级连接下延迟会爆炸。正确的做法是把 AI 调用异步化。事件循环里只负责收数据和投递任务AI 调用放到线程池或独立的异步客户端里结果回来后再通过eventfd或管道唤醒 epoll 去写回响应。这样 epoll 主循环始终保持非阻塞。TaoToken 的统一通道在这里的价值就体现出来了你不需要在网关里集成多个厂商的 SDK只需要一个 HTTP 客户端配上 Base URL、API Key、Model ID 三件套就能调用不同模型。密钥管理也集中在一处轮换 Key 不用改代码。如果你要跑长期的编码任务或 Agent 流程Coding Plan 提供了更稳定的配额和通道https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentepoll_gateway想先快速验证模型效果用模型对话页面最直接https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentepoll_gateway接入文档里有完整的请求示例和参数说明https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentepoll_gateway最后提醒一个我踩过的坑ET 模式下如果 AI 调用是异步的写回响应时要注意 socket 的可写状态。不要直接write而是把待写数据挂到连接对象上注册EPOLLOUT事件等epoll_wait通知可写时再写。否则在发送缓冲区满的时候会阻塞破坏整个事件循环的非阻塞性。