
1. 高并发场景的网络瓶颈到底卡在哪先讲个我自己的案例。某天晚上我们一个模拟的统计查询系统突然报警线上Nginx的活跃连接数直接顶到几万但业务其实远没到这个量级大量连接堆在SYN_RECV和TIME_WAIT状态后端接口响应倒是不慢但客户端就是觉得“卡”。当时第一反应是加机器后来冷静下来查了一圈发现问题不在应用代码而在操作系统网络栈的默认参数上。这个事给我的触动很大。很多团队一提到高并发优先想到的是扩容、负载均衡、微服务拆分却忽略了一个基本事实你所有请求最终都要穿过Linux内核的网络协议栈。即使你把应用层做得再高效如果内核默认参数不支持那么大的并发量连接队列、文件句柄、TIME_WAIT、缓冲区这些环节早晚会变成瓶径。这篇文章不是教科书式地罗列参数而是从我实际调优的经验出发把高并发场景下真正管用的Linux网络内核参数掰开揉碎讲一遍。调优方向涉及的参数解决的核心问题文件句柄上限ulimit -n、fs.file-max进程能打开多少socket连接队列somaxconn、tcp_max_syn_backlog握手队列能积压多少新连接TIME_WAIT管理tcp_tw_reuse、tcp_fin_timeout四挥后端口和连接资源释放速度Socket缓冲区rmem、wmem单条连接收发数据的瞬时吞吐通用连接优化tcp_syncookies、tcp_keepalive_time半连接和存量连接的心跳管理能不能扛住突发流量通常就是靠这几个模块的配合下面一节一节点拆开聊。2. 文件句柄与最大连接数地基没打牢谈何并发2.1 别小看“能打开多少文件”这件事Linux下一切皆文件socket也不例外。每建立一个TCP连接就占用一个文件描述符fd。你在高并发接入层看到的连接打满很多时候根本不是Nginx、Redis或者Java进程的线程不够而是进程能使用的文件描述符上限太小。我见过一个真实的例子某内部管理平台上线第一天压测到5000并发就报错错误信息是“Too many open files”。运维以为是进程配置问题翻了半天systemd配置最后发现是shell启动方式没有加载limits.conf进程fd上限只有1024。高并发应用默认单进程1024个fd这个数字注定了你连做个小规模压测都不够。所以调优的第一步是先看基础配额。确认ulimit的时候要注意soft limit和hard limit都要看只改soft不改hard进程在运行中可能还是会受限。查看方式ulimit -n ulimit -Hn这两个值默认情况下通常是1024有些发行版是4096。2.2 永久调优的正确姿势不只改limits.conf修改文件句柄上限最常见的做法是改/etc/security/limits.conf* soft nofile 1048576 * hard nofile 1048576但这里有个容易被坑的地方这个文件只对通过PAM登录的会话生效。如果你用systemd服务方式启动Java进程或Docker容器systemd会给服务单独设一个进程级限制你得在service文件里显式设置LimitNOFILE1048576如果应用是跑在systemd环境里光改limits.conf是没用。这个坑我踩过不止一次某次压测Redis集群明明limits.conf已经改成百万级了重启完服务发现fd上限还是4096查了半天才发现是docker容器里的进程默认继承的是容器自身的限制后来在容器编排文件的ulimits段里显式指定才生效。还有两个全局参数需要配合调整。一个是fs.file-max这是内核级别的全局fd上限另一个是fs.file-nr它是在动态反馈当前系统fd使用情况sysctl -w fs.file-max 1000000 sysctl fs.file-nrfile-nr输出的三个数字分别是已分配fd数、未使用fd数、总上限。如果第一个数字长期接近第三个数字说明你的全局fd已经告急光调进程级限制已经不够了。3. TCP握手与队列参数高并发下最先撞上的墙3.1 半连接队列和全连接队列各管一段TCP建立连接要走三次握手内核在这期间维护两个队列一个是半连接队列SYN队列存放还没完成握手的连接一个是全连接队列accept队列存放已经完成握手、等待应用调用accept()的连接。和这两个队列相关的核心参数是net.core.somaxconn全连接队列长度上限默认一般是4096部分内核是128net.ipv4.tcp_max_syn_backlog半连接队列长度默认一般是1024或2048高并发场景下这两个默认值基本不够用。尤其是somaxconn很多应用比如Nginx和Redis在listen监听时还会传入一个backlog值最终生效的实际长度是应用传入值和内核somaxconn值之间较小的那个。换句话说即使你的应用设置了backlog1024如果内核somaxconn只有128实际全连接队列就被限制在128。我实际调优时一般把这个参数设为65535sysctl -w net.core.somaxconn65535 sysctl -w net.ipv4.tcp_max_syn_backlog65535关于他们做“说了什么”。linux默认1024“全连接队列”的上限是因为这两个队列的长度需要消耗内存。连接排队的本质是让内核帮你保住这些socket。排队长了如果应用处理得不快内存消耗就会上升。所以调优的时候不要只调队列还要观察应用的accept速度。如果队列满了应用侧日志或ss -lnt输出里能看到全连接队列溢出这个后面讲排查时会细说。3.2 防御SYN洪水syncookies是保险丝不是万能药net.ipv4.tcp_syncookies这个参数争议不大一般建议直接设为1开启。它开启后当半连接队列满时内核不再丢弃SYN包而是通过一种特殊的cookie机制完成握手校验从而让服务在遭受大量SYN攻击时仍然能处理合法连接。但要提醒一句syncookies是一根保险丝不能完全依赖它。内核文档明确说过syncookies会破坏TCP的一些扩展特性比如时间戳选项的部分功能而且它本身也只能缓解SYN Flood不能替代对半连接队列长度的合理设置。而且开启syncookies时如果还希望TCP时间戳正常工作需要配合tcp_timestamps参数一起考虑。我自己生产环境一般这样配net.ipv4.tcp_syncookies 1 net.ipv4.tcp_timestamps 13.3 全连接队列监控用ss看溢出比看日志快得多排查队列溢出最快的工具是ss。看某个端口当前的全连接队列使用情况ss -lnt sport :8080输出里的Send-Q字段在LISTEN状态下显示的就是全连接队列的最大长度而Recv-Q显示当前等待accept的连接数。当Recv-Q接近Send-Q甚至等于Send-Q时说明应用层消费连接的速度跟不上了。如果出现大量溢出ss还能看到drop计数。需要说明的是不同内核版本输出格式可能有差异但Recv-Q和Send-Q两列的含义大体一致。总原则是Recv-Q长期接近Send-Q就要排查应用accept线程模型或处理逻辑是否需要优化。4. 四次挥手与TIME_WAIT高并发短连接的隐形杀手4.1 TIME_WAIT为什么那么多以及为什么不能直接清零客户端主动关闭连接后服务端会进入TIME_WAIT状态等待2MSLMaximum Segment Lifetime之后才彻底释放。其实TIME_WAIT本身是TCP设计里的一个安全机制防止上一个连接的延迟报文干扰新连接。它的问题在于高并发短连接场景下TIME_WAIT连接会大量堆积。每个TIME_WAIT连接要占用一个五元组源IP、源端口、目的IP、目的端口、协议如果源端口耗尽新连接就会建立失败。net.ipv4.tcp_fin_timeout可以控制TIME_WAIT的持续时间默认60秒。如果线上短连接多可以适当调小net.ipv4.tcp_fin_timeout 304.2 开启timewait复用但要知道它复用的是哪类连接net.ipv4.tcp_tw_reuse是优化TIME_WAIT的重头戏它允许内核把处于TIME_WAIT状态的连接端口重新分配给新的连接。但它有一个严格的限制条件只有同时开启TCP时间戳tcp_timestamps且新连接的时间戳比TIME_WAIT记录的时间戳更新时才会复用。所以合理的做法是同时开启net.ipv4.tcp_timestamps 1 net.ipv4.tcp_tw_reuse 1注意tcp_tw_reuse和tcp_tw_recycle是两个不同的参数。tcp_tw_recycle在NAT环境下会有严重的坑它开启后会把同一源IP的报文时间戳做递增校验但由于不同设备通过同一个出口访问时间戳未必递增很可能导致丢包和连接异常。除非业务完全可控且没有NAT否则我不建议开启tcp_tw_recycle。新版内核里这个参数也已经被移除。4.3 终极方案用连接复用替代TIME_WAIT产生上面这些调优都在做“事后处理”更高级的做法是从根源上减少TIME_WAIT的产生。对于HTTP短连接场景尽量使用HTTP Keep-Alive让客户端复用同一个TCP连接避免频繁建连、断连。只要连接不频繁重建TIME_WAIT的数量自然就下来了。另外net.ipv4.tcp_max_tw_buckets这个参数可以限制系统内TIME_WAIT连接的最大数量超出后内核会直接销毁多余的TIME_WAIT连接。很多人觉得这个参数能“清理”TIME_WAIT但需要注意销毁TIME_WAIT连接打破的是TCP的可靠交付保障可能在极低概率下导致旧报文串扰到新连接。所以我一般不靠它来控制TIME_WAIT只把它当作一个兜底防内存泄漏的手段比如设成50000防止异常情况下TIME_WAIT无限膨胀。5. 缓冲区与窗口决定单连接吞吐量的关键5.1 Socket收发缓冲区别一味求大TCP的收发缓冲区决定了单条连接能够缓冲多少数据。在自研网关或文件传输场景里如果发送方的wmem太小数据来不及发送就会被丢弃接收方的rmem太小则可能频繁触发零窗口通告严重影响吞吐量。查看默认值sysctl net.ipv4.tcp_rmem sysctl net.ipv4.tcp_wmem输出一般是三个数字最小值、默认值、最大值。比如4096 87380 6291456表示最小4KB、默认约87KB、最大6MB。在高带宽、长肥管道高带宽延迟积场景下默认的缓冲区和窗口可能不够用。修改为net.ipv4.tcp_rmem 4096 87380 16777216 net.ipv4.tcp_wmem 4096 65536 16777216这里有一个常见误解把最大缓冲区调大就能提升吞吐不是的。内核只有在内存压力不大的情况下才允许缓冲区动态涨到最大值。如果系统内存紧张tcp_rmem再大也不会真正涨上去。所以调buffer之前先确认系统内存余量充足。5.2 如何估算合理的缓冲区大小带宽延迟积缓冲区调多大合适一个简单的估算公式是带宽延迟积BDP 带宽 × RTT。它表示一个RTT周期内在途数据量本质上就是TCP滑动窗口的理想大小。比如服务器出口带宽1Gbps、客户端RTT约20ms那么1Gbps ≈ 125MB/s乘以0.02秒 ≈ 2.5MB。也就是说缓冲区至少需要2.5MB才能让一条连接跑满1Gbps带宽。超过这个值意义不大因为TCP窗口本身受限于通告窗口和拥塞窗口buffer再大也就那样。当然正常业务下高并发系统追求的是同时服务大量连接而不是单条连接占满带宽所以缓冲区设计需要结合场景权衡。如果你做的是网关或文件传输类高并发服务每连接的内存占用 rmem wmem假设同时有5万连接每连接平均占用64KB那就是3.2GB内存。这个账一定要算清楚否则调优调优调出内存爆掉。5.3 拥塞控制算法别乱换但可以试net.ipv4.tcp_congestion_control控制的是拥塞控制算法。Linux默认一般是cubic在长距离高带宽场景下可以尝试bbr。BBR的优势在于它不依赖丢包来判断拥塞而是基于实时带宽和延迟的测量在有一定丢包率的链路上吞吐表现往往更好。切换方式sysctl -w net.ipv4.tcp_congestion_controlbbr需要确认内核里是否加载了bbr模块sysctl net.ipv4.tcp_available_congestion_control如果没有bbr先执行modprobe tcp_bbrBBR并不是万能的在丢包率低、延迟极其稳定的内网环境里bbr相对cubic的优势并不明显。所以我的建议是接入层出口、跨地域长链路用bbr数据中心内部短链路保持cubic问题不大。6. 其他影响高并发的重要参数与整体配置建议6.1 本地端口范围连接发起方的弹药库net.ipv4.ip_local_port_range决定了本机作为客户端主动向外发起连接时可用的源端口范围。默认通常是32768到60999也就是大约28000多个端口。如果这台机器要同时向外发起几十万条连接比如在高并发代理、爬虫采集、或者大量回调请求场景端口很快就用完了。调优net.ipv4.ip_local_port_range 1024 65535改到1024起始可用端口从28000多扩到64000多差不多翻了一倍多。注意端口耗尽会出现什么现象程序一直报“Cannot assign requested address”但你的fd和内存都还充裕。6.2 长连接探测与存活管理tcp_keepalive_time、tcp_keepalive_intvl、tcp_keepalive_probes这三个参数控制空闲TCP连接的存活探测机制。默认keepalive_time是7200秒2小时也就是一条空闲连接要等2小时才开始探测。对于高并发的长连接网关默认值偏长如果对端已经崩溃这条连接会一直占着资源很久才被清理。可以适当缩短net.ipv4.tcp_keepalive_time 600 net.ipv4.tcp_keepalive_intvl 30 net.ipv4.tcp_keepalive_probes 3意思是空闲600秒后开始发送探测包每30秒发一次连续3次没有响应就判定对端不可达并断开连接。这套配置对大并发长连接场景能明显减少“僵尸连接”挤占fd的情况。6.3 一键配置参考生产环境常用sysctl组合下面是我在高并发网关场景常用的一套内核网络参数各位可以按需加减fs.file-max 1000000 net.core.somaxconn 65535 net.core.netdev_max_backlog 65535 net.ipv4.tcp_max_syn_backlog 65535 net.ipv4.ip_local_port_range 1024 65535 net.ipv4.tcp_syncookies 1 net.ipv4.tcp_timestamps 1 net.ipv4.tcp_tw_reuse 1 net.ipv4.tcp_fin_timeout 30 net.ipv4.tcp_max_tw_buckets 50000 net.ipv4.tcp_rmem 4096 87380 16777216 net.ipv4.tcp_wmem 4096 65536 16777216 net.core.rmem_max 16777216 net.core.wmem_max 16777216 net.ipv4.tcp_keepalive_time 600 net.ipv4.tcp_keepalive_intvl 30 net.ipv4.tcp_keepalive_probes 3注意我明确没有写tcp_tw_recycle原因前面已经提过。netdev_max_backlog控制的是网卡接收队列的长度高并发收包场景下可以相应调大它能缓解短时突发流量导致的丢包。7. 常见问题与排查技巧这些坑我替你踩过了7.1 连接数上去了内存却爆了调优后并发连接从5万升到20万看起来是好事但如果你没有同步关注内存服务器很可能先挂掉。每一条TCP连接在内核里都有对应的socket结构体即使没有任何数据传输也要占几KB内存。20万连接就意味着几百MB甚至上GB的内核内存开销。再加上业务缓冲区、日志、线程栈内存很容易吃紧。排查时可以看ss -s它会汇总当前socket的使用情况能直观看到TCP连接总数以及各状态连接数量。如果发现total值异常高结合free -h看内存优先排查是不是连接回收过慢。7.2 调了参数不起作用先检查这些原因提示调优参数不生效是我被问得最多的问题这个优先级极其重要。三条常见原因供排查没有用sysctl -p或重启网络服务加载配置。改完/etc/sysctl.conf后不重载不会生效。执行sysctl -p才是真正加载。应用覆盖了内核参数。比如Nginx的worker_rlimit_nofile、Java的-XX:MaxOpenFiles、或者systemd服务里的LimitNOFILE这些应用层配置优先级高于全局limits改了系统参数仍然受限。容器环境里的参数是宿主机的。容器内的/etc/sysctl.conf通常不能修改内核参数即使修改也起不了作用。要么在宿主机调优并注意容器共享内核要么在容器编排层面设置ulimits。7.3 用tcpdump确认丢包发生在哪一层很多人对“丢包”判断很粗糙直接就说“网络有问题”。但丢包可能发生在网卡队列、协议栈缓冲区、应用socket缓冲区等多个层面。排查顺序建议看/proc/net/netstat里的ListenOverflows和ListenDrops对应全连接队列溢出次数。用netstat -s查看TCP层的重传、丢包计数。用tcpdump -i eth0 -nn port 8080抓包确认是否有SYN重传、乱序等。举个例子之前排查一个网关抖动问题ListenOverflows持续增长看起来是全连接队列满了但应用处理线程一点都不忙。后来才发现是应用把accept改成非阻塞之后代码里忘记了循环accept直到EAGAIN导致accept事件处理不彻底系统队列越积越满。这是一个典型的“不是调优能解决而是代码要修”的案例。内核参数只是给应用一个更大的缓冲池但它治不了应用层的效率问题。7.4 调优后还是要压测验证别凭感觉调每次改完参数我都建议用压测工具跑一轮对比。不要求压测多么专业但至少要做“改前/改后”对照。我习惯用wrk或ab分别测三个指标最大并发连接数、RPS每秒请求数、P99延迟。压测时有一个容易被忽视的坑压测机本身的网络参数也要同步调优。如果服务端调好了压测机的端口范围没调大客户端反而先成为瓶颈结果就是服务端参数看起来“没效果”。调优时两边都检查最忌讳只看服务端。8. 最后的实操心得关于调优这件事本身再分享一个我判断调优是否到位的思路。调优并不是“参数越大越好”而是在资源约束下找到一个稳定的平衡点。比如把tcp_fin_timeout调得太小虽然TIME_WAIT少了但极端情况下可能影响到TCP的可靠传输把缓冲区调得过大又有内存风险。每个参数都有其适用的场景和副作用懂原理比抄配置重要得多。我个人的习惯是每次调优只动一个变量压测验证后再动下一个。这样即使出问题也知道回退到哪一个步骤。切忌一次改几十个参数然后“看运气”这种调优方式在生产环境里非常危险。最后说一个小技巧。如果你不想在业务高峰期手动执行sysctl命令可以把这套参数写进系统启动脚本或者交付给配置管理工具统一分发。但无论怎么分发都建议先在预发环境跑一轮压测验证。毕竟内核参数这个东西部署错了不像普通代码一样能看到报错它是在你遇到高并发的那一瞬间才暴露出来到那时候再排查就有点晚了。