🐧 第七章 文件系统
📄 一句话概括

一次往返:本机 0.05 毫秒、同机房 0.5 毫秒、跨省 20 毫秒、跨洲 150 毫秒。一次 ls -l 要对两万个文件取属性——在跨省的 20 毫秒上,这就是 400 秒。

📝 本节和前面几节的关系

7.13 把文件系统放进了内存,这一节把它放到另一台机器上:接口完全不变,代价是每一次操作都变成一次网络往返。

一、NFS 干的事:把系统调用转发出去

NFS 的做法很直接:你调 open/read/stat,客户端把这次操作打包成一个 RPC 请求,发给服务器,服务器在自己的本地文件系统上做一遍,把结果发回来。 所以从程序的角度,接口完全没变——这也是【虚拟文件系统】那一层的价值(7.15)。 但代价变了:本地一次 stat 是【几十微秒的本地调用】;网络上一次 stat 是【一次往返】。 而且失败模式也变了:服务器挂了、网络断了、包丢了、超时了——这些在本地文件系统上根本不存在的情况,现在都要处理。 最要命的一点:本地文件系统的【快】建立在【元数据都在内存里】;NFS 的元数据在另一台机器上,所以每个以前免费的操作现在都要付费。

二、往返值多少时间

这几个数决定一切:本机磁盘 0.05 毫秒;同机房 0.5 毫秒;跨省 20 毫秒;跨洲 150 毫秒。 换算成 1000 次小操作:本机 50 毫秒;同机房 500 毫秒;跨省 20 秒;跨洲 2.5 分钟。 注意这还是【理想情况】——每次都是干净的一去一回,没有重传、没有排队、没有并发争抢。真实现场只会更慢。 物理原因是光速:跨洲往返一次,光要走两三万公里;光纤里还慢三分之一,150 毫秒基本就是物理极限。这不是【优化网络设备】能解决的。 所以【把文件放到远端】这件事,在【小操作很多】的负载上天然吃亏——而文件系统恰恰是【小操作很多】的典型。

三、为什么小文件多是灾难

关键在【一次 ls -l 到底做了什么】。 它先读目录,拿到两万个名字(这只是一次操作)。然后对每个名字 stat 一次拿属性。 本地:这两万次全在 dentry 与 inode 缓存里,几十毫秒就完了。NFS:如果属性没有跟着目录一起返回,理论上就是两万次往返。 在跨省的 20 毫秒上,这就是 400 秒。 这就是【git status 在 NFS 上慢到不能用】的真正原因——它要对每个文件取一次属性,判断有没有改过。同理还有 find、rsync --dry-run、grep -r。 注意【理论上】这三个字:NFS 有属性缓存,实际不会每次都真的发请求。但缓存有超时,而且【第一次】一定全要发。所以症状是【第一次特别慢,之后快一点】——这在交互式使用上依然很难受。

四、怎么救

① 属性缓存。重复 stat 同一个文件不用再问服务器;代价是可能拿到旧属性(几分钟的窗口)。 ② 一次批量取属性。有些协议支持【读目录时把属性一起带回来】,把往返次数除以批大小。NFSv4 的 READDIR 就能带属性。 ③ 加大块读。rsize/wsize 调到 1 MB,每次多拿数据;代价是小文件浪费带宽。 ④ 最土也最有效:先把整包拷到本地。要编译一个放在 NFS 上的大项目,先 rsync 到本地再编译,比直接在上面编译快一个数量级。这一条在工程上比前三条加起来都管用。 所以【要不要用网络文件系统】其实是一个判断题:你的负载是【少量大文件】还是【大量小文件】?前者很合适(比如共享视频素材),后者是自找麻烦。

五、动手 Demo:网络文件系统——为什么 NFS 上不能放大批小文件

这一节是算账,不是实测——把【一次往返】的典型值换算成【1000 次小操作】,再算一遍【ls -l 两万个文件】要多久。

hops = [("本机磁盘", 0.05), ("同机房", 0.5),
        ("跨省", 20.0), ("跨洲", 150.0)]   # 一次往返,毫秒

for name, ms in hops:
    total_s = ms * 1000 / 1000.0      # 1000 次折算成秒
    print("%-8s %8.2f 毫秒/次 -> 1000 次 %.1f 秒" % (name, ms, total_s))

n = 20000                             # 一个目录两万个文件
for name, ms in hops:
    print("%-8s ls -l %d 个文件 -> %.0f 秒" % (name, n, n * ms / 1000))

