Project: Curve (Runtime / Cloud Native Storage)
File: CurveFS支持多挂载.pdf

CurveFS 支持多挂载  
• 背景 
• 本地文件系统的文件并发读写   
o write和read行为 
o 内核数据结构  
o 多进程共享同一个文件   
▪  O_APPEND   
▪ 文件锁flock/fcntl  
▪ 总结 
• 多挂载文件系统调研   
o GPFS  
▪ 简介 
▪ 数据和元数据资源并发保护   
▪ 数据保护：字节区间锁  
▪ 元数据保护：共享写锁  
▪ Allocation Map 保护 
▪ 其他元数据信息保护  
▪ 总结 
o Lustre  
▪ 简介 
▪ Lustre分布式锁管理器模型   
▪ 基本锁模型  
▪ 意图锁 
▪ 范围锁 
▪ 总结 
o JuiceFS  ▪ 简介 
▪ JuiceFS 使用DBServer 管理锁  
▪ flock锁 
▪ fcntl区间锁 
▪ 总结 
o ChubaoFS  
o CephFS  
▪ cephfs使用cap实现分布式锁   
▪ caps permission  
▪ caps combination  
▪ caps管理 
▪ 总结： 
▪ fuse write 实例 
▪ 总结 
o NFS  
▪ 简介 
▪ 数据和元数据缓存一致性  
▪ 客户端缓存  
▪ 结论 
• 业务场景   
o AI场景 
• CurveFS 如何支持多挂载  
背景 
当前有较多应用场景对于文件系统有一写多读、多写的需求，即同一个文件系统可以在多
台机器上同时挂载并进行读写。例如：  
▪ 云原生数据库：多台计算节点共同使用一个文件系统，其中主计算节点支持读写，
从计算节点支持读。  ▪ AI 训练：多台计算节点共享同一个文件系统，基本没有写同一个文件的场景，一
个节点的写入数据是需要对其他节点立即可见。  
下图描述的是 curvefs 的多挂载场景， client1 和client2分别在不同的节点上，挂载到
相同的文件系统 fs1上。 
 
一写多读场景： client1 写入文件 write(/mnt/mount1/test, 0, 4, "aaaa") ，client2
读取文件 read(/mnt/mount1/test, 0, 4) ，期望可以读到 "aaaa"。对于一个文件系统，
同一时刻只有一个节点在写入。  
多写多读场景 1：client1 写入文件 write(/mnt/mount1/test, 0, 4, "aaaa") ，同时
client2写入文件 write(/mnt/mount1/test2, 0, 4, "bbbb") ，在两个节点写入完成之
后，client2读取到read(/mnt/mount1/test, 0, 4) 为"aaaa", client1 读取到
read(/mnt/mount1 /test2, 0, 4) 为"bbbb"。对于同一个文件系统，同一 时刻有多个节
点写入，但写入的是不同文件。  
多写多读场景 2：client1 写入文件 write(/mnt/mount1/test, 0, 4, "aaaa") ，同时
client2写入文件 write(/mnt/mount1/test, 4, 4, "bbbb") ，在两个节点写入完成之
后，client1和client2 读取到read(/mnt/mount1/test, 0, 8) 为"aaaabbbb" 。对于同
一个文件系统，同一时刻有多个节点写入同一个文件的不同位置。  
多读多写场景 3：client1 写入文件 write(/mnt/mount1/test, 0, 4, "aaaa") ，同时
client2写入文件 write(/mnt/mount1/test, 0, 4, "bbbb") ， 在两个节点写入完成之
后，client1和client2 读取到read(/mnt/mount1/test, 0, 4) 要不是"aaaa", 要不是
"bbbb"。对于同一个文件系统，同一时刻有多个节点写入同一个文件的相同位置。  
对于以上四中场景， curvefs 在并发读写场景下需要支持何种一致性？如何支持？是本文
需要得出的结论 。 
本地文件系统的文件并发读写  
在调研多挂载文件系统之前，我们梳理一下本地文件系统对于文件读写的并发操作行为。  
write和read行为 
posix接口说明：  
1. posix并没有规定并发写行为。  
This volume of POSIX.1 ‐2008 does not specify behavior of concurrent 
writes to a file from multiple processes. Applications should use some 
form of concurrency control.  
2. posix规定read在write返回后可以获取新的数据。无论读写在同一个进程还是
在不同进程，读写都要保证这样的语义。  
3. POSIX requires that a read(2) that can be proved to occur after a 
write() has returned will return the new data.  Note that not all 
filesystems are POSIX conforming.  
If a read() of file d ata can be proven (by any means) to occur after a 
write() of the data, it must  reflect  that write(),  even  if the calls 
are made by different processes. A similar requirement applies to 
multiple write operations to the same file position. This is needed  to 
guarantee the propagation of data from write() calls to subsequent 
read() calls. This requirement is particularly significant for networked 
file systems,  where  some  caching  schemes  violate these semantics.  
但是linux提供了一些系统调用，使得用户可以在使用这些接口实现实现期望的并发行
为。 
内核数据结构  
linux支持多进程间共享打开文件，同一时刻允许多个进程同时打开文件，每个进程之间
的读写操作互不影响。为了实现这一个机制， linux内核使用了三种数据结构表示打开的
文件。 
1、每个进程的进程表中有一个记录项，包含了当前进程所有打开的文件描述符，它包含
了一个指向文件表项的指针和文件描述符标志。  
2、内核中，为所有打开的文件维持一张表，它包含了以下内容：  
▪ 当前文件打开状态：以何种方式打开该文件，只读、只写或者可读可写  
▪ 当前文件的偏移量：当前文件指针所处的位置  
▪ 指向该文件节点表的指针：节点包含当前文件的属性信息  3、每个文件的信息被封装在 v节点表项中，包含了当前文件名、所有者以及 inode等信
息。 
三者之间的关系为：  
 当对文件进行写操作时，在文件表项中的文件偏移量将增加写入的字节数。如果此时文件
偏移量超过了文件长度，则更新文件长度为当前的文件偏移量  
多进程共享同一个文件  
因为每个文件描述符都有一个属于自己的文件表项，所以每个进程之间的文件指针偏移互
相独立，互相读写不干扰：每 次write完成之后，文件表项的当前文件指针偏移量会立马
加上写入的字节数。  
那如何协调多进程之间的数据写入呢？ linux 提供了以下几种方式：  
 O_APPEND   
