1. 为什么TCP内核参数值得花时间调干运维这行十几年有一个现象我观察了很久绝大多数线上服务的性能瓶颈最后排查下来都跟TCP协议栈的默认配置脱不了干系。你可能会说Linux内核的默认参数已经足够好了确实对于日活几百的小站点默认值完全够用。但一旦你的服务开始扛几千甚至上万的并发连接或者跨机房传输大量数据默认参数就会变成一堵看不见的墙。我印象很深的一次经历是帮一个朋友排查问题。他们做的是文件同步服务用户反馈上传大文件特别慢尤其是跨地域的场景。一开始他们怀疑是带宽不够加了带宽效果不明显又怀疑是应用层代码效率低折腾了一轮代码优化提升也很有限。最后我上去看了一眼内核参数net.ipv4.tcp_wmem和net.ipv4.tcp_rmem用的还是内核默认值发送缓冲区最大才4MB接收缓冲区也差不多。跨地域传输的带宽时延积远大于这个值TCP窗口根本撑不满带宽利用率上不去。改了几个参数之后同样的硬件和带宽条件下传输速度直接翻了一倍多。这就是TCP内核参数优化的价值所在。它不需要你改一行业务代码不需要加机器不需要换更好的网卡只需要理解几个关键参数的含义根据自己的业务场景做针对性调整就能拿到立竿见影的效果。当然前提是你得知道每个参数在控制什么改大了会有什么副作用改小了又会卡在哪里。这篇文章我会把Linux TCP内核参数优化这件事从头到尾讲清楚。不管你是刚接触Linux运维的新手还是已经有一定经验但想系统梳理一遍的老手都能从中找到可以直接用的东西。我会先讲清楚TCP协议栈里哪些参数在什么场景下会成为瓶颈然后逐个拆解核心参数的含义和调整逻辑接着给出不同业务场景下的参数组合方案最后分享一些我实际踩过的坑和排查技巧。2. TCP内核参数的整体调优思路2.1 先搞清楚你的业务属于哪种类型TCP参数优化最忌讳的一件事就是照搬别人的配置。网上流传着各种“万能优化脚本”把几十个参数一股脑全改了结果可能适得其反。因为不同的业务场景对TCP协议栈的需求是完全不同的。我一般把业务分成三大类来看。第一类是高并发短连接场景比如Web服务、API网关特点是连接数多但每个连接传输的数据量小连接建立和断开的频率极高。这类场景的核心矛盾在于连接队列够不够大、端口够不够用、TIME_WAIT状态会不会堆积。第二类是大流量长连接场景比如文件传输、视频推流、数据库主从同步特点是连接数不多但每个连接持续传输大量数据。这类场景的核心矛盾在于缓冲区够不够大、拥塞控制算法是否合适、窗口缩放有没有开启。第三类是低延迟交互场景比如实时通信、在线游戏、金融交易系统特点是数据量不大但对延迟极其敏感。这类场景的核心矛盾在于Nagle算法要不要关、延迟确认要不要关、拥塞控制的初始窗口够不够大。你得先明确自己的业务属于哪一类或者哪几类的混合然后才能有针对性地调整参数。一个视频推流服务和一个API网关它们的优化方向几乎是相反的。2.2 调优的三个层次我把TCP内核参数优化分成三个层次从浅到深依次是连接管理层面、数据传输层面、拥塞控制层面。连接管理层面解决的是“能不能连上”和“连上之后能不能快速释放”的问题。核心参数包括半连接队列大小、全连接队列大小、TIME_WAIT状态的回收策略、本地端口范围等。这一层的调整相对安全副作用小效果也最直观。如果你发现服务在高并发下出现连接超时、连接被拒绝优先从这一层入手。数据传输层面解决的是“连上之后传得快不快”的问题。核心参数包括发送缓冲区、接收缓冲区、窗口缩放因子、TCP分段卸载等。这一层的调整需要你对带宽时延积有基本的计算能力缓冲区设小了带宽跑不满设大了浪费内存还可能增加延迟。拥塞控制层面解决的是“网络拥堵时怎么优雅地降速和恢复”的问题。核心参数包括拥塞控制算法的选择、初始拥塞窗口大小、快速重传和快速恢复的开关等。这一层最复杂但也最能体现调优的深度。不同的拥塞控制算法适用于不同的网络环境选对了算法同样的网络条件下吞吐量能差出好几倍。2.3 调优前必须做的两件事在动手改任何参数之前有两件事必须先做。第一件事是建立基线。你得知道当前系统的TCP运行状态是什么样的才能判断调优有没有效果。我通常会收集这些指标当前活跃连接数、TIME_WAIT连接数、SYN_RECV队列长度、Accept队列溢出次数、TCP重传率、带宽利用率。这些数据可以通过ss、netstat、sar等工具获取后面我会详细讲怎么采集。第二件事是确认内核版本。不同版本的Linux内核TCP参数的默认值和可用选项都不一样。比如tcp_tw_recycle这个参数在4.12之后的内核里已经被移除了tcp_notsent_lowat是3.6之后才有的。你得先知道自己用的是什么内核才能确定哪些参数可以调。用uname -r看一眼就行。注意生产环境调参一定要先在测试环境验证确认没问题再上生产。而且每次只改一到两个参数改完观察至少24小时确认稳定后再改下一批。一次性改一堆参数出了问题你根本不知道是哪个参数导致的。3. 连接管理类核心参数拆解3.1 半连接队列和全连接队列TCP三次握手的过程中内核维护了两个队列。客户端发来SYN包服务端回复SYNACK这时候连接进入SYN_RECV状态被放入半连接队列也叫SYN队列。客户端回复ACK之后连接进入ESTABLISHED状态被移入全连接队列也叫Accept队列等待应用程序调用accept()取走。这两个队列的长度直接决定了服务能承受多大的并发连接建立请求。如果半连接队列满了新的SYN包会被丢弃客户端会超时重试如果全连接队列满了内核默认行为是丢弃客户端的ACK客户端会认为连接没建立成功也会重试。控制半连接队列大小的参数是net.ipv4.tcp_max_syn_backlog默认值通常是1024或2048。控制全连接队列大小的参数是net.core.somaxconn默认值通常是128或1024。注意全连接队列的实际大小还受应用程序调用listen()时传入的backlog参数影响取两者中的较小值。我一般会把这两个值都调到65535。有人可能会担心内存问题实际上这两个队列占用的内存非常有限每个半连接大约占用几百字节65535个半连接也就几十MB。相比连接被丢弃导致的业务损失这点内存开销完全可以接受。# 查看当前值 sysctl net.ipv4.tcp_max_syn_backlog sysctl net.core.somaxconn # 临时修改 sysctl -w net.ipv4.tcp_max_syn_backlog65535 sysctl -w net.core.somaxconn65535 # 永久修改写入配置文件 echo net.ipv4.tcp_max_syn_backlog 65535 /etc/sysctl.conf echo net.core.somaxconn 65535 /etc/sysctl.conf sysctl -p还有一个相关的参数是net.ipv4.tcp_syncookies。当半连接队列满了的时候如果开启了syncookies内核会通过cookie机制来验证连接而不是直接丢弃。这个参数默认是1开启我建议保持开启。它不会影响正常情况下的连接建立只在队列满的时候起作用相当于一个保险。3.2 TIME_WAIT状态的管理TIME_WAIT是TCP连接主动关闭方在发送最后一个ACK之后进入的状态持续2MSL通常60秒。这个状态存在的意义是确保最后一个ACK能被对方收到以及让本次连接的残留数据包在网络中消散避免影响后续使用相同四元组的新连接。问题在于对于高并发短连接的服务TIME_WAIT状态会大量堆积。假设你的服务每秒处理1000个请求每个请求都是一个短连接那么每秒就会产生1000个TIME_WAIT60秒就是60000个。这些TIME_WAIT会占用本地端口资源当端口耗尽时新的连接就无法建立了。处理TIME_WAIT有几个方向。第一个方向是减少TIME_WAIT的产生让服务端不要主动关闭连接而是让客户端先关。但这在很多场景下不可控比如HTTP/1.0协议默认就是服务端关闭连接。第二个方向是加快TIME_WAIT的回收通过调整内核参数来缩短TIME_WAIT的持续时间或者允许复用TIME_WAIT状态的连接。这里涉及几个参数。net.ipv4.tcp_tw_reuse控制是否允许将TIME_WAIT状态的连接重新用于新的 outgoing 连接默认是0关闭。开启后内核会在安全的前提下复用TIME_WAIT连接这个“安全”的判断是基于时间戳的所以需要同时开启net.ipv4.tcp_timestamps。我建议开启这个参数它只影响outgoing连接对incoming连接没有影响风险很低。net.ipv4.tcp_max_tw_buckets控制TIME_WAIT连接的最大数量默认值通常是180000。当超过这个数量时新的TIME_WAIT会被立即销毁并打印警告。这个值可以适当调大比如调到262144或524288但不要无限调大否则会占用大量内存。# 开启TIME_WAIT复用 sysctl -w net.ipv4.tcp_tw_reuse1 sysctl -w net.ipv4.tcp_timestamps1 # 调大TIME_WAIT最大数量 sysctl -w net.ipv4.tcp_max_tw_buckets262144注意网上有些老文章会推荐开启net.ipv4.tcp_tw_recycle这个参数在NAT环境下会导致连接异常而且在Linux 4.12之后已经被移除了。如果你还在用老版本内核也强烈建议不要开启这个参数。3.3 本地端口范围对于需要主动发起大量outgoing连接的服务比如反向代理、爬虫、消息推送服务本地端口范围是一个容易被忽略的瓶颈。Linux默认的本地端口范围是32768到60999总共约28000个端口。如果TIME_WAIT堆积加上并发连接数很高端口很容易耗尽。调整net.ipv4.ip_local_port_range可以扩大端口范围。我一般会设成1024到65535这样可用端口数增加到约64000个。有人担心1024以下的端口被占用会影响系统服务实际上ip_local_port_range只控制自动分配的端口范围系统服务监听的端口是固定的不会冲突。sysctl -w net.ipv4.ip_local_port_range1024 655354. 数据传输类核心参数拆解4.1 发送缓冲区和接收缓冲区TCP的发送缓冲区和接收缓冲区是影响吞吐量最直接的因素。发送缓冲区存放的是应用程序写入但还没发送出去的数据接收缓冲区存放的是收到但还没被应用程序读取的数据。这两个缓冲区的大小决定了TCP窗口的最大值而TCP窗口决定了在等待ACK确认之前能发送多少数据。带宽时延积BDP是计算缓冲区大小的关键指标。BDP 带宽 × 往返时延。假设你的服务部署在北京和上海之间往返时延大约30ms带宽是1Gbps那么BDP 1Gbps × 30ms 125MB × 0.03 3.75MB。这意味着发送缓冲区至少要有3.75MB才能填满这条链路的带宽。如果缓冲区小于这个值TCP窗口就撑不满带宽利用率就会下降。Linux提供了三个参数来控制发送缓冲区net.ipv4.tcp_wmem格式是“最小 默认 最大”。默认值通常是4096 16384 4194304也就是最大4MB。接收缓冲区对应的是net.ipv4.tcp_rmem默认值通常是4096 131072 6291456最大约6MB。对于跨地域传输或者高带宽场景我一般会把最大值调到16MB甚至32MB。但要注意缓冲区不是越大越好。缓冲区越大内存占用越高而且在网络出现拥塞时大缓冲区会导致“缓冲区膨胀”问题延迟会显著增加。所以需要根据实际场景权衡。# 查看当前值 sysctl net.ipv4.tcp_wmem sysctl net.ipv4.tcp_rmem # 调整发送和接收缓冲区 sysctl -w net.ipv4.tcp_wmem4096 65536 16777216 sysctl -w net.ipv4.tcp_rmem4096 131072 16777216另外还有两个全局参数net.core.wmem_max和net.core.rmem_max它们限制了应用程序通过setsockopt()能设置的最大缓冲区大小。如果你在应用程序层面也调整了缓冲区需要确保这两个全局参数足够大。4.2 窗口缩放因子TCP头部的窗口字段只有16位最大能表示65535字节的窗口。但在高带宽高延迟的网络中65535字节的窗口远远不够用。为了解决这个问题TCP引入了窗口缩放选项通过一个缩放因子将窗口值左移若干位最大可以表示1GB的窗口。窗口缩放是在三次握手时协商的一旦协商完成就不能更改。Linux内核会根据接收缓冲区的最大值自动计算缩放因子所以只要你把tcp_rmem的最大值调大了窗口缩放因子会自动调整。但有一个参数net.ipv4.tcp_window_scaling控制是否启用窗口缩放默认是1启用我建议保持启用。4.3 TCP分段卸载和通用接收卸载这两个是网卡层面的卸载技术。TCP Segmentation OffloadTSO把TCP分段的工作交给网卡来做减轻CPU负担Generic Receive OffloadGRO把多个小包合并成一个大包再交给协议栈处理减少协议栈的处理次数。这两个参数通常默认就是开启的可以通过ethtool查看。在高流量场景下它们能显著降低CPU使用率。但在某些虚拟化环境或者特定的网络设备下它们可能会导致问题比如包被合并后延迟增加。如果你发现网络延迟异常但带宽正常可以尝试关闭GRO看看。# 查看网卡的TSO和GRO状态 ethtool -k eth0 | grep -E tso|gro # 关闭GRO ethtool -K eth0 gro off5. 拥塞控制类核心参数拆解5.1 拥塞控制算法的选择Linux内核支持多种拥塞控制算法常见的有CUBIC、BBR、Reno、Vegas等。不同的算法适用于不同的网络环境。CUBIC是Linux的默认算法它使用三次函数来控制拥塞窗口的增长在高带宽长距离网络中表现不错但在有丢包的网络中会过度降速。BBR是Google提出的算法它不依赖丢包来判断拥塞而是通过测量带宽和往返时延来调整发送速率在有丢包的网络中表现更好。Reno是经典算法现在基本不用了。我一般会这样选如果是内网环境丢包率极低CUBIC就够了如果是公网环境尤其是跨国或跨运营商的链路丢包率可能在1%到5%之间BBR的优势非常明显。我实测过一条跨地域的链路CUBIC下吞吐量只有200Mbps左右换成BBR之后直接跑到800Mbps以上。# 查看当前可用的拥塞控制算法 sysctl net.ipv4.tcp_available_congestion_control # 查看当前使用的算法 sysctl net.ipv4.tcp_congestion_control # 切换为BBR sysctl -w net.ipv4.tcp_congestion_controlbbr # 永久生效 echo net.ipv4.tcp_congestion_control bbr /etc/sysctl.conf注意BBR需要内核4.9及以上版本才支持。如果你的内核版本较低需要先升级内核。另外BBR和CUBIC可以共存你可以通过tcp_congestion_control参数在两者之间切换不需要重启系统。5.2 初始拥塞窗口TCP连接建立后发送方并不是一开始就能全速发送的而是从一个较小的拥塞窗口开始逐步增大。这个初始窗口的大小直接影响短连接的性能。如果初始窗口太小短连接可能需要好几个往返才能传完数据延迟就会增加。Linux 3.x之后的默认初始拥塞窗口是10个MSS约14KB这个值对于大多数场景已经够用了。但在某些低延迟高带宽的场景下可以适当调大。参数是net.ipv4.tcp_init_cwnd不过这个参数在很多内核版本中并不直接暴露需要通过路由表或者应用程序层面来设置。5.3 快速重传和快速恢复当发送方收到三个重复的ACK时会触发快速重传立即重传丢失的报文而不需要等待超时。这个机制由net.ipv4.tcp_fastopen和net.ipv4.tcp_sack等参数控制。net.ipv4.tcp_sack控制是否启用选择性确认默认是1启用。SACK允许接收方告诉发送方哪些报文已经收到了这样发送方只需要重传真正丢失的报文而不是重传之后的所有报文。在高丢包环境下SACK能显著提升吞吐量建议保持启用。net.ipv4.tcp_fastopen控制TCP Fast Open功能允许在三次握手的SYN包中携带数据减少一个往返的延迟。这个功能需要客户端和服务端同时支持目前支持度还不够广泛可以开启但不要依赖它。# 启用SACK sysctl -w net.ipv4.tcp_sack1 # 启用TCP Fast Open客户端和服务端都开启 sysctl -w net.ipv4.tcp_fastopen36. 不同业务场景的参数组合方案6.1 高并发短连接场景这类场景的典型代表是Web服务和API网关。核心矛盾是连接建立和释放的频率极高TIME_WAIT堆积和队列溢出是主要问题。我的参数组合是这样的# 连接队列 net.core.somaxconn 65535 net.ipv4.tcp_max_syn_backlog 65535 # TIME_WAIT管理 net.ipv4.tcp_tw_reuse 1 net.ipv4.tcp_timestamps 1 net.ipv4.tcp_max_tw_buckets 262144 # 端口范围 net.ipv4.ip_local_port_range 1024 65535 # 缓冲区短连接不需要太大 net.ipv4.tcp_wmem 4096 16384 4194304 net.ipv4.tcp_rmem 4096 131072 6291456 # 拥塞控制 net.ipv4.tcp_congestion_control cubic net.ipv4.tcp_sack 1这套配置的核心思路是把队列开大避免连接被丢弃开启TIME_WAIT复用加快端口回收缓冲区保持默认或略调因为短连接传输的数据量小大缓冲区用不上还浪费内存。6.2 大流量长连接场景这类场景的典型代表是文件传输、视频推流、数据库同步。核心矛盾是带宽利用率缓冲区大小和拥塞控制算法是关键。# 缓冲区根据BDP计算 net.ipv4.tcp_wmem 4096 65536 33554432 net.ipv4.tcp_rmem 4096 131072 33554432 net.core.wmem_max 33554432 net.core.rmem_max 33554432 # 窗口缩放 net.ipv4.tcp_window_scaling 1 # 拥塞控制公网环境用BBR net.ipv4.tcp_congestion_control bbr # SACK net.ipv4.tcp_sack 1 # 队列不需要太大连接数不多 net.core.somaxconn 4096 net.ipv4.tcp_max_syn_backlog 4096这套配置的核心思路是缓冲区开到32MB确保能填满高带宽长距离链路的BDP拥塞控制用BBR在有丢包的公网环境下保持高吞吐队列保持适中即可因为长连接的连接数不会太多。6.3 低延迟交互场景这类场景的典型代表是实时通信、在线游戏、金融交易。核心矛盾是延迟Nagle算法和延迟确认是主要优化对象。# 关闭Nagle算法应用程序层面设置TCP_NODELAY # 内核层面可以调整延迟确认 net.ipv4.tcp_delack_min 1 # 初始拥塞窗口调大减少慢启动时间 # 需要通过路由或应用层设置 # 缓冲区适中避免缓冲区膨胀 net.ipv4.tcp_wmem 4096 16384 4194304 net.ipv4.tcp_rmem 4096 16384 4194304 # 拥塞控制用CUBIC或BBR net.ipv4.tcp_congestion_control cubic # 开启低延迟模式 net.ipv4.tcp_low_latency 1这套配置的核心思路是缓冲区不要太大避免数据在缓冲区里排队导致延迟增加关闭Nagle算法让小包立即发送延迟确认时间调短加快ACK的回复速度。7. 常见问题排查与避坑经验7.1 连接被拒绝或超时如果你发现服务在高并发下出现连接被拒绝或超时第一件事是检查队列溢出情况。用netstat -s | grep -i listen可以查看全连接队列的溢出次数用netstat -s | grep -i syn可以查看半连接队列的溢出次数。如果溢出次数在持续增长说明队列太小需要调大somaxconn和tcp_max_syn_backlog。还有一个容易被忽略的点是应用程序的accept速度。如果应用程序处理accept的速度跟不上连接建立的速度即使队列开得再大也会溢出。这种情况下需要优化应用程序的并发模型比如使用多线程或多进程来accept。7.2 带宽跑不满带宽跑不满的原因有很多需要逐层排查。首先确认物理链路没问题用ethtool查看网卡速率和双工模式。然后检查TCP重传率用netstat -s | grep retrans查看重传次数如果重传率超过1%说明网络质量有问题。接着检查缓冲区大小用ss -tin查看当前连接的窗口大小和缓冲区使用情况。最后检查拥塞控制算法尝试切换到BBR看看效果。我遇到过一次带宽跑不满的情况排查了很久才发现是网卡的TSO功能在虚拟化环境下有bug导致大包被错误地分段。关闭TSO之后问题就解决了。所以如果所有参数都调了还是不行不妨试试关闭网卡的卸载功能。7.3 TIME_WAIT堆积导致端口耗尽TIME_WAIT堆积是短连接服务的常见问题。除了调整内核参数之外还可以从架构层面优化。比如使用连接池来复用连接而不是每个请求都新建连接或者在服务端和客户端之间加一层反向代理由代理来管理连接的生命周期。如果TIME_WAIT已经堆积了很多可以用ss -s查看统计信息用ss -tan state time-wait | wc -l查看具体数量。如果数量超过tcp_max_tw_buckets内核会开始强制回收并打印警告这时候需要调大这个参数或者加快TIME_WAIT的回收速度。7.4 参数调整后没有效果有时候你改了参数但发现没有效果。最常见的原因是参数没有真正生效。sysctl -w是临时修改重启后会丢失写入/etc/sysctl.conf后需要执行sysctl -p才会生效。另外有些参数是只读的或者被其他配置覆盖了需要仔细检查。还有一个原因是应用程序层面的设置覆盖了内核参数。比如应用程序通过setsockopt()设置了SO_SNDBUF那么这个连接的实际发送缓冲区就是应用程序设置的值而不是内核参数的值。这种情况下需要同时调整应用程序的配置。7.5 常见问题速查表问题现象可能原因排查命令解决方向连接被拒绝全连接队列溢出netstat -s | grep listen调大somaxconn连接超时半连接队列溢出netstat -s | grep syn调大tcp_max_syn_backlog带宽跑不满缓冲区太小ss -tin调大tcp_wmem/tcp_rmem带宽跑不满拥塞控制不合适sysctl tcp_congestion_control切换为BBR延迟高缓冲区膨胀ss -tin调小缓冲区延迟高Nagle算法应用程序设置开启TCP_NODELAY端口耗尽TIME_WAIT堆积ss -tan state time-wait | wc -l开启tcp_tw_reuse参数不生效配置未加载sysctl -a | grep 参数名执行sysctl -p8. 我个人的调优体会调了这么多年TCP参数我最大的体会是不要为了调而调。很多参数在内核默认值下已经工作得很好盲目调整反而会引入问题。每次调整之前先问自己三个问题当前系统的瓶颈在哪里这个参数控制的是什么调整之后可能带来什么副作用另外一个体会是监控比调优更重要。你得先能看清楚系统的运行状态才能判断哪里需要调。我建议每个运维人员都熟练掌握ss、netstat、sar、ethtool这几个工具它们能帮你快速定位大部分网络问题。最后分享一个小技巧如果你不确定某个参数该设多大可以先设一个比较激进的值然后观察系统的内存使用和延迟变化再逐步回调到合适的值。比如缓冲区可以先设成64MB如果发现内存占用太高或者延迟明显增加再降到32MB或16MB。这种“从大到小”的调法比“从小到大”更容易找到最优值因为大缓冲区带来的问题通常比小缓冲区更容易观察到。还有一点调参之后一定要做压力测试。我见过太多人改完参数就直接上生产结果流量一上来就出问题。用wrk、ab、iperf这些工具在测试环境模拟真实流量观察各项指标的变化确认稳定后再上生产。这一步花的时间远比出了问题再回滚要少得多。
阅读完成 · 觉得有帮助?