🐧 第八章 外部存储器
📄 一句话概括

同样读 2000 个 4 KB,沙箱里顺序 16.43 毫秒、随机 24.96 毫秒,只差 1.5 倍;换成真机械盘的模型,顺序 0.026 毫秒、随机 12.69 毫秒,差 487 倍。1.5 与 487 之间,隔的就是【数据到底在不在内存里】。

📝 本节和前面几节的关系

8.5 只改块大小、路径始终连续,这一节改另一件事:字节数不变,只把访问位置打乱,看代价怎么变。

一、沙箱里只差 1.5 倍

Demo 83 读同一个 64 MB 文件里的 2000 个 4 KB。顺序读:从头连着读 2000 块,16.43 毫秒,475 MB/秒。随机读:按预先打乱的偏移跳着读,24.96 毫秒,313 MB/秒。 只差 1.5 倍。这点差距来自多跑的一万次 seek 与多出的一万次调用——都是用户态与内核态之间的开销,与磁盘无关。 为什么没有更大差距?因为两次读的都是页缓存。在内存里,跳到哪都一样快。

二、换成真盘就是 487 倍

用 8.1 的模型算同一个实验: 顺序读 4 KB 在同一磁道上顺着走,只付传输——4 KB ÷ 150 MB/秒 = 0.026 毫秒,2000 次共 52 毫秒,150 MB/秒。 随机读 4 KB 每次都要重新寻道 + 等半圈——12.69 毫秒 × 2000 = 25385 毫秒(25 秒!),折算 0.31 MB/秒。 差 487 倍。同一个文件、同样的字节数、同样的程序,只把偏移打乱,速度掉到五百分之一。 这个差距比 8.5 里块大小带来的 12 倍大得多,差了两个数量级。所以【数据放哪】比【代码怎么写】重要得多。调块大小是顺手的事,改访问模式才是决定性的事。

三、为什么偏偏是【带宽】难看

表格里【一次折算】那一列,机械盘随机 12.69 毫秒、顺序 0.026 毫秒,看着是 487 倍。但很多人会盯着【一次多少毫秒】觉得【也还行】——12 毫秒而已,人感觉不到。 真正难看的是带宽:0.31 MB/秒。这个数字意味着读满一张 700 MB 的光盘镜像要 37 分钟。而顺序读只要 5 秒。 所以判断一个存储访问方式好不好,看带宽,不看单次延迟。单次延迟说【等多久】,带宽说【多久能干完活】。批量任务只关心后者。

四、为什么 SSD 上这个差距会消失

SSD 随机读 4 KB 约 0.1 毫秒,顺序约 0.05 毫秒,只差 2 倍。为什么?因为它没有磁头。随机与顺序的唯一区别,是【读哪一页】,而寻址是电信号,代价可以忽略。 于是【随机 IOPS 能到几万到几十万】——机械盘是 79。三个数量级的差距,彻底改变了数据库的设计:索引可以放心随机跳、B 树不必强求顺序、甚至【把随机当默认】也没关系。 但 SSD 有自己的代价:写有寿命、写有放大(8.11)。所以不是【换 SSD 就一切都好】,是【换了一套瓶颈】。

五、动手 Demo:随机访问——同一份数据,两种走法

同一个 64 MB 文件,同样读 2000 个 4 KB,一次连续读,一次按打乱的偏移跳着读,再把这两个数换算到真机械盘上。

import os, random, tempfile, time

KB, MB = 1024, 1024 * 1024
d = tempfile.mkdtemp(prefix="ch29c8_")
p = os.path.join(d, "rand.bin")
size = 64 * MB
open(p, "wb").write(b"\0" * size)

BS, n = 4096, 2000
random.seed(11)
offs = [random.randrange(0, size - BS) for _ in range(n)]

def seq_read():
    with open(p, "rb", buffering=0) as f:
        for _ in range(n):
            f.read(BS)

def rnd_read():
    with open(p, "rb", buffering=0) as f:
        for o in offs:
            f.seek(o)
            f.read(BS)

seq_read(); rnd_read()     # 预热

def best(f, rep=5):
    r = []
    for _ in range(rep):
        t0 = time.perf_counter()
        f()
        r.append((time.perf_counter() - t0) * 1000)
    return min(r)

a, b = best(seq_read), best(rnd_read)
print("顺序 %.2f 毫秒  随机 %.2f 毫秒  差 %.1f 倍" % (a, b, b / a))

# 换算到真机械盘(8.1 的模型)
rot = 30000.0 / 7200
xf = BS / (150.0 * MB) * 1000
print("模型顺序 %.3f 毫秒/次  模型随机 %.2f 毫秒/次  差 %.0f 倍"
      % (xf, 8.5 + rot + xf, (8.5 + rot + xf) / xf))

