4. 本地日志存储基础:磁盘分区、文件系统选择与写入性能

日志存储的第一道坎,就是选磁盘、分好区、挑文件系统。很多新手上来就一个分区、ext4 默认挂载,结果线上跑几天就发现 IO 打满、日志丢失、排查半天找不到问题。这一节我尽量把踩过的坑和常用的思路拆开讲,你跟着走一遍,至少能避开 80% 的“低配”陷阱。

日志写入路径与性能瓶颈 应用写入日志 用户态缓冲区 write() / O_SYNC VFS + 文件系统 块层 + IO 调度 磁盘/SSD 持久化 瓶颈点 fdatasync 频率 IO 合并效率 文件系统日志模式 磁盘类型 NVMe vs HDD

4.1 磁盘分区——别让对齐和隔离吃掉性能

分区这件事,很多人觉得“不就 fdisk 嘛”,但其实对日志场景来说有两个关键点:分区对齐和单独数据分区。

分区对齐:现代 SSD 和 HDD 都按 4K 扇区物理格式化。如果你分区起始偏移不是 4K 的整数倍,一个逻辑块可能会跨两个物理扇区,写放大直接翻倍。我用 sgdisk 时习惯 --align-end 或默认对齐到 MiB 边界,基本不出问题。

  • 独立挂载点:日志量大的服务器,我建议单独划一个分区给 /var/log,甚至把不同服务分到不同磁盘。避免日志写满挤占系统分区导致服务挂掉。
  • RAID 考量:如果用了硬件 RAID,注意 stripe size 对齐 – 日志常写 4KB~64KB 块,stripe 设 64KB 一般不会产生补零写。我曾经在一台 Dell R740 上因为 RAID stripe 默认 256KB,日志 IO 全是小写,性能掉了一半。

4.2 文件系统选择—— ext4、XFS、ZFS 怎么挑?

日志写入场景和数据库不太一样:写一次就不再读,但对延迟敏感,尤其怕 fsync 卡住。我简单列个对比表,后面细说。

文件系统日志模式大目录性能并发写入卸盘速度推荐场景
ext4ordered, writeback, journal一般(树深度影响)中等(单锁瓶颈)快中小量日志,IOPS < 5k
XFSmetadata journal + 延迟日志优秀(B+树)高(并发预分配)快大量日志,多线程写入
ZFSZIL + SLOG优秀(dnode)极高(事务组)需手动调优需要压缩/去重/校验场景

我的个人选择:常规 Linux 服务器首选 XFS。ext4 在千万级文件下 ls -l 能把 CPU 打满,XFS 的 B+ 树大目录遍历快得多。而且 XFS 的延迟日志写(延迟动作日志)对顺序写友好,实际测试单线程顺序写 IOPS 可以比 ext4 高 15% 左右。但是,如果你的日志有很高的 fsync 调用频率(比如 syslog-ng 每写一条就 fsync),ZFS + SLOG 反而能利用 NVRAM 降低延迟。

注意: 在 CentOS 7 上用 ext4 做日志分区,默认又没开 noatime,每个月都会产生大量元数据更新,磁盘磨损加剧。我曾在某金融项目中发现一块 SSD 一年就写死了,查下来 ext4 的 atime 更新占了 30% 的写入量。后来全换成 XFS + noatime + nodiratime,寿命延长两倍。

4.3 日志写入性能——从小字节到落盘

日志写入有三个层次:应用层缓冲、内核页缓存、磁盘。咱们一个一个看。

4.3.1 缓冲区与刷盘策略

应用进程写日志时,标准做法是用 fwrite 攒一批,再 fflush。为什么要攒?因为每次 write() 都是系统调用,你想想看,一条日志几十字节,每次调用内核开销比数据本身还大。我一般让日志库攒到 4KB 或一个 MTU 大小再写,效果很明显。

