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

预读在赌【你还要读下一块】。顺序负载下 1000 次请求只需读 4096 块,随机负载下要读 128000 块。浪费率一个 2%,一个 97%。所以同一个预读窗口,对一种负载是加速,对另一种是灾难。

📝 本节和前面几节的关系

8.8 让块层把已有的请求排好队,这一节讲另一种手段:在请求还没来之前,先替你把下一块读进来。

一、预读在赌什么

你读第 100 块,内核猜你还要读第 101、102、103 块,于是【一次把后面几块也读进来】。 猜对了:后面几次读全是命中,一次设备都不碰。这等于把 4 次随机读变成 1 次顺序读。 猜错了:多读的那些白读,白占内存,白占带宽。更糟的是它会把有用的缓存页挤出去。 所以预读是一笔【有赌注的贷款】:赌你的访问是连续的。赌赢了收益巨大(省掉寻道),赌输了代价也很实在(白读 + 挤缓存)。

二、两种负载,两种结果

Demo 86 按块数算了一遍。假设:块 4 KB,每次请求要 4 块(16 KB),共 1000 次请求,预读窗口 128 块(512 KB)。 顺序负载:1000 次请求连成一片,共 4000 块。预读按 128 块一个窗口往前推,32 个窗口覆盖 4096 块。浪费只有 96 块,2%。有效速率 98%。 随机负载:每次请求跳到一个新地方,预读照样读 128 块,但只用 4 块。1000 × 128 = 128000 块,其中 124000 块白读。浪费 97%。 同一个窗口,一个浪费 2%,一个浪费 97%。所以预读不是一个【越大越好】的参数,它是一个【必须和负载匹配】的参数。

三、所以要能被关掉

数据库做索引扫描时,访问模式是随机的。这时候预读纯属浪费,还会把缓存冲掉。所以有了 posix_fadvise。 POSIX_FADV_RANDOM:告诉内核【这个文件我随机读,别预读】。 POSIX_FADV_SEQUENTIAL:反过来,我要顺序读,窗口开大点。 POSIX_FADV_WILLNEED:现在就把这些页读进来(预加载)。 POSIX_FADV_DONTNEED:这些页我用完了,可以丢(备份程序拷完大文件后特别有用)。 这是一个【应用告诉内核自己的意图】的接口——内核猜不准的时候,让它别猜。同类的东西还有 madvise(内存)与 sched_setaffinity(CPU)。系统的通用思路:默认策略 + 可覆盖的钩子。

四、这一节的数字是模型

必须说清楚:8.9 的数字全是【按块数算的】,不是实测。 为什么测不了?因为沙箱里读什么都命中页缓存,预读根本没有【缺页】可补,效果自然是 0。8.10 会把这件事讲清楚。 模型能给出的东西:倍数关系与趋势。模型不能给出的东西:绝对值。写报告时把这两类分开,读者才知道能信到哪一步。 这也是这本书反复示范的方法:能测就测,测不了就老实说【这是模型】。

五、动手 Demo:预读——顺手多读一点,赌你还要

用块数算两种负载下的【真正用到】与【实际读了】,两个数一比就是浪费率。 这是纯模型:块 4 KB、每次要 4 块、1000 次请求、窗口 128 块。

BLK, PER_REQ, WIN = 4, 4, 128   # 块大小无关,只按块数算
nreq = 1000

used = nreq * PER_REQ

