500 封大附件邮件之后:一次邮件集群 hung IO 的全链路复盘
500 封大附件邮件之后:一次邮件集群 hung IO 的全链路复盘
僵尸任务不能靠
kill -9清理,hung IO 也不能靠“修复文件系统”或“关闭 fsync”解决。真正需要回答的是:谁在等 IO、等的是哪一层、为什么等这么久,以及恢复后如何避免再次发生。
一次批量发送大约 500 封、单封约 10MB 的邮件后,邮件集群出现性能异常:应用侧写入变慢,内核开始打印 blocked for more than 120 seconds,多个 mssvr 任务进入 D 状态,邮件队列一度积压。
现场最容易出现的误判是:
- 看到 D 状态就认为进程死锁。
- 看到 XFS 调用栈就准备执行
xfs_repair。 - 看到
fsync很慢就想关闭fsync。 - 看到 OCFS2 日志刷屏就认为 OCFS2 损坏。
把应用日志、内核栈、虚拟机磁盘拓扑、宿主机存储路径和写入量放在一起之后,结论并不是“XFS 坏了”,而是:邮件写入在 XFS 日志提交处等待底层 IO,形成了长时间排队。XFS 和 mssvr 更像等待者,根因要继续向下追。
本文中的节点和地址均已脱敏,重点保留可复用的分析方法。
一、先分清三个概念
1. D 状态不是 CPU 忙
Linux 进程状态中的 D 表示不可中断睡眠,通常是在等待内核完成 IO。处于 D 状态的进程不能被普通信号及时终止,kill -9 也要等 IO 返回后才可能生效。
短暂进入 D 状态并不罕见。真正需要关注的是:
- 持续时间超过
hung_task_timeout_secs,默认通常是 120 秒。 - D 状态任务数量持续增加。
- 邮件队列年龄、写入 P99 和应用超时同时恶化。
- 出现
xfsaild、xfs_log_worker、qemu等关键任务长期阻塞。
2. XFS 调用栈不等于 XFS 损坏
事件中的 mssvr 调用栈集中为:
xlog_cil_force_lsn
xfs_log_force_lsn
xfs_file_fsync
do_fsync这条链路表示应用调用了 fsync(),XFS 正在等待日志推进到指定 LSN。调用栈里没有出现:
metadata I/O error
log I/O error
filesystem shutdown
corruption因此,仅凭这段栈不能得出“XFS 元数据损坏”的结论。
3. 僵尸进程不能直接杀
僵尸进程已经退出,只保留进程表项,等待父进程调用 wait() 回收。处理对象不是僵尸 PID,而是它的父进程,或者让父进程退出后由 PID 1 接管回收。
ps -eo pid,ppid,stat,comm,args | awk '$3 ~ /^Z/'如果父进程本身卡在 D 状态,应先解决底层 IO 阻塞,再处理僵尸回收问题。
二、应用日志告诉我们什么
一天的应用日志共有约 770 万行、超过 180 万条带耗时记录。统计结果如下:
| 操作耗时 | 数量 |
|---|---|
| 大于等于 1 秒 | 16,806 |
| 大于等于 10 秒 | 4,801 |
| 大于等于 30 秒 | 2,847 |
| 大于等于 60 秒 | 1,480 |
| 大于等于 300 秒 | 100 |
| 最大单次耗时 | 392,976ms,约 6 分 33 秒 |
耗时超过 1 秒的操作主要集中为:
| 操作类型 | 超过 1 秒 |
|---|---|
put mail data | 7,860 |
copy mail | 4,638 |
parse mail | 220 |
get mail offset | 9 |
这组数据的意义是:
- 问题集中在邮件写入、复制和落盘路径。
- 读取邮件偏移基本正常。
- 小到几 KB 的邮件也出现百秒级等待,因此不能简单归因于“附件太大”。
- 大量操作在
21:39附近集中完成,符合底层资源长时间阻塞后集中释放的特征。
从写入来源看,约 65% 的相关流量来自集群同步 Copy,约 33% 来自投递侧 Put。p:338 端口是否属于同步组件,仍需在连接端通过 ss、netstat 和进程信息确认,不能只凭端口号下结论。
三、三台邮件节点的内核证据
三台邮件虚拟机都使用 virtio_blk 虚拟磁盘,guest 内没有看到 iSCSI、multipath 或 SCSI 设备。
| 节点 | 阻塞时间 | 关键现象 |
|---|---|---|
| 节点 A | 启动后 68813 秒 | 10 个 mssvr 等待 XFS 日志提交 |
| 节点 B | 启动后 68445 秒 | 10 个 mssvr,调用栈与节点 A 一致 |
| 节点 C | 启动后 492 秒 | XFS 日志线程、EOF blocks 线程和 8 个 mssvr 同时阻塞,涉及 dm-2 |
节点 A 和节点 B 的异常时间接近,可能对应同一轮群发或同一存储后端抖动。节点 C 是启动后早期的另一类事件,不能和另外两台直接合并为同一时间线。
节点 C 的调用栈还出现:
xfs_log_worker
xfs_eofblocks_worker
xfs_free_eofblocks
xfs_inode_free_eofblocks这说明不仅邮件进程在等待,XFS 自己的日志和空间回收工作线程也被拖住。
但要注意:虚拟机内只看到 virtio_blk,真正的 IO 路径通常还要继续向下:
guest dm-2/dm-3
-> virtio_blk
-> QEMU/KVM 虚拟磁盘
-> 宿主机 qcow2、LVM 或 LUN
-> 数据存储或 SAN 后端因此,guest 里的 LUN reset、XFS 修复和内核参数调整都不一定能触及真正的瓶颈。
四、单流性能正常,为什么还会 hung IO
现场做过一个 20MiB 顺序写测试,fdatasync 后单文件耗时约 34ms 到 174ms,速度约 120MB/s 到 625MB/s。
这说明:
- 单线程顺序写入本身没有坏。
- 单次
fdatasync正常时可以落在几十毫秒级。 - 故障不是“磁盘完全失去写能力”。
但这个测试不能证明高并发健康。
它没有覆盖:
- 83 个甚至更多任务同时执行
fsync。 - 邮件正文、索引、队列和集群同步的多路写入。
- 小文件创建、复制、重命名和元数据锁竞争。
- 宿主机 QEMU IO 线程、虚拟磁盘队列和后端 LUN 的排队。
- 备份、快照、清理或其他虚拟机争抢同一数据存储。
单流吞吐正常,不代表尾延迟和队列深度正常。
五、OCFS2 日志刷屏代表了什么
存储或管理节点上曾执行:
dmesg | grep -E "iscsi|multipath|device-manager|dm-3|block|error|hung|virtio|iqn"结果被大量 OCFS2 信息日志淹没:
ocfs2_free_dinode
ocfs2_free_dinode_lock
ocfs2_mknod_locked原因是关键词 block 命中了 Free dinode block 和 claim lock block。这些是 OCFS2 的元数据操作记录,不是错误码。
从三个管理节点的日志看,rm 和 python3 在持续删除、创建文件,形成明显的 inode 元数据活动。这可能是一个合理清理任务,也可能对共享存储造成额外压力,但日志本身没有出现真正的 IO error、SCSI timeout 或 OCFS2 损坏。
正确做法是先确认:
- 清理任务是什么、周期多长、操作多少文件。
- 是否与邮件高峰、群发和集群同步重叠。
- 是否与邮件虚拟机共用同一数据存储或后端 LUN。
- 清理期间宿主机
await、aqu-sz和 QEMU IO 延迟是否同步上升。
OCFS2 活动可以作为 IO 负载嫌疑,但不能直接作为邮件 hung IO 的根因。
六、判断是否真的存在存储异常
要确认 IO 问题,至少要把 guest 时间和宿主机指标对齐。
guest 的 dmesg 使用启动后相对时间,例如:
boot=$(date -d "$(uptime -s)" +%s)
date -d "@$((boot + 68813))"然后在同一时间窗口检查宿主机:
iostat -xdm 1 60
pidstat -d 1 60
iotop -oPa
vmstat 1 601. 找到虚拟机与 QEMU 进程
virsh list --all
virsh domblklist <domain>
virsh dumpxml <domain> | sed -n '/<disk/,/<\/disk>/p'
pgrep -af 'qemu.*<domain>'重点确认:
vdc对应 qcow2、LV 还是直通块设备。- 使用
cache=writeback、writethrough、none还是directsync。 - 是否配置
iothread或块设备限速。
2. 映射宿主机存储路径
qemu-img info --backing-chain /path/to/disk.qcow2
lsblk -o NAME,KNAME,TYPE,SIZE,FSTYPE,MOUNTPOINT,MODEL,SERIAL
pvs
lvs -o +devices,data_percent,metadata_percent
multipath -ll
dmsetup table3. 观察异常特征
真正支持“存储路径异常”的证据包括:
- 宿主机块设备
await明显高于历史基线。 aqu-sz持续增长。- QEMU 进程长期 D 状态。
- 后端 LUN、HBA、FC/iSCSI 端口出现队列积压或超时。
- 邮件虚拟机与宿主机 IO 延迟在同一时间窗口同步恶化。
- 暂停清理任务或群发后,D 状态和延迟同步缓解。
仅有 OCFS2 信息日志或单次吞吐下降,不足以证明存储硬件故障。
七、推荐处置顺序
第一步:止血
- 暂停新的批量发送和自动重试。
- 将发送并发降到 2 到 4,分批 20 到 50 封。
- 暂停与邮件高峰重叠的备份、快照、全盘扫描和清理任务。
- 保存
dmesg、iostat、D 状态进程栈和邮件队列指标。
第二步:确认 IO 链路
ps -eo pid,ppid,stat,wchan:32,comm,args | awk '$3 ~ /^D/'
cat /proc/<PID>/stack
cat /proc/<PID>/wchan
iostat -xdm 1 60
dmsetup deps -o devname /dev/dm-3如果只有 mssvr 在等待,检查应用并发和同步策略;如果 xfsaild、xfs_log_worker、kworker 或 QEMU 同时 D,优先处理存储路径。
第三步:由虚拟化和存储团队处理
- 单路径故障优先切换。
- 全路径挂起时,再评估取消未完成 IO 或受控 LUN reset。
- 不要在没有映射关系和厂商确认时直接重置共享 LUN。
- 没有 XFS metadata/log error 时,不把
xfs_repair作为首选。
第四步:恢复写入
先做小文件 fdatasync 测试,再逐步放量:
timeout 10 dd if=/dev/zero \
of=/maildata/test/recovery_test \
bs=4K count=1 conv=fdatasync status=none恢复判定应同时满足:
- 没有新的 hung task。
- 关键任务 D 状态消失。
vdc/dm-3 await回到基线。- 小文件同步成功。
mssvr写入耗时恢复毫秒级。- 邮件队列停止增长并开始下降。
八、是否应该减少或关闭 fsync
正常的低延迟存储上,fsync 通常只需毫秒级。出现百秒级等待,先说明存储尾延迟或队列异常,而不是“fsync 本身多余”。
直接关闭邮件正文、索引和已确认投递记录的 fsync,会带来:
- 虚拟机掉电后最近邮件丢失。
- SMTP 已返回成功但邮件不存在。
- 索引与邮件正文不一致。
- 邮件重复投递或邮箱需要重建索引。
更合理的优化顺序是:
- 修复存储尾延迟和共享资源争抢。
- 在应用支持的前提下做 group commit 或批量提交。
- 将正文、索引、队列和可重建缓存的同步策略分开。
- 使用有掉电保护的缓存、企业级 SSD 或同步副本提升持久化能力。
- 只有具备可验证的恢复链和副本保证时,才评估降低同步频率。
评估 fsync 时,必须做强制杀进程、虚拟机 reset、存储链路中断和主副本切换测试。验证目标不是“能启动”,而是“已确认邮件不丢、不重复、索引一致”。
九、长期治理
1. 控制入口写入
发送端至少同时限制:
- 全局并发连接数。
- 每分钟邮件数。
- 每秒字节数。
- 单域名和单发件账号配额。
限流要发生在读取附件、MIME 编码和扫描之前,不能等写入已经并发后再限制 SMTP 连接。
2. 隔离批量任务
批量投递、集群同步、普通用户邮件和索引维护应使用不同队列和优先级。出现磁盘压力时,优先限制批量和同步流量,不影响交互邮件。
3. 做好存储分层
- 邮件正文、索引、队列和数据库日志尽量分离。
- 高写放大路径使用稳定的企业级 SSD/NVMe。
- 避免备份、清理、快照与群发高峰共用同一后端。
- 对共享文件系统清理任务设置 IO 限额和错峰窗口。
4. 监控真正有用的信号
不要只监控 CPU 和磁盘利用率。建议增加:
mssvr写入 P95/P99。put mail data、copy mail的秒级超时数量。- 每个节点的 D 状态任务数。
vdc/dm-3 await和aqu-sz。- QEMU IO 线程延迟。
- 邮件队列长度和队列年龄。
fsync失败率和同步复制延迟。
5. 建立自动背压
达到以下任一条件时,自动降低 50% 发送速率:
- 写入 P99 超过 1 秒。
await明显高于基线。- D 状态任务持续增加。
- 队列年龄连续增长。
- 小文件
fdatasync超过设定阈值。
恢复后每次只提升 10% 到 20%,稳定后再继续。
十、这次复盘真正留下的结论
mssvr卡在fsync和 XFS 日志提交,不等同于 XFS 元数据损坏。- 单流写入正常,不代表并发
fsync、虚拟磁盘队列和共享存储健康。 - 邮件写入问题必须从 guest 文件系统追到宿主机 QEMU、数据存储和后端 LUN。
- OCFS2 信息日志刷屏不能作为存储故障证据,但共享清理负载值得核对。
- 重启只能清理瞬时队列,不能替代根因治理。
- 关闭
fsync不是免费的提速手段,必须用持久化副本和故障恢复测试兜底。
复盘的目标不是写一句“磁盘慢”,而是建立一条可以复查的证据链:
业务高峰
-> 并发写入和集群同步
-> mssvr fsync 和 XFS 日志排队
-> virtio 虚拟磁盘 IO 延迟
-> 宿主机数据存储或后端 LUN 队列
-> 定位资源争抢并恢复
-> 用限流、隔离和自动背压防止再次放大只有这条链路能闭合,下一次出现类似问题时,才不必在 xfs_repair、LUN reset 和重启之间反复猜测。
