线上跑着跑着,监控突然报连接建立失败率上升,SSH 上去dmesg一刷,满屏都是nf_conntrack: table full, dropping packet。这种场景做系统运维的多少都碰过。Linux Netfilter 内核参数调优这件事,说大不大,说小不小——它不像换框架、加机器那样立竿见影,但一旦踩到坑,排查成本极高,因为现象往往表现为"偶发丢包""连接时通时不通",日志里还不一定留痕。这篇文章想聊的,就是这套参数体系到底怎么理解、哪些该动、动到什么程度、动完之后怎么验证。
我把它定位成一份"能直接照着做"的调优笔记:前面讲清楚 Netfilter 在数据包路径上的位置和参数的生效逻辑,中间给出容量测算方法和三类典型场景的配置模板,后面是这几年在各种机器上踩过的坑和对应的排查路径。不管你是刚开始接触 Linux 网络调优,还是已经能背下几个 sysctl 名字但不确定该填多少,应该都能从里面拿到能用的东西。
需要提前说明一点:Netfilter 参数没有"标准答案",同一组数值在 Web 服务器上合适,搬到 NAT 网关上可能就是灾难。所以我会尽量把每个参数的推导过程写出来,而不是只丢一个数字让你抄。抄数字容易,理解为什么是这个数字,才能在你自己的机器上算出合适的值。
1. Netfilter 在数据包路径上的位置,决定了哪些参数值得动
聊参数之前,得先知道参数作用在哪。很多人调优调不出效果,根本原因是没搞清楚自己改的那个值到底在哪个环节起作用。比如把tcp_tw_reuse打开了却发现 TIME_WAIT 一点没少,很可能是因为这台机器主要是被动关闭方,这个开关压根不生效。类似的情况太多了,所以这一节先把路径捋清楚。
1.1 五个钩子点与一次连接的真实旅程
Netfilter 是内核里的一套框架,它在网络协议栈的关键位置埋了五个钩子:PRE_ROUTING、LOCAL_IN、FORWARD、LOCAL_OUT、POST_ROUTING。数据包从网卡进来,先经过PRE_ROUTING,在这里做目的地址判断——如果目标是自己就走上层LOCAL_IN,如果目标是别人就走FORWARD转发出去。本机发出的包走LOCAL_OUT,无论哪种情况,最后出网卡前都要过POST_ROUTING。
连接跟踪(conntrack)的初始化就发生在PRE_ROUTING和LOCAL_OUT这两个位置。也就是说,只要一个包进入了这两个钩子,它就有可能被记录进连接跟踪表,无论后面是被 ACCEPT 还是 DROP。这一点很关键:很多人以为只有放行的连接才占表项,实际上被防火墙拒绝的包同样会短暂占用表项(进入 UNREPLIED 状态),在高频扫描场景下,这就是表被打满的常见原因。
理解这条路径之后,参数的归属就很清楚了。控制表项数量的是nf_conntrack_max,控制表项存活时间的是各种nf_conntrack_*_timeout,控制收包队列深度的是netdev_max_backlog,控制应用层 accept 能力的是somaxconn和tcp_max_syn_backlog。它们分属不同环节,改错地方等于白改。
提示:判断一个参数是否与 Netfilter 相关,可以看它的路径。
/proc/sys/net/netfilter/下面的是连接跟踪直接相关,/proc/sys/net/core/是协议栈通用队列,/proc/sys/net/ipv4/里则混杂着 TCP 状态机和端口分配相关的项。这三类经常被混为一谈,实际作用层面差别很大。
1.2 参数调优的三个层次:队列、表项、超时
我把这套参数按作用对象分成三层,按这个顺序排查,思路会清晰很多。
第一层是队列层,解决的是"包进来了但来不及处理"的问题。典型代表是netdev_max_backlog、somaxconn、tcp_max_syn_backlog。这一层出问题的典型现象是ListenOverflows和ListenDrops计数上涨,或者在高 PPS 场景下出现网卡侧丢包而协议栈毫无感知。
第二层是表项层,解决的是"连接太多记不下"的问题。核心是nf_conntrack_max和hashsize,以及 NAT 场景下的ip_local_port_range。这一层出问题的现象非常明确——dmesg里会出现 table full,或者conntrack -S里insert_failed、drop计数持续增长。
第三层是超时层,解决的是"连接记了太久没释放"的问题。各种nf_conntrack_tcp_timeout_*都在这一层。它的影响最隐蔽,因为不会直接报错,而是让表项长期被占用,最终把第一层和第二层的问题放大。很多表满的根因其实在超时设置上,只是表现出来在容量上。
调优的正确顺序是先看超时是否合理,再看容量是否够用,最后看队列是否匹配。反过来做,往往是加了容量过几天又满,反复折腾。我见过有团队把nf_conntrack_max从 65536 一路调到 200 万,问题依然复发,最后一查是nf_conntrack_tcp_timeout_established保持默认的 432000 秒没动过。
2. 核心参数逐个掰开:取值逻辑与踩坑边界
这一节是重头戏。我会把最常被调整的那批参数分成四组,每组说清楚它的作用、默认值、推荐范围和推导逻辑。需要强调的是,下面给出的数值都是起点而非终点,最终取值必须结合你自己的业务特征来算。
2.1 conntrack 容量:nf_conntrack_max 与 hashsize 的联动
nf_conntrack_max是连接跟踪表能容纳的最大表项数。这个值不是孤立的,它和hashsize(哈希桶数量)必须配套看。表项被放进哈希桶组成的链表里,查找时先定位桶再遍历链表。如果桶太少而表项太多,每个桶挂的链表就会很长,查找退化成线性遍历,CPU 在软中断里的开销会明显上升。
经验公式是这样:hashsize 取 nf_conntrack_max 的四分之一,此时平均每个桶挂 4 个表项,查找效率比较均衡。内核默认的 hashsize 通常是 16384 或 65536,对应默认 max 值 65536 或 262144,基本符合这个比例。如果你把 max 调到 100 万而 hashsize 保持 16384 不动,平均链表长度会达到 61,这在连接频繁新建和销毁的场景下会带来可观的 CPU 消耗。
内存账也要算。每条 conntrack 表项的结构体大小随内核版本有差异,大致在 280 到 320 字节之间,加上扩展结构和 slab 对齐开销,按400 字节/条估算比较保守。100 万表项就是约 400MB 常驻内存。哈希桶本身是指针数组,100万/4 = 25 万个桶,每桶 8 字节,总共约 2MB,可以忽略。所以调 max 之前先看机器内存余量,别把内核内存吃干了。
| 参数 | 默认值(常见) | 推荐区间 | 关键约束 |
|---|---|---|---|
nf_conntrack_max | 65536 / 262144 | 峰值的 1.2~1.5 倍 | 受内存限制,100万约 400MB |
nf_conntrack_buckets(hashsize) | 16384 / 65536 | max / 4 | 运行时改动会重建哈希表 |
nf_conntrack_count(只读) | - | 持续低于 max 的 70% | 长期高位说明要查超时或异常连接 |
修改 hashsize 有两个途径。模块加载参数写在/etc/modprobe.d/下,只能在模块加载时生效;运行时修改则是往/sys/module/nf_conntrack/parameters/hashsize写值,但这个过程会重建整张哈希表,期间可能出现短暂的表项丢失,必须在业务低峰做,而且要有回滚预案。
注意:
nf_conntrack_max在新版本内核里是 per-netns 的 sysctl。容器里看到的这个值和宿主机不是同一个,容器 netns 创建时会从当前值复制一份。这意味着你在宿主机上改了参数,已经存在的容器 netns 并不会跟着变。容器场景下这块要单独处理,后面第 5 节会展开。
2.2 超时参数:established 不是越大越好
超时参数是这套体系里最容易被忽略、又最容易出事的一组。先看最核心的几个:
nf_conntrack_tcp_timeout_established默认 432000 秒,也就是 5 天。这个值的设计初衷是让长期空闲的长连接不被误清理,但在短连接为主的业务里,它意味着一条已经关掉的连接还要在表里躺 5 天。假设你的业务峰值 QPS 是 5000,平均连接存活 3 秒,那天产生的表项理论上就有 1.4 亿条次,只要有一部分没被正常回收,表就会被顶满。
合理的做法是根据业务的最长空闲时间来定。Web 服务、API 网关这类,连接空闲超过一小时基本就是异常了,设 3600 到 7200 秒完全够用。数据库连接池、消息队列这类长连接,可以设到 86400 秒。关键是这个值要 ≥ 业务侧 keepalive 的间隔,否则会出现连接被 conntrack 先清掉、业务侧还没察觉、下一个包过来被判成 INVALID 的情况。
nf_conntrack_tcp_timeout_time_wait默认 120 秒。这个值对应的是主动关闭方进入 TIME_WAIT 后的跟踪时长。理论上要覆盖 2MSL(约 60 秒),所以降到 30 秒是有风险的——可能在新连接复用四元组时,旧表项还没过期,导致新连接的首包被误判。稳妥的下限是 60 秒。
nf_conntrack_udp_timeout默认 30 秒,nf_conntrack_udp_timeout_stream默认 120 秒。前者针对单包的 UDP 交互,后者针对检测到双向流量后的"流式" UDP。DNS 查询用默认值就行;如果是基于 UDP 的长连接业务,需要把 stream 那个值调高,否则会出现连接被中途清理导致丢包。
下面这张表是我在不同场景下实际用过的取值,可以直接作为起点:
| 参数 | 短连接 Web | 长连接服务 | NAT 网关 |
|---|---|---|---|
tcp_timeout_established | 3600 | 86400 | 7200 |
tcp_timeout_time_wait | 60 | 120 | 60 |
tcp_timeout_close_wait | 30 | 60 | 30 |
tcp_timeout_fin_wait | 60 | 120 | 60 |
udp_timeout | 30 | 30 | 30 |
udp_timeout_stream | 120 | 300 | 180 |
icmp_timeout | 30 | 30 | 30 |
改超时参数的收益往往比加容量大得多。同样一台机器,把 established 从 5 天降到 1 小时,表项占用可能直接下降一个数量级。这也是为什么我坚持"先调超时、再调容量"的顺序。
2.3 队列与端口:backlog、somaxconn、ip_local_port_range
net.core.netdev_max_backlog控制的是网卡收包到协议栈之间的 per-CPU 队列长度,默认 1000。这个队列在开启 RPS(接收包散列)或者多队列网卡场景下非常关键。当收包速率超过协议栈处理速率时,包会先堆在这个队列里,队列满了就直接丢,而且不计入任何协议栈统计,只能通过网卡的 rx_dropped 或者/proc/net/softnet_stat里的 dropped 列看到。
万兆网卡、高 PPS 场景下,1000 明显不够。我的经验值是千兆环境 3000 到 5000,万兆环境 8000 到 20000。判断依据是看/proc/net/softnet_stat第二列(dropped)是否在涨,涨了就加。但也不能无脑加大,队列越深,排队延迟越高,对延迟敏感的业务反而受害。
net.core.somaxconn和net.ipv4.tcp_max_syn_backlog是一对。前者是已完成三次握手的连接队列上限,后者是半连接(SYN_RECV)队列上限。应用调用listen(fd, backlog)时,实际生效的 accept 队列长度是min(backlog, somaxconn)。所以如果你的应用写了listen(fd, 1024)但somaxconn是 128,实际只有 128。这两个值建议一起调到 4096 或 8192,同时确认应用侧的 backlog 参数也跟上了。
net.ipv4.ip_local_port_range决定本机作为客户端发起连接时可用的本地端口范围,默认通常是32768 60999,约 2.8 万个。做 NAT 出站或者反向服务端场景下,这个范围直接决定了单 IP 对单目标能建立多少并发连接。因为四元组里本地端口是变量,同一个目标 IP:Port 最多只能用满这个范围内的端口数。
调大是有边界的。范围上限是 65535,下限不建议低于 1024(避开知名端口),也不建议低于系统里net.ipv4.ip_local_reserved_ports里预留的端口。常见的调整是10240 65535,约 5.5 万个。真到了端口不够用的地步,更根本的解法是增加出口 IP 或者做多目标分流,而不是一味压缩端口范围。
2.4 容易被误用的几个开关
有几个参数在网上的文章里被反复推荐,但实际用起来坑不少,单独拎出来说。
net.ipv4.tcp_tw_reuse:这个开关只对主动发起连接的一方生效,而且要求开启 TCP 时间戳。它的作用是在 TIME_WAIT 状态超过 1 秒后允许复用该四元组。对客户端角色有帮助,对纯服务端角色基本无效。很多文章把它当成"减少 TIME_WAIT"的万能药,其实方向就错了——服务端的大量 TIME_WAIT 来自被动关闭,这个开关管不着。
net.ipv4.tcp_tw_recycle:已经在新版本内核里被移除(4.12 之后没有了)。老文章里还在推荐这个参数,看到直接跳过。它在 NAT 环境下会导致连接失败,当年坑了不少人。
net.ipv4.tcp_fin_timeout:默认 60 秒。很多人以为它控制 TIME_WAIT 时长,其实它控制的是FIN_WAIT_2状态的超时。TIME_WAIT 的时长是固定的 2MSL,改不了。这个误解流传很广,值得单独记一下。
nf_conntrack_checksum:默认 1,表示校验和错误的包不建立跟踪。但在虚拟化和容器环境里,网卡和 veth 的 checksum offload 会导致到达 conntrack 时校验和字段还没算好,从而被误判为错误包,表现为大量 INVALID。这种情况下把它设为 0 是标准解法,代价是失去一层校验保护,在可信的内网环境里可以接受。
nf_conntrack_tcp_be_liberal:默认 0,严格按 TCP 窗口判断包是否合法。在存在乱序、重传或者非对称路由的环境里,严格模式会把合法包判成 INVALID 并丢弃。设为 1 会宽松很多,代价是对某些扫描行为的识别能力下降。我的经验是:只要监控里 INVALID 计数在涨而业务侧有异常,先开这个开关验证一下,能定位就说明是判定过严的问题。
3. 从测算到落地的完整实操流程
参数理解了,接下来是怎么落到机器上。这一节按"测算 - 配置 - 分场景 - 验证"四步走,每一步都给出可以直接用的命令和文件内容。
3.1 先量化:峰值连接数、内存账、余量系数
调参之前必须先知道自己的峰值在哪。最直接的办法是看历史数据,如果没有监控,就抓一段业务高峰的实时数据。
# 当前 conntrack 使用量 cat /proc/sys/net/netfilter/nf_conntrack_count cat /proc/sys/net/netfilter/nf_conntrack_max # 使用率百分比,一行算出 awk '{printf "%.1f%%\n", $1*100/'$(cat /proc/sys/net/netfilter/nf_conntrack_max)'}' \ /proc/sys/net/netfilter/nf_conntrack_count # 按协议和状态分布,看表项都是什么构成的 conntrack -L 2>/dev/null | awk '{print $1, $4}' | sort | uniq -c | sort -rn | head -20nf_conntrack_count的读取是常数时间,可以放进监控高频采集,不用担心性能。相比之下conntrack -L会遍历整张表,在几十万条表项时能跑好几秒,只适合在排查时用,别写进定时任务。
拿到峰值之后,nf_conntrack_max按峰值 × 1.3 到 1.5来设。留余量是因为流量有突发,而且表项回收有延迟,瞬时值会高于稳态值。同时要观察 24 小时内的曲线,如果峰值出现在固定时段,说明是业务规律;如果曲线持续爬升不回落,那就不是容量问题,是泄漏或者超时设置不合理,加容量只会推迟爆炸时间。
内存测算用这个公式:max × 400 字节 + 少量固定开销。举个具体例子,峰值 40 万连接,max 设 60 万,内存占用约 240MB,加上内核其他开销按 300MB 预留。这台机器如果有 8GB 内存,完全不是问题;如果是 1GB 的小规格实例,就得掂量一下,可能需要先优化超时把峰值压下来。
3.2 配置落地:sysctl.d 与 modprobe.d 的正确写法
配置不要直接写进/etc/sysctl.conf,用/etc/sysctl.d/下的独立文件更清晰,也方便回滚。文件名带数字前缀控制加载顺序,我习惯用99-netfilter.conf保证最后加载。
# /etc/sysctl.d/99-netfilter.conf # ---- 连接跟踪容量 ---- net.netfilter.nf_conntrack_max = 655360 net.netfilter.nf_conntrack_tcp_timeout_established = 3600 net.netfilter.nf_conntrack_tcp_timeout_time_wait = 60 net.netfilter.nf_conntrack_tcp_timeout_close_wait = 30 net.netfilter.nf_conntrack_tcp_timeout_fin_wait = 60 net.netfilter.nf_conntrack_udp_timeout = 30 net.netfilter.nf_conntrack_udp_timeout_stream = 120 net.netfilter.nf_conntrack_icmp_timeout = 30 net.netfilter.nf_conntrack_tcp_be_liberal = 1 net.netfilter.nf_conntrack_checksum = 0 # ---- 队列层 ---- net.core.netdev_max_backlog = 16384 net.core.somaxconn = 8192 net.ipv4.tcp_max_syn_backlog = 8192 # ---- 端口与 TCP 状态 ---- net.ipv4.ip_local_port_range = 10240 65535 net.ipv4.tcp_tw_reuse = 1 net.ipv4.tcp_fin_timeout = 15 net.ipv4.tcp_syncookies = 1加载命令是sysctl -p /etc/sysctl.d/99-netfilter.conf。这里有个非常常见的坑:如果 nf_conntrack 模块还没加载,写net.netfilter.*会报"no such file or directory"。原因是这些 sysctl 节点是模块初始化时创建的。解决办法是先modprobe nf_conntrack,或者在配置文件里加上模块预加载。
hashsize 通过 modprobe 配置:
# /etc/modprobe.d/nf_conntrack.conf options nf_conntrack hashsize=163840这个值对应 max 655360 的四分之一。改完之后需要重新加载模块才生效,而这会影响所有依赖 conntrack 的规则,生产环境不能直接 rmmod。可行的做法是排进变更窗口做重启,或者用 sysfs 在低峰期热调:
# 热调 hashsize,会重建哈希表,务必低峰执行 echo 163840 > /sys/module/nf_conntrack/parameters/hashsize热调期间已有连接的表项会被迁移,过程通常在毫秒级,但在高负载机器上我见过造成短暂丢包的。所以如果条件允许,我还是倾向于走重启流程,可控性更好。
提示:配置写完之后一定要用
sysctl -a | grep netfilter核对一遍实际生效值。有些发行版会在别的地方覆盖你的配置,比如 cloud-init 注入的 sysctl、容器运行时写入的命名空间参数。核对成本很低,但能省下大量"明明配了却不生效"的排查时间。
3.3 三类典型场景的配置模板
不同角色对这套参数的敏感点完全不同,下面给三个模板,都是实战中跑过的组合。
场景一:高并发 Web 服务 / API 网关
特征是短连接多、连接生命周期短、并发量高,但对单连接时长不敏感。核心矛盾是表项回收速度要跟得上新建速度。
net.netfilter.nf_conntrack_max = 524288 net.netfilter.nf_conntrack_tcp_timeout_established = 1800 net.netfilter.nf_conntrack_tcp_timeout_time_wait = 60 net.core.somaxconn = 8192 net.ipv4.tcp_max_syn_backlog = 8192 net.core.netdev_max_backlog = 8192 net.ipv4.tcp_tw_reuse = 1established 压到 1800 秒是关键,配合 60 秒的 time_wait,表项周转很快,同样的 max 能承载的实际连接数比默认配置高一个数量级。
场景二:NAT 网关 / 出口转发节点
这类机器的特点是流量全部经过 FORWARD 链,每条连接都必须在表里,且本地端口范围直接决定并发上限。同时因为做地址转换,insert_failed的概率会上升。
net.netfilter.nf_conntrack_max = 1048576 net.netfilter.nf_conntrack_tcp_timeout_established = 7200 net.netfilter.nf_conntrack_tcp_timeout_time_wait = 60 net.ipv4.ip_local_port_range = 1024 65535 net.ipv4.ip_local_reserved_ports = 20000-20010 net.netfilter.nf_conntrack_tcp_loose = 1 net.core.netdev_max_backlog = 16384ip_local_reserved_ports那行是给内部服务预留端口用的,避免被 NAT 随机分配撞上。这个细节很多模板里没有,但在实际网关机器上很必要——我遇到过 NAT 分配到了本地某个管理服务监听的端口,导致端口冲突的诡异问题。
场景三:容器节点 / 虚机宿主机
这类环境叠加了 netns 隔离和 veth 设备,conntrack 参数需要在两个层面考虑。宿主机层面的配置只管宿主机 netns,容器内部的要单独设。
net.netfilter.nf_conntrack_max = 1048576 net.netfilter.nf_conntrack_checksum = 0 net.netfilter.nf_conntrack_tcp_be_liberal = 1 net.core.netdev_max_backlog = 16384 net.ipv4.ip_forward = 1checksum = 0在容器环境几乎是必选项,原因前面说过——veth 的 offload 会让校验和检查失效。容器内部的 conntrack_max 则要通过 securityContext 或者 initContainer 在容器 netns 内设置,具体取决于你的编排平台。
3.4 压测验证与灰度变更
配置写完不代表结束,必须验证。最容易被忽略的是变更后验证,很多人改完看一眼没问题就走了,结果流量一上来才暴露。
验证分三个层次。第一层看配置是否生效,sysctl取值核对就行。第二层看功能是否正常,用conntrack -L确认新建连接能正确入表,conntrack -S里没有异常计数。第三层是压力验证,这个最有价值。
压测不需要真实流量,可以用curl或压测工具打一批短连接,同时观察计数变化:
# 起 2000 个并发短连接,观察 conntrack 计数变化 seq 1 2000 | xargs -P 200 -I{} curl -s -o /dev/null http://127.0.0.1:8080/health # 同时另开一个窗口盯计数 watch -n 0.5 'cat /proc/sys/net/netfilter/nf_conntrack_count'正常情况下计数会快速上涨然后随超时回落。如果一直不回落,说明超时参数没生效或者有连接没正常关闭。这个简单的测试能发现大多数配置错误。
灰度变更的原则是一次只改一类参数。先改超时,观察一天;再改容量,观察一天;最后改队列。同时改一堆,出问题就不知道是谁的锅。回滚方案也要提前准备:把当前生效值dump一份存起来,sysctl -a | grep -E "netfilter|net.core|net.ipv4" > /tmp/sysctl-backup-$(date +%F).txt,出问题直接sysctl -p回灌。
4. 线上故障排查实录
理论说再多不如看几个真实故障。这一节整理三类最常见的现象,每类给出定位路径。
4.1 table full dropping packet 的三层定位法
看到这条日志,说明表已经满了,内核在丢包。别急着调大 max,先分三层查。
第一层:确认是不是真的容量不够。看nf_conntrack_count是否长期贴近nf_conntrack_max。如果是一天里某个时段短暂打满然后回落,那是流量峰值超过容量,加 max 合理;如果是一路爬升、从不回落,加多少都会满,问题在别处。
第二层:看表项构成。用conntrack -L按状态统计:
conntrack -L 2>/dev/null | awk '{print $4}' | sort | uniq -c | sort -rn如果ESTABLISHED占比极高且数量远超业务并发,基本可以断定超时设置太长。如果TIME_WAIT堆积,看是不是主动关闭方且tcp_tw_reuse没开。如果出现大量UNREPLIED或者奇怪的目标地址,多半是在被扫描。
第三层:看是不是异常流量。按源 IP 或目的端口聚合,找异常来源:
conntrack -L 2>/dev/null | awk '{print $5}' | cut -d= -f2 | sort | uniq -c | sort -rn | head -20某个源 IP 贡献了不成比例的表项,要么是业务侧连接泄漏,要么是攻击。这种时候调容量只是给攻击者更多空间,正确做法是加限速规则。
| 现象 | 大概率原因 | 处理方向 |
|---|---|---|
| count 长期贴顶 + ESTABLISHED 占多数 | established 超时过长 | 降超时,观察表项释放速度 |
| count 尖峰式冲高后回落 | 突发流量 | 提升 max 并配突发限流 |
| 大量 UNREPLIED | 扫描或单向流量 | 加限速/黑名单,临时提高 max |
| 单一源 IP 表项异常多 | 业务连接泄漏 | 查应用连接池配置 |
| 表项数正常但频繁 table full | max 设置过低 | 按峰值重新测算 |
4.2 insert_failed 与端口耗尽的区分
这两个现象都表现为"连不上",但根因完全不同,搞混了会浪费时间。
insert_failed是 conntrack 统计里的一个计数器,表示表项插入失败。常见原因是NAT 端口冲突:两个连接经过 NAT 后映射到同一个外部 IP 和端口,内核尝试插入时发生冲突。这通常发生在ip_local_port_range设置过窄、或者短时间内大量连接到同一目标的场景。
查看方式很直接,conntrack -S会按 CPU 列出各类计数:
conntrack -S | head -5 # 关注 insert_failed、drop、early_drop、error 这几列early_drop这个计数器特别值得关注。它表示表快满时内核主动丢弃了未收到回应的表项。这个值在涨,说明表已经在临界点,是容量不足的早期信号,比 table full 日志出现得更早,非常适合作为预警指标。
端口耗尽则是另一回事。现象是应用报Cannot assign requested address,ss -s里能看到大量 TIME_WAIT。定位方法是统计当前到某个目标 IP 的连接数:
ss -tan | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -rn | head如果某个目标 IP 的连接数接近ip_local_port_range的跨度,那就是端口被占满了。解法是扩端口范围、增加出口 IP,或者让应用侧做连接复用。
4.3 大量 INVALID 包的成因清单
nf_conntrack_invalid计数上涨,意味着有包无法匹配到任何表项,同时也不满足新建条件。这类问题的排查难度最高,因为日志里看不出所以然。我整理了一个排查顺序,从上往下试,命中率比较高。
| 排查项 | 检查方法 | 处理 |
|---|---|---|
| 校验和误判 | nstat -az | grep -i invalid配合ethtool -k看 offload | nf_conntrack_checksum = 0 |
| 窗口判定过严 | 乱序/重传场景下 INVALID 涨 | nf_conntrack_tcp_be_liberal = 1 |
| 非对称路由 | 入包和出包走不同网卡 | 调整路由或开tcp_loose |
| 连接被提前清理 | 业务 keepalive 间隔 > conntrack 超时 | 提高 established 超时 |
| 中途启用防火墙 | 已建立的连接没有表项 | nf_conntrack_tcp_loose = 1 |
| NAT 设备重启/切换 | 表项丢失后继续收到旧连接的包 | 属正常,业务侧重连即可 |
这里第一个和最后一个是最容易踩的。nf_conntrack_tcp_loose的默认值是 1,如果被安全加固脚本改成 0,那么防火墙重启后所有已建立的连接都会失效,表现为"重启防火墙导致业务大面积中断"。这是我在一次加固变更后遇到的,排查了大半天才发现是这一个开关。
5. 观测体系与长期维护
参数调完不是终点,建立观测才能保证长期不出事。这一节聊监控指标、容器环境的特殊处理,以及变更纪律。
5.1 必须盯住的六个指标
我习惯在每台涉及转发的机器上固定采集这几个值,有条件的做成告警。
第一个是nf_conntrack_count / nf_conntrack_max的使用率,阈值设 70% 预警、85% 严重。第二个是conntrack -S里的early_drop,这个涨了说明表在临界,比使用率更早发现问题。第三个是insert_failed,涨了说明有 NAT 冲突或者端口问题。第四个是ListenOverflows(nstat -az | grep -i listen),对应 accept 队列溢出。第五个是/proc/net/softnet_stat的 dropped 列,对应收包队列丢包。第六个是网卡的ethtool -S里的 drop 和 error 计数,对应最底层的硬件侧丢包。
这六个指标覆盖了从硬件到应用的完整链路,出问题时按链路从上往下看,能很快定位到是哪一层的问题。我遇到过最隐蔽的一次是 softnet dropped 在涨但协议栈和网卡侧计数器都正常,最后发现是 RPS 配置不合理导致单个 CPU 的队列被打爆。
# 一行看全:softnet 每 CPU 的丢包和超时 awk '{print NR-1": "$2" "$3" "$11}' /proc/net/softnet_stat # 各 CPU conntrack 插入失败与丢弃 conntrack -S # 监听队列溢出 nstat -az | grep -E "ListenOverflows|ListenDrops|TcpExt"5.2 容器与虚拟化环境的特殊处理
容器环境最大的坑在于 sysctl 的命名空间隔离。nf_conntrack_max、各种 timeout 都是 per-netns 的,宿主机改了不影响已经运行的容器。这会导致一个典型现象:宿主机上cat /proc/sys/net/netfilter/nf_conntrack_count看起来很低,但容器里业务频繁报连接失败。
排查方法是进到容器的 netns 里看:
# 找到容器进程的 netns pid=$(docker inspect -f '{{.State.Pid}}' <容器名>) nsenter -t $pid -n cat /proc/sys/net/netfilter/nf_conntrack_max nsenter -t $pid -n cat /proc/sys/net/netfilter/nf_conntrack_count如果容器里的 max 是 65536 而宿主机设的是 100 万,就说明容器 netns 用的是创建时的默认值。解决办法是在容器启动时设置,主流编排平台都支持通过安全上下文声明 netfilter 类别的 sysctl。另外要确认内核对 netns 内 sysctl 写入的权限限制,某些配置需要privileged或者特定 capability,这个要按平台文档来。
虚拟化环境另一个注意点是 checksum offload。veth、bridge、virtio 网卡的 offload 行为不一致,会导致 conntrack 看到校验和错误的包。nf_conntrack_checksum = 0是通用解法,同时建议用ethtool -K检查一下虚拟网卡的 offload 状态,做到心里有数。
5.3 变更纪律与回滚预案
最后说点偏流程但很重要的东西。这类内核参数调整的特点是影响面大、现象滞后,改完之后可能要等一两个小时流量高峰过了才知道行不行。所以变更纪律比技术本身更重要。
我的做法是三条:一是所有变更必须有书面记录,包括改前值、改后值、改动理由和观察指标;二是变更窗口避开业务高峰,并且预留至少一小时的观察期,别改完立刻下班;三是每次只动一组参数,超时、容量、队列三者分开做,这样出问题能快速归因。
回滚预案要具体到命令。我习惯在变更前先 dump 一份:
sysctl -a 2>/dev/null | grep -E "^net\.(netfilter|core|ipv4)\." \ > /root/sysctl-backup-$(date +%Y%m%d-%H%M).conf这份文件可以用sysctl -p直接回灌。hashsize 这类模块参数没法在线回滚,所以改动它之前一定要想清楚,最好放在有重启窗口的计划里做。
还有个细节:如果机器用了配置管理工具统一下发 sysctl,手动改的值可能在某次自动化跑批时被覆盖。这种情况下的正确做法是改配置管理的模板源,而不是在机器上临时改。我见过一次线上参数莫名其妙回到默认值,查了两天才发现是每次发布都会重新渲染 sysctl 配置文件,手动改的全被冲掉了。
我个人在实际操作中的体会是,这套参数里真正需要花心思的其实只有三四个——established 超时、nf_conntrack_max、hashsize 和 ip_local_port_range,其余多数保持默认就好。把精力集中在这几个上面,配合前面说的观测指标,绝大多数连接层的问题都能提前发现。至于那些看起来"更高级"的开关,除非监控明确指向某个问题,否则不建议主动去碰,内核默认值背后往往是大量场景权衡的结果,改错了比不改更难查。