WebP Cloud Services Blog

Hetzner AX102 评测:为什么数据库需要企业级 NVMe?(对比 AX41),从 fsync 表现谈掉电保护 (PLP) 的重要性

最近我们买了几台 Hetzner 的 AX102 独立服务器,并加入了 15.36 TB NVMe SSD Datacenter Edition 用于我们内部的服务,在网上看到少有关于这台机器的评测,我们想来分享一些这个机器的信息,希望可以帮助到在观望这个机器配置的小伙伴们。

基本信息

机器使用的是 AMD Ryzen™ 9 7950X3D CPU,自带 128 GB DDR5 ECC 和 2x 1.92 TB NVMe SSD Datacenter Edition (Gen4),每个月基本价格 104EUR(芬兰区域)。

此外添加了一块 Hetzner 称为 15.36 TB NVMe SSD Datacenter Edition 的 SSD,每月 130EUR。

AX102 对于 Hetzner AX 线来说这个是第一个「企业级」配置了。

在前面的几个配置中,不是内存没有 ECC(例如 AX52,和早期的 AX41-NVMe),就是自带的硬盘不是 Datacenter Edition,对于没有 ECC 和 Datacenter 的机器跑跑无状态应用还行,但是一旦到了生产环境,容易付出一些本不必付出的代价,例如:

https://x.com/n0vad3v/status/1917941496155980105

所以这一次特地选用了全部 ECC + “Datacenter Edition” 的配置,在做足了安全措施(指冷/热备份)的情况下将一些重要服务迁移到 AX102 上,来评估整体稳定性和 ROI 表现。

基本 Benckmark

话不多说,我们首先参考本博客第一篇也是唯一登录过 Hackernews 首页的博文「Hetzner CAX 系列 ARM64 服务器性能简评以及 WebP Cloud Services 在其上的实践」文章中的基本测试开始!

使用测试脚本指令:

curl -sL yabs.sh | bash -s -- -i

基本信息

Processor  : AMD Ryzen 9 7950X3D 16-Core Processor
CPU cores  : 32 @ 4865.977 MHz
AES-NI     : ✔ Enabled
VM-x/AMD-V : ✔ Enabled
RAM        : 124.9 GiB
Swap       : 0.0 KiB
Disk       : 15.6 TiB
Distro     : Ubuntu 24.04.3 LTS
Kernel     : 6.8.0-85-generic
VM Type    : NONE
IPv4/IPv6  : ✔ Online / ✔ Online

IPv6 Network Information:
---------------------------------
ISP        : Hetzner Online GmbH
ASN        : AS24940 Hetzner Online GmbH
Location   : Helsinki, Uusimaa (18)
Country    : Finland


Geekbench 6 Benchmark Test:
---------------------------------
Test            | Value                         
                |                               
Single Core     | 2365                          
Multi Core      | 14981                         

基本磁盘信息

由于这台机器带有三块磁盘,型号分别如下:

  • 2x 1.92 TB NVMe SSD Datacenter Edition
    • Micron_7450_MTFDKCC1T9TFR
  • 15.36 TB NVMe SSD Datacenter Edition
    • MTFDKCC15T3TGP-1BK1DABYY

通过搜索我们可以知道:

1.92 TB 的是 Micron 7450 NVMe SSD ,官网链接:https://www.micron.com/products/storage/ssd/data-center-ssd/7450-ssd ,标称性能如下:

- Sequential 128KB READ: Up to 6800 MB/s
- Sequential 128KB WRITE: Up to 5600 MB/s
- Random 4KB READ: Up to 1,000,000 IOPS
- Random 4KB WRITE: Up to 400,000 IOPS

15.36 TB 的还是 Micron,不过是 7500 PRO,标称性能如下:

Read speed (max.)	7000 MB/s
Write speed (max.)	5900 MB/s
Read access (max.)	1,100,000 IOPS
Write access (max.)	250,000 IOPS

由于 1.92TB 的 NVMe 做了 RAID,测试结果可能不太具有代表性,这次我们着重对于 15T 的 NVMe 进行测试,在上面的测试脚本中,我们得到了这样的数据:

fio Disk Speed Tests (Mixed R/W 50/50) (Partition /dev/nvme2n1):
---------------------------------
Block Size | 4k            (IOPS) | 64k           (IOPS)
  ------   | ---            ----  | ----           ---- 
Read       | 775.69 MB/s (193.9k) | 2.69 GB/s    (42.0k)
Write      | 777.74 MB/s (194.4k) | 2.70 GB/s    (42.2k)
Total      | 1.55 GB/s   (388.3k) | 5.39 GB/s    (84.3k)
           |                      |                     
Block Size | 512k          (IOPS) | 1m            (IOPS)
  ------   | ---            ----  | ----           ---- 
