上一篇 下一篇 分享链接 返回 返回顶部

如何在香港服务器运行Debian系统时,利用 ZFS ARC 把数据库查询“喂”得飞快

发布人:Minchunlin 发布时间:2025-08-27 11:26 阅读量:902


台风登陆香港的那个晚上,机房外面呼呼作响,机房里只有我和一台咆哮的 1U 服务器。运营同事在群里喊:“查询延迟从 30ms 飙到 180ms 了!”我盯着 pg_stat_statements 和磁盘队列,心里很清楚:I/O 抢不过来。那一夜,我把数据库从 ext4 迁到 ZFS,调了 ARC/L2ARC 和 SLOG,第二天早上业务高峰时 TPS 提升了 ~42%,P95 延迟回到了 40ms 左右。下面就是我复盘下来的完整操作与坑点,尽可能写清楚每一步、每一个参数。

1. 环境与目标

机型与硬件(香港机房托管/租用的常见规格之一)

  • CPU:AMD EPYC 7443P(24C/48T,3.0GHz)
  • 内存:128GB DDR4 ECC
  • 系统盘:SATA SSD 480GB(Debian 系统&工具)
  • 数据盘:NVMe SSD ×2(3.84TB ×2,企业级,带 PLP)——做 ZFS pool
  • 日志盘/SLOG:Intel P5800X 400GB(选择其 16–32GB 分区做 SLOG,剩余闲置)
  • 网络:10GbE(香港—内地链路 5–20ms,取决于时段与线路)

软件栈

  • OS:Debian 12(bookworm,内核 6.1 LTS)
  • ZFS:OpenZFS 2.2.x(Debian 仓库)
  • 数据库:PostgreSQL 15(生产库),MySQL 8(报告服务)
  • 监控:Prometheus + node_exporter + postgres_exporter + 自写 arcstat 采集脚本

性能目标

  • 把热点查询命中率拉高(ARC/L2ARC 命中率 > 90%/70%)
  • 高峰期 TPS 至少 +30%,P95 延迟降低 30%+
  • 写安全(崩溃后一致性)不打折:SLOG + sync=standard + logbias=latency

2. 为什么用 ZFS + ARC(而不是“再加点内存”)

ARC = ZFS 自带的页缓存,在内核缓存之外再做一层智能缓存。好处是:

  • 可控:zfs_arc_max 可以硬限制,避免把内存吃光。
  • 感知数据布局:配合 recordsize 和 压缩,缓存更贴合数据库页读。
  • L2ARC = 二级缓存:把冷热之间的数据放在 NVMe 上,命中就不走转盘或慢 SSD。
  • SLOG(ZIL 日志设备):把同步小写的低延迟落到带 PLP 的专用盘,减少主池写延迟。

对数据库(尤其是 PostgreSQL 的 8KB 页)来说,把 ZFS 的 recordsize 对齐到数据库页大小,再把 ARC/L2ARC 填满热点,读放大会明显下降;配合 SLOG,同步写的尾延迟也能稳住。

3. 规划与容量分配

3.1 内存分配原则(以 128GB 为例)

  • PostgreSQL shared_buffers:32GB(25%)
  • ZFS ARC(zfs_arc_max):64GB(50%)
  • 系统与其他缓存/进程:~32GB(25%)

经验:PG 的 shared_buffers 与 ARC 是两层缓存,不要两边都拉满。我更愿意给 ARC 多一些,原因是同机还有 MySQL/日志/文件服务也能受益。

3.2 ZFS 池与数据集设计

  • 池(pool):tank
  • vdev:mirror(NVMe ×2 镜像),ashift=12(对齐 4K),compression=zstd-3
  • SLOG:从 P5800X 划一个 32GB 分区,作为 log vdev

数据集:

  • tank/pgdata(PostgreSQL 数据):recordsize=8K,logbias=latency,atime=off,xattr=sa
  • tank/mysqldata(InnoDB):recordsize=16K(匹配 InnoDB 页),其余同上
  • tank/pgwal(PG WAL):可单独 dataset,recordsize=128K,logbias=latency

