评课社区存储性能问题始末
评课社区本月遭遇了一次持续近两周的存储性能问题,导致服务响应缓慢、用户体验下降。本文记录了问题的发现、排查和解决过程,涉及 NFS 性能、ZFS 日志、Proxmox VE 虚拟化存储配置等多个层面。
12 月 9 日:存储节点性能急剧下降
从 12 月 9 日下午开始,NFS 存储节点的性能急剧下降。在 NFS 主机上测试磁盘性能:
1 | debian@debian100:~$ dd if=/dev/zero of=test bs=64k count=16k conv=fdatasync |
写入速度只有 8.1 MB/s,而且大量 nfsd 进程处于 D 状态(不可中断睡眠,等待 I/O):
1 | PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND |
评课社区服务变得非常缓慢,但没有完全挂掉(当天还新增了不少点评),只是时常收到监控告警。
临时解决方案
当晚与大松鼠沟通后(评课社区的服务器放在他管理的机房 rack 上,他提供了计算节点和存储节点的 bare metal 服务器),决定临时在一个新的计算节点上开一台本地 SSD 的机器,把数据库和静态文件同步过去,让服务先跑起来。
由于 NFS 性能太差,这个同步过程非常缓慢。但 2 小时后终于把数据库和静态文件同步过去了,当天晚上通过 CloudFlare 把流量切换到临时服务器上。
但临时服务器上没有配置邮件服务、备份服务等,完整的服务恢复还是要把老服务器搞好。
原有架构与问题分析
大松鼠跟我分析了原有架构的问题。
原有架构
评课社区的服务器运行在一个 Proxmox VE 集群上,存储架构如下:
- NFS 存储节点:一台服务器,配置 12 块 HDD 组成 ZFS 存储池
- 计算节点:运行 Proxmox VE,通过 NFS 挂载存储
- VM 磁盘:raw 格式的磁盘镜像文件存放在 NFS 上
这意味着 VM 的 rootfs 访问路径是:
1 | VM 文件系统 → VirtIO 块设备 → 磁盘镜像文件 → NFS → 网络 → ZFS → HDD |
这种 Block Device over NFS 的架构本身就存在性能隐患。
为什么 Block Device over NFS 会有性能问题?
Block Device over NFS 的核心问题是文件碎片化导致 I/O 放大。
当 VM 内的文件系统写入数据时,它认为自己在写入连续的磁盘块。但实际上:
- VM 内的”连续”写入 → 映射到磁盘镜像文件的某个偏移
- 磁盘镜像文件在 NFS 服务端的 ZFS 上可能是碎片化存储的
- ZFS 上的文件块又分布在多个 HDD 上
结果是:在 VM 内看起来连续的 I/O,在底层物理磁盘上变成了大量随机 I/O。
1 | VM 内连续写入 4KB × 100 次: |
这种 I/O 放大效应在 HDD 上尤其严重,因为 HDD 的随机 I/O 性能比顺序 I/O 差 100 倍以上。
除了碎片化,还有多层缓存失效、同步写入开销、延迟叠加等问题。
NFS 的正确使用方式
NFS 应该被当作文件存储来使用,类似于 Google Cloud Storage 或 AWS S3:
- ✅ 正确用法:在 VM 内部直接挂载 NFS,存放用户上传的文件、日志、备份等
- ❌ 错误用法:在宿主机上挂载 NFS,然后把 VM 的磁盘镜像文件放在上面
如果一定要通过网络挂载块设备,应该使用专门为此设计的协议:
- iSCSI:专门的网络块设备协议,直接暴露块设备给客户端
- Ceph RBD:分布式块存储,专为虚拟化设计
这些协议直接在块设备层面工作,避免了文件系统层的碎片化问题。
因此,大松鼠建议我后续 VM 的 rootfs 都放本地 SSD,不再放 NFS。但由于 NFS 硬件的问题还没解决,copy rootfs 非常缓慢,一时半会儿没法把所有数据拷出来。
12 月 12 日:迁移 rootfs 到本地 SSD
经过漫长的数据拷贝,老服务器的 rootfs 终于从 NFS 全部迁移到了本地 SSD。系统重启后使用新的本地存储。
我从临时服务器同步数据回老服务器,但完成数据同步后,测试服务性能时,发现磁盘性能仍然不稳定。原因是那台物理机上还有其他 VM 没迁移完,仍在使用 NFS。NFS 的 I/O 阻塞影响了整个 Proxmox VE 的 I/O 线程——没有性能隔离。
这是虚拟化环境中常见的问题:当一个 VM 的存储后端出现问题时,可能会影响同一宿主机上的其他 VM。
决定等老机器上所有 NFS 数据都迁移走后,再把服务迁回来。
12 月 16 日:NFS 问题恶化
NFS 的性能问题这几天一直没有解决,反而更严重了。我尝试把老机器上其他 VM 的数据从 NFS 里搬出来,但发现 NFS 访问更慢了。
甚至在 NFS 主机本地挂载一个 qcow2 磁盘镜像都会卡死:
1 | # 尝试挂载 NFS 里的 qcow2 镜像 |
我发邮件给大松鼠:
NFS 还是抓紧修一下吧,现在 NFS 访问非常缓慢,我尝试挂载一个 NFS 里面存了数据的 qcow2 盘,直接卡死了。我担心里面存的用户数据丢失。
在 NFS 宿主机上面,rootfs 也非常缓慢,装个包都半天装不上。
12 月 17 日:根因定位——ZFS 日志在坏盘上
大松鼠查到了问题的根本原因:
NFS 存储节点使用了 ZFS 文件系统,而 ZFS 的日志(ZIL/SLOG)恰好放在一块接近损坏的 HDD 上。
ZFS 的写入流程:
- 数据先写入 ZIL(ZFS Intent Log)
- ZIL 确认后,返回写入成功
- 后台将数据写入主存储池
默认情况下,ZIL(ZFS Intent Log)存储在每块磁盘上。但为了提升同步写入性能,ZFS 允许将 ZIL 单独放在一个快速设备上,这个单独的日志设备就叫做 SLOG(Separate LOG)。
SLOG 的作用类似于数据库的 WAL(Write-Ahead Log):
- 接收所有同步写入请求
- 快速确认写入成功(因为 SLOG 通常是高速 SSD)
- 后台再将数据写入主存储池
SLOG 的性能直接决定了同步写入的响应速度。
当 ZIL 所在的磁盘延迟异常高时,所有同步写入操作都会被阻塞。而我挂载 NFS 时使用了 sync 选项,导致每次写操作都要等待 ZIL 确认,性能急剧下降。
SLOG 的历史配置
为什么 SLOG 会被单独配置在一块 HDD 上呢?这里有段历史背景。
最初,SLOG 被单独放在一块 1 万多转的高速 HDD 上。理想情况下,SLOG 应该放在 NVMe SSD 上以获得最佳性能。然而,几个月前那台机器的 NVMe 盘被拿走了,临时换上了这块高速 HDD。
当时选择 HDD 而不是 SSD 的原因是:
- 机器上的 SSD 容量比较小
- SLOG 写入量非常大,担心频繁写入会消耗 SSD 寿命
- SSD 的寿命和容量成正比,小容量 SSD 更容易被写坏
这个决策在几个月前是合理的。但后来机器已经升级到了大容量 SSD,SLOG 配置却没有跟着更新——这次性能问题才暴露出这个隐患。
解决方案
大松鼠把 ZFS 的日志设备从坏掉的 HDD 迁移到了 SSD。迁移完成后,NFS 性能恢复正常。
12 月 21 日:本地 SSD 性能问题
这周公司的事情非常忙,12 月 20 日还有用户私信我为什么收不到注册邮件——因为临时服务器上没有部署邮件服务。
12 月 21 日我终于有时间处理这个问题。准备把服务迁回老服务器(已经用上本地 SSD),却发现本地 SSD 的性能也很差。
使用 iostat 查看磁盘性能:
1 | $ iostat -x 5 |
写入延迟 1266ms!这对于 SSD 来说是不可接受的。正常 SSD 的写入延迟应该在 1ms 以下。
问题一:Proxmox VE 磁盘缓存配置
检查 VM 配置:
1 | cat /etc/pve/qemu-server/100.conf | grep scsi1 |
发现磁盘没有配置缓存模式,默认是 none(无缓存)。
缓存模式的区别:
- none(默认):每次写操作都直接写入物理磁盘,等待磁盘确认后才返回。最安全但最慢。
- writeback:写操作先写入宿主机内存缓存,立即返回成功。后台异步写入磁盘。性能好,但宿主机崩溃可能丢失最近的写入。
- writethrough:写操作同时写入缓存和磁盘,等待磁盘确认。介于两者之间。
对于有 RAID 或 UPS 保护的环境,writeback 是合理的选择。
修复:
1 | # 编辑 VM 配置 |
discard=on 启用 TRIM 支持,让 VM 删除文件时通知底层存储回收空间,对 SSD 寿命和性能都有好处。
问题二:物理磁盘写缓存被禁用
修改 VM 配置后,性能仍然不理想。继续排查,测试物理磁盘的原始写入速度:
1 | dd if=/dev/zero of=/dev/sda bs=1M count=1000 oflag=direct |
13.8 MB/s!这是 SSD 该有的速度吗?正常 SATA SSD 应该有 400-500 MB/s 的顺序写入速度。
检查磁盘写缓存状态:
1 | $ hdparm -W /dev/sda |
写缓存被禁用了! 这就是性能差的根本原因。
磁盘写缓存的作用:
- 启用时:写操作先写入磁盘内部的 DRAM 缓存,立即返回成功。磁盘控制器后台将数据写入闪存。
- 禁用时:每次写操作都要等待数据真正写入闪存才返回。
企业级 SSD(如这台机器上的 Toshiba THNSNJ1T02CSY)默认禁用写缓存是为了数据安全,但在有 RAID1 保护的环境下,可以安全地启用。
修复:
1 | # 启用写缓存 |
设置开机自动启用:
1 | # 创建 udev 规则 |
启用写缓存后,磁盘性能恢复正常:
1 | $ iostat -x 5 |
写入延迟从 1266ms 降到 2ms,终于正常了。
问题三:残留的 LVM 元数据卷
物理机器上还有一个老的、不再使用的 LVM metadata volume,删除后进一步改善了性能。
不停机迁移服务
存储性能问题解决后,开始将服务从 12 月 10 日开的那台临时 VM(以下称 源服务器)迁移回修好的老服务器(以下称 目标服务器)。为了最小化停机时间,采用两阶段同步的方法。
准备工作
在目标服务器上,先关闭 MySQL 和 Web 服务,确保数据目录不会被写入:
1 | # 在目标服务器上执行 |
1. 预同步用户上传数据(源服务器服务继续运行)
1 | # 在源服务器上执行,同步大部分静态文件到目标服务器 |
2. 预同步数据库文件(源服务器服务继续运行)
1 | # 在源服务器上执行,先做一次完整的数据库目录同步 |
3. 锁定数据库并增量同步(停机)
1 | # 在源服务器上执行,锁定数据库,阻止写入 |
由于之前已经同步过大部分数据,这次增量同步只需要几十秒钟。
4. 增量同步用户上传数据(源服务器服务继续运行)
1 | # 在源服务器上执行,再次增量同步用户上传数据 |
5. 启动目标服务器上的服务
1 | # 在目标服务器上执行 |
6. 验证目标服务器上的服务正常
1 | # 检查 Web 服务 |
7. 切换流量
在 Cloudflare 上将域名指向目标服务器的 IP。然后,查看目标服务器日志、源服务器日志,确保流量已经切到目标服务器。
整个过程的停机时间仅几十秒(数据库锁定期间)。
经验总结
- 避免 Block Device over NFS:VM 的 rootfs 应该放在本地存储或专用的块存储(如 iSCSI、Ceph RBD),而不是 NFS 上的镜像文件。NFS 适合在 VM 内部挂载,用于文件共享。
- 存储性能隔离:当一个 VM 的存储出问题时,不应该影响其他 VM。考虑使用独立的存储后端或 I/O 限制。
- ZIL/SLOG 放在快速设备上:ZFS 的日志设备决定了同步写入的性能。虽然 SLOG 写入量大,但现代大容量 SSD(寿命与容量成正比)完全可以胜任。如果使用 HDD 作为 SLOG,一旦该盘出问题,整个存储池的写入性能都会受影响。理想情况下应该使用 NVMe SSD。
- 监控磁盘健康:定期检查 SMART 数据,在磁盘完全损坏前发现问题。
- 合理配置磁盘缓存:根据存储后端的特性选择合适的缓存模式。对于有冗余保护的存储,
writeback通常是更好的选择。物理磁盘需要开启写缓存。
时间线回顾
| 日期 | 事件 |
|---|---|
| 12/9 | NFS 性能急剧下降,服务缓慢,临时迁移到本地 SSD 服务器 |
| 12/12 | 老服务器 rootfs 迁移到本地 SSD |
| 12/16 | NFS 问题恶化,老服务器上未迁移的其他服务几乎无法访问 |
| 12/17 | 定位到 ZFS 日志在坏盘上,修复 NFS |
| 12/21 | 发现本地 SSD 性能问题,修复后迁移服务回老服务器 |
从问题出现到最终解决,历时近两周。期间 12 月 9 日服务访问缓慢,间歇能收到告警,但没有完全故障。12 月 10 日后,服务虽然没有中断,但注册邮件无法发送。
这次事件提醒我们:存储是服务稳定性的基石,任何一层的性能问题都会层层放大,最终影响用户体验。