Read       | 3.10 GB/s     (6.0k) | 3.10 GB/s     (3.0k)
Write      | 3.26 GB/s     (6.3k) | 3.30 GB/s     (3.2k)
Total      | 6.37 GB/s    (12.4k) | 6.41 GB/s     (6.2k)

说实话,看上去比较惊艳了,为了帮助读者对比 Hetzner 其他机器的性能,我们又挑选并购买了一台不是 Datacenter NVMe 的 AX41-NVMe 机器和两台 Cloud 的机器的测试结果粘贴如下。

参与对比的机器为如下:

  • AX41-NVMe(另一个独立服务器,AMD Ryzen™ 5 3600 CPU,搭配 512G 普通 NVMe)
    • 磁盘是 SAMSUNG MZVL2512HCJQ-00B00(Samsung PM9A1)
  • CCX63(Dedicated Resources 下最大的配置,拥有 48Core,192GB RAM 和 960GB LocalSSD)
  • CAX41(ARM64 下的最大配置,拥有 16Core,32GB RAM 和 320GB LocalSSD)

对于 Cloud 测试均在机器的 / 目录测试,用来测试机器自带的 LocalSSD 性能,AX41-NVMe 机器在挂载了硬盘的分区上测试。

AX41-NVMe

---------------------------------
Block Size | 4k            (IOPS) | 64k           (IOPS)
  ------   | ---            ----  | ----           ---- 
Read       | 509.95 MB/s (127.4k) | 1.43 GB/s    (22.4k)
Write      | 511.30 MB/s (127.8k) | 1.44 GB/s    (22.5k)
Total      | 1.02 GB/s   (255.3k) | 2.87 GB/s    (44.9k)
           |                      |                     
Block Size | 512k          (IOPS) | 1m            (IOPS)
  ------   | ---            ----  | ----           ---- 
Read       | 1.87 GB/s     (3.6k) | 2.05 GB/s     (2.0k)
Write      | 1.97 GB/s     (3.8k) | 2.19 GB/s     (2.1k)
Total      | 3.84 GB/s     (7.5k) | 4.24 GB/s     (4.1k)

可以看出在 NVMe 裸盘上跑出的性能都挺不错,到了 Cloud 的测试中会略微逊色一些,如下:

CCX63

fio Disk Speed Tests (Mixed R/W 50/50) (Partition /dev/sda1):
---------------------------------
Block Size | 4k            (IOPS) | 64k           (IOPS)
  ------   | ---            ----  | ----           ---- 
Read       | 109.71 MB/s  (27.4k) | 1.00 GB/s    (15.7k)
Write      | 110.00 MB/s  (27.5k) | 1.01 GB/s    (15.8k)
Total      | 219.71 MB/s  (54.9k) | 2.02 GB/s    (31.5k)
           |                      |                     
Block Size | 512k          (IOPS) | 1m            (IOPS)
  ------   | ---            ----  | ----           ---- 
Read       | 1.33 GB/s     (2.5k) | 1.18 GB/s     (1.1k)
Write      | 1.40 GB/s     (2.7k) | 1.26 GB/s     (1.2k)
Total      | 2.73 GB/s     (5.3k) | 2.44 GB/s     (2.3k)

CAX41

fio Disk Speed Tests (Mixed R/W 50/50) (Partition /dev/sda1):
---------------------------------
Block Size | 4k            (IOPS) | 64k           (IOPS)
  ------   | ---            ----  | ----           ---- 
Read       | 96.69 MB/s   (24.1k) | 579.80 MB/s   (9.0k)
Write      | 96.62 MB/s   (24.1k) | 597.04 MB/s   (9.3k)
Total      | 193.32 MB/s  (48.3k) | 1.17 GB/s    (18.3k)
           |                      |                     
Block Size | 512k          (IOPS) | 1m            (IOPS)
  ------   | ---            ----  | ----           ---- 
Read       | 816.20 MB/s   (1.5k) | 1.09 GB/s     (1.0k)
Write      | 886.02 MB/s   (1.7k) | 1.22 GB/s     (1.1k)
Total      | 1.70 GB/s     (3.3k) | 2.31 GB/s     (2.2k)

哦,不得不提一下,如果你是挂载了 Hetzner 的 Volume 的话,得到的结果是这样的:

fio Disk Speed Tests (Mixed R/W 50/50):
---------------------------------
Block Size | 4k            (IOPS) | 64k           (IOPS)
  ------   | ---            ----  | ----           ---- 
Read       | 29.70 MB/s    (7.4k) | 314.04 MB/s   (4.9k)
Write      | 29.68 MB/s    (7.4k) | 323.38 MB/s   (5.0k)
Total      | 59.38 MB/s   (14.8k) | 637.43 MB/s   (9.9k)
           |                      |                     
