恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
百万并发TCP服务器调优:从内核参数到epoll事件驱动
首页
资讯中心
/
百万并发TCP服务器调优:从内核参数到epoll事件驱动
百万并发TCP服务器调优:从内核参数到epoll事件驱动
发布时间:2026/9/26 4:46:50
1. 从单机千兆到百万并发先搞清楚我们要优化什么聊到TCP服务器百万并发很多人第一反应是改内核参数把文件描述符上限拉高然后开一堆线程。但实际上百万并发这个目标的瓶颈通常根本不在内核参数上而在应用层架构、内存占用和事件模型的选择上。在我接手过的几个高并发项目中真正吃透百万并发这个概念是先从几个基础事实开始的。首先百万并发指的是同时维持100万个TCP连接处于ESTABLISHED状态而不是每秒处理100万次请求。这两个概念经常被混淆。如果业务要求的是百万QPS每秒查询率那是吞吐量问题思路完全不同如果要求的是同时在线100万客户端那是连接容量问题核心在于内存和文件描述符的规划。标题里的并发我按连接容量的场景来展开。其次百万连接对服务器的硬件要求并不像想象中那么恐怖。一个空闲的TCP连接在内核层面占用大约几十KB的内存主要是socket缓冲区在应用层如果采用事件驱动模型每个连接仅需几KB的用户态内存。算下来100万连接内存占用大概在几GB到十几GB之间一台16GB内存的服务器完全扛得住。真正容易翻车的是文件描述符数量、端口范围和内核的一些隐藏限制。第三要实现百万连接服务端必须采用epoll这类事件驱动机制传统的一个连接一个线程模型在万级连接时就会因为线程上下文切换开销而性能急剧下降。这一点在选型时就要定死不要指望靠调参把阻塞IO模型救活。所以这篇文章要解决的核心问题很明确在一台Linux服务器上如何通过系统配置、应用层代码设计和网络参数调整让TCP服务器稳定维持100万个并发连接。我给出的配置过程基于常见的x86_64架构Linux发行版内核版本5.x使用的应用层模型是epoll 非阻塞IO这也是目前C/C和Go等语言在高并发场景下的主流做法。提示下面的参数配置和验证方法在CentOS、Ubuntu、Debian等主流发行版上均可复现部分参数在低版本内核上可能不存在建议内核版本不低于4.9。2. 内核参数调整每一条都要知道它是干什么的很多人调Linux内核参数喜欢从网上复制一大段sysctl配置直接粘上去也没搞明白每一条在干什么。这样做的问题在于一旦线上出问题你根本不知道是哪条参数引起了行为变化。我下面给出的每一组参数都会说明它的作用、修改依据和实际踩坑情况。2.1 文件描述符上限百万连接的第一道坎TCP连接在Linux中本质上是文件描述符fd每个连接至少占用一个fd。如果fd上限不够accept时就会报Too many open files。这里涉及两个层级的限制内核级的fs.file-max和用户进程级的ulimit -n。# 内核级最大文件句柄数按每连接约1个fd估算百万连接建议留足余量 sysctl -w fs.file-max2097152 sysctl -w fs.nr_open2097152 # 用户进程级限制在 /etc/security/limits.conf 中添加 # * soft nofile 1048576 # * hard nofile 1048576 ulimit -n 1048576这里有个注意点fs.file-max是系统级限制而ulimit是进程级限制两者取小值才是进程实际可用的fd数。很多教程只改了sysctl没改limits.conf结果进程只能开1024个连接排查半天才发现在这。另外单进程的fd理论上限是fs.nr_open默认约1048576即使你把它调得更高进程侧通常也不会超过这个值。在我看来直接设置为1048576就够用不需要过度调高因为百万连接不会同时并发出百万个文件句柄操作。2.2 端口范围与TIME_WAIT控制别让连接耗尽端口服务端监听一个固定端口比如8080理论上不受客户端端口限制的约束。但如果服务端主动发起连接例如反向代理、主动推送或者客户端连上来后由服务端再回连其他服务就需要考虑临时端口耗尽问题。# 临时端口范围默认32768-60999放宽到1024-65535 sysctl -w net.ipv4.ip_local_port_range1024 65535 # TIME_WAIT复用和回收对主动发起大量短连接的场景至关重要 sysctl -w net.ipv4.tcp_tw_reuse1 sysctl -w net.ipv4.tcp_fin_timeout30tcp_tw_reuse允许内核在安全条件下复用TIME_WAIT状态的端口给新连接但这只对主动连接方有效对被动接收方无效。关于tcp_tw_recycle我不建议开启它在NAT场景下会导致严重的连接随机失败问题曾经在生产环境坑过我一次——开启后部分用户反馈连接超时关闭后恢复。旧内核里这个参数能用新内核4.12已将其移除老教程里抄来的配置要审慎。2.3 内存相关参数每条连接的内存账要算清楚TCP连接占用的内存主要来自socket发送/接收缓冲区。默认的缓冲区很大可能达到几百KB如果100万连接都按默认值来内存瞬间就爆了。所以高并发场景下需要手动收紧。# socket默认缓冲区单位字节 sysctl -w net.core.rmem_default16384 sysctl -w net.core.wmem_default16384 sysctl -w net.core.rmem_max4194304 sysctl -w net.core.wmem_max4194304 # TCP读写缓冲区自动调优范围对长连接低流量场景很重要 sysctl -w net.ipv4.tcp_rmem4096 87380 4194304 sysctl -w net.ipv4.tcp_wmem4096 65536 4194304 # 每个连接的内存页限制 sysctl -w net.ipv4.tcp_mem786432 1048576 1572864tcp_mem的三个值分别是压力阈值下限、压力阈值上限、最大页数。当内存使用超过上限时内核会拒绝新的TCP连接分配。有些教程说这三个值越大越好这是错误的。对百万连接场景应保证缓冲区够用又不过度分配从而锁住内存预算。我通常按如下方式估算如果设置了tcp_wmem为16KB初始值每个连接读写缓冲区合计约32KB100万连接就是32GB这显然超出了16GB内存的范围。所以对于空闲长连接如物联网设备在线、推送服务心跳需要进一步把tcp_rmem和tcp_wmem的最小值压低并配合应用程序层面设置SO_RCVBUF/SO_SNDBUF让实际缓冲区分配远小于默认值。注意内存账必须落在纸面上算清楚不要拍脑袋。我的经验公式是每条连接的内存开销 socket读写缓冲区可压到4KB4KB 内核协议栈控制块约2KB 应用层业务结构体自己评估。按每连接10KB算100万连接约10GB内存这样16GB内存的机器还能给程序留足空间。2.4 backlog队列和连接接受速度高并发下accept队列的长度直接影响握手能否成功。如果backlog太短客户端会感觉连接被丢弃表现为SYN重传或者直接超时。# 全连接队列长度即已完成三次握手等待accept的队列 sysctl -w net.core.somaxconn65535 # 半连接队列长度即SYN到达但未完成握手的队列 sysctl -w net.ipv4.tcp_max_syn_backlog65535 # 对SYN洪水攻击的防护阈值默认1000即可调太大会掩盖问题 sysctl -w net.ipv4.tcp_syncookies1需要说明的是somaxconn是应用层listen(fd, backlog)的上限。你即使在内核里设置了65535如果应用层listen只传了128那实际队列还是128。在Go语言里net.Listen有时候听不到这个参数的直接设置方式可以通过net.ListenConfig控制在C语言里就是listen(sockfd, 10240)这样明确传参。2.5 keepalive参数与半开连接探测百万长连接场景下一个非常容易被忽视的问题客户端断网或强制断电后服务端在一段时间内仍认为连接是活的。这些僵尸连接会持续占用fd和内存最终导致真实连接数下降。# 空闲连接存活探测 sysctl -w net.ipv4.tcp_keepalive_time7200 sysctl -w net.ipv4.tcp_keepalive_intvl75 sysctl -w net.ipv4.tcp_keepalive_probes9这套默认参数大约需要2小时才开始探测探测周期又长对业务层来说太慢。我通常会在应用层实现自己的心跳机制比如每30秒发一次心跳包而内核keepalive只作为兜底。应用层心跳能及时发现死连接并主动关闭比完全依赖内核探测靠谱得多这也是百万连接调优中非常重要的一环。3. 应用层架构epoll事件驱动的设计方式内核参数调对了只是地基真正决定百万并发能否落地的是应用层的事件模型设计。下面我会用伪代码展示一个基本的epoll服务器骨架并说明为什么每一步这样做。3.1 事件驱动的核心逻辑epoll之所以能支撑百万连接关键在于它只通知你哪些fd有事件发生而不是让你遍历所有100万个fd检查状态。配合非阻塞IO单线程/少量线程就能处理高吞吐的事件流。// epoll_server.c 核心框架伪码省略了错误处理 int epfd epoll_create(1); struct epoll_event ev, events[1024]; // 监听socket注册到epoll使用边缘触发模式ET ev.events EPOLLIN | EPOLLET; ev.data.fd listen_fd; epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, ev); while (1) { int n epoll_wait(epfd, events, 1024, -1); for (int i 0; i n; i) { if (events[i].data.fd listen_fd) { // 有新的连接到达循环accept直到没有新连接 while ((conn_fd accept(listen_fd, ...)) 0) { set_nonblocking(conn_fd); ev.events EPOLLIN | EPOLLET | EPOLLRDHUP; ev.data.fd conn_fd; epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, ev); } } else { // 处理已有连接的可读事件 handle_read(events[i].data.fd); } } }这里有几个细节在实战中很有讲究。第一个是是否要设置EPOLLRDHUP——它表示对端关闭连接可以在read返回0之前提前感知断开帮助你更快地回收fd。第二个是ET模式和LT模式的选择——ET模式下必须循环读取直到返回EAGAIN否则会漏数据LT模式不需要但每次事件都可能重复触发效率略低。我倾向于ET模式毕竟目标就是承载更多连接。3.2 为什么不用多线程每连接一模型假设100万连接你开100万个线程。每个线程栈按8MB虚拟内存预留光线程栈就8000GB虚拟内存即使物理内存不立即分配线程调度开销、上下文切换也会把CPU拖死。实测中单机超过5000线程时系统性能就急转直下。而epoll 单线程可以轻松处理数万连接配合线程池处理耗时操作比如数据库查询即可。这个架构的核心原则是IO事件用epoll驱动业务逻辑用线程池/队列解耦。3.3 连接对象的生命周期管理连接对象不能裸用fd应用层需要为每个fd维护一个上下文结构体。当连接数达到百万级时这个结构体的大小直接影响内存。很多初学者会把连接上下文设计得很复杂塞入各种缓冲区和业务字段一个连接轻松几百字节甚至上KB百万连接就浪费上百MB内存。合理做法是连接上下文只保存必要字段如fd、对端地址、最近活跃时间、读缓冲索引大量连接的读缓冲区可以共享并动态分配空闲连接不分配大buffer连接状态切换用状态机避免锁竞争struct conn_context { int fd; uint32_t ip; uint16_t port; uint64_t last_active_time; int state; // INIT, HANDSHAKE, ESTABLISHED, CLOSING char *read_buf; size_t read_len; };一个面面俱到的连接结构体在百万连接下就是存储的浪费。我在早期版本里加过很多统计字段后续排查发现overhead已经占了每连接好几百字节立刻减掉了。4. 百万并发实测连接数上升过程中出现的典型问题配置和代码都就绪后真正压测才会暴露问题。下面是我在实测过程中遇到的几个高发问题以及对应的解决思路。4.1 现象一连接数到3万左右就上不去了这通常不是容量不够而是某个隐藏限制被触发。首先要看的就是somaxconn和进程的nofile是否真的生效。有一个常被忽略的地方systemd启动的服务会有自己的LimitNOFILE设置即使你在/etc/security/limits.conf里改了也没用必须修改service文件里的LimitNOFILE1048576。# 查看进程实际fd限制 cat /proc/pid/limits # systemd服务示例 [Service] LimitNOFILE1048576其次检查/proc/sys/net/ipv4/tcp_max_tw_buckets这个参数控制TIME_WAIT数量上限默认值可能是180000。如果你的场景产生了大量TIME_WAIT连接超过这个值后新连接会被直接丢弃。适当调高到50万以上但不要过高否则TIME_WAIT连接会占用内存。4.2 现象二连接数到70万左右出现大量请求超时这个阶段往往是内存带宽或CPU中断处理达到瓶颈。当网络包到达时内核会产生软中断。如果CPU的网卡队列不均某个CPU核的中断负载可能会明显偏高导致处理不过来。排查方式查看/proc/interrupts确认网络中断分布如果集中在某几个CPU启用网卡的RSSReceive Side Scaling或者设置smp_affinity把中断均匀分散到多个核。另外net.core.netdev_max_backlog如果过小在突发流量下会丢包sysctl -w net.core.netdev_max_backlog65535还有一点经常被忽略如果用的是虚拟机或云主机网络性能可能受虚拟化层限制百万连接压测在物理机上和虚拟机上表现差异很大。如果是云服务器需要留意带宽是否被限速不一定是配置问题。4.3 现象三内存实际占用比预估高很多老生常谈内核的页缓存和socket缓冲区会占用额外内存。当内存吃紧时kswapd进程CPU占用率飙升系统响应变慢。这时候可以通过ss -m查看每个socket的实际内存占用和预估对比ss -m state established ( sport :8080 ) | head -20如果发现每条连接的skmem远高于预期检查是否应用层设置了较大的SO_RCVBUF/SO_SNDBUF或者内核的tcp_rmem/tcp_wmem被业务逻辑自动调大。对于长连接低流量场景可以显式设置SO_RCVBUF为8192字节避免内核动态扩大缓冲区。5. 配置清单一键脚本和验证手段日常部署时我把上面的参数整理成一个sysctl配置文件方便在裸机或容器宿主机上快速应用。同时我会额外加几条有助于网络性能但不会引入风险的参数。# /etc/sysctl.d/99-highconcurrency.conf fs.file-max 2097152 fs.nr_open 2097152 net.ipv4.ip_local_port_range 1024 65535 net.ipv4.tcp_tw_reuse 1 net.ipv4.tcp_fin_timeout 30 net.core.somaxconn 65535 net.ipv4.tcp_max_syn_backlog 65535 net.ipv4.tcp_syncookies 1 net.core.rmem_default 16384 net.core.wmem_default 16384 net.core.rmem_max 4194304 net.core.wmem_max 4194304 net.ipv4.tcp_rmem 4096 87380 4194304 net.ipv4.tcp_wmem 4096 65536 4194304 net.ipv4.tcp_mem 786432 1048576 1572864 net.core.netdev_max_backlog 65535 net.ipv4.tcp_max_tw_buckets 500000执行sysctl --system即可生效。验证参数是否生效sysctl net.ipv4.tcp_rmem net.ipv4.tcp_wmem fs.file-max cat /proc/sys/net/ipv4/tcp_keepalive_time压测工具推荐使用wrk做HTTP短连接压力测试使用tcploop或者自己写一个epoll客户端做长连接数量测试。单纯测维持100万连接不重连可以写一个分段连接器客户端每间隔几百毫秒建立一批连接达到目标数量后进入静默状态观察服务端连接数是否稳定。在压测期间重点监控这几个指标服务端ss -s查看socket总量和TCP状态分布pidstat -d查看进程的IO等待vmstat观察CPU us/sy比例si/so是否出现换页netstat -s查看丢包和重传统计6. 再谈几个容易翻车的边界情况百万并发不是调完参数就完事后续运维阶段也有一些场景要特别留心我把实际运维中遇到的边界情况列出来供参考。6.1 连接被对端半关闭收不到通知客户端正常发FIN后服务端read会返回0但如果你不处理这个事件而继续关注EPOLLIN连接不会自动消失。参考TCP状态机当对端发送FIN并进入FIN_WAIT_1服务端收到后进入CLOSE_WAIT状态此时若不及时close连接就会一直卡在CLOSE_WAIT占满所有fd。常规做法对fd注册EPOLLIN | EPOLLRDHUPread返回0时立即close对于超过心跳阈值仍无收发的连接通过超时扫描强制清理。在百万连接场景下这个超时扫描不能全量遍历我用的是小根堆维护最近活跃时间每次只检查堆顶的若干连接效率远高于全量遍历。6.2 突增连接时的SYN队列溢出如果某个时间点有大量客户端同时重连比如网络恢复瞬间涌入的连接可能导致SYN队列溢出。内核的tcp_max_syn_backlog只是用于限制队列长度实际能否收到SYN还取决于tcp_synack_retries等参数。当发生这种毛刺时netstat -s中会出现SYNs to LISTEN sockets dropped计数增加。应对手段设置合理的tcp_syncookies1当半连接队列满时自动启用syncookie机制服务端不保存SYN队列条目通过加密的序列号完成握手验证相当于以CPU换内存对突发场景很有效。这个参数对百万连接是默认建议开启的。6.3 关于IPv6和单IP下连接数上限如果服务端只监听IPv4单IP理论上最大连接数受客户端端口范围限制约为6万多个这个限制意味着百万连接必须由数百万个不同客户端IP发起。在压测环境里如果没有足够多的客户端IP就需要配置多个IP或者使用虚拟IP来模拟。有些团队会用多个压测机器分散连接一个真实IP上的6万连接上限是不可忽视的问题这也是很多人在自建压测时卡住的原因。如果服务端开启IPv6并支持双栈或者监听多个IP单机承载能力会进一步扩展。在容器环境里如果把服务绑定在0.0.0.0同样受单IP限制改成[::]双栈监听理论连接数由客户端地址数量决定规模上限会高很多。7. 验证案例一次接近百万连接的实际配置经历最后分享一个我实际调过的配置案例。场景是物联网设备接入网关设备数量约82万台分布在多个地域通过长连接上报心跳和数据。服务端是基于C的epoll服务部署在4台物理机上每台承载20万左右连接。当时遇到的最大问题不是内核参数而是业务层处理不过来。每台机器连接数到达18万时CPU几乎全部消耗在业务逻辑的数据解析上IO事件本身占比不高。我当时的调整思路是把每连接的业务缓冲区分池化减少动态分配带来的锁争用。高频心跳消息走专用处理线程普通业务消息走队列防止心跳风暴拖垮主逻辑。把每连接每秒的收发量控制在较低水平降低CPU软中断。实测心跳频率从5秒一次降到10秒一次后单机连接数成功提升到22万左右。这个案例说明百万并发配置不只是内核参数的问题应用层逻辑的效率和网络模型同等重要。内核参数决定上限应用层决定你是否能触到这个上限。如果从头到尾只让我给一个建议那就是先在后端服务上把ss -s、/proc/interrupts和pidstat三个监控命令跑起来建立连接数、CPU、内存三者之间的数据关联再动手改参数。比如压测中如果CPU总占用率不高但连接数上不去大概率是某个隐藏限制起作用而不是资源不够。所有的调优动作都应该以监控数据为依据而不是靠感觉和猜测。最后再分享一个体感非常明显的小技巧压测前把服务端的tcp_fin_timeout调小比如15秒同时开启tcp_tw_reuse这样大量短连接场景下的TIME_WAIT堆积会好很多。但如果你的场景是纯长连接这两个参数几乎不产生作用真正的关键还是fs.file-max和ulimit的配合是否正确。按照上述流程操作我认为在主流Linux发行版上做到单机百万连接是完全可行的。