实跑输出:

--------------------------------------------------------------------
网络文件系统——为什么 NFS 上不能放大批小文件
--------------------------------------------------------------------
① 一次往返值多少时间

  在哪         一次往返        1000 次小操作
  ---------------------------------------------
  本机磁盘     0.05 毫秒       50 毫秒
  同机房       0.50 毫秒       500 毫秒
  跨省         20.00 毫秒      20.0 秒
  跨洲         150.00 毫秒     2.5 分钟

  **1000 次小操作,本机几十毫秒,跨洲要两分半。**
  **注意这还是【理想情况】——每次都是干净的一去一回,没有重传、没有排队。**

② 为什么【小文件多】在网络盘上是灾难

  操作             本地要几次往返         网络盘要几次往返
  -----------------------------------------------------------
  打开一个文件     1 次(查缓存)         2~3 次
  读它的属性       0 次(已在缓存里)     1 次
  读内容           1 次                   1 次 + 数据
  列一个目录       1 次                   1 次,可能分页

  **一次 `ls -l` 要做什么?**
  **读目录拿到 20000 个名字,然后对每个名字取属性。**
  **本地:20000 次以内全在缓存里。**
  **NFS:如果属性没被一起返回,理论上就是 20000 次网络往返。**

  **在跨省的 20 毫秒上,这就是 400 秒。**
  **这就是【`git status` 在 NFS 上慢到不能用】的真正原因**——
  **它要对每个文件取一次属性。**

③ 网络文件系统怎么救

  手段                 省下什么               代价
  ---------------------------------------------------------------
  属性缓存             重复 stat 不用再问     可能拿到旧属性
  一次批量取属性       往返次数 ÷ 批大小      要协议支持
  大块读(1 MB)       每次多拿数据           小文件浪费带宽
  先把整包拷到本地     彻底没有往返           要额外空间与同步

  **最后一条最土也最有效**:
  **要编译一个放在 NFS 上的大项目,先把源码 rsync 到本地再编译**,
  **比直接在上面编译快一个数量级。**

  📋 **Linux 上验证**:`mount | grep nfs` 看挂载参数;
  `nfsstat -c` 看往返次数与重传;
  `time ls -l 大目录` 本地与 NFS 各跑一次对比。

1000 次小操作:本机 50 毫秒,跨洲 2.5 分钟。两万个文件取属性:本机 1 秒,跨省 400 秒,跨洲 50 分钟。 这些是【理论上限】——真实 NFS 有属性缓存,不会每次都发请求。但【第一次】一定全要发,所以症状是【第一次特别慢】。 跨洲 150 毫秒不是夸张:光在光纤里走两万公里来回就是 130 毫秒左右,这已经是物理极限,优化设备没用。 所以【把 git 仓库放在跨洋的 NFS 上】是必然痛苦的,这不是配置问题。

📋 Linux 上验证:mount | grep nfs 看挂载参数。 📋 nfsstat -c 看往返次数与重传; 📋 time ls -l 大目录 本地与 NFS 各跑一次,对比。

30 秒实验

  1. mount | grep -E "nfs|cifs",看你有没有挂网络盘。
  2. time ls -l /一个有几万文件的目录,本地跑一次记住数。
  3. 想一想:为什么【先 rsync 到本地再编译】比直接编译快一个数量级?

常见误区

  1. 【网络盘只是慢一点】。在【小操作多】的负载上,它慢的是数量级,而且会让【原本 0 成本的操作】变成【每次都要付费】。
  2. 【加大带宽就能解决】。瓶颈是【往返延迟】,不是带宽;加带宽对【一万次小请求】几乎无用。
  3. 【NFS 上的文件加锁是可靠的】。文件锁要额外协议(NLM),而且各路客户端行为不一致,所以很多数据库明确规定【数据目录不能放 NFS】。
自测三问(点开对答案) 1. 为什么小文件在 NFS 上是灾难? —— **因为每个文件都要一次往返去取属性**;**本地这些操作是免费的(都在缓存里)。** 2. 跨洲 150 毫秒能优化掉吗? —— **基本不能**——**光在光纤里来回就是 130 毫秒量级**,**这是物理极限。****只能靠【减少往返次数】或【把数据搬到近处】。** 3. 最有效的办法为什么是【先拷到本地】? —— **因为它把【一万次往返】变成【一次大传输】**,**而大传输吃的是带宽、不是延迟。**

相关笔记