如果打开文件的时候加了  O_APPEND 参数，每次写入数据前先把偏移量设置到文件末尾  
文件锁flock/fcntl  
flock 
flock - apply or remove an advisory lock on an open file  
▪ flock 提供的文件锁是建议性质的。即一个进程可以忽略其他进程加的锁，直接对
目标文件进行读写操作。只有当前进程主动调用 flock去检查，文件锁才会在多进
程同步中起到作用。  
▪ 文件锁必须作用在一个打开的文件上。  
Locks created by flock() are associated with an open file table entry.  
flock() creates locks on systems's "Open file descriptions". "Open file 
descriptions" are generated by open() calls.  
a filedescriptor (FD) is a reference to a "Open file description". F Ds 
generated by dup() or fork() refer to the same "Open file description".  
flock对已打开的文件加锁时，是加在文件表项上 。考虑这种场景：进程 1打开file1拿
到fd0，进程2打开file1拿到fd1： 
▪ flock 提供的文件锁是建议性质的。即一个进程可以忽略其他进程加的锁，直接对
目标文件进行读写操作。只有当前进程主动调用 flock去检查，文件锁才会在多进
程同步中起到作用。  
▪ 文件锁必须作用在一个打开的文件上。  
Locks created by flock() are associated with an open file table entry.  
flock() creates locks on systems's "Open file descriptions". "Open file 
descriptions" are generated by open() calls.  
a filedescriptor (FD) is a reference to a "Open file description". F Ds 
generated by dup() or fork() refer to the same "Open file description".  
flock对已打开的文件加锁时，是加在文件表项上 。考虑这种场景：进程 1打开file1拿
到fd0，进程2打开file1拿到fd1：  
如果进程 2对fd1加上了排他锁，实际就是加在了 fd1指向的文件表项上。此时，进程 1
对fd0加锁会失败。  
因为操作系统会检测到进程 1中fd0对应的文件表项  和 进程2中fd1对应的文件表项指
向相同的 v节点，且进程 2中fd2对应的文件表项已经加了排它锁。  
fcntl 
fnctl locks work as a Process < --> File relationship, ignoring filedescriptors  
  
#include <unistd.h>  
#include <fcntl.h>  
 
int fcntl(int fd, int cmd, ... /* arg */ );  
fcntl 有多种功能，这里主要介绍文件锁相关的，当  cmd 被设置的 是与文件锁相关的宏
时，fcntl 就是用于实现文件锁。  
cmd关于锁的三个命令 : F_GETLK ，F_SETLK，F_SETLKW  
fcntl对已打开文件加锁时，是加在进程上，和文件描述符无关。 fcntl 可以对文件区间
上锁。所有进程共享操作同一个文件的时候，只有一个 v节点。共享同一个文件相当于共
享同一个链表。  
  
