一次往返:本机 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 秒实验
mount | grep -E "nfs|cifs",看你有没有挂网络盘。time ls -l /一个有几万文件的目录,本地跑一次记住数。- 想一想:为什么【先 rsync 到本地再编译】比直接编译快一个数量级?
常见误区
- 【网络盘只是慢一点】。在【小操作多】的负载上,它慢的是数量级,而且会让【原本 0 成本的操作】变成【每次都要付费】。
- 【加大带宽就能解决】。瓶颈是【往返延迟】,不是带宽;加带宽对【一万次小请求】几乎无用。
- 【NFS 上的文件加锁是可靠的】。文件锁要额外协议(NLM),而且各路客户端行为不一致,所以很多数据库明确规定【数据目录不能放 NFS】。
自测三问(点开对答案)
1. 为什么小文件在 NFS 上是灾难? —— **因为每个文件都要一次往返去取属性**;**本地这些操作是免费的(都在缓存里)。** 2. 跨洲 150 毫秒能优化掉吗? —— **基本不能**——**光在光纤里来回就是 130 毫秒量级**,**这是物理极限。****只能靠【减少往返次数】或【把数据搬到近处】。** 3. 最有效的办法为什么是【先拷到本地】? —— **因为它把【一万次往返】变成【一次大传输】**,**而大传输吃的是带宽、不是延迟。**- ☐ 7.15 虚拟文件系统
相关笔记
- 基于内存的文件系统 — 相反的极端
- 虚拟文件系统 — 为什么接口能不变
- 系统调用 — 过一趟门的成本
- 🐧 Linux是怎样工作的 · 索引