Block Size | 512k          (IOPS) | 1m            (IOPS)
  ------   | ---            ----  | ----           ---- 
Read       | 298.03 MB/s    (582) | 288.82 MB/s    (282)
Write      | 323.52 MB/s    (631) | 322.23 MB/s    (314)
Total      | 621.56 MB/s   (1.2k) | 611.05 MB/s    (596)

所以不要用 Volume 上面跑什么奇怪的高负载应用,不然 IOWait 会让你怀疑人生。

Datacenter SSD?

这次我们特别提到了 Datacenter NVMe,那么它和普通的 NVMe 主要的差异是什么?

首先在宏观设计哲学上,两者的目标截然不同:

  • 普通 NVMe (Consumer): 设计目标是 “爆发力” 。它假设用户的负载是突发的(Bursty)、读多写少的(如系统启动、游戏加载)。它极度依赖 SLC Cache(模拟 SLC),一旦缓存写满,性能会呈断崖式下跌。
  • 企业级 NVMe (Enterprise): 设计目标是 “确定性” (Determinism)稳态性能 (Steady State)。它假设设备将 24/7 满负荷运行,不仅看重平均延迟,更看重 99.99th 百分位(P99.99)的尾延迟稳定性。

所以我们往往可以看到一些消费级的 NVMe 都有着很高的(顺序)读写速度,而且会发现同样的价格下企业级的 NVMe 比消费级的要贵不少。

好,那这个时候就有读者问了:那我不 care 这些所谓的高负载平均延迟和尾延迟稳定性,我有充足的灾备措施,是不是就没有必要去买企业级 NVMe ,直接用消费级 NVMe 就行了?

还记得上面那张 Twitter 截图嘛?我当时也是这么想的…

这个时候就会引入企业级 NVMe 的另一个优势——PLP(Power Loss Protection)。在理解 PLP 之前,我们需要看看 fsync 是什么:

在 Linux 系统中,当你调用 write 写入数据时,操作系统出于性能考虑,并不会立刻把数据写到 SSD 上,而是先扔进内存里的 Page Cache(页缓存),然后立刻告诉你“写完了”。

这在一般场景下没有什么问题,操作系统上许多标准的文件操作并不要求数据即刻落盘,而是依赖内核的 pdflush 或工作队列在后台从容地进行脏页回写(Dirty Page Writeback),以此换取极致的内存读写性能。”

比如你在 Windows 的设备管理器 -> 磁盘管理中可以看到这么个设置(默认都是开启状态的):

但数据库不这么认为,为了保证每次数据是真正被保存了(而不是等待操作系统提交),数据库(如 MySQL 的 redo log 落盘)会强制调用 fsyncfdatasync

fsync 的语义是: “我不管你(OS)和硬盘怎么协调,我现在就要阻塞线程,直到硬盘明确回复我‘数据已经物理刻录在介质上了’,我才继续下一步。”

对于没有掉电保护(PLP)的普通消费级 SSD,当它收到主机的 fsync 指令时,它会经历如下步骤:

  1. 强制刷盘: 主控必须将 DRAM 缓存中所有相关的脏数据,立刻编程写入到 NAND Flash 颗粒中。
  2. 物理延迟: NAND Flash 的写入并不快。TLC/QLC 的编程周期(Program time)通常在 50us 到几百 us 甚至更久。
  3. 返回确认: 只有当电荷真正注入到晶圆的单元格里,主控才敢向主机发送“ACK”确认信号。

在这个过程中,CPU 在等,数据库线程在等,所有的业务都在等这几百微秒的物理操作。因此,在频繁 Sync Write 的场景下(如数据库事务提交),消费级 SSD 的 IOPS 被物理介质的写入延迟死死锁住,通常只有几千 IOPS。

对于企业级 SSD 而言,由于配备了 PLP。

  • 当收到 fsync 指令,主控只需要把数据写入板载的 DRAM 缓存,就可以立刻向 OS 返回“成功”。
  • 原理: 即使此刻突然拔电,板载电容提供的电力足够主控在几毫秒内将 DRAM 中的脏数据刷入 NAND。
  • 后果: fsync 的延迟等同于写入 DRAM 的延迟(纳秒级)。这使得企业级 SSD 在 Sync Write 场景下的 IOPS 可以比消费级高出 10 倍甚至更多

上面的差异反应到大家常会在服务器上跑的数据库(如今谁的服务器上还不会跑一个数据库呢?)上就是:

数据库为了保证 ACID 中的 D (Durability),通常要求 Write Ahead Log (WAL) 必须落盘(例如 MySQL 的 innodb_flush_log_at_trx_commit=1)。

  • 现象: 这种写入通常是小块(4K~16K)、顺序但极其频繁的 fsync 调用。
  • 普通 NVMe: 即使标称顺序写速度有 3000MB/s,但在频繁 fsync 下,吞吐量可能跌至几十 MB/s,导致数据库 TPS(每秒事务数)上不去。
  • 企业级 NVMe: 依靠 PLP,能轻松吃下高频的小块 Sync Write,TPS 几乎随 CPU 线性扩展。

