select在高并发反向代理中因线性扫描fd_set及用户/内核态全量拷贝,连接数超1000时即达毫秒级开销,故迅速成为瓶颈;应改用epoll/kqueue,配合多线程独立实例与client/backend fd分离管理。

为什么 select 在高并发反向代理里很快成为瓶颈
因为 select 每次调用都要线性扫描整个 fd_set,且内核需在用户态和内核态之间拷贝整个集合。当连接数超 1000,哪怕只是检查就耗掉毫秒级时间——这对毫秒级延迟敏感的代理来说不可接受。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 改用
epoll(Linux)或kqueue(macOS/BSD),它们基于事件就绪列表,O(1) 复杂度获取就绪 fd - 避免单线程 +
epoll_wait处理所有连接:CPU 成为瓶颈时,要拆成多线程 + 独立epoll实例,或用SO_REUSEPORT让内核分发新连接 - 别把 client fd 和 backend fd 混在一个
epoll实例里轮询——它们生命周期、错误语义、缓冲区策略完全不同,分开管理更易调试
如何安全地双向转发 TCP 数据流而不丢包或死锁
核心矛盾在于:一端 recv 到 EOF(FIN)不代表另一端也该立即关闭;而任意一端 write 失败(如对端崩溃)必须及时终止双向通道,否则残留连接会卡住资源。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 每个方向独立维护状态:
client_to_backend_active和backend_to_client_active,初始为 true;收到 FIN 或 write 返回-1且errno != EAGAIN && errno != EWOULDBLOCK时置 false - 绝不在
recv返回 0 后立刻 close 对端 socket——先尝试把本端 send buffer 中剩余数据 flush 完(用shutdown(fd, SHUT_WR)触发 FIN,再等对端 ACK) - 用非阻塞 socket +
EPOLLET(边缘触发)时,必须循环recv/send直到返回-1且errno == EAGAIN,否则会漏数据 - 示例关键逻辑片段:
ssize_t n = recv(client_fd, buf, sizeof(buf), MSG_DONTWAIT); if (n > 0) { /* 转发到 backend */ } else if (n == 0) { client_to_backend_active = false; shutdown(client_fd, SHUT_RD); } else if (errno == EAGAIN || errno == EWOULDBLOCK) { /* 继续等待事件 */ } else { /* 错误,标记失效 */ }
sendfile 能不能用于代理?什么情况下反而更慢
sendfile 只支持「文件描述符 → socket」,且要求源 fd 是普通文件或支持 mmap 的设备。TCP socket 不是合法的 src_fd,所以不能直接用 sendfile 在两个 socket 间零拷贝转发。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- Linux 5.7+ 提供
copy_file_range,但仅限于支持 splice 的 fd 对(如 pipe ↔ socket),仍不支持 socket ↔ socket - 真正能用的零拷贝路径只有
splice+ pipe:先splice(client_fd, NULL, pipe_fd, NULL, len, SPLICE_F_MOVE),再splice(pipe_fd, NULL, backend_fd, NULL, len, SPLICE_F_MOVE)。但 pipe 容量有限(默认 64KB),需控制每次长度,否则阻塞 - 实践中,
splice在小包(read+write慢;建议只在大块数据(如静态文件传输)启用 - 务必检查
splice返回值是否等于预期长度,不等则说明 pipe 满或对端不可写,需降级为常规转发
为什么代理层容易出现 “半开连接” 导致内存泄漏
客户端异常断网(如拔网线)、NAT 超时、防火墙静默丢包,都会让一端认为连接还活着,另一端已失联。若代理不主动探测,fd 和缓冲区会长期驻留。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 对 client_fd 启用
TCP_KEEPALIVE(setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &on, sizeof(on))),并调小TCP_KEEPIDLE/TCP_KEEPINTVL(如 30s/10s) - backend_fd 建议不用 keepalive:后端服务通常自己做健康检查;代理应依赖后端返回的 RST 或超时重试逻辑
- 为每个连接维护最后读/写时间戳,结合定时器线程(或
epoll的EPOLLONESHOT+ 定时事件)清理空闲超时连接(如 60s 无活动) - 别依赖
close()自动释放:必须显式epoll_ctl(EPOLL_CTL_DEL)移除 fd,再close(),否则 epoll 可能触发已释放 fd 的事件
实际部署时,最常被忽略的是 EPOLLONESHOT 的使用时机和 splice 的 fallback 机制——前者不设好会导致事件重复触发和状态错乱,后者缺失会让大流量下突然退化成全量 memcpy。



