坑点提示:recordsize 修改只对新写入的块生效。已有数据要么 pg_dump/restore,要么触发重写(如 VACUUM FULL/重建索引)。我踩过一次“以为改了就全对齐”的坑,命中率提升不明显,最后夜间做了一次 dump/restore,命中率才上来。

4. 详细部署步骤(一步步照做)

4.1 安装 ZFS

# 1) 启用 contrib 源
sudo apt update
sudo apt install -y dpkg-dev linux-headers-$(uname -r) build-essential

# 2) 安装 ZFS(DKMS 自动编译模块)
sudo apt install -y zfs-dkms zfsutils-linux
sudo modprobe zfs

确认:

zfs version
zpool version
lsmod | grep zfs

4.2 创建 ZFS 池与 SLOG

假设数据 NVMe 分别为 /dev/nvme0n1、/dev/nvme1n1,SLOG 盘为 /dev/nvme2n1。

# SLOG 盘划分 32GB 分区
sgdisk -n 1:0:+32G -t 1:bf01 /dev/nvme2n1
partprobe

# 创建主池(镜像)
zpool create -o ashift=12 \
  -O compression=zstd \
  -O atime=off \
  -O xattr=sa \
  -O acltype=posixacl \
  tank mirror /dev/nvme0n1 /dev/nvme1n1

# 加入 SLOG
zpool add tank log /dev/nvme2n1p1
zpool status -v
  • ashift=12:4K 对齐,不要偷懒。NVMe 基本都是 4K 物理扇区。
  • SLOG 强烈建议 企业盘 + 断电保护(PLP),否则崩溃/断电风险更高。

4.3 数据集与属性

# PostgreSQL 数据目录
zfs create tank/pgdata
zfs set recordsize=8K tank/pgdata
zfs set logbias=latency tank/pgdata
zfs set primarycache=all tank/pgdata    # 既缓存数据也缓存元数据
zfs set relatime=on tank/pgdata         # 兼顾一致性与性能

# PostgreSQL WAL 可单独
zfs create tank/pgwal
zfs set recordsize=128K tank/pgwal
zfs set logbias=latency tank/pgwal

# MySQL 数据目录(如需)
zfs create tank/mysqldata
zfs set recordsize=16K tank/mysqldata
zfs set logbias=latency tank/mysqldata

挂载点:

zfs set mountpoint=/var/lib/postgresql/15/main tank/pgdata
zfs set mountpoint=/var/lib/postgresql/15/wal  tank/pgwal
# /etc/postgresql/15/main/postgresql.conf 中把 wal_dir 指向 /var/lib/postgresql/15/wal (或使用符号链接)

4.4 ARC 与 L2ARC 设置

ARC 上限(64GB):

编辑 /etc/modprobe.d/zfs.conf:

options zfs zfs_arc_max=68719476736 zfs_arc_min=17179869184
# 64G & 16G,按需调整

立即生效(重启最稳妥;临时也可):

echo 68719476736 | sudo tee /sys/module/zfs/parameters/zfs_arc_max
echo 17179869184 | sudo tee /sys/module/zfs/parameters/zfs_arc_min

L2ARC(可选,但我强烈推荐)

如果还有一块空闲 NVMe(或同一 P5800X 划出另一个分区):

# 准备 200~800GB 的 L2ARC 分区,根据热点量估算
sgdisk -n 2:0:+400G -t 2:bf01 /dev/nvme2n1
partprobe

# 加入 L2ARC
zpool add tank cache /dev/nvme2n1p2
zpool status

L2ARC 参数:默认就很好用。数据库负载通常 关闭预取到 L2ARC 更稳(避免顺序流量污染),可以在 /etc/modprobe.d/zfs.conf 里加:

options zfs l2arc_noprefetch=1

我在压测里看到 l2arc_noprefetch=1 后,二级缓存命中更“干净”,P95 起伏更小。

4.5 PostgreSQL 配置(关键点)

/etc/postgresql/15/main/postgresql.conf 里重点:

shared_buffers = 32GB
effective_cache_size = 96GB          # shared_buffers + ARC + OS 估算
random_page_cost = 1.1               # 有高命中缓存时可调低,默认 4 太保守
maintenance_work_mem = 4GB
work_mem = 64MB
wal_compression = on
synchronous_commit = on              # 保持写安全,配合 SLOG
checkpoint_timeout = '15min'
max_wal_size = '8GB'

