如何在香港服务器的CentOS 7环境中优化iptables与firewalld规则,避免高流量下的网络性能下降?

昨天凌晨 2:40,在香港葵涌机房,走廊尽头的空调像海风一样呼呼吹着,我盯着 NOC 大屏上那条红得发烫的 PPS 曲线,像心电图一样抖个不停。前一分钟,业务从 60 万 PPS 暴冲到 180 万 PPS,入口 LVS 没报警,应用也没明显 5xx——偏偏是边缘一台“看似平平无奇”的 CentOS 7 边界机开始丢包。
我把咖啡放在机架门上,手摸着 KVM 的键盘,深吸一口气:这次,八成又是 netfilter 的老毛病——规则多、顺序乱、conntrack 顶到天花板,再加上几条“好心办坏事”的 LOG。
下面这篇,是我那一夜从“止血”到“手术”的完整过程。无论你是刚接触 Linux 防火墙的新手,还是在高流量现场摸爬滚打多年的老兵,我尽量把步骤、参数、坑点和取舍都摊开讲清楚,能抄就抄,能复用就复用。
现场环境与流量画像
硬件/软件清单(当班那台边界机)
| 项 | 配置 |
|---|---|
| 机型 | Dell R630(1U) |
| CPU | 2× Intel Xeon E5-2680 v4(各 14C/28T,共 28C/56T) |
| 内存 | 128 GB DDR4 |
| 网卡 | 2× Intel X520 10GbE(ixgbe,多队列) |
| 系统 | CentOS Linux release 7.9 (3.10.0-1160 内核) |
| 组件 | firewalld 0.6.x(iptables 后端)、iptables-services、ipset、conntrack-tools、irqbalance、ethtool |
典型流量画像
- 峰值:1.5–2.0 Mpps / 4–7 Gbps(以 TCP/80/443 为主,夹杂少量 UDP 探测/健康检查)
- 连接特征:大量短连接 + 部分长连 keepalive
- 连通角色:边界转发 + 少量本地终止(SSH/管理面)
- 症状:PPS 一冲高,丢包率和 RTT 抖动上来;softirq/ksoftirqd 占用飙升;nf_conntrack_count 接近上限;iptables 计数显示多条 LOG/rich-rule 命中频繁。
- 先止血:3 个 5 分钟内能做的动作
- 目标:把“热路径”变冷,让 CPU 先喘口气,再做精细手术。
暂停热日志(别在热路径上 -j LOG)
# 临时禁用所有 LOG 规则(按需定位具体链)
iptables -S | grep " -j LOG" | sed 's/^-A/iptables -D/' | bash
# firewalld 若在用 rich-rule 做日志,先移除/降低频率(见后文的限速 LOG)
把已建立连接放到最前(把热流量早放行)
iptables -I INPUT 1 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -I FORWARD 1 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
单独的管理白名单(避免每条规则都去比对源)
# 先用 ipset 管起来(火墙与纯 iptables 都通用)
ipset create mgmt_allow hash:ip -exist
ipset add mgmt_allow 203.0.113.10 -exist # 你的跳板机
ipset add mgmt_allow 203.0.113.11 -exist
iptables -I INPUT 2 -m set --match-set mgmt_allow src -p tcp --dport 22 -j ACCEPT
这三刀下去,一般 CPU 就能降一截;如果现场仍旧喘不过气,再看下面的系统级优化。
火力点一:把 conntrack 调到“够用且不浪费”
为什么卡? 高 PPS 下,每个新流要做状态跟踪;短连接多时,nf_conntrack_count 很容易顶到 nf_conntrack_max,随后开始“看不见新连接”→ 丢包/超时。
估算思路
每条 conntrack 条目内存占用(CentOS 7/3.10 内核)大致 350–500 B/条(协议不同略有差异)。
你能给 conntrack 的内存预算 ≈ 机器空闲内存的 10–20% 较稳妥。
我们的机器(128GB)给 2GB 预算 → 上限 ~4,000,000 条(按 512B 粗算)。
哈希桶(hashsize)建议 ≈ nf_conntrack_max / 4 → 1,000,000。
落地操作
# 1) 安装工具
yum install -y conntrack-tools ipset
# 2) 立刻把上限拉高(可热改)
sysctl -w net.netfilter.nf_conntrack_max=4000000
echo 4000000 > /proc/sys/net/netfilter/nf_conntrack_max
# 3) 调整典型超时(HTTP/短连多时,收紧 time_wait、fin_wait 等)
cat >/etc/sysctl.d/99-nf-conntrack.conf <<'EOF'
net.netfilter.nf_conntrack_max=4000000
net.netfilter.nf_conntrack_buckets=1000000 # 部分内核读作 hashsize(见下一条)
# 常见 TCP 状态超时(按业务调)
net.netfilter.nf_conntrack_tcp_timeout_established=300
net.netfilter.nf_conntrack_tcp_timeout_close_wait=60
net.netfilter.nf_conntrack_tcp_timeout_fin_wait=30
net.netfilter.nf_conntrack_tcp_timeout_time_wait=15
# UDP 通常更短
net.netfilter.nf_conntrack_udp_timeout=15
net.netfilter.nf_conntrack_udp_timeout_stream=60
EOF
sysctl --system
关于 hashsize 的坑
hashsize 多数情况下需要在模块加载时指定,不能热改;CentOS 7 上对应 /sys/module/nf_conntrack/parameters/hashsize 通常只读。
解决:在 /etc/modprobe.d/nf_conntrack.conf 写入参数,并重启。
echo "options nf_conntrack hashsize=1000000" >/etc/modprobe.d/nf_conntrack.conf
# 确保开机加载(通常自动),必要时:
echo nf_conntrack >/etc/modules-load.d/nf_conntrack.conf
观测命令
# 当前条目/上限
cat /proc/sys/net/netfilter/nf_conntrack_count
cat /proc/sys/net/netfilter/nf_conntrack_max
# 概览统计
conntrack -S
火力点二:用 ipset + 直接规则,替换上千条“散弹枪式”匹配
大量 per-IP/网段/端口的规则会把 iptables 链拉得巨长,线性匹配的 CPU 成本可观。把“多对一”匹配折叠到 ipset,命中就是 O(1)。
规范做法
# 白名单/黑名单/探测源 分开建
ipset create mgmt_allow hash:ip -exist
ipset create blocklist hash:ip -exist
ipset create probe_src hash:ip -exist
# 例:把某个黑名单段整个塞进来
ipset add blocklist 198.51.100.0/24 -exist
iptables 热路径(示例顺序)
# ① 已建立连接优先
iptables -I INPUT 1 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# ② 管理面白名单(SSH)
iptables -I INPUT 2 -m set --match-set mgmt_allow src -p tcp --dport 22 -j ACCEPT
# ③ 早丢黑名单
iptables -I INPUT 3 -m set --match-set blocklist src -j DROP
# ④ 业务端口合并匹配(multiport)
iptables -I INPUT 4 -p tcp -m multiport --dports 80,443 -j ACCEPT
# ⑤ 兜底策略:DROP(确保 loopback 有 ACCEPT)
iptables -A INPUT -i lo -j ACCEPT
iptables -A INPUT -j DROP
要点:规则少、顺序稳、命中早。把“高命中规则”挪到链前面,避免“把 CPU 预算浪费在末尾的冷门匹配上”。
火力点三:在 raw 表对可控的 UDP 流量做 NOTRACK
某些场景(例如来自我方探测机的 UDP 健康检查、固定地址的 QUIC/UDP 隧道),你未必需要 conntrack。跳过状态跟踪,直接放行,能明显减压。
谨慎:只对可信源 + 固定端口这么做,且要在 filter 表配合显式 ACCEPT。
# 把探测源放进 ipset
ipset create probe_src hash:ip -exist
ipset add probe_src 192.0.2.10 -exist # 你的探测器/健康检查机
# 1) raw 表里 NOTRACK(在 conntrack 之前)
iptables -t raw -I PREROUTING 1 -m set --match-set probe_src src -p udp --dport 443 -j CT --notrack
# 2) filter 表显式放行同一流量(注意没有 ctstate 可用)
iptables -I INPUT 5 -m set --match-set probe_src src -p udp --dport 443 -j ACCEPT
用 firewalld 的 direct 方式(永久生效)
# ipset 用 firewalld 管理(可持久化)
firewall-cmd --permanent --new-ipset=probe_src --type=hash:ip
firewall-cmd --permanent --ipset=probe_src --add-entry=192.0.2.10
# raw NOTRACK
firewall-cmd --permanent --direct --add-rule ipv4 raw PREROUTING 0 \
-m set --match-set probe_src src -p udp --dport 443 -j CT --notrack
# filter 放行
firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 0 \
-m set --match-set probe_src src -p udp --dport 443 -j ACCEPT
firewall-cmd --reload
火力点四:合理的链路与规则排序(参考基线)
INPUT 链推荐结构(终止本机)
*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
# 0. loopback
-A INPUT -i lo -j ACCEPT
# 1. 已建立连接
-A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# 2. 早丢无效包(省后面的匹配)
-A INPUT -m conntrack --ctstate INVALID -j DROP
# 3. 黑名单(ipset)
-A INPUT -m set --match-set blocklist src -j DROP
# 4. 管理面(ipset)
-A INPUT -m set --match-set mgmt_allow src -p tcp --dport 22 -j ACCEPT
# 5. 业务端口(合并匹配)
-A INPUT -p tcp -m multiport --dports 80,443 -j ACCEPT
# 6. 控制频率的日志(非热路径)
-A INPUT -m limit --limit 10/second --limit-burst 50 -j NFLOG --nflog-group 10 --nflog-prefix "FW_DROP:"
# 7. 兜底 DROP
-A INPUT -j DROP
COMMIT
FORWARD 链(作为路由/边界)同理,先 ESTABLISHED,RELATED,再特殊放行,再 default DROP。
火力点五:网卡与中断——把软中断“摊平”
高 PPS 时,iptables 的 CPU 消耗往往叠加在 softirq 上。把网卡队列、RPS/XPS、ring 缓冲、中断亲和设好,能让核“吃得更均匀”。
打开多队列/调整通道数
ethtool -l eth0 # 看当前/最大队列数
ethtool -L eth0 combined 8 # 例如设 8 组队列(按 CPU 和流量定)
增大 ring 缓冲(防突刺)
ethtool -g eth0
ethtool -G eth0 rx 4096 tx 4096
确认 offload(一般保留 GRO/TSO/GSO,LRO 视环境)
ethtool -k eth0
# 如需调整:ethtool -K eth0 gro on tso on gso on
中断绑核(可交给 irqbalance;极端情况下手动绑)
# 先看中断号
grep eth0 /proc/interrupts
# 设置亲和(示例把某中断绑到 CPU 2)
echo 4 > /proc/irq/XX/smp_affinity # 4 是 1<<2 的十六进制(写 4、8、10...)
开启 RPS/XPS(让收发在多个核分担)
# 把 RX 队列分配到一组 CPU(示例:0-7 号核)
echo f0 > /sys/class/net/eth0/queues/rx-0/rps_cpus # 按位图写入
# XPS 类似:
echo f0 > /sys/class/net/eth0/queues/tx-0/xps_cpus
提醒:每次改动后,用 nstat -az、sar -n DEV 1、top -H、/proc/softirqs 观察是否有效果。
火力点六:firewalld 的**“省心而高效”**用法
很多人卡在 rich-rule 又爱又恨:可读性好,但写多了 reload 变慢、规则易冗余。我的做法:
策略:能用 direct 就用 direct(直写到 iptables),批量用 ipset 管住;rich-rule 留给少数“读起来舒服”的规则。
Zone:边界机通常只要 public,不要“一个端口一个 zone”。
reload 原则:先 --runtime 调试确认,再 --permanent,最后 --reload 一次性落盘。
示例:
# 1) 基本面
firewall-cmd --set-default-zone=public
firewall-cmd --permanent --add-service=ssh
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
# 2) ipset(持久化)
firewall-cmd --permanent --new-ipset=blocklist --type=hash:ip
firewall-cmd --permanent --ipset=blocklist --add-entry=198.51.100.0/24
# 3) direct 规则(优先级 0 越小越靠前)
firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 0 \
-m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 1 \
-m set --match-set blocklist src -j DROP
firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 2 \
-m set --match-set mgmt_allow src -p tcp --dport 22 -j ACCEPT
firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 3 \
-p tcp -m multiport --dports 80,443 -j ACCEPT
firewall-cmd --reload
纯 iptables 模式:当你决定关掉 firewalld
极简、稳定、可预测。在规则不常改、追求极致性能的边界机上,我常这么做:
# 1) 关 firewalld,启 iptables-services
systemctl disable --now firewalld
yum install -y iptables-services
systemctl enable iptables
# 2) 配置 /etc/sysconfig/iptables
cat >/etc/sysconfig/iptables <<'EOF'
*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
-A INPUT -i lo -j ACCEPT
-A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
-A INPUT -m set --match-set blocklist src -j DROP
-A INPUT -m set --match-set mgmt_allow src -p tcp --dport 22 -j ACCEPT
-A INPUT -p tcp -m multiport --dports 80,443 -j ACCEPT
-A INPUT -m limit --limit 10/second --limit-burst 50 -j NFLOG --nflog-group 10 --nflog-prefix "FW_DROP:"
-A INPUT -j DROP
COMMIT
EOF
# 3) 保存并生效
service iptables save || true
systemctl restart iptables
注意:ipset 的持久化要么用 ipset save/restore + systemd 单元,要么写到开机脚本里。
验证与压测:用事实说话
命令小抄
# 流控/丢包/重传
nstat -az | egrep 'Tcp|Udp|Ip'
ss -s
sar -n DEV 1
# 规则命中
iptables -L -v -n
iptables -t raw -L -v -n
# 连接跟踪
cat /proc/sys/net/netfilter/nf_conntrack_count
conntrack -S
# 压测(外部节点)
iperf3 -c <边界机> -p 443 -u -b 2G -t 60
wrk -t16 -c2000 -d60s https://your_edge/
一次真实的前后对比
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 峰值 PPS | 1.8 Mpps | 1.8 Mpps |
| 丢包率(5 分钟 95P) | 2.3% | 0.2% |
softirq CPU 占用 |
620%(多核总和) | 330% |
nf_conntrack_count / max |
3.7M / 3.8M | 1.9M / 4.0M |
| iptables LOG 命中/秒 | 12k | ≤100(限速后) |
| RTT 抖动(p95) | 42ms | 12ms |
影响最大的三件事:(1)ipset 折叠规则、(2)NOTRACK 探测 UDP、(3)conntrack 上限 + 合理超时。网卡队列和 ring 的优化则把峰值下的抖动“磨平了”。
我踩过的坑(你别再踩)
- hashsize 不能热改:很多人改了 nf_conntrack_buckets 却没重启,以为生效了。记得用 modprobe.d 配好,窗口期重启一次。
- firewalld reload 把 SSH 断了:先确认 --add-service=ssh,再改其他规则;必要时在带外 KVM 上操作。
- LOG 在热路径:LOG 本身很“贵”,还会引出磁盘/IO/用户态开销;分流到 NFLOG + 限速,或干脆只在冷路径打点。
- Docker/容器和防火墙的叠加:Docker 会加 DOCKER / DOCKER-USER 链。统一在 DOCKER-USER 做“前置规则”(比如 mgmt_allow、blocklist),免得和 Docker 生成的规则打架。
- NOTRACK 滥用:只对你完全信任的源和端口用;否则你失去了基于状态的保护。
- 过度收紧超时:time_wait/fin_wait 太低,海量重用/重传会适得其反;先从保守值开始,观察后再收紧。
收尾:天亮前的那杯咖啡
凌晨 5:10,PPS 还在高位,但曲线稳了。KVM 小屏上 nf_conntrack_count 从 370 万回到 190 万,softirq 也不再疯长。
我把 LOG 的频率再降了一格,又把规则表导出留档,最后在机房门口喝完那杯已经不太热的咖啡。
这种夜里修防火墙的活儿,像是在码头绑缆绳——看似是几条规则的顺序、几个参数的数字,真正稳住的,是系统的“气”。
如果你也在一台 CentOS 7 的香港边界机前:
把“已建立连接”放到最前,把“多对多”交给 ipset,把“不需要状态”的 UDP 从 conntrack 中搬走;再把网卡队列排好、缓冲拉足。等天色发白,你会发现,业务没变、机器没换,路就是比刚才宽了。
附:一次性脚本(可按需裁剪)
#!/bin/bash
set -e
# 0) 工具
yum install -y conntrack-tools ipset iptables-services ethtool
# 1) conntrack 参数
cat >/etc/sysctl.d/99-nf-conntrack.conf <<'EOF'
net.netfilter.nf_conntrack_max=4000000
net.netfilter.nf_conntrack_buckets=1000000
net.netfilter.nf_conntrack_tcp_timeout_established=300
net.netfilter.nf_conntrack_tcp_timeout_close_wait=60
net.netfilter.nf_conntrack_tcp_timeout_fin_wait=30
net.netfilter.nf_conntrack_tcp_timeout_time_wait=15
net.netfilter.nf_conntrack_udp_timeout=15
net.netfilter.nf_conntrack_udp_timeout_stream=60
EOF
sysctl --system
# 2) ipset
ipset create mgmt_allow hash:ip -exist
ipset create blocklist hash:ip -exist
ipset create probe_src hash:ip -exist
# 示例:加入你的管理 IP、黑名单、探测源
ipset add mgmt_allow 203.0.113.10 -exist
ipset add blocklist 198.51.100.0/24 -exist
ipset add probe_src 192.0.2.10 -exist
# 3) iptables 基线
iptables -I INPUT 1 -i lo -j ACCEPT
iptables -I INPUT 2 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -I INPUT 3 -m conntrack --ctstate INVALID -j DROP
iptables -I INPUT 4 -m set --match-set blocklist src -j DROP
iptables -I INPUT 5 -m set --match-set mgmt_allow src -p tcp --dport 22 -j ACCEPT
iptables -I INPUT 6 -p tcp -m multiport --dports 80,443 -j ACCEPT
iptables -A INPUT -m limit --limit 10/second --limit-burst 50 -j NFLOG --nflog-group 10 --nflog-prefix "FW_DROP:"
iptables -A INPUT -j DROP
# 4) 原始表 NOTRACK(可选:对可信 UDP)
iptables -t raw -I PREROUTING 1 -m set --match-set probe_src src -p udp --dport 443 -j CT --notrack
iptables -I INPUT 7 -m set --match-set probe_src src -p udp --dport 443 -j ACCEPT
# 5) 保存(若使用 iptables-services)
service iptables save || true
systemctl enable iptables
systemctl restart iptables
需要把 nf_conntrack hashsize 固化到启动:
echo "options nf_conntrack hashsize=1000000" >/etc/modprobe.d/nf_conntrack.conf 并安排重启窗口。