# 顺序:请求连成一片,窗口一个接一个往前推
seq_read = ((used + WIN - 1) // WIN) * WIN

# 随机:每次请求开一个新窗口
rnd_read = nreq * WIN

print("真正用到 %d 块" % used)
print("顺序:实际读 %6d 块  浪费 %4.0f%%  有效 %.0f%%"
      % (seq_read, (1 - used / float(seq_read)) * 100,
         used * 100.0 / seq_read))
print("随机:实际读 %6d 块  浪费 %4.0f%%  有效 %.1f%%"
      % (rnd_read, (1 - used / float(rnd_read)) * 100,
         used * 100.0 / rnd_read))

实跑输出:

--------------------------------------------------------------------
预读——顺手多读一点,赌你还要
--------------------------------------------------------------------
① 预读在赌什么

  **你读第 100 块,内核猜你还要读第 101、102、103 块,于是【一次把后面几块也读进来】。**
  **猜对了:后面几次读全是命中,不用碰设备。**
  **猜错了:多读的那些白读,白占内存,白占带宽。**

② 两种负载,两种结果

  下面按【预读窗口】算一遍:
  **顺序负载**(读完一块接着读下一块)与**随机负载**(每次跳到很远的地方)。
  **假设:块 4 KB,每次请求要 4 块(16 KB),共 1000 次请求,预读窗口 128 块(512 KB)。**


  负载     真正用到     实际读了      浪费率     有效速率
  -----------------------------------------------------------
  顺序     4000 块      4096 块       2%         98%
  随机     4000 块      128000 块     97%        3.1%

  **顺序负载下,预读几乎全用得上**——**多读的那部分是【提前读】,不是【浪费】。**
  **随机负载下,97% 的读都是白读**——**每次都读 128 块,只用了 4 块。**

③ 所以预读要能被关掉

  **数据库做索引扫描时,访问模式是【随机】的。****这时预读纯属浪费,还会把缓存冲掉。**
  **所以有了 `posix_fadvise(POSIX_FADV_RANDOM)`**——
  **告诉内核【这个文件的访问模式是随机的,别预读】。**
  **反过来,`POSIX_FADV_SEQUENTIAL` 是【我要顺序读,多预读点】。**

  建议                内核的反应           谁会用
  -------------------------------------------------------------
  FADV_RANDOM         关掉预读             数据库索引扫描
  FADV_SEQUENTIAL     把窗口开大           顺序读大文件
  FADV_WILLNEED       现在就帮我读进来     预加载
  FADV_DONTNEED       这些页可以丢了       用完就扔的大文件

  **注意这一节的数字是【按块数算的模型】**,
  **不是实测**——因为沙箱里读什么都命中缓存,预读的效果测不出来(8.10 会讲这件事)。

  📋 **Linux 上调预读窗口**:`cat /sys/block/sda/queue/read_ahead_kb`;
  `blockdev --setra 4096 /dev/sda`(单位是 512 字节扇区)。

顺序负载下,预读几乎不浪费:32 个窗口读 4096 块,用掉 4000 块。多读的那 96 块是【提前读】,迟早用得上。 随机负载下,97% 是白读:每次都读 128 块,只用了 4 块。 内存带宽被浪费 32 倍——如果盘是瓶颈,这等于把可用带宽砍到三分之一。 注意这只是【块数】的账,没算【挤缓存】的账。真实系统里后者往往更贵:把有用的页挤掉,后面又要重新读。

📋 Linux 上调预读窗口:cat /sys/block/sda/queue/read_ahead_kb; 📋 blockdev --setra 4096 /dev/sda(单位是 512 字节扇区)。

30 秒实验

  1. 把 WIN 改成 32,看随机负载的浪费率怎么变。
  2. 把 nreq 改成 100,看顺序负载的浪费率会不会变大。
  3. cat /sys/block/sda/queue/read_ahead_kb,看你系统的窗口是多大。

常见误区

  1. 【预读窗口越大越好】。随机负载下窗口越大浪费越多,而且会把有用的缓存页挤出去。
  2. 【预读是操作系统的事,应用管不着】。posix_fadvise 就是给应用表态用的,数据库、备份工具都在用。
  3. 【预读只在读文件时发生】。它其实发生在【页缓存缺页】的时候,所以内存映射访问(mmap)也会触发预读。
自测三问(点开对答案) 1. 预读赌的是什么?赌错了代价是什么? —— **赌【你接下来还要读相邻的块】**。**赌对了,后续几次读全部命中,省掉寻道;赌错了,白读、白占带宽、还把有用的缓存页挤出去。** 2. 为什么顺序负载只浪费 2%,随机负载浪费 97%? —— **因为顺序负载的请求连成一片**,**一个预读窗口读进来的 128 块,96 块会被后续请求用上。****随机负载每次跳到新地方,窗口读进来的 128 块只用 4 块。** 3. 为什么这一节的数字只能算模型,不能算实测? —— **因为沙箱里读什么都命中页缓存**,**没有缺页可补,预读的效果根本显现不出来。****模型能给出倍数与趋势,但给不出绝对值。**

相关笔记