这个时候有同学就要问了,如何直观感受到这种差距呢?

PGBench

为了方便直观感受,我们使用 PGSQL 自带的 pgbench 来搞个测试,测试统一使用如下方式启动 PGSQL(我知道用容器启动并不是一个被推荐的方式,这不是用来测试嘛?),在 Hetzner Cloud 上就是在 / 目录下运行,在 AX102 和 AX41-NVMe 机器上是在使用 ext4 文件系统挂载的 15T NVMe 的磁盘下运行。

version: "3.8"

services:
  db:
    image: postgres:18-alpine
    restart: always
    shm_size: 1g
    environment:
      PGUSER: postgres
      POSTGRES_PASSWORD: hellopassword
      POSTGRES_DB: tester
    volumes:
      - ./db_data:/var/lib/postgresql/18/docker
    healthcheck:
      test: [ "CMD", "pg_isready", "-h", "db", "-U", "${PGUSER:-postgres}", "-d", "${POSTGRES_DB:-tester}" ]
      interval: 1s
      timeout: 3s
      retries: 60

测试指令如下:

createdb pgbench_test
pgbench -i -s 100 pgbench_test
pgbench -c 16 -j 4 -T 60 pgbench_test

AX102

pgbench (18.0)
starting vacuum...end.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 100
query mode: simple
number of clients: 16
number of threads: 4
maximum number of tries: 1
duration: 60 s
number of transactions actually processed: 1556888
number of failed transactions: 0 (0.000%)
latency average = 0.617 ms
initial connection time = 9.362 ms
tps = 25951.831902 (without initial connection time)

AX41-NVMe

让我们来同样是独立服务器 + NVMe,且根据上面硬盘跑分来看差异不大,但是不是 Datacenter SSD 的 AX41-NVMe 的表现:

pgbench (18.1)
starting vacuum...end.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 100
query mode: simple
number of clients: 16
number of threads: 4
maximum number of tries: 1
duration: 60 s
number of transactions actually processed: 323149
number of failed transactions: 0 (0.000%)
latency average = 2.971 ms
initial connection time = 12.635 ms
tps = 5385.896737 (without initial connection time)

可以看到,一旦遇到了 fsync 场景下,TPS 一下子就掉到了只有 1/5。

请注意,这里的差异不仅仅是硬盘性能带来的,由于 AX102 配备的 7950X3D CPU 性能远强于 AX41 的 Ryzen 3600,也导致了 TPS 比 AX41 高。不过还请注意观察 latency average (延迟) 和稍后的 fio fsync 专项测试,在 latency average 表现中,AX102 的平均延迟为 0.617 ms,而 AX41-NVMe 是 2.971 ms。

在 fio 这种排除 CPU 干扰的纯 IO 测试中,企业级硬盘展示了 50 倍以上的 IOPS 优势,这才是数据库不卡顿的关键。

顺便,同样的方式我们来看看 Cloud 的表现:

CCX63

pgbench (18.0)
starting vacuum...end.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 100
query mode: simple
number of clients: 16
number of threads: 4
maximum number of tries: 1
duration: 60 s
number of transactions actually processed: 388748
number of failed transactions: 0 (0.000%)
latency average = 2.469 ms
initial connection time = 19.118 ms
tps = 6480.864372 (without initial connection time)

CAX41

pgbench (18.0)
starting vacuum...end.
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 100
query mode: simple
number of clients: 16
number of threads: 4
maximum number of tries: 1
duration: 60 s
number of transactions actually processed: 301215
number of failed transactions: 0 (0.000%)
latency average = 3.185 ms
initial connection time = 39.615 ms
tps = 5023.234052 (without initial connection time)

FIO

最后,我们也使用了同样的 fio 程序对磁盘的带 fsync 性能进行了测试。

测试指令:

TEST_FILE=/mnt/fio_fsync_test.dat

fio --name=fsync-test --filename=$TEST_FILE --size=5G --ioengine=libaio --direct=0 --rw=write --bs=8k --numjobs=1 --iodepth=1 --fsync=1 --group_reporting

简单来说结论如下:

  • AX102
    • min= 8306, max=10842, avg=10318.05, stdev=512.82, samples=126
  • AX41-NVMe
    • min= 146, max= 194, avg=187.15, stdev= 4.94, samples=7003
  • CCX63
    • min= 602, max= 1178, avg=822.16, stdev=115.33, samples=1594
  • CAX41
    • min= 418, max= 991, avg=727.56, stdev=93.69, samples=1801

