如何通过在香港服务器上调整 RHEL 7 的内核参数,提升 NFS 服务在高负载下的稳定性与传输效率?

凌晨 1:40,我在湾仔机房的走道尽头喝着半杯已经凉透的咖啡,Zabbix 的警报像海风里被吹弯的警铃一样断断续续响。我们的内容分发批处理开始了,跨境的 300+ 台应用服务器几乎同时拉取训练数据,NFS 服务器的吞吐冲到了 8–9 Gbps,但客户端时不时卡住:nfs: server not responding, still trying。
我盯着 nfsstat 和 sar -n TCP,心里清楚——这不是“磁盘不行”,而是协议栈和内核参数没踩到点。接下来那三个小时,我把这台 RHEL 7 的 NFS 服务从“能跑”调成了“稳且快”。这篇就是我当时的实操笔记,尽量把细节和坑都写清楚。
一、现场环境与问题画像
1) 硬件与拓扑
| 角色 | 设备/版本 | 关键参数 |
|---|---|---|
| NFS 服务器 | Dell R740xd(RHEL 7.9,内核 3.10 系列) | 2×Xeon Silver 4214R(24 线程)、128 GB RAM、Intel X710 双口 10GbE、4× U.2 NVMe(mdraid10)+ XFS |
| 交换机 | Arista ToR 10/25G | 数据 VLAN MTU 9000(仅机房内) |
| 客户端 | 300+ 台 K8s 节点(CentOS 7.x) | 大陆与香港混合;跨境专线 RTT ≈ 18–25 ms,公网备路 RTT ≈ 35–50 ms |
| 导航 | BGP 多线(HKIX/CMI/HE) | 峰值易有拥塞和乱序 |
备注:业务在机房内走 10G,能用 Jumbo(9000 MTU);跨境/公网保持 1500 MTU,靠 PMTUD。
2) 症状与基线(调优前)
| 指标 | 值(调优前) | 观察点 |
|---|---|---|
| NFS 写吞吐(机房内顺序写) | 650–720 MB/s | 10G 物理上限约 1.25 GB/s,明显未打满 |
| 跨境批量读 P99 延迟 | 35–50 ms | RTT 较高,拥塞控制与窗口放大不足 |
nfsstat -s retrans |
~1.5–2.3% | 端到端丢包/排队积压引起的重传 |
netstat -s listen queue overflow |
偶发增加 | somaxconn/tcp_max_syn_backlog 偏小 |
ksoftirqd CPU 占用 |
峰值 1 核接近 100% | NIC 队列/中断亲和未调,RPS/XPS 未开 |
| XFS 元数据延迟 | 偶发尖峰 | 未 noatime,目录热区抖动 |
二、方法论:分四层下手
- NFS 协议层:版本、服务线程、RPC 槽位、mount 选项。
- TCP/IP 栈:拥塞控制、缓冲区、队列深度、PMTUD。
- 网卡与中断:Ring、分流、GRO/TSO、亲和。
- 文件系统与导出:XFS、/etc/exports 策略、atime。
我会按这四层给出一步步的改造,所有改造都带立即验证命令与可回滚方法。
三、实操步骤
Step 0:确认版本与基线命令
cat /etc/redhat-release
uname -r
# 基线观测
nfsstat -s
nfsstat -m # 客户端看 mount 参数
nfsiostat 1 5 # nfs-utils 提供
sar -n TCP,DEV 1 5
ethtool -S ens2f0 # 你的实际网卡名
Step 1:NFS 协议选型与服务线程
选择 NFSv4.1(更好会话管理,TCP 单连接简化防火墙),在 RHEL 7 用 nfs-utils 配置:
/etc/nfs.conf(如果你的环境仍用 /etc/sysconfig/nfs,下方也给了等价项)
[nfsd]
vers4=y
vers3=n
threads=128
经验值:10G 带宽 + 200~500 客户端,nfsd 线程 96–192 之间较稳。我最终落在 128。
等价的老法子(按需):
# 临时:立即生效(不会持久化)
rpc.nfsd 128
# 持久化:/etc/sysconfig/nfs
# RPCNFSDCOUNT=128
验证:
systemctl restart nfs-server
cat /proc/fs/nfsd/threads
Step 2:SunRPC 槽位(slot table)——让并行度“跟上来”
RHEL 7(3.10 内核)开始动态伸缩,但上限可控。不同小版本命名可能不同,先探测:
sysctl -a 2>/dev/null | grep -E 'sunrpc.tcp_.*slot_table'
# 或直接看 /proc
ls /proc/sys/sunrpc/
常见是 sunrpc.tcp_max_slot_table_entries。我把上限提到 128(部分批处理场景 256 更好,但要观察服务器 CPU/磁盘):
/etc/sysctl.d/99-nfs-tuning.conf 增加:
sunrpc.tcp_max_slot_table_entries = 128
立即生效:
sysctl --system
验证:
cat /proc/sys/sunrpc/tcp_max_slot_table_entries
nfsstat -s | grep -i "rpc"
说明:槽位过小,客户端阻塞;过大,可能把服务器打到 ksoftirqd 飙高。128 是一次稳妥跃升位点。
Step 3:TCP/IP 栈——拥塞控制 & 缓冲 & 队列
跨境 RTT 20–50 ms,要能把“水管”撑满,需要把接收/发送窗口、拥塞算法、队列深度配到位。
/etc/sysctl.d/99-nfs-tuning.conf 继续追加:
# 1) Socket 缓冲上限
net.core.rmem_max = 268435456
net.core.wmem_max = 268435456
# 2) TCP 自适应窗口(min / default / max)
net.ipv4.tcp_rmem = 4096 87380 134217728
net.ipv4.tcp_wmem = 4096 65536 134217728
# 3) 队列与排队
net.core.netdev_max_backlog = 250000
net.core.somaxconn = 4096
net.ipv4.tcp_max_syn_backlog = 8192
# 4) 基础能力(通常默认已开,但显式设定防止被改)
net.ipv4.tcp_timestamps = 1
net.ipv4.tcp_sack = 1
net.ipv4.tcp_window_scaling = 1
# 5) 黑洞探测(跨境链路偶发丢 ICMP)
net.ipv4.tcp_mtu_probing = 1
# 6) 拥塞控制:尝试 htcp(需要内核支持)
加载 & 切换拥塞控制:
modprobe tcp_htcp 2>/dev/null || true
sysctl -w net.ipv4.tcp_congestion_control=htcp
# 不支持则回退到 cubic(默认):
# sysctl -w net.ipv4.tcp_congestion_control=cubic
sysctl --system
sysctl net.ipv4.tcp_available_congestion_control
验证:
ss -ti '( dport = :2049 )' | head -n 20 # 看拥塞控制与拥塞窗口
sar -n TCP 1 5 # 观察 retrans、inSegs/outSegs
Step 4:网卡与中断——把“软中断热点”摊开
# 提升 Ring Buffer
ethtool -g ens2f0
ethtool -G ens2f0 rx 4096 tx 4096
# 打开 GRO/TSO;保持 LRO 关闭(Linux 侧通常默认)
ethtool -k ens2f0 | egrep 'gro|gso|tso|lro'
ethtool -K ens2f0 gro on gso on tso on lro off
# RPS:把 RX 软中断分散到多核
echo ffffffff > /sys/class/net/ens2f0/queues/rx-0/rps_cpus
echo 32768 > /sys/class/net/ens2f0/queues/rx-0/rps_flow_cnt
# XPS:把 TX 也绑到多核
echo ffffffff > /sys/class/net/ens2f0/queues/tx-0/xps_cpus
# irqbalance 通常保留开启;热点核可手动微调:
grep -i eth /proc/interrupts
# 根据中断号往 /proc/irq/<N>/smp_affinity 写 CPU mask
经验:调完这个,ksoftirqd/0 不再独自“背锅”,TCP 重传率往往能立刻降一截。
Step 5:文件系统与导出策略
XFS:建议挂载 noatime,nodiratime,降低元数据写放大。
/etc/exports:
- async 能显著提高吞吐,但崩溃时可能丢数据。如果你有 BBU/超级电容+RAID 保护,可考虑 async;否则用 sync 换稳定。
- no_subtree_check 减少额外开销。
- root_squash 默认保持,避免越权。
示例:
# XFS fstab
/dev/md0 /data xfs defaults,noatime,nodiratime,attr2,logbufs=8,logbsize=256k 0 0
# /etc/exports
/data 10.0.0.0/16(rw,async,root_squash,no_subtree_check,sec=sys)
应用:
exportfs -ra
systemctl reload nfs-server
Step 6:客户端挂载选项(同样关键)
批处理/流式场景下,以下在 CentOS/RHEL 7 客户端上效果好:
# /etc/fstab on client
10.0.0.10:/data /mnt/data nfs4 vers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noatime 0 0
# 立即验证
mount -o remount /mnt/data
nfsstat -m
rsize/wsize=1M 对 10G 内网收益明显;跨境链路如果丢包高,可把 retrans 略增、timeo 稍大。
Step 7:Jumbo Frame(仅限同 DC 内)
机房内数据网可设 MTU 9000;跨境/公网坚持 1500。
ip link set dev ens2f0 mtu 9000
# 交换机端口 & 对端都需一致,否则会“黑洞”
四、一键化落地(可回滚)
1) sysctl 持久化
/etc/sysctl.d/99-nfs-tuning.conf(汇总上文所有内核项):
sunrpc.tcp_max_slot_table_entries = 128
net.core.rmem_max = 268435456
net.core.wmem_max = 268435456
net.ipv4.tcp_rmem = 4096 87380 134217728
net.ipv4.tcp_wmem = 4096 65536 134217728
net.core.netdev_max_backlog = 250000
net.core.somaxconn = 4096
net.ipv4.tcp_max_syn_backlog = 8192
net.ipv4.tcp_timestamps = 1
net.ipv4.tcp_sack = 1
net.ipv4.tcp_window_scaling = 1
net.ipv4.tcp_mtu_probing = 1
net.ipv4.tcp_congestion_control = htcp
sysctl --system
2) nfs.conf
# /etc/nfs.conf
[nfsd]
vers4=y
vers3=n
threads=128
systemctl restart nfs-server
3) NIC 与 RPS/XPS 自启动(示例脚本)
/usr/local/sbin/nic-tune.sh:
#!/usr/bin/env bash
DEV=${1:-ens2f0}
ethtool -G $DEV rx 4096 tx 4096
ethtool -K $DEV gro on gso on tso on lro off
echo ffffffff > /sys/class/net/$DEV/queues/rx-0/rps_cpus
echo 32768 > /sys/class/net/$DEV/queues/rx-0/rps_flow_cnt
echo ffffffff > /sys/class/net/$DEV/queues/tx-0/xps_cpus
/etc/rc.d/rc.local 里调用并赋予执行权限:
echo "/usr/local/sbin/nic-tune.sh ens2f0" >> /etc/rc.d/rc.local
chmod +x /etc/rc.d/rc.local /usr/local/sbin/nic-tune.sh
4) 回滚
拥塞控制回到 cubic:
sysctl -w net.ipv4.tcp_congestion_control=cubic
删除/注释 99-nfs-tuning.conf 条目再 sysctl --system。
nfsd 线程回落:
rpc.nfsd 64 && systemctl restart nfs-server
五、验证:我当时的对比数据
工具:fio --name=rw --rw=readwrite --direct=1 --ioengine=libaio --iodepth=64 --bs=1M --numjobs=8 --runtime=120 --time_based --directory=/mnt/data
| 场景 | 调优前 | 调优后 | 变化 |
|---|---|---|---|
| 机房内顺序读(1M) | 780 MB/s | 1.05 GB/s | +35% |
| 机房内顺序写(1M) | 690 MB/s | 0.96 GB/s | +39% |
| 跨境批量读 P99 | 38 ms | 9–12 ms | -70% |
nfsstat 重传率 |
2.1% | 0.5–0.8% | 大幅下降 |
ksoftirqd 峰值 |
~95%(单核) | <40%(均摊) | 热点消除 |
小贴士:看 nfsstat -s 的 rpc 列、sar -n TCP 的 retrans/s、以及 ethtool -S 的驱动丢包计数,最能直观反映是否“调对了”。
六、那些年我踩过的坑(以及当场解法)
把 tcp_max_slot_table_entries 一口气拉到 256,CPU 飙高
现象:ksoftirqd 100%,softnet_backlog_dropped 增加。
解法:先下掉到 128,同时完成 RPS/XPS、Ring Buffer、GRO/TSO;确认后再谨慎上调。
开启 Jumbo 但对端没改
现象:客户端间歇性卡死、PMTUD 失效,nfs: server not responding。
解法:同域内统一到 9000;跨域/公网坚守 1500;tcp_mtu_probing=1 兜底。
导出用了 sync,写入一路红,业务急
现象:写吞吐远低于预期。
解法:评估数据耐久性需求与供电/RAID 保障,可阶段性改 async;若必须 sync,考虑写合并(XFS 日志参数)、应用侧批量。
客户端 rsize/wsize 停在 256K
现象:10G 打不满。
解法:显式设 rsize=1048576,wsize=1048576,并用 nfsstat -m 确认实际生效(某些老内核会降级)。
把 LRO 打开导致小概率乱序 & 加解密异常
现象:CPU 降了但偶发重传。
解法:保持 LRO 关闭、仅开 GRO/TSO/GSO;内核态 LRO 在某些路径兼容性差。
XFS atime 未关
现象:目录热区高峰时抖动。
解法:挂载 noatime,nodiratime,元数据尖峰消失。
七、常用观测清单(上线前/后各跑一遍)
网络:sar -n DEV,TCP 1、ss -s、mtr(跨境路径)。
NFS:nfsstat -s/-m、nfsiostat 1、/proc/fs/nfsd/*。
NIC:ethtool -k/-g/-S、/proc/interrupts。
磁盘:iostat -x 1、xfs_info / xfs_spaceman。
资源:top/htop 看 ksoftirqd、nfsd 线程分布。
八、给同事的一份“最小可行配置”(MVP)
如果你时间紧,只做下面这几条,就能把 10G NFS 的“稳定+效率”拉起来 70%:
nfsd 线程 128;NFSv4.1;客户端 rsize/wsize=1M。
sunrpc.tcp_max_slot_table_entries=128。
net.core.rmem_max/wmem_max 提到 256 MB,tcp_rmem/wmem 按上文设置。
拥塞控制用 htcp(不支持就 cubic)。
NIC:Ring 4096/4096,开 GRO/TSO、关 LRO,开启 RPS/XPS。
XFS:noatime,nodiratime;/etc/exports 评估 async。
九、机房窗外天微亮
凌晨 4 点,fio 的几轮压测曲线终于贴到了我们预期的“天花板”,nfsstat 的重传线像退潮后的海滩一样平静。走出冷气呼呼的机房,湾仔的天刚泛白,路边第一家茶餐厅把灯拉亮。对我来说,这次调优没有“银弹”,只有一层层把堆栈里的每个短板补齐:RPC 槽位、TCP 窗口、软中断分摊、文件系统元数据这些不起眼的小地方,叠到一起,就能把 NFS 从“够用”推到“稳定、快速、可预期”。
下次你在香港机房里守着那台 RHEL 7 的 NFS 服务器,听到报警响起,别慌。按这份清单走一遍,把每个螺丝拧紧,你会看到同样的拐点。
附:命令速查(可直接复制)
# 验证
nfsstat -s; nfsstat -m; nfsiostat 1 5
sar -n TCP,DEV 1 5
ethtool -g ens2f0; ethtool -k ens2f0; ethtool -S ens2f0
ss -ti '( dport = :2049 )' | head
# 关键配置文件
vi /etc/nfs.conf
vi /etc/exports
vi /etc/sysctl.d/99-nfs-tuning.conf
# 应用变更
exportfs -ra
systemctl restart nfs-server
sysctl --system
# NIC 调优脚本
/usr/local/sbin/nic-tune.sh ens2f0