// C 伪代码示意
char buf[4096];
int pos = 0;
void log_write(const char *msg) {
    int len = strlen(msg);
    if (pos + len > 4096) {
        write(fd, buf, pos);   // 一次系统调用
        pos = 0;
    }
    memcpy(buf+pos, msg, len);
    pos += len;
}
// 定期或程序退出时 flush 剩余

但攒了不刷永远落不了盘,这里就要聊到 fsync() 和 O_SYNC 的取舍。

经验值: fsync 频率建议控制在 1~10 次/秒,不能再高了。日志丢失容忍度高的场景可以用 5 秒刷一次,配合 kernel 的异步回写。如果必须每条确认,就用 O_DSYNC 打开日志文件,只保证数据落盘而不是元数据,延迟能降低 30%。

4.3.2 文件系统挂载选项调优

同样一块 SSD,挂载参数不同性能差异很大。我常用的日志分区挂载示例:

# /etc/fstab 条目
/dev/sdb1  /var/log  xfs  defaults,noatime,nodiratime,allocsize=1m,logbsize=256k  0 0
  • noatime,nodiratime:要加,省掉 atime 更新的随机小写。
  • allocsize=1m:XFS 预分配空间,减少文件碎片。日志基本是追加写,碎片少对顺序读有帮助。
  • logbsize=256k:XFS 内部日志缓冲区,写日志多的场景加大可以降低元数据 IO 频次。

如果是 ext4,可以加 data=writeback,journal_async_commit,减少日志提交频率,但注意异常断电可能丢几秒数据。我的观点是:能接受丢几秒日志的场景,性能优先;金融审计类必须同步,那就用 XFS + sync 挂载项同时保留元数据一致性。

4.3.3 直接 IO 还是缓存 IO?

这是个经典问题。缓存 IO 靠 page cache,读延时低,但是刷盘仍由内核控制,存在“写已回但未落盘”的时间窗。直接 IO 绕过 page cache,每次 write 都直接下发,延迟更可控——但如果你写小包(< 512 字节),磁盘往往会触发 read-modify-write,性能反而降低。所以我个人只在两种情况下用直接 IO:

  1. 使用 NVMe 且 IOPS 充足,比如写入带宽大于 1GB/s 时,直接 IO 可以稳定延迟。
  2. 搭配 O_DIRECT + O_DSYNC,给需要严格持久化的硬件审计日志用。

一般业务日志,用缓存 IO + 定期 fsync 就够用了。别盲目追“直接 IO”,你看很多系统默认用缓冲 IO,不是没有道理的。

4.4 避坑清单——我过去咬过的“磁盘”

  • 分区没对齐,SSD 写放大 3 倍。用 parted -a optimal 或 gdisk 创建分区,避免早期 fdisk 的默认。
  • 日志目录丢在 /home 下,结果 /home 被写满,用户无法登录。必须独立分区。
  • 使用 RAID0 日志盘,坏一块盘全丢。至少 RAID1 或 RAID10,或者用三副本分布式存储做异地备份。
  • 忽略 TRIM。SSD 不支持 TRIM 的话,垃圾回收变慢,新写入延迟会飙升。确保挂载了 discard 或者定期 fstrim。
  • 日志文件和系统文件放同一块 HDD,随机 IO 与顺序 IO 相互干扰,最终两败俱伤。

我曾经帮一家电商排查日志任务堆积问题,发现他们的日志应用每秒 fsync 超过 100 次,底层硬盘 IO 利用率直接 100%,Latency 飙到 200ms。最后改成攒 64KB 再 fsync,延迟降到 2ms。频率降下来比什么文件系统优化都管用。记住:减少 fsync 次数永远是第一要务。

4.5 快速自检清单

当你要部署一个新日志服务器,可以按这个列表检查一遍:

检查项推荐做法错误案例
分区对齐MiB 对齐(256 扇区以上)fdisk 默认 63 扇区
挂载选项noatime, nodiratime默认 atime 更新
文件系统XFS(优先),ext4(轻量)btrfs(不稳定)
fsync 频率< 10次/秒> 50次/秒
磁盘类型SSD (NVMe > SATA)单独 HDD 凑合