Glock 内部锁规则¶
本文档介绍了 glock 状态机内部的基本原理。每个 glock(fs/gfs2/incore.h 中的 struct gfs2_glock)都有两个主要的(内部)锁
一个自旋锁 (gl_lockref.lock),用于保护内部状态,例如 gl_state、gl_target 和持有者列表 (gl_holders)
一个非阻塞位锁,GLF_LOCK,用于防止其他线程同时调用 DLM 等。如果一个线程获得了此锁,它必须在释放锁时调用 run_queue(通常通过工作队列),以确保完成任何挂起的任务。
gl_holders 列表包含与 glock 关联的所有排队的锁请求(不仅仅是持有者)。如果存在任何已持有的锁,则它们将是列表头部的连续条目。锁严格按照排队顺序授予。
glock 层的用户可以请求三种锁状态,即共享 (SH)、延迟 (DF) 和独占 (EX)。它们转换为以下 DLM 锁模式
Glock 模式 |
DLM |
锁模式 |
---|---|---|
UN |
IV/NL |
已解锁(没有与 glock 关联的 DLM 锁)或 NL |
SH |
PR |
(受保护的读取) |
DF |
CW |
(并发写入) |
EX |
EX |
(独占) |
因此,DF 基本上是一种共享模式,它与“正常”的共享锁模式 SH 不兼容。在 GFS2 中,DF 模式专门用于直接 I/O 操作。glock 基本上是一个锁加上一些处理缓存管理的例程。以下规则适用于缓存
Glock 模式 |
缓存元数据 |
缓存数据 |
脏数据 |
脏元数据 |
---|---|---|---|---|
UN |
否 |
否 |
否 |
否 |
DF |
是 |
否 |
否 |
否 |
SH |
是 |
是 |
否 |
否 |
EX |
是 |
是 |
是 |
是 |
这些规则是使用为每种类型的 glock 定义的各种 glock 操作实现的。并非所有类型的 glock 都使用所有模式。例如,只有 inode glock 使用 DF 模式。
glock 操作表和每种类型的常量
字段 |
目的 |
---|---|
go_sync |
在远程状态更改之前调用(例如,同步脏数据) |
go_xmote_bh |
在远程状态更改之后调用(例如,重新填充缓存) |
go_inval |
如果远程状态更改需要使缓存无效时调用 |
go_instantiate |
当 glock 被获取时调用 |
go_held |
每次获取 glock 持有者时调用 |
go_dump |
调用以打印 debugfs 文件的对象内容,或在错误时将 glock 转储到日志中。 |
go_callback |
如果 DLM 发送回调以删除此锁时调用 |
go_unlocked |
当 glock 被解锁 (dlm_unlock()) 时调用 |
go_type |
glock 的类型, |
go_flags |
如果 glock 具有与之关联的地址空间,则设置 GLOF_ASPACE |
每个锁的最小保持时间是在远程锁授权之后,我们忽略远程降级请求的时间。这是为了防止锁在集群中的节点之间来回反弹,而没有任何节点取得任何进展的情况。这种情况在多个节点正在写入的共享内存映射文件中最为常见。通过延迟响应远程回调的降级,可以让用户空间程序在页面取消映射之前取得一些进展。
最终,我们希望使 glock “EX” 模式在本地共享,这样任何本地锁定都将根据需要使用 i_mutex 完成,而不是通过 glock 完成。
glock 操作的锁定规则
操作 |
GLF_LOCK 位锁持有 |
gl_lockref.lock 自旋锁持有 |
---|---|---|
go_sync |
是 |
否 |
go_xmote_bh |
是 |
否 |
go_inval |
是 |
否 |
go_instantiate |
否 |
否 |
go_held |
否 |
否 |
go_dump |
有时 |
是 |
go_callback |
有时 (N/A) |
是 |
go_unlocked |
是 |
否 |
注意
如果操作在入口时持有位锁或自旋锁,则不得放弃它们。go_dump 和 do_demote_ok 绝对不能阻塞。请注意,仅当 glock 的状态表明它正在缓存最新数据时,才会调用 go_dump。
GFS2 中 glock 的锁定顺序
i_rwsem(如果需要)
重命名 glock(仅用于重命名)
Inode glock(s)(父节点在前,同一父节点中“同一级别”的 inode 按锁定编号顺序排列)
Rgrp glock(s)(用于(取消)分配操作)
事务 glock(通过 gfs2_trans_begin)用于非读取操作
i_rw_mutex(如果需要)
页面锁(始终最后,非常重要!)
每个 inode 有两个 glock。一个用于访问 inode 本身(如上所述的锁定顺序),另一个称为 iopen glock,与 inode 中的 i_nlink 字段结合使用,以确定相关 inode 的生命周期。inode 的锁定是基于每个 inode 进行的。rgrp 的锁定是基于每个 rgrp 进行的。一般来说,我们倾向于在集群锁之前锁定本地锁。
Glock 统计信息¶
统计信息分为两组:与超级块相关的统计信息和与单个 glock 相关的统计信息。为了尽量减少收集它们的开销,超级块统计信息是按每个 CPU 进行的。它们还按 glock 类型进一步划分。所有计时均以纳秒为单位。
在超级块和 glock 统计信息的情况下,在每种情况下都会收集相同的信息。超级块计时统计信息用于为 glock 计时统计信息提供默认值,以便新创建的 glock 应尽可能具有一个合理的起点。在创建 glock 时,每个 glock 的计数器都会初始化为零。当 glock 从内存中弹出时,每个 glock 的统计信息会丢失。
统计信息分为三对均值和方差,外加两个计数器。均值/方差对是平滑的指数估计,所使用的算法对于那些习惯于计算网络代码中的往返时间的人来说非常熟悉。请参阅“TCP/IP Illustrated, Volume 1”,W. Richard Stevens,第 21.3 节,“往返时间测量”,第 299 页及之后。另请参阅,第二卷,第 25.10 节,第 838 页及之后。与 TCP/IP Illustrated 的情况不同,均值和方差没有缩放,而是以整数纳秒为单位。
三对均值/方差测量以下内容
DLM 锁时间(非阻塞请求)
DLM 锁时间(阻塞请求)
请求间隔时间(再次到 DLM)
非阻塞请求是指无论相关 DLM 锁的状态如何,都将立即完成的请求。目前这意味着 (a) 当锁的当前状态是独占时,即锁降级;(b) 请求的状态为空或解锁时(同样是降级);或 (c) 设置了“尝试锁定”标志时的任何请求。阻塞请求涵盖所有其他锁请求。
有两个计数器。第一个计数器主要用于显示已发出了多少锁请求,从而显示已输入均值/方差计算的数据量。另一个计数器计算 glock 代码顶层持有者的排队情况。希望这个数字远大于发出的 dlm 锁请求的数量。
那么为什么要收集这些统计信息?我们希望更好地了解这些计时的几个原因
为了能够更好地设置 glock 的“最小保持时间”
为了更容易地发现性能问题
为了改进选择用于分配的资源组的算法(使其基于锁等待时间,而不是盲目地使用“尝试锁定”)
由于更新的平滑作用,仅在 8 个样本(或方差为 4 个)之后才会完全考虑被采样的某些输入数量的阶跃变化,在解释结果时需要仔细考虑这一点。
了解完成锁请求所需的时间和 glock 的锁请求之间的平均时间意味着我们可以计算出节点能够使用 glock 的总时间百分比与集群其余部分共享的时间百分比。这在设置锁的最小保持时间时非常有用。
我们已非常注意确保我们尽可能准确地测量我们想要的数量。任何测量系统都存在不准确之处,但我希望这是我们能合理做到的最准确的测量。
每个 sb 统计信息可以在这里找到
/sys/kernel/debug/gfs2/<fsname>/sbstats
每个 glock 统计信息可以在这里找到
/sys/kernel/debug/gfs2/<fsname>/glstats
假设 debugfs 安装在 /sys/kernel/debug 上,并且
输出中使用的缩写如下
srtt |
非阻塞 dlm 请求的平滑往返时间 |
srttvar |
srtt 的方差估计 |
srttb |
(可能)阻塞的 dlm 请求的平滑往返时间 |
srttvarb |
srttb 的方差估计 |
sirt |
平滑的请求间隔时间(用于 dlm 请求) |
sirtvar |
sirt 的方差估计 |
dlm |
发出的 dlm 请求数量(glstats 文件中的 dcnt) |
queue |
排队的 glock 请求数量(glstats 文件中的 qcnt) |
sbstats 文件包含每个 glock 类型的一组这些统计信息(因此每种类型 8 行),以及每个 CPU 的信息(每个 CPU 一列)。glstats 文件包含每个 glock 的一组这些统计信息,格式与 glocks 文件类似,但对于每个计时统计信息,使用平均值/方差的格式。
gfs2_glock_lock_time 跟踪点打印出相关 glock 的当前统计信息值,以及收到的每个 dlm 回复的一些附加信息。
状态 |
dlm 请求的状态 |
标志 |
dlm 请求的标志 |
tdiff |
此特定请求所用的时间 |
(剩余字段与上面的列表相同)