注意
本文档适用于 Ceph 的开发版本。
Ceph MDS 锁
为什么要使用锁?
MDS 中的锁定基础设施(显然)是为了保护各种元数据的状态。MDS 有不同的锁,覆盖 inode 和 dentry 的不同部分。此外,MDS 使用不同种类的锁,因为不同的元数据(在 inode 和 dentry 中)在不同情况下具有不同的行为。MDS 缓存分布在多个 MDS 排名和所有客户端之间。锁定基础设施用于确保所有排名和客户端对文件系统的视图保持一致。
MDS 管理的数据可能非常大,以至于无法将整个数据集存储在单个元数据服务器的内存中。这也会导致单点故障。因此,MDS 有一个分布式子树分区的概念。目录树可以被划分为更小的子树。这是通过记录目录树中每个节点的“热度”(访问频率)来完成的。当子树热度达到配置的阈值时,MDS 会通过拆分目录片段来划分该子树。每个片段负责原始目录的一部分,但是,这些片段会有一个单一的授权节点。每个 MDS 在分片后都可以承担读写请求。如果一个文件被频繁访问,MDS 将生成多个副本,分布在活动的 MDS 中,以满足并发 I/O。通常有多个客户端同时读取和写入文件。MDS 为相关的元数据定义了锁定规则,例如,对于很少并发修改的元数据(如 inode 的 UID/GID),共享读取和排他写入访问规则就足够了。然而,目录的统计信息可能需要由多个客户端同时更新。这个大目录可能已经被划分为多个分片(碎片),不同的客户端可以写入不同的分片。这些分片可以共享读取,也支持同时写入。
因此,除了覆盖 inode 的不同元数据片段的不同锁类型之外,MDS 还有锁类,用于定义特定锁类型的访问规则。本文档将进一步解释锁类型和锁类。
锁类型
MDS 定义了一些与 inode 或 dentry 的不同元数据相关的锁类型。保护 inode 和 dentry 元数据的锁类型如下:
CEPH_LOCK_DN - dentry
CEPH_LOCK_DVERSION - dentry version
CEPH_LOCK_IQUIESCE - inode quiesce lock (a type of superlock)
CEPH_LOCK_IVERSION - inode version
CEPH_LOCK_IAUTH - mode, uid, gid
CEPH_LOCK_ILINK - nlink
CEPH_LOCK_IDFT - dirfragtree, frags
CEPH_LOCK_IFILE - mtime, atime, size, truncate_seq, truncate_size, client_ranges, inline_data
CEPH_LOCK_INEST - rstats
CEPH_LOCK_IXATTR - xattrs
CEPH_LOCK_ISNAP - snaps
CEPH_LOCK_IFLOCK - file locks
CEPH_LOCK_IPOLICY - layout, quota, export_pin, ephemeral_*
注意
修改 ctime 时的锁定规则有点不同 - 要么在 versionlock 下,要么根本没有特定的锁(即,它可以与其他持有的锁一起修改,例如,在 CEPH_LOCK_IAUTH 下修改 (比如) uid/gid 时)。
锁类
锁类定义了处理分布式锁所需的关联锁类型的锁定行为。MDS 定义了 3 个锁类:
LocalLock - Used for data that does not require distributed locking such as inode or dentry version information. Local locks are versioned locks.
SimpleLock - Used for data that requires shared read and mutually exclusive write. This lock class is also the base class for other lock classes and specifies most of the locking behaviour for implementing distributed locks.
ScatterLock - Used for data that requires shared read and shared write. Typical use is where an MDS can delegate some authority to other MDS replicas, e.g., replica MDSs can satisfy read capabilities for clients.
注意
此外,MDS 还定义了 FileLock,它是 ScatterLock 的特例,用于需要共享读取和共享写入的数据,但也用于保护需要共享读取和互斥写入的其他元数据片段。
锁类型的分类如下:
SimpleLock
CEPH_LOCK_DN
CEPH_LOCK_IAUTH
CEPH_LOCK_ILINK
CEPH_LOCK_IXATTR
CEPH_LOCK_ISNAP
CEPH_LOCK_IFLOCK
CEPH_LOCK_IPOLICY
ScatterLock
CEPH_LOCK_INEST
CEPH_LOCK_IDFT
FileLock
CEPH_LOCK_IFILE
LocalLock
CEPH_LOCK_DVERSION
CEPH_LOCK_IVERSION
读取、写入和排他锁
获取锁有 3 种模式:
rdlock - shared read lock
wrlock - shared write lock
xlock - exclusive lock
rdlock 和 xlock 不言自明。
wrlock 是特殊的,因为它允许并发写入,并且对 ScatterLock 和 FileLock 类有效。从前面的章节可以看出,INEST 和 IDFT 属于 ScatterLock 类。wrlock 允许同时有多个写入者,例如,当一个(大)目录被拆分为多个分片(碎片)并且每个分片被“分配”给一个活动的 MDS 时。当在这些目录下创建新文件时,递归统计信息在活动的 MDS 上独立更新。之后,为了获取更新后的统计信息,“分散”的数据会在授权 MDS(inode 的)上聚合(“收集”);这通常发生在对这种锁类型请求 rdlock 时。
注意
MDS 还定义了 remote_wrlock,它主要用于重命名操作,当目标 dentry 位于与源 MDS 不同的(活动的)MDS 上时。
锁状态和锁状态机
MDS 定义了各种锁状态(定义在 src/mds/locks.h 源代码中)。并非所有锁状态都对给定的锁类有效。每个锁类定义了自己的锁转换规则,并组织为锁状态机。锁状态(LOCK_*)本身不是锁,而是控制是否允许获取锁。每个状态都遵循 LOCK_<STATE> 或 LOCK_<FROM_STATE>_<TO_STATE> 命名术语,可以总结为:
LOCK_SYNC - anybody (ANY) can read lock, no one can write lock and exclusive lock
LOCK_LOCK - no one can read lock, only primary (AUTH) mds can write lock or exclusive lock
LOCK_MIX - anybody (ANY) can write lock, no one can read lock or exclusive lock
LOCK_XLOCK - someone (client) is holding a exclusive lock
锁转换表(部分)使用以下概念:
ANY - Auth or Replica MDS
AUTH - Auth MDS
XCL - Auth MDS or Exclusive client
其他锁状态(如 LOCK_XSYN、LOCK_TSYN 等)是额外的状态,被定义为某些客户端行为的优化(LOCK_XSYN 允许客户端保留缓冲写入而不将其刷新到 OSD,并暂时暂停写入)。
中间锁状态(LOCK_<FROM_STATE>_<TO_STATE>)表示锁从一个状态(<FROM_STATE>)转换到另一个状态(<TO_STATE>)。
每个锁类都定义了自己的锁状态机,可以在 src/mds/locks.c 源代码中找到。在下面的部分讨论锁转换时会解释状态机。
锁转换
锁从一个状态到另一个状态的转换主要是由(客户端)请求或 MDS 正在经历的更改(例如树迁移)触发的。让我们考虑一个简单的案例:两个客户端。一个客户端执行 stat()(getattr() 或 lookup())来获取 inode 的 UID/GID,另一个客户端执行 setattr() 来更改同一 inode 的 UID/GID。第一个客户端(很可能)持有由 MDS 颁发的 As (iauth shared) caps。现在,当另一个客户端向 MDS 发出 setattr() 调用时,MDS 会向 inode 的 authlock(CEPH_LOCK_IAUTH)添加一个 xlock。
Server::handle_client_setattr()
if (mask & (CEPH_SETATTR_MODE|CEPH_SETATTR_UID|CEPH_SETATTR_GID|CEPH_SETATTR_BTIME|CEPH_SETATTR_KILL_SGUID))
lov.add_xlock(&cur->authlock);
请注意,MDS 会为这个 inode 添加许多其他锁,但现在我们只关注 IAUTH。CEPH_LOCK_IAUTH 是 SimpleLock 类,其锁转换状态机如下:
// stable loner rep state r rp rd wr fwr l x caps,other
[LOCK_SYNC] = { 0, false, LOCK_SYNC, ANY, 0, ANY, 0, 0, ANY, 0, CEPH_CAP_GSHARED,0,0,CEPH_CAP_GSHARED },
[LOCK_LOCK_SYNC] = { LOCK_SYNC, false, LOCK_LOCK, AUTH, XCL, XCL, 0, 0, XCL, 0, 0,0,0,0 },
[LOCK_EXCL_SYNC] = { LOCK_SYNC, true, LOCK_LOCK, 0, 0, 0, 0, XCL, 0, 0, 0,CEPH_CAP_GSHARED,0,0 },
[LOCK_SNAP_SYNC] = { LOCK_SYNC, false, LOCK_LOCK, 0, 0, 0, 0, AUTH,0, 0, 0,0,0,0 },
[LOCK_LOCK] = { 0, false, LOCK_LOCK, AUTH, 0, REQ, 0, 0, 0, 0, 0,0,0,0 },
[LOCK_SYNC_LOCK] = { LOCK_LOCK, false, LOCK_LOCK, ANY, 0, 0, 0, 0, 0, 0, 0,0,0,0 },
[LOCK_EXCL_LOCK] = { LOCK_LOCK, false, LOCK_LOCK, 0, 0, 0, 0, XCL, 0, 0, 0,0,0,0 },
[LOCK_PREXLOCK] = { LOCK_LOCK, false, LOCK_LOCK, 0, XCL, 0, 0, 0, 0, ANY, 0,0,0,0 },
[LOCK_XLOCK] = { LOCK_SYNC, false, LOCK_LOCK, 0, XCL, 0, 0, 0, 0, 0, 0,0,0,0 },
[LOCK_XLOCKDONE] = { LOCK_SYNC, false, LOCK_LOCK, XCL, XCL, XCL, 0, 0, XCL, 0, 0,0,CEPH_CAP_GSHARED,0 },
[LOCK_LOCK_XLOCK]= { LOCK_PREXLOCK,false,LOCK_LOCK,0, XCL, 0, 0, 0, 0, XCL, 0,0,0,0 },
[LOCK_EXCL] = { 0, true, LOCK_LOCK, 0, 0, REQ, XCL, 0, 0, 0, 0,CEPH_CAP_GEXCL|CEPH_CAP_GSHARED,0,0 },
[LOCK_SYNC_EXCL] = { LOCK_EXCL, true, LOCK_LOCK, ANY, 0, 0, 0, 0, 0, 0, 0,CEPH_CAP_GSHARED,0,0 },
[LOCK_LOCK_EXCL] = { LOCK_EXCL, false, LOCK_LOCK, AUTH, 0, 0, 0, 0, 0, 0, CEPH_CAP_GSHARED,0,0,0 },
[LOCK_REMOTEXLOCK]={ LOCK_LOCK, false, LOCK_LOCK, 0, 0, 0, 0, 0, 0, 0, 0,0,0,0 },
状态转换条目是 src/mds/locks.h 源代码中的 sm_state_t 类型。TODO:详细描述这些内容。
我们到达一个点,MDS 填充 LockOpVec 并调用 Locker::acquire_locks(),它根据锁类型和模式(rdlock 等)尝试获取该特定锁。锁的起始状态是 LOCK_SYNC(这可能并非总是如此,但为简单起见考虑这种情况)。为了获取 iauth 的 xlock,MDS 参考状态转换表。如果当前状态允许获取锁,MDS 就会获取锁(这只是增加一个计数器)。当前状态(LOCK_SYNC)不允许获取 xlock(LOCK_SYNC 状态下的 x 列),因此需要进行锁状态切换。此时,MDS 切换到中间状态 LOCK_SYNC_LOCK - 表示从 LOCK_SYNC 转换到 LOCK_LOCK 状态。中间状态有两个目的 - a. 中间状态定义了允许客户端持有的 caps,从而撤销在此状态下不允许持有的 caps,b. 阻止获取新的锁。此时 MDS 向客户端发送 cap 撤销消息。
2021-11-22T07:18:20.040-0500 7fa66a3bd700 7 mds.0.locker: issue_caps allowed=pLsXsFscrl, xlocker allowed=pLsXsFscrl on [inode 0x10000000003 [2,head] /testfile auth v142 ap=1 DIRTYPARENT s=0 n(v0 rc2021-11-22T06:21:45.015746-0500 1=1+0) (iauth sync->lock) (iversion lock) caps={94134=pAsLsXsFscr/-@1,94138=pLsXsFscr/-@1} | request=1 lock=1 caps=1 dirtyparent=1 dirty=1 authpin=1 0x5633ffdac000]
2021-11-22T07:18:20.040-0500 7fa66a3bd700 20 mds.0.locker: client.94134 pending pAsLsXsFscr allowed pLsXsFscrl wanted -
2021-11-22T07:18:20.040-0500 7fa66a3bd700 7 mds.0.locker: sending MClientCaps to client.94134 seq 2 new pending pLsXsFscr was pAsLsXsFscr
如上所示,client.94134 持有 As caps,正在被 MDS 撤销。caps 被撤销后,MDS 可以继续转换到更进一步的状态:LOCK_SYNC_LOCK 到 LOCK_LOCK。由于目标是获取 xlock,状态转换继续(根据锁转换状态机):
LOCK_LOCK -> LOCK_LOCK_XLOCK
LOCK_LOCK_XLOCK -> LOCK_PREXLOCK
LOCK_PREXLOCK -> LOCK_XLOCK
最终获取 iauth 上的 xlock。
TODO:解释锁定顺序和路径遍历锁定。