• 对于一个文件的任意字节 ,最多只能存在一种类型的锁 (读锁或者写锁 ); 
• 一个给定字节可以有多个读锁 ,但是只能有一个写锁 ; 
• 当一个文件描述符不是打开来用于读时 ,如果我们对它请求一个读锁 ,会出错,同理,
如果一个描述符不是打开用于写时 ,如果我们对它请求一个写锁 ,也会出错 ; 
• 正如前面所讲的 ,锁不能通过 fork由子进程继承 . 
总结 
▪ linux 通过文件锁  flock 和 区间锁 fcntl，支持多进程访问时的写写互斥、读写
互斥、读读共享；  
▪ linux 通过 open 时设置 O_APPEND 标志，以及  pread/pwrite 实现原子写，支持
多进程追加写的原子性；  
GPFS的DLM，使用一个集中的 global lock manager 运行在集群中的某个 node，与运行
在每个node的local lock manager 协作，global lock mana ger通过分发 lock token
在local lock manager 之间协调锁资源， lock token 传递授予分布式锁权利，而不需要
每次获取或释放所得时候产生一个单独的消息交换，同一个 inode重复访问相同的 disk 
object，只需要一次消息交互，当另一个 inode需要该锁时，需要额外的消息从第一个
inode撤回lock token ，以授予第二个 node。local token 也扮演这维护 cache一致性的
作用。 
GPFS使用字节区间锁 (byte-range locking) 同步file data 读写。GPFS的区间锁并没有
简单的实现为：在读写调用期间获取一个字节范围的令牌并在读写完成之后释放，因为这
种方式锁的开销是不可接受的。 GPFS使用更复杂的字节区间锁协议以减少常见访问模式
下的令牌竞争。  
区间令牌的实现如下：  
1. 第一个节点写文件时会获取整个文件范围的区间锁  [0, ∞]。在没有其他节点竞争
的情况下，所有的读写操作都不需要再和其他节点交互。  
2. 第二个节点写同样的文件时，发起撤销第一个节点持有的部分区间锁的请求  
3. 第一个节点接受到请求后，检查当前文件是否还在写入。   
1. 文件已经关闭，第一个节点会解锁所有区 间，第二个节点会获取整个文件范
围的区间锁。 因此在没有并发写的共享场景下， GPFS中的字节区间锁就是
整个文件锁，单个锁就足以访问整个文件  
2. 文件还在写入，第一个节点会解锁部分字节区间锁。如果当前是顺序写，第
一个节点从 offset1开始，第二个节点需要从 offset2开始，当 offset1< 
offset2，第一个节点会解锁  [offset2, ∞]，当offset1>offset2 ，第一
个节点会解锁 [0, offset1] 。这种方式允许两个节点写一定的范围。  
通常多个节点顺序写入同一文件的非重叠部分时，每个节点只需要获取一次字节区
间锁。 
4. 发起区间锁的请求包含两个信息：一个是当前正在处理的 write请求中的参数
offset和length，另一个是未来可能访问的范围，对于顺序写，这个范围是  [当
前写入偏移量，∞ ]。当访问模式允许预测被访问文件在特定 node即将访问的区
域，token协商协议能够通过分割 byte-range token 最小化冲突，不仅仅是简单
的顺序访问模式，也包括方向顺序、向前或者向后跨越式访问等访问模式。  
下图对比了多节点读写同一个文件和读写不同文件 的性能。在单文件测试中，文件被分为
n个连续的区间，每个节点读或写其中一个区间。读、写性能可以线性扩展。在 GPFS中
多节点写入单个文件的速度和每个节点写入不同文件的速度相同，这说明了区间令牌的有
效性。  
这种方式 适用于每个节点在文件的不同且相对较大的区域中写入。 如果每个节点写入很
多更小的区域，令牌的状态和相应的消息流将会增长。  
字节区间标记保证 posix语义，IO的最小区间是一个山区，字节区间令牌的粒度不能小
于一个扇区。 GPFS也使用字节区间令牌来同步数据分配， 因此即使单个写入操作不重叠
("虚假共享 ")，多个节点写入同一数据块也会导致令牌冲突。为了优化不需要 posix语义
的应用程序的细粒度共享， GPFS允许禁用字节区间锁。  
元数据保护：共享写锁  
和其他文件系统一样， GPFS使用inode存储文件属性和数据块。  
▪ inode的更新。多个节点写入同一个文件会导致并发更新文件的 inode(修改文件大
小、时间、分配空间 )。如果inode独占写锁来同步，那会导致每次写操作都有锁
冲突。 
GPFS在inode上使用共享写锁 (shared write lock )，允许多个 node的并发写入，
仅需要精确的文件大小或 mtime的操作会冲突 (如：stat系统调用 )。某个访问该文
件的node会被指定为 metanode ，只有metanode 可以读写 inode。其他node仅更新
本地缓存的 inode拷贝，定期或者当 shared write tok en被另一个 node上的
stat()或read()操作撤回时，将其更新传给 metanode ，metanode 通过保留最大 size
和最新的 mtime合并来自多个 node的inode更新。非单调更新文件大小或 mtime的
操作，如 (trunc() 或者utimes()) 需要独占 exclusive inode lock 锁。 
▪ 数据块的更新。使用类似方式的同步。  
GPFS使用分布式锁保证 posix语义(例如stat()系统调用 )，inode及间接快使用集
中方式。这允许多个 node写入同一个文件，更新 metanode 没有锁冲突，每次写操作
不需从metanode 获取信息。指定文件的 metanode 是借助token server 动态选举
的，当一个 node第一次访问文件，它尝试获取该文件的 metanode token ，其他node
学习，当 metanode 不再访问该文件，并且相关修改已移出 cache，该node会放弃它
的metanode token ，并停止相应行为，当它随后收到一个来自其他 node的metadata
请求，它会发送一个拒绝的答复，其他 node会尝试通过获取 metanode token 接管
metanode角色。 
Allocation Map 保护 
alloction map 用来记录文件系统中 disk block 的分配状态。每一个 disk block 可以划
分为32个subblock 存储小文件，用 32位的allocation map 标记。用于查找指定大小空
闲disk block 或者subblock 的链表。  
分配磁盘空间需要更新 allocation map ，这个资源也是要在个节点之间同步的。  
将map分成若干个固定数量的可独立锁定的 region，不同的 node可以从不同的 region
分配空间，仅一个 node负责维护所有 region的空闲空间统计信息。  
该allocation manager node 在mount阶段读取 allocation map 初始化空闲空间统计信
息，其他 node定期汇报分配回收情况。每当 node正在使用的 region空间耗尽时，它会
向allocation map 请求一个 region，而不是所有的 node单独查找包含空闲空间的
region。 
一个文件可能包含不同 region的空间，删除一个文件需要更新 allocation map ，其他
node正在使用的 region会发送到相应的 node执行，allocation manager 会定期发布哪
些node正在使用哪些 region，促进构造发送释放请求。  
其他元数据信息保护  除了上述资源外， GPFS还包含的全局 metadata 有：文件系统配置信息，空间分配
quota，访问控制权限，以及扩展属性。 GPFS使用中心管理节点来协调或者收集来自不同
节点的元数据更新。例如： quota  
manager 向写入文件的各节点分发较大的磁盘空间，配额检查在本地完成，仅偶尔与
quota manager 交互。 
总结 
GPFS 用户手册：
https://www.ira.inaf.it/centrocal/tecnica/GPFS/GPFS_31_Admin_Guide.pdf  
GPFS 提供用户态文件接口：  
 
GPFS 对 posix 部分接口的支持：  
posix 是否支持  
open with O_APPEND  支持 
  