详细结果如下(由于结果太长,已经折叠):

点我展开看 fio 结果

CAX41

fio --name=fsync-test --filename=$TEST_FILE --size=5G --ioengine=libaio --direct=0 --rw=write --bs=8k --numjobs=1 --iodepth=1 --fsync=1 --group_reporting
fsync-test: (g=0): rw=write, bs=(R) 8192B-8192B, (W) 8192B-8192B, (T) 8192B-8192B, ioengine=libaio, iodepth=1
fio-3.36
Starting 1 process
fsync-test: Laying out IO file (1 file / 5120MiB)
Jobs: 1 (f=1): [W(1)][100.0%][w=6064KiB/s][w=758 IOPS][eta 00m:00s]
fsync-test: (groupid=0, jobs=1): err= 0: pid=7397: Tue Nov 11 13:22:04 2025
  write: IOPS=726, BW=5815KiB/s (5955kB/s)(5120MiB/901547msec); 0 zone resets
    slat (usec): min=8, max=7282, avg=34.96, stdev=31.44
    clat (nsec): min=1120, max=3602.4k, avg=3768.39, stdev=8391.96
     lat (usec): min=9, max=7368, avg=38.73, stdev=33.14
    clat percentiles (usec):
     |  1.00th=[    3],  5.00th=[    3], 10.00th=[    3], 20.00th=[    3],
     | 30.00th=[    3], 40.00th=[    4], 50.00th=[    4], 60.00th=[    4],
     | 70.00th=[    4], 80.00th=[    5], 90.00th=[    5], 95.00th=[    6],
     | 99.00th=[   13], 99.50th=[   21], 99.90th=[   45], 99.95th=[   80],
     | 99.99th=[  277]
   bw (  KiB/s): min= 3344, max= 7935, per=100.00%, avg=5821.29, stdev=749.69, samples=1801
   iops        : min=  418, max=  991, avg=727.56, stdev=93.69, samples=1801
  lat (usec)   : 2=0.65%, 4=74.41%, 10=23.75%, 20=0.67%, 50=0.44%
  lat (usec)   : 100=0.05%, 250=0.03%, 500=0.01%, 750=0.01%, 1000=0.01%
  lat (msec)   : 2=0.01%, 4=0.01%
  fsync/fdatasync/sync_file_range:
    sync (usec): min=468, max=50797, avg=1336.17, stdev=774.61
    sync percentiles (usec):
     |  1.00th=[  717],  5.00th=[  783], 10.00th=[  832], 20.00th=[  898],
     | 30.00th=[  963], 40.00th=[ 1037], 50.00th=[ 1123], 60.00th=[ 1237],
     | 70.00th=[ 1401], 80.00th=[ 1598], 90.00th=[ 1975], 95.00th=[ 2474],
     | 99.00th=[ 4490], 99.50th=[ 5342], 99.90th=[ 8979], 99.95th=[11207],
     | 99.99th=[17957]
  cpu          : usr=1.06%, sys=8.48%, ctx=1312544, majf=0, minf=13
  IO depths    : 1=200.0%, 2=0.0%, 4=0.0%, 8=0.0%, 16=0.0%, 32=0.0%, >=64=0.0%
     submit    : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
     complete  : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
     issued rwts: total=0,655360,0,655359 short=0,0,0,0 dropped=0,0,0,0
     latency   : target=0, window=0, percentile=100.00%, depth=1

Run status group 0 (all jobs):
  WRITE: bw=5815KiB/s (5955kB/s), 5815KiB/s-5815KiB/s (5955kB/s-5955kB/s), io=5120MiB (5369MB), run=901547-901547msec

Disk stats (read/write):
  sda: ios=18/1968375, sectors=528/31683384, merge=0/1321571, ticks=6/714675, in_queue=986506, util=77.33%

AX102

