ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Linux TCP/IP协议栈调优实战:从三次握手到拥塞控制

Linux TCP/IP协议栈调优实战:从三次握手到拥塞控制 干了这么多年Linux运维和网络架构说真的能静下心来把TCP/IP协议栈吃透的人不多。大多数时候我们都在敲命令、调参数但真正让服务器性能产生质变的往往是那些藏在内核里、平时根本没人动的网络参数。这篇文章我想聊聊Linux下TCP/IP协议栈的深度调优从三次握手到拥塞控制把我在线上环境里踩过的坑、验证过的参数一次性讲清楚。无论你是刚入行的运维新人还是被线上延迟问题折磨的架构师这篇文章都值得你花十分钟读下去。我会用实际场景和数据说话告诉你每个参数到底什么时候该调、什么时候千万别碰。1. 拆解TCP连接建立的三个关键阶段1.1 三次握手的内核处理路径客户端发SYN服务器内核收到后会在半连接队列里创建一个SYN_RECV状态的条目同时向客户端回SYNACK。这个半连接队列的大小直接决定了服务器能承受多少并发连接压力。很多人在高并发场景下发现服务器丢SYN包第一反应是看防火墙、看带宽其实大多数时候问题出在内核的半连接队列和全连接队列上。半连接队列的大小由net.ipv4.tcp_max_syn_backlog控制默认是1024对现在的互联网业务来说这远远不够。全连接队列则是accept队列大小由应用层监听socket的backlog参数决定但内核里还有个net.core.somaxconn会限制它的上限。很多编程框架默认backlog是128而内核的somaxconn默认也是128但如果你用的是Nginx这类高并发软件它自己的backlog可能设到了4096却被内核的somaxconn拦在了128这就非常尴尬。1.2 半连接队列溢出时会发生什么当半连接队列满了内核会启动一个保护机制丢弃新的SYN包。但这会导致客户端一直收不到SYNACK客户端内核就会按指数退避重传SYN从1秒、2秒、4秒递增直到net.ipv4.tcp_syn_retries默认6次用尽。表现就是客户端建连非常慢甚至到最后直接超时失败。这里有个排查经验当服务器出现SYN丢包时在服务器上用ss -s看统计如果TCPLostData或者某个连接数指标异常飙升再用netstat -s | grep -i SYN看具体数值就能快速定位问题。1.3 全连接队列溢出的隐蔽性全连接队列溢出比较隐蔽因为系统不会报明显的错误只是新连接被静默丢弃。此时应用进程可能还在正常运行但客户端已经连不上了。用ss -ltn查看LISTEN状态的socket如果Send-Q那列显示的值非常接近实际队列最大值说明队列已经快满了。我遇到过一个真实案例某电商大促前一天接入层服务器频繁出现Connection reset by peer排查发现就是全连接队列溢出导致的。当时net.core.somaxconn设的是128Nginx的listen backlog设的4096但被内核上限卡死了。把这个值调成65535后问题立刻消失。2. 内核网络参数调优的四个核心维度2.1 TCP连接建立参数的精细配置先说三次握手阶段最关键的几个参数。net.ipv4.tcp_syn_retries控制客户端SYN重传次数默认6次总耗时约127秒。对于内部服务调用如果对端宕机了你等两分钟才知道是很痛苦的。我通常建议内部服务把它调小到3次这样7秒左右就能感知到对端故障方便快速触发熔断和切换。但公网服务不要乱调因为公网丢包率比内网高调小了可能误判故障。net.ipv4.tcp_synack_retries是服务器回SYNACK后等不到客户端的ACK时重传的次数。这个参数在SYN Flood攻击场景下很关键调太小会影响正常用户的弱网建连调太大会加重半连接队列的压力。生产环境一般保持默认5次同时配合SYN cookies机制来防攻击。net.ipv4.tcp_syncookies是个非常有意思的参数。它设为1时当半连接队列满了内核不再丢包而是计算一个cookie直接回SYNACK把这个连接记账下来等收到客户端ACK时再重建半连接。这样即使队列满了也能正常建立连接。但它有个副作用会跳过一些TCP选项协商比如时间戳、SACK对高延迟大带宽链路有轻微影响。所以我建议内网全链路可以设0公网入口设1。net.ipv4.tcp_abort_on_overflow这个参数默认是0意思是全连接队列满了时静默丢弃连接让客户端重试。如果设成1则直接回RST。有人觉得设1可以快速失败但我建议不要设1因为RST会让客户端认为是应用层错误有些编程框架会直接报异常不重试反而放大了故障。保持默认0让TCP层的重传机制来兜底更平滑。2.2 TCP缓冲区与窗口管理参数详解TCP的吞吐量很大程度上取决于发送窗口和接收窗口的大小。内核用net.ipv4.tcp_rmem和net.ipv4.tcp_wmem来管理收发缓冲区这两个参数各有三个值最小值、默认值、最大值。很多教程让直接把这三个值都调大这是很不负责任的。你想如果有1万个连接每个连接都用了最大缓冲区内存直接被打爆。正确的姿势是区分业务场景。对于高并发短连接比如API网关连接存活时间短缓冲区不需要大用默认值就行。对于长连接大流量传输比如视频推流、文件传输才需要调大。我习惯在内网文件传输服务器上设net.ipv4.tcp_wmem 4096 65536 16777216让默认值从16KB提到64KB最大到16MB但内核只有在实际流量需要时才会自动增长到高位不会一上来就全部占用。再说net.ipv4.tcp_mem这个参数的单位是页不是字节。它控制的是TCP协议栈全局能使用的内存页总数同样有三个阈值。看它的实时值可以直接读/proc/net/sockstat如果TCP mem那个值接近第三档说明TCP内存吃紧这时就算单个连接缓冲区够大整体也是堵的。net.ipv4.tcp_window_scaling是TCP窗口缩放因子开关默认1开启。RFC 1323里定义的窗口缩放能让TCP窗口从最大64KB扩展到1GB。99%的场景这个参数都不用动但有一种情况要注意某些老旧防火墙或者TCP/IP协议栈实现BUG的设备处理不了窗口缩放选项会出现明明带宽很大但传输速率卡在几Mbps的诡异现象。排查时可以先sysctl -w net.ipv4.tcp_window_scaling0做对比测试如果速率立刻上来了那问题在中间网络设备上而不是Linux服务器本身。2.3 TIME_WAIT与连接回收的高效处理TIME_WAIT这个状态日常接触最多的就是它。主动关闭连接的一方会进入TIME_WAIT默认要等2MSLMaximum Segment Lifetime一般是60秒。对于高并发的短连接服务如果主动关闭方是服务器自己就很容易积累几万个TIME_WAIT连接。针对TIME_WAIT业界的标准建议有三个方向。第一net.ipv4.tcp_fin_timeout默认60秒可以适当调小到30秒。但要注意2MSL的设计是为了防止旧连接的重复包干扰新连接如果调太小极端情况下确实可能误收旧包。我个人的压测经验是30秒对绝大多数业务足够安全但线上关键业务我一般不调。第二打开net.ipv4.tcp_tw_reuse。这个参数只能在客户端使用也就是作为发起连接的一方它的作用是让内核在安全条件下主动复用TIME_WAIT状态的连接。注意tcp_tw_reuse和tcp_tw_recycle是两个完全不同的东西。tcp_tw_recycle千万不要开它在NAT环境下会造成灾难性的建连失败Linux内核4.12之后已经把tw_recycle移除了。第三如果短连接量大到连tw_reuse都扛不住那就不应该用短连接了。改用连接池或者长连接把连接的创建和销毁频率降下来才是治本。我在为某支付通道做优化时就干过这事把单条HTTP短连接改成gRPC长连接TIME_WAIT瞬间清零性能提升了三倍。net.ipv4.ip_local_port_range这个参数也值得一提。客户端连接四元组里本地端口是随机分配的默认范围是32768到60999总共约2.8万个端口。如果客户端需要发起大量并发连接2.8万个端口很快就用尽了。论证一下就明白了例如一个抓取服务要同时开5万条连接默认端口范围根本不够用。我一般会把它调到1024 65535但要注意如果把起始端口调到1024意味着小于1024的端口不能被动态分配给客户端连接使用不过这通常不影响正常业务。2.4 拥塞控制算法与BBR的实测效果说到拥塞控制就不能不提BBR。传统的CUBIC算法是靠丢包来感知网络拥塞的它的逻辑很简单没丢包就线性增加发送速率一丢包就减半。这种策略在传统网络里很有效但在高带宽、长链路、随机丢包的环境里CUBIC会过于保守明明带宽够却因为少量丢包就把速率减半了造成吞吐量上不去。BBR的核心思路是放弃以丢包为拥塞信号改为周期性探测带宽上限和最小RTT让发送速率贴近链路真实极限。我用BBR做过的实测同一条跨国链路CUBIC的吞吐量是42Mbps换成BBR后直接飙到118Mbps丢包率没变就只换了算法。切换方法很简单# 查看当前可用的拥塞控制算法 sysctl net.ipv4.tcp_available_congestion_control # 启用BBR内核4.9才支持 modprobe tcp_bbr sysctl -w net.core.default_qdiscfq sysctl -w net.ipv4.tcp_congestion_controlbbr但BBR不是银弹。在内网低延迟、无拥塞的环境里BBR和CUBIC的差距其实很小。而在地面链路质量不稳定的场景中BBR会激进地占用带宽可能会挤压其他业务的TCP连接。我做过多租户共享出口的场景BBR开在下载服务上其他业务的延迟明显变高。这种情况我后来用Cake队列配合限速才解决。BBR还有一个升级版叫BBRv2它加入了对丢包和RTT更精细的响应机制但内核主线还没默认集成生产使用需要编译定制内核我目前只在边缘节点上做过实验还没敢大规模上生产。3. 实战从发现问题到参数落地的完整过程3.1 一次典型的SYN丢包排查实录某天深夜监控系统发来告警业务接口P99延迟从80ms飙升到2000ms以上。登录服务器先用ss -lnt查看监听队列$ ss -lnt State Recv-Q Send-Q Local Address:Port Peer Address:Port LISTEN 1024 4096 0.0.0.0:8080 0.0.0.0:*Recv-Q 1024说明全连接队列里积压了1024个连接而且这个数值接近Send-Q4096的四分之一意味着队列快满了。再用netstat -s看整体TCP统计$ netstat -s | grep -A5 TCP 12345 times the listen queue of a socket overflowed 5678 SYNs to LISTEN sockets droppedsocket overflowed和SYNs to LISTEN dropped这两个计数在持续增长确认就是握手队列的问题。检查内核参数果然net.core.somaxconn还是默认的128。处理过程# 调大全连接队列上限 echo 65535 /proc/sys/net/core/somaxconn echo 65535 /proc/sys/net/ipv4/tcp_max_syn_backlog # 同时把Nginx的backlog也调上去 vi /etc/nginx/nginx.conf # listen 8080 backlog65535; # 刷新生效 sysctl -p systemctl reload nginx改完之后再观察Recv-Q马上降到了两位数P99延迟恢复正常。这次事故其实只花了十分钟就定位了原因是白天发了一个新版本把Nginx配置里的worker_connections调大了但access层的内核参数没跟着调导致阻塞点从用户态转移到了内核态。所以说调优要成体系地调单点调参很容易顾此失彼。3.2 长连接中断与TCP KeepAlive的坑另一个高频问题是长连接莫名其妙被断开。很多人第一时间想到TCP KeepAlive以为设置了就能保活。内核里有三个跟KeepAlive相关的参数net.ipv4.tcp_keepalive_time空闲多久开始探测默认7200秒2小时net.ipv4.tcp_keepalive_intvl每次探测的间隔默认75秒net.ipv4.tcp_keepalive_probes连续探测失败几次后判定连接断开默认9次按默认值算一个空闲连接从开始探测到最终判定失败最长要7200 75*9 7875秒超过两个小时。对很多业务来说这个时间太长了数据库连接池如果依赖KeepAlive来剔除坏连接等到它反应过来业务早就超时了。我一般会把这三个参数调成net.ipv4.tcp_keepalive_time 600 net.ipv4.tcp_keepalive_intvl 30 net.ipv4.tcp_keepalive_probes 3也就是10分钟空闲开始探测每30秒一次试三次失败就断开最迟15分钟能感知到连接异常。这套参数对绝大多数内网服务适用但要注意如果应用层本身有心跳机制比如gRPC的keepalive、WebSocket的ping/pong那TCP层的KeepAlive其实可以不用调那么激进两层叠加反而会增加无谓的探测包。最好的实践是应用层有心跳就依赖应用层TCP层KeepAlive保持默认或适度调小即可。3.3 网卡队列与RPS/RSS的联动调优TCP调优依赖的不只是协议栈参数还有网卡的中断分发。多队列网卡开了RSSReceive Side Scaling让不同队列绑定不同CPU核。但Linux还有个软件层的东西叫RPSReceive Packet Steering可以在网卡不支持RSS时用软件方式把收包中断按哈希分发到多个CPU上。先看看你的网卡中断亲和性cat /proc/interrupts | grep eth0如果发现所有收包中断都打在一个CPU核上比如CPU0的数字大到离谱就是典型的单核瓶颈。这时要么调网卡队列的RPS配置要么确认是否正确设置了smp_affinity_list。一个线上经验某缓存集群出现流量小时段CPU全核空闲但单一网卡中断核已达到100%的诡异现象。平均CPU使用率只有20%但整个服务吞吐上不去每秒5万QPS。后来把网卡队列的中断从CPU0分散到CPU0-7QPS直接翻倍到了10万。TCP这层栈再优秀如果收包都卡在一个核上整体性能永远上不去。RPS的配置示例适用于网卡不支持RSS的情况# 把eth0收到的包hash到CPU0-7 echo 0xff /sys/class/net/eth0/queues/rx-0/rps_cpus注意RPS对跨NUMA节点会有缓存一致性开销最稳的做法是只把RPS绑定到和网卡同NUMA node的CPU上用lscpu查看。4. 调优参数的总览测试方法4.1 压测时如何准确判断瓶颈在哪一层调参之后怎么验证效果不能只看业务侧延迟要用压测工具和系统观测工具配合来判断。我用iperf3做过纯TCP带宽测试这能排除应用层干扰直接看到内核TCP协议栈能达到的极限。注意命令要双向打TCP调优不能只看单向因为收方向、发方向占用的内核路径不同瓶颈可能只在某一个方向。# 服务端 iperf3 -s -p 5201 # 客户端跑60秒打满带宽 iperf3 -c 192.168.1.10 -p 5201 -t 60 -P 8-P 8表示8个并发流如果8个流的总体带宽比单流带宽大很多说明单流上限受限于协议栈的窗口或CPU单核处理能力。此时再去调整tcp_rmem/wmem这类参数效果会好很多。同时用sar -n DEV 1持续观察网卡吞吐和错误包用mpstat -P ALL 1看内核占比用perf top看是不是tcp_v4_rcv这种函数占了太多CPU。如果softirq软中断CPU占比接近100%那瓶颈在网络协议栈的处理上光调TCP参数解决不了得往网卡多队列和RPS方向使劲。4.2 一份可直接借鉴的内核参数配置文件我把自己生产环境里验证过、运行稳定的TCP调优参数整理成一份/etc/sysctl.d/99-tcp-tuning.conf供你参考# 连接建立 net.ipv4.tcp_syn_retries 3 net.ipv4.tcp_synack_retries 3 net.ipv4.tcp_max_syn_backlog 65535 net.core.somaxconn 65535 net.ipv4.tcp_syncookies 1 net.ipv4.tcp_abort_on_overflow 0 # 缓冲区 net.ipv4.tcp_rmem 4096 87380 16777216 net.ipv4.tcp_wmem 4096 65536 16777216 net.ipv4.tcp_mem 786432 1048576 1572864 net.ipv4.tcp_adv_win_scale 2 # 连接回收 net.ipv4.tcp_fin_timeout 30 net.ipv4.tcp_tw_reuse 1 net.ipv4.ip_local_port_range 1024 65535 # KeepAlive net.ipv4.tcp_keepalive_time 600 net.ipv4.tcp_keepalive_intvl 30 net.ipv4.tcp_keepalive_probes 3 # 拥塞控制 net.ipv4.tcp_congestion_control bbr net.core.default_qdisc fq注意net.ipv4.tcp_mem那组数字的单位是内存页一般是4KB也就是字节数的1/4096要根据你的服务器内存实际来换算。你服务器总内存32GB时可以这么评估三个值对应大约3GB、4GB、6GB的TCP协议栈内存上限对大多数中大型业务是够用的。这份配置不是让你无脑照抄内存大小、业务类型不同参数必须有差异化。比如一个小内存的容器实例把这套配置放进去TCP内存上限反而会增加OOM风险。4.3 观察调优效果的关键指标调完参数不是看一眼延迟降低了就完事得系统性观察。我用ss -s看整体连接状态用netstat -s | grep -E overflow|drop|retrans看丢包和重传再用vmstat和dstat确认CPU和内存没有异常。一个实用的观察逻辑是调优前先记录一组基线数据包括建连成功率、握手平均耗时、吞吐量、重传率。调优后再同口径测一次。如果只看到延迟优化但重传率上升那说明配置过于激进需要回退。我定的安全阈值是重传率不超过0.1%建连成功率在99.99%以上延迟P99和P50的比值不超过3倍。这三个指标都正常调优才算真正成功。5. 常见误区与我的经验总结5.1 那些年我们信过的错误调优建议网上一搜TCP调优能看到很多过时甚至错误的建议。我逐个说下我的看法。tcp_tw_recycle必须拒绝。它会在同一个源IP下根据时间戳判断是否接受SYN包。现在大量用户走NAT出口几十个用户共享同一个公网IP有的设备时间戳可能比我这边还先进一点结果就是这些用户的SYN包被内核直接丢弃。当年某论坛频繁出现有些用户能连、有些用户死活连不上的问题查了几天最后发现就是某台服务器开了tw_recycle。Linux 4.12之后这个参数已经删除了如果你还在维护老内核记得确认它没被打开。tcp_max_syn_backlog不是越大越好。这个队列里存的都是半连接每个要占用不少内存。如果设到几百万遇到SYN Flood攻击时内存直接被打满。正确姿势是配合tcp_syncookies使用让队列大小作为一个缓冲而不是唯一防线。net.core.rmem_max和wmem_max调太高也有坑。有的建议直接设为16MB甚至64MB但对单机几万并发连接来说一旦每个连接都冲到这个上限内存消耗是不可接受的。这两个参数只是上限不是默认值如果应用主动调了SO_RCVBUF才会被限制到这个值。合理方式是根据业务并发连接数和平均缓冲区用量来综合评估。5.2 因地制宜不同业务场景的参数差异化互动聊天类长连接特点是连接数多、单连接速率低重点关注文件描述符和内存占用TCP缓冲区不要调大KeepAlive可以适度调短方便快速清理死连接。CDN节点和视频转发特点是大流量、大窗口需要调大tcp_rmem/wmem开BBR同时开启tcp_window_scaling。我做过的实测里光是把tcp_rmem默认值从87KB提到256KB某视频源的峰值吞吐就从300Mbps提升到了1.2Gbps。高并发短连接网关特点是建连频率极高重点处理TIME_WAIT打开tcp_tw_reuse调小tcp_fin_timeout同时配合连接池降低建连频率。数据库内部网络低延迟敏感、但不希望TCP过度占用带宽。这类场景不建议开BBR保持CUBIC即可同时可以调小缓冲区避免内核为单条查询缓存过多数据。5.3 调优之后的下一步应该看什么TCP调优到了后期你会遇到瓶颈不再出现在协议栈本身的情况。网卡中断、CPU亲和性、内存带宽、锁竞争这些都是新的瓶颈点。我曾经优化一台对象存储节点把TCP参数调了个遍性能只能到每秒4万请求后来发现是网卡单队列的中断都挤在CPU0上调整RSS后直接翻倍。所以TCP调优从来不只是修改sysctl.conf的事它是整个内核网络路径的系统工程。另外一个方向是深入理解应用socket编程接口。TCP调优的天花板最终取决于应用是否合理使用了Nagle算法、延时ACK、非阻塞IO、epoll这些机制。我在分析P99延迟时发现很多TCP层问题其实是应用层一次read只读了一个小包导致的内核和用户态上下文切换暴增。最后想说的是TCP调优不能只靠文档和参数备一台测试机压测不出来的东西放在生产环境用10%的灰度流量慢慢试探才能找到最稳的阈值。你踩进去了才能真正理解什么叫从三次握手到拥塞控制的完整闭环。
返回列表