flock/fcntl  支持多节点的  flock 
支持maxFcntlRangePerFile 个数的fcntl()  
Lustre 
简介 
Lustre文件系统是一个具有高性能、高可扩和高可靠性的基于对象的分布式文件系统。
它通过Lustre锁管理器 (Lustre Distributed Lock Manager ，LDLM)技术为整个系统的共
享资源提供同步访问和一致性视图。  
Lustre提供了Posix兼容的UNIX文件系统接口。 Lustre采用了基于对象存储的技术，元
数据和存储数据分离的存储结构，带宽和存储容量可以随存储服务器增加而动态扩展。  
Lustre 由三个部件组成：  
▪ 元数据服务器（ Metadata Server, MDS ）维持整个系统全局一致性命名空间，负责
处理lookup、create、unlink、stat等与文件元数据有关的命名空间操作的交以
及与元数据相关的锁服务。  
▪ 对象存储服务器（ Object Storage Server, OSS ）负责与文件数据相关的锁服务及
实际的文件 I/O，并将I/O数据保存到后端基于对象存储的设备中。  
▪ 客户端（ Client File System, CFS ）  
Lustre文件系统中，每个节点都有 DLM模块，为并发文件访问提供了各种锁服务。基于
DLM, Lustre 使用一种范围锁来维护细粒度的文件数据并发访问的一致性。  
在访问文件数据前，客户端首先要从 OSS获得对文件数据访问的范围锁，获得对文件访问
授权；结合范围锁， Lustre还实现了客户端数据的写回缓冲功能。  
在打开文件时，客户端先和 MDS交互，同时获得文件数据对象布局属性；在进行文件读写
时，根据已获得的数据对象布局属性，在 DLM范围锁的保护下可以直接并行地与多个 OSS
进行并行数据 I/O交互。 
Lustre分布式锁管理器模型  
Lustre在很大程度上借鉴了传统的分布式锁管理器的设计理念，其基本模型在 Lustre文
件系统中 被称为普通锁 (Plain Lock) 。再此基础上， Lustre对普通锁模型进行了扩张，
引入了两种新类型锁：意图锁 (Intent Lock) 和范围锁 (Extent Lock) 。 
基本锁模型  
Lustre DLM 模型使用锁命名空间 (Lock Namespace) 来组织和管理共享资源以及资源上的
锁。当要获得某个资源的锁时，先要将该资源在锁命名空间中命名。任何资源都属于一个
锁资源命名空间，而且都有一个相应的父资源。每个锁资源都有一个资源名，该资源名对
其父节点是唯一的。所有的锁资源组成一个锁资源树型结构。当要获得某个 资源的锁时，
系统必须首先从该资源的所有祖先获得锁。 Lustre支持六种锁模式，如下：  
模
式 名称 访问授
权 含义 
EX 执行 RW 允许资源的读写访问，且其他任何进程不能获得读写权限  
PW 保护
写 W 允许资源的写访问，且其他任何进程不能获得写权限  
PR 保护
读 R 允许资源的读访问，且其他任何进程不能获得写权限但可共享
读 
CW 并发
写 W 对其他进程没有限制，可以对资源并发写访问，无保护方式写  
CR 并发
读 R 对其他进程没有限制，可以对资源并发读访问，无保护方式读  
NL 空 无 仅仅表示对该资源有兴趣，对资源没有访问权限  
每种锁都有一定的兼容性，如果一个锁可以与已经被授权的锁共享访问资源，则称为锁模
式兼容。下图是锁兼容性转换表， "1"表示锁模式可以并存。执行锁严格性最高，兼容性
最差；空锁严格性最低，兼容性最好。  
 
在Lustre锁管理器中，锁命名空间中每个资源都维持三个锁队列：  
1. 授权锁队列：该队列包含所有已被锁管理器授权的锁。但正在转换为与已授权锁模
式不兼容的锁除外，该队列的锁的持有者可以对资源进行访问。  
2. 转换锁队列：该队列锁已经授权，但试图转换的锁模式与当前授权队列中的锁模式
不兼容，正在等待其他锁的释放或降低访问权限。锁管理器按照 FIFO的顺序处理
转化队列中的锁。  
3. 等待锁队列：该队列包含所有正在等待授权的新锁请求。该队列中锁模式与已授权
的锁模式不兼容。锁管理器按 FIFO的顺序处理等待队列中的锁。  
新锁请求只有在三个队列为空或者请求的锁模式与该资源所有的锁模式兼容时才会被授
权，否则就会被加入等待锁队列中。如下图所示：  
 
对于资源 res1 
▪ CR锁L1和CW锁L2是相互兼容的锁，都已经获得了访问授权；  
▪ 新请求PW锁L3因为与锁 L1和锁L2模式不兼容，所以被加入到等待队列中。  
对于资源 res2 
▪ CR锁L1、CR锁L2和CW锁L3都获得访问授权；  
▪ 假设某个时刻持有锁 L2的客户请求将锁转换为 EX模式，因与授权的 L1、L3不兼
容，所以转换请求必须等待 L1、L3的释放才能获得锁而被加入到转换队列中。  
意图锁 
Lustre中，意图锁主要用于文件元数据的访问，它通过执行锁的意图来减少元数据访问
所需要的消息传递次数，从而减少每次操作的延迟。  
当客户端向 MDS发起元数据操作请求时，在请求中指明操作意图，在交付给锁管理器处理
之前，先由 MDS执行意图锁的意图，然后返回不同锁资源。  
例如，在客户端请求创建一个新文件时：  
▪ 首先将该请求标志为文件创建的意图 (IT_CREATE)  
▪ 然后必须请求从 MDS获得它的父目录的锁来执行 lookup操作  
▪ 如果锁请求被授权，那么 MDS就用请求指定的意图来修改目录，创建请求的
文件，成功之后并不返回父目录锁，而是返回创建文件的锁给客户端  
 
范围锁 
范围锁用于维护细粒度的文件数据并发访问。其实现过程与 GPFS文件系统采用的范围锁
类似。与普通锁不同的是，范围锁增加了一个表示获得授权的锁定文件范围的域。由于同
一资源的不同锁的锁定范围之间是有关联的，所以范围锁的语义也发生了一些变化。  
Lustre结合范围锁和本地锁管理器实现了文件数据的写回缓冲策略。下图展示了使用范
围锁的示例：  
 