双缓存的取舍:shared_buffers 我通常给 25% 左右;ARC 给 50%。如果你的查询几乎全在 PG 的缓存里完成,适当把 random_page_cost 调到 1.1–1.3,能让规划器更愿意走索引。

4.6 MySQL(InnoDB)要点(如你也跑它)

# /etc/mysql/mysql.conf.d/mysqld.cnf
innodb_buffer_pool_size = 24G
innodb_flush_log_at_trx_commit = 1     # 数据安全
sync_binlog = 1
innodb_io_capacity = 4000               # 视 NVMe 能力调
innodb_flush_method = O_DIRECT          # 避免双缓存(ZFS 上 O_DIRECT 由内核&ZFS处理,观测有效果更依赖版本)

InnoDB 页 16KB,所以 dataset recordsize=16K。同时建议单独数据集给 binlog/WAL,logbias=latency。

5. 压测与观测

5.1 压测方法(PostgreSQL)

# 初始化 pgbench(在一个单独的 schema/db)
sudo -u postgres pgbench -i -s 200 mydb

# 读为主(简单场景)
sudo -u postgres pgbench -c 64 -j 64 -T 300 -S mydb

# 混合读写
sudo -u postgres pgbench -c 64 -j 64 -T 300 mydb

ARC/L2ARC 观测(简易脚本,读 /proc):

#!/usr/bin/env bash
# arcstat_min.sh
f=/proc/spl/kstat/zfs/arcstats
while true; do
  hits=$(awk '/hits/{print $3}' $f)
  misses=$(awk '/misses/{print $3}' $f)
  if [[ -n "$hits" && -n "$misses" ]]; then
    total=$((hits+misses))
    pct=$(( 100*hits/ (total==0?1:total) ))
    echo "$(date '+%H:%M:%S') ARC hit: $pct% ($hits/$total)"
  fi
  sleep 5
done

L2ARC 命中(同文件里有 l2_hits/l2_misses 指标):

awk '/l2_hits/{h=$3} /l2_misses/{m=$3} END{t=h+m; if(t>0) printf "L2ARC hit: %.2f%%\n", 100*h/t;}' /proc/spl/kstat/zfs/arcstats

5.2 压测结果(真实一线场景的量级示例)

读为主(-S),64 并发,5 分钟跑 3 轮取中位:

场景 ARC 上限 L2ARC TPS(中位) P95 延迟 ARC 命中 L2ARC 命中
A:ext4 基线 101k 180ms
B:ZFS,ARC=16G 16G 118k 120ms 78%
C:ZFS,ARC=64G 64G 139k 85ms 92%
D:ZFS,ARC=64G + L2ARC=400G 64G 144k 72ms 90% 68%

混合读写(默认脚本),64 并发:

场景 SLOG TPS(中位) P95 延迟 Checkpoint 偏移
E:无 SLOG(sync=standard) × 18.2k 420ms 偏大
F:有 SLOG(P5800X 32G 分区) 26.0k 220ms 明显平滑

解读:

  • 把 ARC 从 16G 提到 64G,热点命中明显上升,读延迟下降最明显。
  • 加 L2ARC 后,进一步守住“冷一点的热数据”,峰值波动更小。
  • 写场景里,SLOG 对同步小写的改善非常直观——尤其是 WAL/小事务。

6. 生产化细节与常见坑

6.1 内核更新 & DKMS

Debian 更新内核后,ZFS 模块需要重编译。

我在变更窗口外坚决不动内核;若必须更新,提前 apt install -yq --download-only,维护窗内做升级并验证 zpool import、zfs list 正常。

6.2 记录大小(recordsize)陷阱

改属性不回写历史数据。要全量生效需 dump/restore 或触发重写。

我最终选了夜间只读窗口:pg_dump → zfs set recordsize=8K → pg_restore,并顺手重建索引。

6.3 ARC 抢内存

观察 kswapd 和 OOM。若看到系统压力高,先降 zfs_arc_max;

也可以给 PostgreSQL 适当减 shared_buffers,把空间还给 ARC(或反之)。

6.4 SLOG 选型

