Glock 内部锁规则

本文档描述了 glock 状态机内部的基本原理。每个 glock (fs/gfs2/incore.h 中的 struct gfs2_glock) 都有两个主要的(内部)锁:

  1. 自旋锁 (gl_lockref.lock),用于保护内部状态,例如 gl_state、gl_target 和 holders 列表 (gl_holders)

  2. 非阻塞位锁 GLF_LOCK,用于防止其他线程同时调用 DLM 等。如果一个线程获取了这个锁,它必须在释放锁时调用 run_queue(通常通过工作队列),以确保任何挂起的任务都已完成。

gl_holders 列表包含与 glock 关联的所有排队的锁请求(不仅仅是 holders)。 如果有任何已持有的锁,它们将是列表头部的连续条目。 锁的授予严格按照排队的顺序。

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 操作。 glocks 基本上是一个锁加上一些处理缓存管理的例程。 以下规则适用于缓存:

Glock 模式

缓存元数据

缓存数据

脏数据

脏元数据

UN

DF

SH

EX

这些规则是使用为每种 glock 类型定义的各种 glock 操作来实现的。 并非所有类型的 glocks 都使用所有模式。 例如,只有 inode glocks 使用 DF 模式。

glock 操作表和每种类型的常量

字段

目的

go_sync

在远程状态更改之前调用(例如,同步脏数据)

go_xmote_bh

在远程状态更改之后调用(例如,重新填充缓存)

go_inval

如果远程状态更改需要使缓存无效,则调用

go_instantiate

在获取 glock 时调用

go_held

每次获取 glock holder 时调用

go_dump

调用以打印 debugfs 文件的对象内容,或在发生错误时将 glock 转储到日志。

go_callback

如果 DLM 发送回调以释放此锁,则调用

go_unlocked

在 glock 被解锁时调用 (dlm_unlock())

go_type

glock 的类型,LM_TYPE_*

go_flags

如果 glock 具有关联的地址空间,则设置 GLOF_ASPACE

每个锁的最小保持时间是在远程锁授予后,我们忽略远程降级请求的时间。 这是为了防止锁在集群中的节点之间来回跳动,而没有一个节点取得任何进展的情况。 这往往在多个节点写入的共享 mmapped 文件中最为常见。 通过延迟响应远程回调的降级,可以让用户空间程序在页面被取消映射之前取得一些进展。

最终,我们希望使 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 的锁定顺序

  1. i_rwsem(如果需要)

  2. 重命名 glock(仅用于重命名)

  3. Inode glock(s)(父目录在子目录之前,“同一级别”的 inode 按照锁号排序,具有相同的父目录)

  4. Rgrp glock(s)(用于(取消)分配操作)

  5. 事务 glock(通过 gfs2_trans_begin)用于非读取操作

  6. i_rw_mutex(如果需要)

  7. 页面锁(始终最后,非常重要!)

每个 inode 有两个 glocks。 一个处理对 inode 本身的访问(如上所述的锁定顺序),另一个(称为 iopen glock)与 inode 中的 i_nlink 字段结合使用,以确定相关 inode 的生命周期。 inode 的锁定是基于每个 inode 的。 rgrp 的锁定是基于每个 rgrp 的。 一般来说,我们更喜欢在集群锁之前锁定本地锁。

Glock 统计信息

统计信息分为两组:与超级块相关的统计信息和与单个 glock 相关的统计信息。 超级块统计信息是基于每个 CPU 完成的,目的是尽量减少收集它们的开销。 它们还按 glock 类型进一步划分。 所有计时均以纳秒为单位。

对于超级块和 glock 统计信息,每种情况都会收集相同的信息。 超级块计时统计信息用于为 glock 计时统计信息提供默认值,以便新创建的 glocks 应尽可能具有合理的起点。 创建 glock 时,每个 glock 计数器都初始化为零。 当 glock 从内存中弹出时,每个 glock 统计信息将丢失。

统计信息分为三对平均值和方差,以及两个计数器。 平均值/方差对是平滑的指数估计值,所使用的算法对于那些习惯于计算网络代码中的往返时间的人来说非常熟悉。 请参阅“TCP/IP Illustrated, Volume 1”,W. Richard Stevens,第 21.3 节,“往返时间测量”,第 299 页及之后。 另请参阅第 2 卷,第 25.10 节,第 838 页及之后。 与 TCP/IP Illustrated 案例不同,平均值和方差未缩放,而是以整数纳秒为单位。

三对平均值/方差测量以下内容:

  1. DLM 锁时间(非阻塞请求)

  2. DLM 锁时间(阻塞请求)

  3. 请求间时间(再次针对 DLM)

非阻塞请求是指可以立即完成的请求,无论相关 DLM 锁的状态如何。 目前,这意味着以下情况下的任何请求: (a) 锁的当前状态是独占的,即锁降级 (b) 请求的状态为 null 或已解锁(同样是降级)或 (c) 设置了“尝试锁”标志。 阻塞请求涵盖所有其他锁请求。

有两个计数器。 第一个主要用于显示已发出多少个锁请求,以及有多少数据已进入平均值/方差计算。 另一个计数器正在计算 glock 代码顶层的 holder 的排队。 希望该数字比发出的 dlm 锁请求的数量大得多。

那么,为什么要收集这些统计信息? 我们希望更好地了解这些计时的原因有以下几个:

  1. 为了能够更好地设置 glock“最小保持时间”

  2. 为了更容易地发现性能问题

  3. 为了改进选择资源组进行分配的算法(基于锁等待时间,而不是盲目地使用“尝试锁”)

由于更新的平滑作用,采样的某些输入量的阶跃变化只有在 8 个样本(方差为 4 个)之后才会完全考虑在内,这在解释结果时需要仔细考虑。

了解锁请求完成所需的时间以及 glock 的锁请求之间的平均时间意味着我们可以计算节点能够使用 glock 的总时间百分比,而不是集群其余部分共享的时间。 这对于设置锁最小保持时间非常有用。

已非常谨慎地确保我们尽可能准确地测量我们想要的量。 任何测量系统中总会有不准确之处,但我希望这能尽可能准确地实现。

每个 sb 的统计信息可以在这里找到

/sys/kernel/debug/gfs2/<fsname>/sbstats

每个 glock 的统计信息可以在这里找到

/sys/kernel/debug/gfs2/<fsname>/glstats

假设 debugfs 挂载在 /sys/kernel/debug 上,并且 被替换为相关 gfs2 文件系统的名称。

输出中使用的缩写如下:

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 文件,但每个计时统计信息使用格式 mean/variance。

gfs2_glock_lock_time 跟踪点打印出相关 glock 的统计信息的当前值,以及收到的每个 dlm 回复的一些附加信息。

status

dlm 请求的状态

flags

dlm 请求标志

tdiff

此特定请求所用的时间

(其余字段与上述列表相同)