1. 客户端A要对某文件的存储对象 obj的[a, b]进行读写时，先从 OST上的锁服务器
获得的存储对象。 obj对应锁资源的范围锁 L<PW，[a，b]>，并将该锁资源的副本
加入到本地锁管理器中  
2. 客户端A利用本地文件系统 VFS层的缓存机制进行读写操作  
3. 客户端A执行完操作后并不立即将锁释放给锁服务器，而是采用一种 lazy的思
想，只有当其他的客户端 B要获得的范围锁与该锁有冲突并且锁服务器通过阻塞回
调函数通知 A客户端时，才将脏的缓存数据批处理地刷新到 OST，并将锁 释放返还
给锁服务器，然后再将锁资源授权给客户端 B 
总结 
Lustre 对 posix 部分接口的支持：  
posix 是否支持  
open with 
O_APPEND  支持。有如下问题：  
1. 每个客户端都需要对所有 OST进行EOF锁定，有很大的锁开销  
2. 第二个客户端在第一个客户端完成之前不能获取所有锁，客户
端只能顺序写入  
3. 为了避免死锁，它们以已知的一致性顺序获取锁  
flock/fcntl  支持多节点的  flock/fcntl ，使用"-o flock" 的方式挂载，可以获取
全局一致性；使用 "-o localflock" 可以保证本地节点上的一致性。  
关于lustre, Lustre wiki 上有几个值得参考的问题：  
1. Lustre 数据缓存和缓存一致性的方法是什么？  
为元数据 (names, readdir, lists, inode attribute) 和 文件数据提供了完整的
缓存一致性。客户端和服务器都使用分布式锁管理服务获取锁；在释放锁之前刷新
缓存。 
2. Lustre 是否符合 posix? 有例外吗？  
严格来说， posix并没有说明文件系统将如何在多个 客户端上运行。 Lustre 合理
解释了单节点 posix要求对应到集群环境中意味着什么。  
比如：通过 Lustre分布式锁管理器强制执行读写操作的一致性；如果在多个节点
上运行的应用程序同时读取和写入文件的同一部分，它们将看到一致的结果。有两
个例外：   
1. atime的更新。在高性能集群文件系统中保持完全一致的 atime更新是不实
际的。Lustre延迟更新文件的时间：需要更新 inode时搭载更新 atime; 文
件关闭时更新 atime。客户端读取或者写入数据时，只刷新本地缓存中的时
间更新。  
2. flock和lockf 通过分布式锁管理器实现全局一致性。  
3. 为什么不使用 IBM的DLM? 
Lustre DLM 设计大量借鉴了 VAX Clusters DLM 。尽管我们因不使用现有包 (例如: 
IBM的DLM)而受到一些合理的批评，但迄今为止表名我们做出了正确的选择：它更
小、更简单，并且至少对于我们的需求而言，更具可扩展性。  
Lustre DLM 大约有6000行代码，已被证明是一项可监督的维护任务，尽管其复杂
性令人生畏。相比之下  IBM DLM 几乎是所有 Lustre总和的大小。然而，这不一定
是对 IBM DLM 的批评。值得称赞的是， 它是一个完整的  DLM，它实现了我们在  
Lustre 中不需要的许多功能。  
Lustre的DLM并不是真正的分布式，至少在与其他此类系统相比时不是。 Lustre 
DLM 中的锁始终由管理特定 MDT或OST的服务节点管理，并且不会像其他系统允许
的那样更改主服务器。省略这种类似的功能使我们能够快速开发和稳定文件系统所
需的核心 DML功能，并添加扩展（扩展锁、意图锁、策略函数等）。  JuiceFS 
简介 
JuiceFS是高性能 posix文件系统，数据本身被持久化在对象存储，元数据根据场景需求
被持久化在 Redis、MySQL、SQLite等多种数据库引擎中。  
JuiceFS由三部分组成：  
1. JuiceFS 客户端：协调对象存储和元数据存储引擎，以及  POSIX、Hadoop、
Kubernetes 、S3 Gateway 等文件系统接口的实现；  
2. 数据存储：存储数据本身，支持本地磁盘、对象存储；  
3. 元数据引擎：存储数据对应的元数据，支持  Redis、MySQL、SQLite 等多种引擎；   JuiceFS 使用DBServer 管理锁 
flock锁 
flock锁提供三种类型： F_RDLCK，F_WRLCK，F_UNLCK 
DBServer 中对于每个锁的记录如下：一条记录由 inode、sid、owner、ltype组成。其中
sid是sessionid ，全局递增， mount的时候去 DBServer 申请。 
因此flock可以对应 fuse进程打开的 inode，可以作用到不同的节点。 JuiceFS 用flock
来同时保护数据和元数据资源。  
JuiceFS 使用DBServer 管理锁 
flock锁 
flock锁提供三种类型： F_RDLCK，F_WRLCK，F_UNLCK 
DBServer 中对于每个锁的记录如下：一条记录由 inode、sid、owner、ltype组成。其中
sid是sessionid ，全局递增， mount的时候去 DBServer 申请。 
因此flock可以对应 fuse进程打开的 inode，可以作用到不同的节点。 JuiceFS 用flock
来同时保护数据和元数据资源。  
type flock struct {  
 Inode Ino    `xorm:"notnull unique(flock)"`  
 Sid   uint64 `xorm:"notnull unique(flock)"`  
 Owner int64  `xorm:"notnull unique(flock)"`  
 Ltype byte   `xorm:"notnull"`  
} 
▪ 用户请求 flock锁时，客户端从 DBServer 中获取关 于该inode的所有锁请求，并
判断请求锁是否与已有锁冲突  
▪ 如果没有冲突，将改锁插入数据库，加锁成功；如果有冲突，并且申请锁是阻塞类
型，则等待指定时间后重试；如果有冲突，并且申请锁是非阻塞类型，则加锁失
败。 
fcntl区间锁 
区间锁用 plock表示。DBServer 中对于每个锁的记录如下：一条记录由 inode、sid、
owner、records 组成。 
其中records的数据结构是 plockRecord ，每个Record由ltype、pid、start、end组
成。 
因此plock可以记录 fuse进程打开的 inode上各区间的上锁情况。  
type plock struct {  
 Inode   Ino    `xorm:"notnull unique(plock)"`  
 Sid     uint64 `xorm:"notnull unique(plock)"`  
 Owner   int64  `xorm:"notnull unique(plock)"`  
 Records []byte `xorm:"blob notnull"`  
} 
type plockRecord struct {  
 ltype uint32  pid   uint32  
 start uint64  
 end   uint64  
} 
1. 用户请求 plock锁的流程处理和 flock一样，但是在检测锁冲突的时候，要和所有
客户端持有的区间锁一一比较。  
2. 更新锁列表有所不同， flock的更新是一条记录的删除或者插入， plock的更新的
时候需要按照一定的规则进行新增锁记录的插入。  
 