必须选带断电保护(PLP)的企业 NVMe。我用 P5800X 的一个小分区做 SLOG,不要拿普通消费级 NVMe 冒险。

logbias=latency 用在 WAL/小事务数据集;顺序吞吐型用 throughput。

6.5 压缩与 CPU

compression=zstd 很香,但 CPU 忙时也会顶。一般设 zstd-3 或 zstd-5,别太激进。

观测 CPU iowait 与 system time 的变化,平衡后我停在 zstd-3。

6.6 L2ARC 热身

L2ARC 需要时间“预热”。不要压测 5 分钟就下结论。

生产里观察 24 小时曲线;命中率一般会逐步走高并稳定。

6.7 备份与快照

ZFS 快照太好用了:

zfs snapshot tank/pgdata@pre_migration_2025_08_27
zfs send -R tank/pgdata@pre_migration_2025_08_27 | zfs receive backup/pgdata

但 数据库一致性 仍需配合:PG 用 pg_basebackup/pgbackrest 更稳;ZFS 快照可作为额外保险与快速回滚手段。

7. 我的“上线”步骤清单(可直接当 Runbook)

变更申请与回滚方案(含老卷只读保留 48h)。

创建 ZFS 池(镜像、ashift=12、compression=zstd、SLOG)。

数据集:pgdata 设 recordsize=8K、logbias=latency、atime=off。

ARC/L2ARC:zfs_arc_max=64G、l2arc_noprefetch=1、加入 400G L2ARC。

PG 配置:shared_buffers=32G、effective_cache_size=96G、random_page_cost=1.1。

导入数据:pg_dump/pg_restore(或停机 rsync + VACUUM FULL 重写)。

冷启动预热:回放一部分热点查询脚本,让 ARC/L2ARC 预热。

上线观察:

  • arcstats 命中率、zpool iostat -v 1、PG 延迟与 TPS;
  • dmesg/journalctl -k 观察 ZFS 是否有错误;
  • 业务 P95/P99 曲线是否回落并稳定。

一周复盘:根据峰值内存压力微调 zfs_arc_max 与 PG 参数。

8. 附:常用命令速查

# ZFS 健康与 I/O
zpool status -v
zpool iostat -v 1

# ARC 概览
cat /proc/spl/kstat/zfs/arcstats | head -n 50

# 实时查看某数据集属性
zfs get all tank/pgdata | egrep 'recordsize|logbias|primarycache|compression'

# 调整 ARC(临时)
echo $((64*1024*1024*1024)) | sudo tee /sys/module/zfs/parameters/zfs_arc_max

# 移除/替换 SLOG(维护窗口做)
zpool remove tank /dev/nvme2n1p1
zpool add tank log /dev/nvme2n1p1

9. 成本与收益(肉眼可见)

调整前 调整后
峰值 TPS 18–20k(混合) 26–28k
读为主 TPS ~101k 144k
P95 延迟 180–420ms 72–220ms(场景不同)
灰度回滚 费时 ZFS 快照 1 行回退
维护窗口 常规 内核升级需计划(ZFS DKMS)

10.复盘与启示:缓存命中率带来的确定性

第二天清晨,我回到机房,把空调口结的水珠抹了一把。运营同事发来消息:“今天峰值稳得离谱。”我看着 arcstats 的曲线,ARC 命中在 90% 左右轻轻抖动,L2ARC 也默默兜底。后来每次业务上新、热点切换,我都先翻翻 recordsize、logbias、ARC/L2ARC 的状态。ZFS 不是银弹,但在香港这台忙碌的服务器上,它给了我可预期的性能与可控的复杂度——这就够了。

TL;DR(给赶时间的你)

  • 对齐页:PG 用 recordsize=8K,InnoDB 用 16K。
  • 喂饱缓存:shared_buffers≈25%,zfs_arc_max≈50%。
  • 加速同步写:用 企业级 SLOG + logbias=latency。
  • 二级缓存:有 NVMe 就上 L2ARC,命中率更稳。
  • 监控先行:arcstats、zpool iostat、PG 导出器一起看。
  • 别忘了坑:recordsize 对老数据不生效、内核升级要计划、SLOG 必须 PLP。
目录结构
全文