fio --name=fsync-test --filename=$TEST_FILE --size=5G --ioengine=libaio --direct=0 --rw=write --bs=8k --numjobs=1 --iodepth=1 --fsync=1 --group_reporting
fsync-test: (g=0): rw=write, bs=(R) 8192B-8192B, (W) 8192B-8192B, (T) 8192B-8192B, ioengine=libaio, iodepth=1
fio-3.36
Starting 1 process
fsync-test: Laying out IO file (1 file / 5120MiB)
Jobs: 1 (f=1): [W(1)][100.0%][w=80.2MiB/s][w=10.3k IOPS][eta 00m:00s]
fsync-test: (groupid=0, jobs=1): err= 0: pid=2347193: Tue Nov 11 14:08:52 2025
  write: IOPS=10.3k, BW=80.6MiB/s (84.5MB/s)(5120MiB/63524msec); 0 zone resets
    slat (usec): min=3, max=847, avg= 5.86, stdev= 4.22
    clat (nsec): min=480, max=625479, avg=640.36, stdev=955.14
     lat (usec): min=4, max=848, avg= 6.50, stdev= 4.47
    clat percentiles (nsec):
     |  1.00th=[  490],  5.00th=[  502], 10.00th=[  510], 20.00th=[  510],
     | 30.00th=[  524], 40.00th=[  532], 50.00th=[  540], 60.00th=[  548],
     | 70.00th=[  588], 80.00th=[  684], 90.00th=[  876], 95.00th=[ 1032],
     | 99.00th=[ 1448], 99.50th=[ 1864], 99.90th=[ 8512], 99.95th=[11968],
     | 99.99th=[21632]
   bw (  KiB/s): min=66448, max=86736, per=100.00%, avg=82544.38, stdev=4102.53, samples=126
   iops        : min= 8306, max=10842, avg=10318.05, stdev=512.82, samples=126
  lat (nsec)   : 500=1.22%, 750=83.20%, 1000=9.37%
  lat (usec)   : 2=5.78%, 4=0.17%, 10=0.19%, 20=0.06%, 50=0.01%
  lat (usec)   : 100=0.01%, 750=0.01%
  fsync/fdatasync/sync_file_range:
    sync (usec): min=52, max=6975, avg=90.81, stdev=41.37
    sync percentiles (usec):
     |  1.00th=[   67],  5.00th=[   73], 10.00th=[   77], 20.00th=[   81],
     | 30.00th=[   84], 40.00th=[   87], 50.00th=[   89], 60.00th=[   91],
     | 70.00th=[   93], 80.00th=[   96], 90.00th=[  101], 95.00th=[  111],
     | 99.00th=[  163], 99.50th=[  200], 99.90th=[  375], 99.95th=[  570],
     | 99.99th=[ 1713]
  cpu          : usr=1.54%, sys=20.19%, ctx=1325091, majf=0, minf=16
  IO depths    : 1=200.0%, 2=0.0%, 4=0.0%, 8=0.0%, 16=0.0%, 32=0.0%, >=64=0.0%
     submit    : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
     complete  : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
     issued rwts: total=0,655360,0,655359 short=0,0,0,0 dropped=0,0,0,0
     latency   : target=0, window=0, percentile=100.00%, depth=1

Run status group 0 (all jobs):
  WRITE: bw=80.6MiB/s (84.5MB/s), 80.6MiB/s-80.6MiB/s (84.5MB/s-84.5MB/s), io=5120MiB (5369MB), run=63524-63524msec

Disk stats (read/write):
  nvme2n1: ios=66675/2008316, sectors=4288160/38031992, merge=14/1356438, ticks=9377/40112, in_queue=49490, util=61.98%

CCX63

fio --name=fsync-test --filename=$TEST_FILE --size=5G --ioengine=libaio --direct=0 --rw=write --bs=8k --numjobs=1 --iodepth=1 --fsync=1 --group_reporting
fsync-test: (g=0): rw=write, bs=(R) 8192B-8192B, (W) 8192B-8192B, (T) 8192B-8192B, ioengine=libaio, iodepth=1
fio-3.36
Starting 1 process
fsync-test: Laying out IO file (1 file / 5120MiB)
Jobs: 1 (f=1): [W(1)][100.0%][w=5749KiB/s][w=718 IOPS][eta 00m:00s]
fsync-test: (groupid=0, jobs=1): err= 0: pid=3616829: Tue Nov 11 13:20:52 2025
  write: IOPS=821, BW=6572KiB/s (6730kB/s)(5120MiB/797748msec); 0 zone resets
    slat (usec): min=8, max=4587, avg=26.51, stdev=27.83
    clat (nsec): min=1172, max=594986, avg=2655.72, stdev=2212.45
     lat (usec): min=10, max=4589, avg=29.16, stdev=28.18
    clat percentiles (nsec):
     |  1.00th=[ 1368],  5.00th=[ 1560], 10.00th=[ 1608], 20.00th=[ 1688],
     | 30.00th=[ 1832], 40.00th=[ 2256], 50.00th=[ 2736], 60.00th=[ 2864],
     | 70.00th=[ 2960], 80.00th=[ 3056], 90.00th=[ 3344], 95.00th=[ 3856],
     | 99.00th=[ 5408], 99.50th=[12736], 99.90th=[35072], 99.95th=[39680],
     | 99.99th=[53504]
   bw (  KiB/s): min= 4816, max= 9424, per=100.00%, avg=6578.23, stdev=922.67, samples=1594
   iops        : min=  602, max= 1178, avg=822.16, stdev=115.33, samples=1594
  lat (usec)   : 2=34.57%, 4=61.31%, 10=3.50%, 20=0.32%, 50=0.28%
  lat (usec)   : 100=0.01%, 250=0.01%, 500=0.01%, 750=0.01%
  fsync/fdatasync/sync_file_range:
    sync (usec): min=560, max=99343, avg=1188.78, stdev=407.12
    sync percentiles (usec):
     |  1.00th=[  742],  5.00th=[  799], 10.00th=[  840], 20.00th=[  898],
     | 30.00th=[  971], 40.00th=[ 1045], 50.00th=[ 1139], 60.00th=[ 1254],
     | 70.00th=[ 1369], 80.00th=[ 1434], 90.00th=[ 1516], 95.00th=[ 1614],
     | 99.00th=[ 2114], 99.50th=[ 2835], 99.90th=[ 5080], 99.95th=[ 5735],
     | 99.99th=[ 8356]
  cpu          : usr=0.72%, sys=8.52%, ctx=1314198, majf=0, minf=15
  IO depths    : 1=200.0%, 2=0.0%, 4=0.0%, 8=0.0%, 16=0.0%, 32=0.0%, >=64=0.0%
     submit    : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
     complete  : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
     issued rwts: total=0,655360,0,655359 short=0,0,0,0 dropped=0,0,0,0
     latency   : target=0, window=0, percentile=100.00%, depth=1