总结 
JuiceFS 提供「关闭再打开（ close-to-open）」一致性保证，即当两个及以上客户端同
时读写相同的文件时，客户端  A 的修改在客户端  B 不一定能立即看到。但是，一旦这个
文件在客户端  A 写入完成并关闭，之后在任何一个客户端重新打开该文件都可以保证能
访问到最新写入的数据，不论是否在同一个节点。  
「关闭再打开」是  JuiceFS 提供的最低限度一致性保证，在某些情况下可能也不需要重
新打开文件才能访问到最新写入的数据。例如多个应用程序使用同一个  JuiceFS 客户端
访问相同的文件（文件变更立即可见），或者在不同节点上通过  tail -f 命令查看最
新数据。  
juicefs 对 posix 部分接口的支持：  
posix 是否支持  
open with O_APPEND  不支持。多节点情况下会相互 覆盖 
flock/fcntl  支持多节点的  flock/fcntl ，允许多节点写入，保证最终一致性  
ChubaoFS  
CurveFS的元数据设计架构类似 ChubaoFS ，这里就不再说明了。  
chubaofs 支持： 
1. 一个客户端的写不能立马被其他客户端看到， fsync才会明确去刷这个 buffer，不
然只是定期刷；只有 buffer数据刷出去了，另外的客户端才能看到。  
chubaofs 不支持：  
1. 多个客户端同时写同一个文件的同一个位置   
2. 多个客户端的同一个文件 O_APPEND flag  
chubaofs 对 posix 部分接口的支持：  
posix 是否支持  
open with O_APPEND  不支持。多节点情况下会相互覆盖  
flock/fcntl  不支持。 chubao使用的bazil/fuse 不支持锁的设置  
case opGetlk:  
panic("opGetlk")  
case opSetlk:  
panic("opSetlk")  
case opSetlkw:  
panic("opSetlkw")  
CephFS 
cephfs的官方文档  提到了cepfs和posix标准在多客户端并发写入情况下的差别：  
1. CephFS 比本地 Linux 内核文件系统的 posix语义更宽松（例如，跨越对象边界的写
入可能会不一致）。在多客户端一致性方面，它严格小于  NFS，在写入原子性方面通常小
于 NFS。 
换句话说，当涉及到  POSIX 时： 
HDFS < NFS < CephFS < {XFS, ext4}  
2. 如果客户端尝试编写文件失败，写入操作不一定是原子的。也就是说，客户端可能会
在使用 8MB 缓冲的 O_SYNC 标记打开的文件中调用  write() 系统调用，然后意外终
止，且只能部分应用写入操作。几乎所有文件系统（甚至本地文件系统）都有此行为。  3. 在同时发生写操作的情况下，超过对象边界的写入操作不一定是原子的。例如，写入
器 A 写入 "aa|aa" 和 writer B 同时写入  "bb |bb"， 其中 "|" 是对象边
界，编写  "aa|bb" 而不是正确的  "aa|aa" 或 "bb|bb"。 
cephfs使用cap实现分布式锁  
类似的实现有 samba的oplocks以及NFS的delegation 。 
caps是mds授予client对文件进行操作的许可证，当一个 client想要对文件元数据进
行变更时，比如读、写、修改权限等操作，它都必须先获取到相应的 caps才可以进行这
些操作。  
ceph对caps的划分粒度很细，且允许多个 client在同一个 inode上持有不同的 caps。 
根据元数据内容的不同， cephfs将caps分为了多个类别，每种类别只负责作用于某些特
定的元数据：  
类别 功能 
Pin 
— p mds是否将inode pin 在cache中 
Auth — 
A 权限属性相关的元数据，主要是 owner、group、mode；但如果是完成的鉴权
是需要查看 ACL的，这部分信息保存在 xattr中，这就需要 xattr相关的cap 
  
