做过存储运维的朋友,大概率都经历过这种场景:业务端同时往存储服务器写数据,刚开始跑得好好的,突然某个客户端的写入速度掉到几十KB,甚至直接断开重连。你赶紧ping一下服务器,发现网络本身是通的,带宽也看似正常,折腾半天没找到根源。如果你遇到这种怪异现象,我建议你先别急着怀疑网线、交换机或者磁盘阵列,先看一眼网卡的 Ring Buffer。
这个问题的典型表现就是存储服务器大流量写入时出现丢包、断流,而排查的重点往往被放在TCP参数、磁盘IO、甚至光纤模块上,很少有人第一时间想到网卡环形队列。但恰恰是这个看似底层的硬件缓冲设置,在iSCSI、NFS、SMB这类存储协议场景下特别容易成为瓶颈。这篇文章我就把整个排查和处理过程拆开讲,包括Ring Buffer的工作原理、怎么用ethtool定位硬丢包、如何调整和持久化配置,以及调完之后仍然掉链子的后续排查方向。适合所有用CentOS/RHEL系系统、把服务器当存储用,或者自己搭过iSCSI/NFS共享的朋友参考。
1. 现象与初步判断:存储服务器写入“卡住”的典型表现
1.1 现场症状:从“速度骤降”到“连接断开”
多数存储故障不是突然完全瘫痪,而是先出现一系列恼人的“慢性病”。我遇到过的典型案例是:一台32核、64GB内存的存储服务器,通过四口千兆网卡bonding对外提供iSCSI服务,后端是硬件RAID阵列。白天业务正常时,多台客户端同时往里写视频素材,速度能稳定在200MB/s左右,但一到高峰时段,几个客户端几乎同时开始大批量写文件,写入速度就像坐了过山车——从200MB/s瞬间跌到10MB/s,随后某个客户端的iSCSI会话直接报错断开,应用层提示“连接超时,重试中”。
在服务器上观察,系统负载并不高,CPU使用率不到30%,磁盘IO也没到极限,iostat显示的utilization还远未打满。这时候很多人会怀疑是网络带宽问题,可千兆网卡理论带宽也就120MB/s左右,四口bonding按说足够。但问题恰恰出在网卡收包环节,而不在带宽总量。
1.2 为什么存储写入场景最容易踩Ring Buffer的坑
存储服务器的流量特征和普通Web服务器不一样:它是持续的、大量的、数据包密集的单向或准单向流量。客户端写入时,服务端网卡每秒钟要接收成千上万个数据帧,尤其当MTU为1500字节时,一个10MB的文件也会被拆成七千多个包。如果MTU是9000(巨帧),包数量会少一些,但很多内网环境并没有全链路支持巨帧。
TCP协议栈本身有接收缓冲区和拥塞控制,正常情况下即使偶发丢包,也能通过重传恢复,不会影响最终数据完整性。但丢包的代价是巨大的:一旦某个TCP段丢失,发送端必须等待超时或收到重复ACK才触发重传,这个等待时间会显著增加往返延迟。存储协议如iSCSI、SMB、NFS对延迟极其敏感,延迟一升高,客户端就可能判定会话超时,从而断流重连。而Ring Buffer就是收包路径上最容易被忽略的一环:如果网卡DMA写入数据包的环形队列容量太小,新到的包没有位置存放,网卡只能直接丢弃,这就会造成我们看到的丢包和断流。
1.3 第一步丢包定位:先查ethtool的“硬”计数
遇到这种现象,别急着改TCP栈参数,更不要一上来就换网线。先登录存储服务器,用ethtool看一眼网卡的真实统计计数。CentOS 7/CentOS 8/RHEL系列都自带ethtool,直接执行:
ethtool -S eth0 | grep -E 'rx|drop|discard|miss|error'注意把eth0换成你实际的网卡名,bonding的情况下要看bond成员的物理口。我通常重点看这几个字段:
rx_dropped:表示驱动/网卡因为缓冲不足或资源问题主动丢掉的包rx_missed_errors:表示硬件因为DMA描述符不足而错过接收的包,这是Ring Buffer过小的典型信号rx_fifo_errors:表示FIFO溢出,和Ring Buffer过小也有直接关系rx_errors:综合错误计数,需要进一步分解
如果这些计数从服务器开机到现在一直为0,只能说明“暂时没发生硬丢包”。更好的做法是在故障发生时连续观察,比如每两秒刷新一次,看这些计数是否在肉眼可见地增长。如果rx_missed_errors在高峰时段不断上涨,那基本可以确认就是Ring Buffer或者是中断处理能力不足导致的硬件丢包。
2. Ring Buffer到底是什么?为什么影响如此之大
2.1 网卡环形队列的工作机制:仓库门口的“卸货缓冲区”
Ring Buffer,中文叫环形缓冲队列,是网卡驱动在内存中维护的一段环形数组,专门用来存放网卡收发的数据帧描述符。你可以把它想象成一个港口仓库的卸货区:网线相当于港口外的航道,数据包是一艘艘到港的货船,而CPU的协议栈则是仓库内部的搬运工。货船到达后,港口必须先把它们停在卸货区,搬运工再挨个把货物运进仓库。
Ring Buffer就是这个“卸货区”的大小。它的工作流程是:网卡收到数据包后,通过DMA直接把数据写到Ring Buffer对应的内存区域,然后通知CPU“有新包到了,快来处理”。CPU(确切地说是中断处理程序)从Ring Buffer里拿到数据包,交给协议栈处理,然后释放这个位置,Ring Buffer就空出来了,继续接收新的包。
如果Ring Buffer已经满了,而CPU还没处理完前面的包,新到的包就无处停放。港口只能拒绝后面的船靠岸,网卡也只能将这包直接丢弃。这就是我们所说的“硬丢包”。
2.2 默认值和大流量写入的冲突:256/512真的够吗
很多服务器的网卡默认Ring Buffer大小只有256或512个描述符,如果是千兆网卡面对百兆级别的普通办公流量,这个大小勉强够用;但对于存储服务器这种每秒要处理上万个数据包的大流量写入场景,256的深度实在太浅了。
为什么这么说?因为CPU处理数据包需要时间,特别是当软中断负载较高、或CPU被其他任务抢占时,包在Ring Buffer中的驻留时间会显著增加。假设网卡每毫秒到达100个包,而CPU处理100个包需要2毫秒,那缓冲区至少需要200个位置才能保证不丢包。如果这时又出现一个调度抖动,CPU某1毫秒没有及时处理,缓冲区就瞬间溢出。存储流量往往还有“突发性”,多个客户端同时写入时,包速可能瞬间翻倍,默认的Ring Buffer深度根本兜不住。
我做过一个简单的量化实验:单客户端从存储服务器读取10GB文件,MTU=1500,包速率平均值在每秒8000个左右;当三个客户端同时读写时,包速率峰值能到每秒25000个以上。这种包速率下,512的Ring Buffer如果遇到CPU中断合并延迟,很容易被打满。所以存储服务器上把Ring Buffer从512调到2048或4096,很多时候能直接解决问题。
2.3 为什么iSCSI、NFS这类存储协议对丢包尤其敏感
有人可能会说:“TCP本来就允许丢包重传,丢几个包有什么大惊小怪?”但存储协议对延迟的容忍度远低于普通HTTP浏览。以iSCSI为例,客户端发出一个SCSI写命令,封装成TCP包发到服务端,服务端完成写入后回复一个iSCSI响应。整个交互模式下,任何一个包丢失,客户端都需要等待重传和响应,响应时间可能从正常的0.2毫秒飙升到几毫秒甚至几十毫秒。
如果丢包事件频繁发生,累积的超时会导致TCP拥塞窗口不断缩小,吞吐量急剧下降。存储服务商对iSCSI的延迟要求通常是在毫秒级别,一旦超过上限,客户端会认为对端不可用,直接断开连接,这就是我们看到的“断流”。SMB/NFS也类似,它们都有会话超时机制,网络抖动一大,应用层就报错。
更重要的是,Ring Buffer丢包不只是“丢一个包”这么简单。当你看到Rx missed errors出现时,往往是一瞬间连续丢了大量包(因为队列是满的,新来的全都被拒收),TCP重传需要重发一堆数据,网络状态雪上加霜。
3. 定位与实测:一步步找到“Ring Buffer设置不合理”
3.1 查看当前Ring Buffer状态:ethtool -g 一眼看清上限和当前值
在调整之前,先看当前硬件和驱动支持的最大值。执行:
ethtool -g eth0输出类似:
Ring parameters for eth0: Pre-set maximums: RX: 4096 RX Mini: 0 RX Jumbo: 0 TX: 4096 Current hardware settings: RX: 256 TX: 256这里的Pre-set maximums表示驱动/网卡允许配置的最大深度,Current hardware settings表示当前生效值。如果当前RX数值明显小于上限,比如这里RX只有256,而最大值是4096,那就有很大的调优空间。我见过有些服务器默认RX=512、TX=512,但其实驱动支持到4096,只是没配置而已。
如果Pre-set maximums本身就是1024或更小,说明这块网卡的驱动能力有限,调优空间不大。这时候就要考虑从其他方向入手,比如多队列、中断合并优化。
3.2 用ethtool -S对比丢包计数:在故障窗口下抓增长
静态看一次统计意义不大,关键要看故障时刻计数是否在增长。我的习惯是开两个终端窗口,一个持续打流量,另一个每隔一秒执行:
watch -n 1 'ethtool -S eth0 | grep -E "rx_missed|rx_dropped|rx_fifo" '或者直接在压测过程中手动连续刷几次:
ethtool -S eth0 | egrep "rx_missed|rx_dropped|rx_fifo" ; sleep 1; ethtool -S eth0 | egrep "rx_missed|rx_dropped|rx_fifo"对比两次计数,如果rx_missed_errors从100变成3000,说明在这一秒内丢了几千个包,那基本可以断定Ring Buffer深度不够或者中断处理有瓶颈。如果这两个计数完全不涨,那丢包原因可能不在网卡接收队列,而在内核协议栈或应用层,需要继续排查。
同时,/proc/net/dev也能看到接口层的统计,比如dropped、fifo,可以作为交叉验证:
cat /proc/net/dev3.3 复现压测:用fio和dd模拟大流量写入
为了稳定复现问题,我会在存储服务器上先用开源工具fio打一下块设备或文件,同时从另一个客户端往挂载点写数据,模拟真实存储写入。fio是存储性能测试的标配,简单执行:
fio --filename=/mnt/storage/testfile --size=20G --rw=write --bs=128k --iodepth=32 --numjobs=8 --runtime=60 --time_based --group_reporting --name=write_test这个命令会生成8个线程并发写20GB文件,块大小128k,队列深度32。如果你通过NFS或iSCSI挂载了这个存储,也可以直接在客户端跑fio写挂载点,效果更接近真实业务。
如果不想装fio,用dd凑合也能测:
dd if=/dev/zero of=/mnt/storage/test.bin bs=1M count=20000 conv=fdatasync压测的同时,观察之前说的丢包计数器。很多情况下你会看到rx_missed_errors随着测试启动快速上涨,这就是最有力的证据。
3.4 检查中断合并和多队列:只调Ring Buffer可能不够
如果Ring Buffer调到最大值后丢包依然存在,就要检查另外两个配套因素:合并中断(Coalesce)和多队列(Multi-queue)。
中断合并机制允许网卡攒一批包再通知CPU,从而降低中断次数,提升吞吐。但如果合并间隔设置得太长,比如rx-usecs=200,网卡会等待200微秒才通知CPU,这期间Ring Buffer满了就只能丢包。可以查看当前合并参数:
ethtool -c eth0重点看rx-usecs和rx-frames。如果rx-usecs偏大,可以适当调小,比如改为15~30微秒,提高CPU响应的及时性。但同时会带来更多中断,CPU占用上升,需要平衡。
多队列方面,网卡如果支持RSS(接收侧扩展),应该开启多个队列,让不同队列的中断均匀分布在多个CPU核上。查看队列数:
ethtool -l eth0如果Current hardware settings的Combined值只有1,说明所有流量都挤在一个队列上。驱动支持的情况下可以这样开:
ethtool -L eth0 combined 4开启后,检查中断分布:
cat /proc/interrupts | grep eth0正常情况下可以看到各个队列的中断计数在不同CPU核心上交替增长。
4. 调整方法与持久化配置:从临时生效到重启不掉
4.1 临时调整:一行命令立竿见影
确认问题后,调整Ring Buffer非常简单:
ethtool -G eth0 rx 4096 tx 4096把RX和TX的深度都设置成4096。如果网卡型号比较老,可能只支持RX,不支持TX,甚至不支持手动修改。没关系,能调多少调多少。执行后立刻用ethtool -g eth0确认当前值已经变成4096,再跑一次压测,观察丢失计数是否停止增长。
这里有个小经验:不要一上来就调满,尤其是老驱动,调到最大值可能带来新问题。建议先调成1024或2048,观察一段时间,不够再加。存储场景下一般2048已经足够,盲目调到4096反而可能因为DMA内存分配过大导致驱动不稳定。
4.2 CentOS下持久化配置:ifcfg-ETHTOOL_OPTS、rc.local、systemd三种方案
临时调整在重启后会失效,必须做持久化。不同系统版本方法不太一样,我按实际效果排个序。
如果你的系统还在用network service管理(比如CentOS 7的非NetworkManager环境),可以在网卡配置文件/etc/sysconfig/network-scripts/ifcfg-eth0中加入:
ETHTOOL_OPTS="-G eth0 rx 4096 tx 4096"保存后重启网络服务或重启网卡,配置会生效。这种方式对CentOS 7/8都有效,前提是网卡由ifup脚本管理,而不是NetworkManager。
第二种方法是写成rc.local。CentOS 7的/etc/rc.local需要执行权限,先chmod +x /etc/rc.local,然后在里面加一行:
/usr/sbin/ethtool -G eth0 rx 4096 tx 4096缺点是rc.local的执行时机较晚,可能在网卡启动后但服务启动前,对于某些服务已经足够。CentOS 8/RHEL8之后,rc.local默认可用性有所变化,我不太推荐。
第三种更稳健的方法是创建一个systemd oneshot服务。我在生产环境就用这种方式,因为可控性强,依赖关系明确。创建/etc/systemd/system/ethtool-ringbuffer.service:
[Unit] Description=Set network ring buffer to optimized size After=network.target [Service] Type=oneshot ExecStart=/usr/sbin/ethtool -G eth0 rx 4096 tx 4096 RemainAfterExit=yes [Install] WantedBy=multi-user.target然后执行:
systemctl daemon-reload systemctl enable ethtool-ringbuffer.service systemctl start ethtool-ringbuffer.service这样每次开机都会在network.target启动之后自动设置Ring Buffer。如果你有多个网卡要设置,可以在ExecStart里写多行命令,每行一个网卡。
4.3 配套优化:中断合并、多队列、RSS与CPU亲和性
前面说了,光调Ring Buffer有时候治标不治本。如果网卡支持多队列,强烈建议开启。以下是我在存储服务器上常用的一组组合命令,假设网卡驱动支持:
# 调整Ring Buffer大小 ethtool -G eth0 rx 4096 tx 4096 # 调整中断合并:适当降低合并延迟,提高响应及时性 ethtool -C eth0 rx-usecs 15 rx-frames 8 # 开启多队列(以4个队列为例,按实际CPU核心数调整) ethtool -L eth0 combined 4 # 设置RSS哈希,让流量均匀分布在队列上 ethtool -X eth0 equal 4如果系统里有irqbalance服务,建议让它自动平衡中断;如果没有,可以手动绑定中断到指定CPU核心。手动绑定示例:
# 先查看eth0队列中断号 grep eth0 /proc/interrupts # 然后把第一个队列中断绑定到CPU0 echo 1 > /proc/irq/中断号/smp_affinityCPU亲和性这块要结合自己的部署环境来定,不是越复杂越好。存储服务器上如果业务相对单一,让irqbalance自动处理通常就够了。
4.4 参数不是越大越好:内存占用、延迟与稳定性
Ring Buffer调大带来的主要好处是“缓冲余量”变大,但这并不代表数值越大越优。每个ring描述符都会关联一个数据缓冲区,RX/TX都设成4096时,仅一个网卡就可能占用几十MB内存,虽然对现代服务器来说不算什么,但如果你的机器有多个网卡,每个网卡都设成最大值,累计内存开销也可观。
更重要的是,队列深度过大会增加数据包在缓冲区的排队延迟。对于存储这类延迟敏感场景,延迟升高可能带来副作用。所以我的经验是:先调整到2~4倍的充足度,比如从256调到1024或2048,绝大多数场景已经能解决问题;只有确认峰值流量极大时才考虑4096。如果一个网卡默认最大值连1024都不到,那说明驱动或硬件本身能力有限,与其硬调,不如换网卡或降低单网卡负载。
5. 常见问题排查与经验总结
5.1 调整后依然丢包:检查网卡之外的内核队列
如果调整了Ring Buffer、多队列、中断合并之后,ethtool -S里的rx_dropped、rx_missed_errors不再增长,但客户端仍反馈延迟高、断流,那就需要把排查重心转移到内核协议栈的另一个缓冲:netdev_max_backlog。
这是内核中处理网络设备接收队列的最大包数量,当CPU来不及处理时,数据包会积压在这个队列里。查看它的溢出情况,可以执行:
cat /proc/net/softnet_stat输出第一个数字是CPU0的收包总数,第二个数字是软中断丢包总数。如果第二列不为0且在增长,说明软中断处理速度跟不上,需要调大backlog:
sysctl -w net.core.netdev_max_backlog=10000再配合调整somaxconn和TCP缓冲区:
sysctl -w net.core.somaxconn=1024 sysctl -w net.ipv4.tcp_rmem='4096 87380 16777216' sysctl -w net.ipv4.tcp_wmem='4096 65536 16777216'当然这些参数要结合实际情况,不要照搬。存储服务器上重点还是先解决网卡层的硬丢包。
5.2 网卡型号、驱动版本差异对调参的限制
不同的网卡芯片/驱动对ethtool参数的支持差异很大。Intel的ixgbe、i40e、ice系列驱动都支持Ring Buffer调整和多队列;Broadcom的bnx2x、tg3系列也基本支持;但一些入门级Realtek网卡(比如RTL8111/8168)的驱动功能就非常有限,可能连ethtool -g都看不到多少调节空间,甚至不支持多队列。
如果服务器要用作存储,我建议不要在主板上省网卡的钱。Intel/博通/迈络思等服务器网卡在驱动稳定性和调优参数上会好很多。如果已经遇到Ring Buffer受限的问题,另一个替代思路是使用网卡bonding分流:用多个物理网口做起bond,流量分散后每个口的包速率下降,缓冲区压力自然缓解。但要注意bond模式的选择,存储场景一般选mode 4(802.3ad)配合交换机动态聚合,或者mode 6(balance-alb)在某些环境下也能用。
5.3 一次实际调优的记录:调前调后对比
这里记录一次典型的调优过程,供参考。某存储服务器,两块Intel I350千兆网卡bond成mode 4,对外提供iSCSI存储。高峰时三台客户端同时写入,现象是每秒都有iSCSI会话断开重连,服务器上ethtool -S bond0的成员口eth0、eth1看到rx_missed_errors以每秒几千的速度增长。
当时的默认配置是RX=512、TX=512,Combined队列数1。调整步骤:
- 临时把两个物理口的RX/TX都改成2048:
ethtool -G eth0 rx 2048 tx 2048 ethtool -G eth1 rx 2048 tx 2048- 开启每个口的4队列RSS:
ethtool -L eth0 combined 4 ethtool -L eth1 combined 4- 适当调整中断合并:
ethtool -C eth0 rx-usecs 15 tx-usecs 15 ethtool -C eth1 rx-usecs 15 tx-usecs 15- 写入systemd服务使配置持久化。
调整后,rx_missed_errors不再增长,iSCSI会话稳定,客户端写入速度从原来断断续续的几十MB/s恢复到稳定的230MB/s(千兆bond下的合理水平)。CPU使用率比之前高了5%左右,但远未成为瓶颈。这算是一次非常典型的“Ring Buffer设置不合理导致丢包断流”的解决案例。
5.4 建立监控意识:用开源工具提前发现丢包趋势
与其等到业务报障再去排查,不如提前做监控。CentOS系统里的sysstat自带sar -n EDEV可以看网卡设备错误:
sar -n EDEV 1其中rxdrop/s列就是每秒接收丢包数,我建议运维人员每天登录后都看一眼,同时用脚本设置阈值报警。
如果你们已经有Prometheus监控体系,node_exporter自带的node_network_receive_drop_total指标就是这个数据,直接定义一个告警规则,比如5分钟内的增量超过100就触发警告,可以提前把你从睡梦中叫醒。
这也是我喜欢开源软件的原因——监控、压测、告警,全部有成熟的免费工具链可以搭起来。存储服务器看似只是“把服务器当存储用”,但背后涉及的网络参数调优一点不比应用服务器少,提前架好监控能省下大量救火时间。
最后分享一个我个人的习惯:每次给存储服务器更换或升级网卡驱动、系统内核之后,我都会重新检查一遍ethtool -g的当前设置。因为驱动升级有时会重置硬件参数,如果忘了持久化配置,Ring Buffer会在不知不觉间回到默认值,问题很可能在几周后再次出现。还有一个小技巧:把服务器所有网卡的Ring Buffer、队列数、中断合并参数整理成一个表格放在文档里,每次变更后对照检查,能少踩很多坑。这就是我在多次处理“大流量写入丢包断流”之后最想对大家说的话。