Run status group 0 (all jobs):
  WRITE: bw=6572KiB/s (6730kB/s), 6572KiB/s-6572KiB/s (6730kB/s-6730kB/s), io=5120MiB (5369MB), run=797748-797748msec

Disk stats (read/write):
  sda: ios=0/1980548, sectors=0/33330920, merge=0/1377610, ticks=0/652120, in_queue=906250, util=78.84%

AX41-NVMe

fio --name=fsync-test --filename=$TEST_FILE --size=5G --ioengine=libaio --direct=0 --rw=write --bs=8k --numjobs=1 --iodepth=1 --fsync=1 --group_reporting
fsync-test: (g=0): rw=write, bs=(R) 8192B-8192B, (W) 8192B-8192B, (T) 8192B-8192B, ioengine=libaio, iodepth=1
fio-3.36
Starting 1 process
fsync-test: Laying out IO file (1 file / 5120MiB)
Jobs: 1 (f=1): [W(1)][100.0%][w=1505KiB/s][w=188 IOPS][eta 00m:00s]  
fsync-test: (groupid=0, jobs=1): err= 0: pid=21222: Fri Nov 21 15:39:44 2025
  write: IOPS=187, BW=1497KiB/s (1533kB/s)(5120MiB/3502354msec); 0 zone resets
    slat (usec): min=8, max=1031, avg=19.54, stdev= 6.14
    clat (nsec): min=752, max=1238.4k, avg=1971.06, stdev=1724.91
     lat (usec): min=8, max=1258, avg=21.52, stdev= 6.75
    clat percentiles (nsec):
     |  1.00th=[  988],  5.00th=[ 1320], 10.00th=[ 1496], 20.00th=[ 1560],
     | 30.00th=[ 1608], 40.00th=[ 1720], 50.00th=[ 1832], 60.00th=[ 1976],
     | 70.00th=[ 2160], 80.00th=[ 2320], 90.00th=[ 2640], 95.00th=[ 2800],
     | 99.00th=[ 3504], 99.50th=[ 4048], 99.90th=[13632], 99.95th=[17024],
     | 99.99th=[22912]
   bw (  KiB/s): min= 1168, max= 1555, per=100.00%, avg=1497.67, stdev=39.50, samples=7003
   iops        : min=  146, max=  194, avg=187.15, stdev= 4.94, samples=7003
  lat (nsec)   : 1000=1.08%
  lat (usec)   : 2=59.81%, 4=38.59%, 10=0.36%, 20=0.14%, 50=0.02%
  lat (usec)   : 100=0.01%
  lat (msec)   : 2=0.01%
  fsync/fdatasync/sync_file_range:
    sync (usec): min=4950, max=43794, avg=5323.43, stdev=491.68
    sync percentiles (usec):
     |  1.00th=[ 5145],  5.00th=[ 5145], 10.00th=[ 5145], 20.00th=[ 5211],
     | 30.00th=[ 5211], 40.00th=[ 5276], 50.00th=[ 5276], 60.00th=[ 5276],
     | 70.00th=[ 5276], 80.00th=[ 5276], 90.00th=[ 5342], 95.00th=[ 5407],
     | 99.00th=[ 8356], 99.50th=[ 8717], 99.90th=[12125], 99.95th=[12387],
     | 99.99th=[12911]
  cpu          : usr=0.11%, sys=1.10%, ctx=1312291, majf=0, minf=14
  IO depths    : 1=200.0%, 2=0.0%, 4=0.0%, 8=0.0%, 16=0.0%, 32=0.0%, >=64=0.0%
     submit    : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
     complete  : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
     issued rwts: total=0,655360,0,655359 short=0,0,0,0 dropped=0,0,0,0
     latency   : target=0, window=0, percentile=100.00%, depth=1