Link — 
L inode的link count  
Xattr 
— X xattr 
File 
— F 最重要也是最复杂的一个，用于文件数据，以及和文件数据相关的 size、
atime、ctime、mtim等 
caps permission  
权限类型  权限说明  
CEPH_CAP_GSHARED  client can reads (s)  
CEPH_CAP_GEXCL  client can read and update (x)  
CEPH_CAP_GCACHE  (file) client can cache reads (c)  CEPH_CAP_GRD  (file) client can read (r)  
CEPH_CAP_GWR  (file) client can write (w)  
CEPH_CAP_GBUFFER  (file) client can buffer writes (b)  
CEPH_CAP_GWREXTEND  (file) client can extend EOF (a)  
CEPH_CAP_GLAZYIO  (file) client can perform lazy io (l)  
caps combination  
一个完整的 cap通过[类别+permission] 组成，client可以同时申请多个类别的 caps。
但并不是每种 caps都可以使用每种 permission ，有些caps只能搭配部分 permission 。
有关caps种类和permission 的结合使用，有一些几个规则：  
类型 说明 
Pin 二值型，有 pin就代表client知道这个 inode存在，这样 mds就一定会在
其cache中保存这个 inode。 
Auth、
Link、
Xattr 只能为shared或者exclusive  
▪ shared: client 可以将对应元数据保存在本地并缓存和使用  
▪ exclusive: client 不仅可以在本地缓存使用，还可以修改  
下面是两个例子：  
▪ [A]s：某client对inode 0x11 有As的cap，此时收到一个查看
0x11状态的系统调用，那么 client不需要再向 mds请求，直接通过
查询自身缓存并进行处理和回复  
▪ [A]x：某client对inode 0x11 有Ax的cap，此时收到一个修改
mode的系统调用， client可以直接在本地进行修改并回复，并且在
之后才将修改变更通知 mds File 最复杂的一种，下面是 File cap 的各个类别：  
file cap 种类client权限 
Fs: client 可以将mtime和size在本地cache并读取使用  
Fx: client 可以将mtime和size在本地cache并进行修改和读取  
Fr: client 可以同步地从 osd读取数据，但不能 cache 
Fc: client 可以将文件数据 cache在本地内存，并直接从 cache中读 
Fw: client 可以同步地写数据到 osd中，但是不能 buffer 
Fb: client 可以buffer write ，先将写的数据维护在自己的内存中，在统
一flush到后端落盘  
caps管理 
lock 
caps由mds进行管理，其将元数据划分为多个部分，每个部分都有专门的锁
（SompleLock ，ScatterLock 、FileLock ）来保护， mds通过这些锁的状态来确定 caps可
以怎么样分配。  
mds内部维护了每个锁的状态机，其内容非常负责，也是 mds保证caps分配准确性和数
据一致性的关键。  
caps如何变更  
▪ mds可以针对每个 client进行授予和移出 caps，通常是由其他 client的行为触发   
▪ 例：比如 client1已经拥有了 inode 0x11 的cache read 的cap，此时
client2要对这个文件进行写，那显然除了授予 client2响应的写 caps的
同时，还要剥夺 client1的cache read cap  
▪ 当client被移出caps时，其必须停止使用该 cap，并给mds回应确认消息。 mds
需要等待收到 client的确认消息后才会 revoke。（如果 client挂掉或者处于某
种原因没有回复 ack怎么办？）  
▪ client停止使用并不简单，在不同场景下需要完全不同的处理：   
▪ 例1：client被移出cache read cap ，直接把该 file的cache删掉，并变
更状态就行，这样下次的 read请求过来时，还是到 osd去读 ▪ 例2：client被移出buffer write cap ，已经缓存了大量的数据还没有
flush，那就需要先 flush到osd，再变更状态和确认，这可能就需要较长
的时间 
下面是一个修改权限的例子：   总结： 
总结： 
1. mds需要记住所有 client pin 的inode 
2. mds的cache需要比client的cache更多 
3. caps是由mds和client共同协作维护的，所以 client需要正常运行，否则可能
会block其他client 
fuse write 实例 
以fuse client write 为例，简要分析下 fuse write( 主要流程 )时caps的代码逻辑：  
int64_t Client::_write(Fh *f, int64_t offset, uint64_t size, const char *buf,  
                    const struct iovec *iov, int iovcnt)  
{ 
    want = CEPH_CAP_FILE_BUFFER;  
  // 需要拥有 file write 和auth shared caps 才(即FwAs)能够写， get_caps 中如果检
查没有caps则会 
  // 去向mds申请并等待返回  
  int r = get_caps(in, CEPH_CAP_FILE_WR|CEPH_CAP_AUTH_SHARED, want, &have, 
endoff);  
  if (r < 0)  
    return r;  
 
  // 增加该inode对该caps的引用计数并检查该 caps是否正在使用中  
  put_cap_ref(in, CEPH_CAP_A UTH_SHARED);  
 
  // 如果有buffer 或者lazy io cap 则直接在 objectcacher cache 中写 
  if (cct->_conf->client_oc &&  
      (have & (CEPH_CAP_FILE_BUFFER | CEPH_CAP_FILE_LAZYIO))) {  
 // 缓存写的调用，异步、 cache、非阻塞  
    r = objectcacher ->file_write(&in ->oset, &in ->layout,  
                 in->snaprealm ->get_snap_context(),  
                 offset, size, bl, ceph::real_clock::now(),  
                 0);  
  } else {  
    // 如果没有 buffer cap ，则直接通过 osd写 
    if (f->flags & O_DIRECT)  
      _flush_range(in, offset, size);  
 
    // 同步写的调用  
    filer->write_trunc(in ->ino, &in ->layout, 
in->snaprealm ->get_snap_context(),                 offset, size, bl, ceph::real_clock::now(), 0,  
               in->truncate_size, in ->truncate_seq,  
               &onfinish);  
  } 
} 
总结 
cephfs不需要用户做指定，对文件访问时都需要获取相应的权限。  
posix 是否支持  
open with O_APPEND  不支持 
flock/fcntl  内核版本支持， fuse客户端不确定  
NFS 
简介 
在存储系统中， NFS(Network File System, 即网络文件系统 ) 是一个重要的概念，已成
为兼容Posix语义分布式文件系统的基础。它允许在多个主机之间共享公共文件系统，并
提供数据共享的优势，从而最小化所需的存储空间。  
NFS作为类UNIX系统的标准网络文件系统，在发展过程中逐步原生地支持了文件锁 (从
NFSv4开始)。NFS从上个世界 80年代诞生至今，共发布了 3个版本： NFSv2、NFSv3、
NFSv4。NFSv4最大的变化是有“状态”了。某些操作需要服务端维持相关状态，如文件
锁，例如客户端申请了文件锁，服务端就需要维护该文件锁的状态，否则其他客户端冲突
的访问就无法检测。  
数据和元数据缓存一致性  
NFS客户端是弱缓存一致性，用于满足绝大多数文件共享场景。  
▪  Close-to-open 缓存一致性（ weak cache consistency ，CTO）。通常文件共
享是完全顺序的，比如： clienA进行了open，write，close；clientB 进行
open，然后可以读到 clientA的写入数据。 NFS client 在文件open的时候向
server发送请求查询 open权限；NFS client 在文件close的时候将本地文件的
变更写入到 server以便再次打开可以读取。在 mount的时候可以通过 nocto 选
项关闭。  
▪ 弱缓存一致性（ weak cache consistency ，WCC）。client的data cache 中的数
据不总是最新的。当客户端有很多并发操作同时更新文件时（多客户端异步写），文件上的数据最终是什么是 不确定的。但 NFS为client端提供了接口来检
查当前的数据是否被其他客户端修改过。  
▪ 属性缓存（ Attribute caching ）。在挂载的时候加上  noac 参数保证多 client
情况下的缓存一致性，此时 client端不能缓存文件元数据，元数据每次需要从
server端获取。这种方式使得客户端能及时获取到文件的更新信息，但会增加很
多网络开销。需要注意的是， noac参数情况下， data cache 是允许的，因此，
data cache 的一致性是不能保证的。 如果需要强缓存一致性，应用应该使用文
件锁。或者应用程序可以使用 O_DIRECT 方式打开文件以禁用数据缓存。  
客户端缓存  
当应用程序共享文件的时，无论是否在同一个 client上，应用程序都需要考虑冲突。
NFSv4支持Share reservations 和 byte-range locks 两种实现互斥的方式。 NFSv4需要
有data cache 以支持一些应用。  
Share reservations  是一种控制文件访问权限的机制，独立于 byte-rage locking 。当
client端OPEN文件时，他需要指定访问类型（ READ, WRITE, o r BOTH）以及访问权限
（OPEN4_SHARE_DENY_NONE,OPEN4_SHARE_DENY_READ, OPEN4_SHARE_DENY_WRITE, or 
OPEN4_SHARE_DENY_BOTH ）。伪代码如下：  
if (request.access == 0)  
             return (NFS4ERR_INVAL)  
     else if ((request.access & file_state.deny) ||  
         (request.deny & fi le_state.access))  
             return (NFS4ERR_DENIED)  