实跑输出:

--------------------------------------------------------------------
随机访问——同一份数据,两种走法
--------------------------------------------------------------------
① 沙箱里:同样读 2000 次 4 KB

  走法     耗时           每次折算     折算带宽
  --------------------------------------------------
  顺序     16.43 毫秒     8.2 微秒     475 MB/秒
  随机     24.96 毫秒     12.5 微秒    313 MB/秒

  **差 1.5 倍。**
  **这就是【缓存内】的差距**——**小的那部分来自多跑了一万次 `seek` 与多了一万次调用。**

② 换成真机械盘,差距会变成什么

  **用 8.1 的模型**:**随机 4 KB 一次 12.69 毫秒;**
  **顺序 4 KB 在同一磁道上顺着读,只付传输**——**旋转那一下已经摊到整条磁道里了。**
  走法             一次折算       2000 次        带宽
  -------------------------------------------------------------
  顺序(模型)     0.026 毫秒     52 毫秒        150 MB/秒
  随机(模型)     12.69 毫秒     25385 毫秒     0.31 MB/秒

  **真盘上差 487 倍,而沙箱里只有 1.5 倍。**
  **两者差了两个数量级以上**——**这就是【数据放哪】比【代码怎么写】更重要的原因。**

③ 为什么 SSD 上这个差距会消失

  介质       随机 4 KB 一次     顺序 4 KB 一次     差距
  -----------------------------------------------------------
  机械盘     12.69 毫秒         0.026 毫秒         487 倍
  SSD        0.10 毫秒          0.05 毫秒          2 倍

  **注意机械盘那 487 倍不是【一次慢多少】,是【随机的带宽只有 0.3 MB/秒】。**
  **表格里的【一次折算】看不出真正的痛,【带宽】才看得出。**
  **SSD 的随机 IOPS 能到几万到几十万**,**所以哪怕差 2 倍,绝对值也是【毫秒级对微秒级】的差别。**

  📋 **Linux 上实测**:`fio --name=r --rw=randread --bs=4k --size=1G --direct=1 --iodepth=1`;
  **把 `--rw` 改成 `read` 再跑一次**,两个数一比就是随机与顺序的差距。

沙箱里只差 1.5 倍左右,因为两次都在页缓存里跑。这一点差距全是 seek 与调用的开销,与磁盘无关。 模型算出来差 487 倍——这个数才是【真机械盘上同一个实验】会看到的。 两个数都是对的,只是回答不同的问题:前者说【内存里跳到哪都一样快】,后者说【磁盘上跳到哪都要重付一次寻道】。 所以报告里必须写清【测的是哪一层】,否则读者会以为你把硬盘测成了内存。

📋 Linux 上实测:fio --name=r --rw=randread --bs=4k --size=1G --direct=1 --iodepth=1; 📋 把 --rw 改成 read 再跑一次,两个数一比就是随机与顺序的差距。

30 秒实验

  1. 把 random.seed 换几个值,看沙箱里的 1.5 倍会不会变。
  2. 把 n 从 2000 改成 20000,看倍数会不会更大。
  3. cat /sys/block/sda/queue/rotational,判断你的盘该不该关心随机性能。

常见误区

  1. 【随机读只比顺序读慢一点点】。这只在【内存里】成立。真机械盘上差 400 倍以上,带宽从 150 MB/秒掉到 0.31 MB/秒。
  2. 【单次延迟 12 毫秒没什么大不了】。单次看不出,批量才看得出:一次 12 毫秒,一万次就是 2 分钟;换成顺序只要 0.3 秒。
  3. 【SSD 没有寻道,所以随机访问完全免费】。SSD 随机读仍比顺序读慢约 2 倍,而且写还有寿命与放大问题(8.11)。
自测三问(点开对答案) 1. 为什么沙箱里随机只比顺序慢 1.5 倍,而模型算出来是 487 倍? —— **因为沙箱里两次读的都是页缓存,在内存里跳到哪都一样快**,**差别只剩 `seek` 与系统调用的开销。****模型算的是真机械盘:每次随机都要重付一次寻道与旋转。** 2. 为什么判断存储访问方式要看带宽而不是单次延迟? —— **单次延迟说【等多久】,带宽说【多久能干完活】。****12 毫秒单看不算什么,一万次就是 2 分钟**;**批量任务只关心总时间。** 3. SSD 上随机与顺序只差 2 倍,这会改变哪些设计? —— **索引可以放心随机跳,不必强求顺序扫描;****B 树不必为了顺序性做特殊优化;****I/O 调度器的排序失去意义(8.8)。****但写放大与寿命成了新的约束(8.11)。**

相关笔记