Run status group 0 (all jobs):
  WRITE: bw=1497KiB/s (1533kB/s), 1497KiB/s-1497KiB/s (1533kB/s-1533kB/s), io=5120MiB (5369MB), run=3502354-3502354msec

Disk stats (read/write):
  nvme2n1: ios=0/1966100, sectors=0/31458056, merge=0/1310775, ticks=0/3416205, in_queue=4253444, util=97.54%

说到这里,可能用 Mac 做开发的同学会疑惑:‘我的 Mac 也是消费级 SSD,为什么跑数据库挺快的?’ 这就不得不提 macOS 那个著名的‘语义陷阱’了……”

番外篇:Mac 用户请注意,你的 fsync 可能在“撒谎”

如果你是在 MacBook Pro (M1/M2/M3) 上进行本地开发,你可能会发现本地的数据库跑起来非常轻快,甚至比服务器上还快。但这并不代表苹果的 SSD 也是“企业级”的,而是因为 macOS 在 fsync 的实现上玩了一个文字游戏。

1. API 的语义欺骗:fsync vs F_FULLFSYNC

在 Linux 标准中,fsync(fd) 的定义非常严格:必须把数据写入物理介质才算完。

但在 macOS (以及 iOS) 上,标准的 fsync 系统调用并不保证数据写入物理介质,它往往只是把数据刷入硬盘的DRAM 缓存就返回成功了。这相当于 macOS 默认替所有 SSD 开了“作弊模式”。

如果要在 macOS 上实现和 Linux fsync 等同的“物理落盘安全性”,应用必须调用一个苹果特有的 fcntl 指令:

// macOS 上真正的“物理落盘”
fcntl(fd, F_FULLFSYNC);

PostgreSQL 等数据库在 macOS 上运行时,为了性能往往需要在这两者之间做权衡。如果你强制开启 F_FULLFSYNC,你会发现普通消费级 SSD 的 Mac 性能会瞬间暴跌(虽然 M 系列芯片的控制器优化极强,掩盖了部分延迟)。

总结与建议

通过 fio 基础测试和 pgbench 的实战对比,结论已经非常明显:标称参数只是冰山一角,架构差异才是深海里的巨兽。

在普通的顺序读写测试中,AX41-NVMe 的消费级硬盘甚至能和 AX102 的企业级硬盘打得有来有回。但在涉及到数据库真实负载——即包含大量 fsync 的同步写入场景下,企业级 SSD 凭借 PLP(掉电保护)带来的物理层优势,实现了对消费级硬盘 5 倍以上 的 TPS 碾压(25k vs 5k)。

这恰恰解释了为什么很多开发者在本地开发环境(通常是高性能 NVMe)或者普通 VPS 上跑数据库时,一旦并发量上来,CPU 还没满,IOwait 却先爆表的原因。

对于正在观望 AX102 的小伙伴,我们的建议是:

  • 如果你跑的是无状态应用(如 Web Server、CI/CD Runner、计算节点):普通的 AX 系列甚至 Cloud 实例完全足够,你不需要为 Datacenter SSD 和 ECC 内存支付溢价。
  • 如果你跑的是有状态应用(尤其是 PostgreSQL、MySQL、Redis 等数据库):为了 Datacenter NVMe 每月多出的几十欧,买到的不仅仅是更大的容量,更是 IO 延迟的确定性(Determinism)和数据落盘的安全性。

毕竟,在生产环境中,“稳定”本身就是最大的性能指标。希望这篇评测能帮你少走弯路,把钱花在刀刃上。

Happy Hacking!

哦对了,如果你看完本文后对 Hetzner 的机器感兴趣,可以尝试使用我们的链接来注册 Hetzner 体验: https://hetzner.cloud/?ref=6moYBzkpMb9s

通过我们的链接注册的话你可以在注册成功后直接获得 20EUR 的可用额度,我们也可以获得 10EUR 的奖励,这样也可以支持我们的产品发展。


WebP Cloud Services 团队是一个来自上海和马尔默的三人小团队,由于我们不融资,且没有盈利压力 ,所以我们会坚持做我们认为正确的事情,力求在我们的资源和能力允许范围内尽量把事情做到最好, 同时也会在不影响对外提供的服务的情况下整更多的活,并在我们产品上实践各种新奇的东西。

如果你觉得我们的这个服务有意思或者对我们服务感兴趣,欢迎登录 WebP Cloud Dashboard 来体验,如果你好奇它还有哪些神奇的功能,可以来看看我们的文档 WebP Cloud Services Docs,希望大家玩的开心~


Discuss on Hacker News