为了提供正确的共享语义， client必须使用 OPEN操作来获取初始文件句柄，并指出想要
的访问以及拒绝哪些访问。 OPEN/CLOSE 还需要遵循一下规则：  
▪ client OPEN 文件时， client需要从服务器重新获取数据并判断缓存中的数据是否
已经失效。  
▪ client CLOSE 文件时，需要将所有的缓存数据持久化。  
对于选择文件锁的应用，有一组类似的规则。  
▪ 当client获得指定区域的文件锁后，这段区域 的数据缓存失效。  
▪ 在client释放指定区域文件锁之前，客户端所有的修改必须持久化。  
结论 
posix 是否支持  open with 
O_APPEND  不支持 （https://man7.org/linux/man -
pages/man2/open.2.html ） 
flock/fcntl  支持。并且在文件有强一致性要求时，推荐应用使用。  
业务场景  
1. 对多挂载的要求是什么？支持挂载到同一个文件系统？支持同时读写同一个文件？  
2. 对一致性的要求是什么？ close-to-open? 还是更严格的要求？  
业务 对文件系统的需求  
goblinceph  1. 大部分场景是多个节
点写一个文件系统的
不同文件，很少对相
同文件进行读写。在
对相同文件进行读写
时，不要求文件系统
做正确性和一致性的
保证，如果业务有需
求，可以调用文件锁
保证； 
2. 一致性要求其实不是
很强烈，正常一致性
应该通过其他更专业
的方案。  
eassyai 使用tensorflow 平台，所以
和AI场景的通用痛点类似的
。 
AI场景 
人工智能是数据的消耗大户，对存储有针对性的需求。  
AI访问存储的几个特点：  
• 海量文件 ，训练模型的精准程度依赖于数据集的大小，样本数据集越大，就为模
型更精确提供了基础。通常，训练任务需要的文件数量都在几亿，十几亿的量级，
对存储的要求是能够承载几十亿甚至上百亿的文件数量。  • 小文件，很多的训练模型都是依赖于图片、音频片段、视频片段文件，这些文件
基本上都是在几 KB到几MB之间，对于一些特征文件，甚至只有几十到几百个字
节。 
• 读多写少， 在大部分场景中，训练任务只读取文件，把文件读取上 来之后就开始
计算，中间很少产生中间数据，即使产生了少量的中间数据，也是会选择写在本
地，很少选择写回存储集群， 因此是读多写少，并且是一次写，多次读 。 
• 目录热点， 由于训练时，业务部门的数据组织方式不可控，系统管理员不知道用
户会怎样存储数据。很有可能用户会将大量文件存放在同一个目录，这样会导致多
个计算节点在训练过程中，会同时读取这一批数据，这个目录所在的元数据节点就
会成为热点。跟一些 AI公司的同事交流中，大家经常提到的一个问题就是，用户
在某一个目录下存放了海量文件，导致训练的时候出现性能问题，其实就是碰到了
存储的热点问题。  
综上，对于 AI场景来说， 对多挂载没有一致性需求， 主要的挑战是：  
1. 海量文件的存储  
2. 小文件的访问性能  
3. 目录热点（这里有一个解决方案虚拟目录，后续我们解决这个问题的时候可以参
考。目前 Curve使用的是静态子树 +哈希的方案）  
CurveFS 如何支持多挂载  
本文主要调研了传统分布式文件系统： GPFS、Lustre、CephFS、NFS 和 面向云原生环境
设计的分布式文件系统： JuiceFS 和 Chubaofs 。 
上述分析可以得出：  
▪ GPFS、Lustre、NFS、CephFS 都支持多节点的读写同步，提供数据和元数据多个级
别的一致性。  
▪ JuiceFS 使用中心节点管理锁，支持多节点读写同步，保证最终一致。  
▪ ChubaoFS 支持多节点写不同区域，保证最终一致。不保证多节点写同一区域的一
致性。 
对于以上四中场景， curvefs 在并发读写场景下需要支持何种一致性？如何支持？是本文
需要得出的结论。  
1. CurveFS 需要支持写写互斥，读写互斥？  
1. posix文件接口的语义是针对多进程情况下的读写并发行为，并没有规定多
节点下的读写并发行为。  2. NFS给出了读写并发行为下一致性的保证： close-to-open缓存一致性、弱
缓存一致性 (提供接口检查是否被其他客户端修改过，其他不做保证 )、属性
缓存(挂载的时候指定 noac) 
3. 所以CurveFS 无需默认支持写写互斥、读写互斥。 CurveFS 对读写并发行为
的规定如下：  
1. close-to-open一致性并提供开关 (cto/nocto) 。close完再打开文
件，一定可以读到全部的最新数据。  
2. 支持文件锁 fcntl、flock。让用户可以通过该接口实现并发读写情
况下的强一致语义。（ O_APPEND 可以不做支持，因为性能必然很
差，实际估计不会应用。）  
3. 更多挂载选项 支持：cto/ncto （是否开启 cto，ncto可以提高只读挂
载的性能）、 ac/noac（是否开启元数据缓存）、 local_lock （non、
posix、flock、all 是否仅支持本地锁）  
2. 如何实现？   
1. cto一致性， cto/ncto 开关。ncto时打开文件允许缓存不更新。  
2. flock、fcntl 
实现可以参照： LDML（参考资料：
Understanding_Lustre_Filesystem_Internals ）、NFS（NFS：Opens and 
Byte-Range Locks ）、JuiceFS（源码阅读）。  
3. 更多挂载选项支持：可以参考 NFS。 
参考文档  
1. GPFS: A Shared -Disk File System for Large Computing Clusters  
2. GPFS—三大关键组件  
3. Lustre分布式锁管理器的分析与改进  
4. 文件系统那些事 -第4篇 并行文件系统之开源解决方案 Lustre 
5. Lustre wiki  
6. juicefs/docs/zh_cn/cache_management.md  
7. 被遗忘的桃源 ——flock 文件锁 
8. 多进程之间的文件锁  
9. juicefs readme  
10.多写文件系统与  DLM 设想   
11.CEPH 文件系统限 制和 POSIX 标准 
12.CephFS Caps  
13.CephFS caps 简介 
14.What are “caps”? (And Why Won ’t my Client Drop Them?) - Gregory 
Farnum, Red Hat  
15.CAPABILITIES IN CEPHFS  
16.cephfs：用户态客户端 write 
17.NFS文件锁一致性设计原理解析  
18.Network File System (NFS) Version 4 Protocol  
19.AI 场景的存储优化之路  
20.man5nfs 
21.Lustre文件系统 I/O锁的应用与优化  
22.Lustre & Application I/O