介绍
sanlock 是一个基于 SAN 的分布式锁管理器。集群中的每个节点都各自运行 sanlock 服务,锁的状态都被写到了共享存储上,使用 Disk Paxos 算法读写共享存储以实现对分布式锁的获取、释放和超时。一般认为 SAN 中的 LUN 的可靠性要比集群中的主机高,对主机来说,应用程序进程可能会崩溃、重启,主机的 IP 网络也可能会发生故障,而 SAN 是通过专门的光网连接的,还可以配置多路径以提高可用性和吞吐量,一个 LUN 也可能被冗余映射到阵列中的多块磁盘上,因此 SAN 服务质量很高。通过利用 Disk Paxos 算法,sanlock 服务的所有数据都保存在共享存储上,即使服务器进程崩溃也不会影响可靠性。在一个集群里,通常 SAN 都是核心,可以认为如果 SAN 出现问题,整个集群的功能都会受到严重影响,因此选择以共享存储作为 sanlock 服务的基础是合理的。
oVirt 使用了 sanlock 作为其存储域的分布式锁管理器,sanlock 被用来保证领导节点的唯一性,并且用来保护共享存储设备上的存储域的元数据的一致性,以及保护同一个虚拟机镜像不会被两台虚拟机同时使用。libvirt也使用了 sanlock,它的 sanlock 插件可以在虚拟机启动前为所有的磁盘请求锁保护,以防止在意外情况下有两台虚拟机使用同一个磁盘镜像。
原理
在 sanlock 集群中,不存在专门的锁服务器或节点,每一个节点上都运行 sanlock 后台服务,所有的节点都是平等的。
锁的状态被保存在共享存储上,sanlock 使用的共享存储既可以是通过 SAN 访问的 LUN,也可以是 NAS(比如 NFS 中的某个文件),所有节点都能访问共享存储,共同维护锁的状态。
sanlock 的锁是名义锁,并非强制锁,其语义类似 pthread 的 mutex;被 sanlock 保护的对象,即使不上锁也能直接访问,完全由程序员自由决定 sanlock 的锁保护的是何种资源,何时上锁和释放。
Delta Lease
- Delta Lease只由 sanlock 内部使用,不对外暴露,主要作用是保证节点 ID 的唯一性,并提供简单的成员管理服务
- Delta Lease 用来确保特定的节点 ID(1-2000范围内) 只能由唯一一个节点持有,并且每个 Delta Lease 只需要一个块就能实现
- 原理是在向共享存储写完数据,并等待足够长的时间后再读,如果发现数据没有被起他节点更改,就认为没有冲突并成功获取了租约的所有权。
- 从原理上来看Delta Lease 的获取需要比较长的时间,但比较省空间
- 把所有的 Delta Lease 磁盘块连续的保存在一个文件或者裸磁盘设备里,就形成了一个 sanlock 的 lockspace
- 每个节点每隔20s发送一次心跳,更新lockspace lease的timestamp属性
- 一旦一个节点获取到某个lockspace的delta lease后(加入这个lockspace),表示这个节点上的进程可以获取此lockspace中的resource锁
每个节点通过在不同的 lockspace 中获取 Delta Lease,可以加入不同的 sanlock 集群。
图中主机名为 hostA 的节点,已经成功获取了 Host ID 1 的 Delta Lease 后,hostB 通过读取 Delta Lease 的磁盘块,发现租约已经被占有,就无法再次获取
Paxos Lease
- 基于 Disk Paxos 算法
- 通过对共享存储的特定的顺序的读写操作实现对租约的获取、续租和释放。一旦获取了 Paxos Lease 后,sanlock 就自动为其续租
- 共享存储上的 Paxos Lease 实例被划分为与 sanlock 集群节点数相同数量的块
- 集群中的每个节点都可以写自己 ID 对应的块,但只能读其他节点对应的块
- 某节点要占有一个租约,只需要将请求写入自己对应的块,然后读取所有其他节点的块,查看是否有冲突
- 获取时间短,但是占用大量空间
图中:
- Host ID 为 1 的主机成功获取了 Paxos Lease 后,Host ID 为 3 的主机在读取共享存储时,就能检测到冲突,判定为获取失败
- 按照 Disk Paxos 算法,将正确的所有者编号写入自己的专有块,并递增提案编号
- Host ID 为 2 的主机没有获取 Paxos Lease 的意向,则不必参与到事务中
当节点由于sanlock crash或者无法访问存储等情况导致租期无法续约时,sanlock进入recovery mode,会先停止节点上已经获取resource lease的进程
有几种方式:
- graceful shutdown:调用进程服务暴露给sanlock的graceful_shutdown的handler方法,或者给进程发送sigterm信号;如果没有退出继续使用下面的方法
- force shutdown:发送sigkill信号给进程;如果没有退出继续使用下面的方法
- host reset:触发节点的watchdog进程reset节点
节点异常后其他节点获取锁的流程:
- 正常情况下,进程获取到锁后,resource lease这部分就没有io了,因为resource lease不发送心跳
- 锁所在节点异常后,则节点无法发送delta lease心跳,但是锁本身还是分配给源节点
- 其他节点请求锁时,会检测锁对应的节点,然后检查节点的delta lease心跳,如果心跳正常则获取失败,反之获取成功
共享锁:
在 Sanlock 的共享模式下,多个节点可以同时获取同一个资源的锁,这与独占模式不同,在独占模式下,一次只能有一个节点获取锁
共享模式的主要用途包括:
资源协调:允许多个节点同时访问某个资源,例如,读取配置信息或共享状态。 并行操作:在某些情况下,多个节点可能需要执行不会相互冲突的操作,共享锁可以用来协调这些操作。 共享锁的特点是:
非独占:多个节点可以同时持有锁。 可重入:持有锁的节点可以多次获取同一个锁,而不会导致死锁。 协调:虽然多个节点可以同时持有锁,但Sanlock仍然负责协调锁的获取和释放,确保操作的原子性。
全局锁:
全局锁与全局信息相关联,全局信息包括:
- 全局VG 命名空间
- PV集合和未使用的设备
- PV的属性,例如PV大小
读取这些信息的命令会以共享模式获取全局锁,更改这些信息的命令则以独占模式获取。
例如’vgs’ 命令以共享模式获取全局锁,因为它报告所有VG名称的列表,而vgcreate命令以独占模式获取全局锁,因为它创建了一个新的VG名称,并且从未使用的PV列表中取出一个PV。
当LVM命令收到tag参数,或使用select时,就必须读取所有vg以匹配tag或select,将获取全局锁
VG锁:
vg锁与每个共享vg相关联。VG锁在读取VG时以共享模式获取,在更改vg或激活lvs时以独占模式获取。这个锁将访问vg的操作顺序执行,以防止所有主机上访问该vg的lvm命令并发执行
命令 ‘vgs
LV锁:
LV锁在激活LV之前获取,并在LV停用后释放。如果无法获取LV锁,则不会激活LV。(LV锁是持久的,并且在激活命令完成后仍然保持。全局锁和VG锁是临时的,只在LVM命令运行时保持)
wdmd&fence
sanlock 在启动后会打开一个 wdmd 连接,定时发送保活心跳。
sanlock 同时会监视获取了 Paxos Lease 的进程,如果进程崩溃了,sanlock 就会自动将 Paxos Lease 释放。
如果由于某种原因导致 sanlock 无法访问共享存储,sanlock 就无法续租,这时 sanlock 会自动向持有 Paxos Lease 的进程发 SIGTERM 或者 SIGKILL。
也可以配置 sanlock 的 killpath 参数,在无法续租的时候,让 sanlock 调用 killpath 指向的程序。
如果由于种种原因,sanlock 无法关闭持有 Paxos Lease 的进程,sanlock 就不会再向 wdmd 发送保活更新,于是 watchdog 设备会在系统保活更新超时后,重启整台主机。
有了 wdmd 的配合,sanlock 就能对有问题的节点或进程进行自我 fence
注:由于后端存储性能差会导致心跳发送不及时,触发fence导致机器关闭,安超关闭了fence功能
lvm中的sanlock
lvm对接san存储:
- SAN存储端划分好LUN,然后通过映射给安超每个节点,使用该LUN创建sanlock VG,所有节点都能访问
- 在VG基础上,创建lv,写入qcow2,然后将lv挂载给虚拟机用作系统盘和数据盘
- 每个lv的format为qcow2,每个lv需要active才能在当前节点看到它的路径
- lvchange -asy vgname/lvname可以active这个lv,然后当前节点才能看到lv的完整路径/dev/vgname/lvname
- qemu-img info /dev/vgname/lvname可以查看lv的基本qcow2信息,在active之后执行
lvm中lockspace和resource lease文件是同一个文件,一个vg对应一个lockspace文件(/dev/mapper/[vg-name]-lvmlock)
每个lockspace文件中offset如上图所示:
- offset从0开始是lockspace空间,只需要占用1M空间,一个节点占用512byte,最大支持2k个节点
- 到offset是65M的位置,保存global lock,占用1M
- 到offset是66M的位置,保存vg lock,占用1M
- 到offset是67M之后的位置,保存lv lock
操作
准备
注:这里使用nfs作为共享存储来demo,nfs挂载在两个节点的/data目录下
1
2
3
yum install -y sanlock sanlock-python
systemctl enable sanlock
systemctl start sanlock
初始化lockspace和resource文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 创建两个1M的lease文件
dd if=/dev/zero bs=1048576 count=1 of=/data/hosts_lease
dd if=/dev/zero bs=1048576 count=1 of=/data/resource_lease
# 初始化host_lease
# 命令格式为
# sanlock direct init -s <name>:<host_id>:<path>:<offset>
# <name>: sanlock的lockspace名字,任意字符串即可
# <host_id>: 主机id,前面提到的必须唯一的主机编号,初始化时用0替代
# <path>: 主机管理文件的具体路径,这里是/data/hosts_lease
# <offset>: 偏移量,始终提供0即可
sanlock direct init -s test:0:/data/hosts_lease:0
chown sanlock:sanlock /data/hosts_lease
# 初始化resource_lease
# 注意,这里实际上可以通过offset的方式还使用host_lease文件作为resource_lease来用,demo时使用了一个独立的resource_lease文件,offset设置成0
sanlock direct init -r test:testres:/data/resource_lease:0
chown sanlock:sanlock /data/resource_lease
节点1添加lockspace
1
2
3
4
5
6
7
8
# 命令格式为
# sanlock client add_lockspace -s <name>:<host_id>:<path>:<offset>
# <name>: sanlock的lockspace名字,前面初始化步骤中名为test
# <host_id>: 主机id,前面提到的必须唯一的主机编号,对于主机1,我们编号为1
# <path>: 主机管理文件的具体路径,这里是/data/hosts_lease
# <offset>: 偏移量,始终提供0即可
sanlock client add_lockspace -s test:1:/data/hosts_lease:0
节点2添加lockspace
1
sanlock client add_lockspace -s test:2:/data/hosts_lease:0
查看lockspace中的节点信息
1
2
3
4
[root@node1 ~]# sanlock direct dump /data/hosts_lease
offset lockspace resource timestamp own gen lver
00000000 test 0b8392d3-094f-42af-905d-86514f0d61a7.node2 0002835709 0001 0001
00000512 test 7f9208cd-90a5-4c81-8e91-d2d52f32103e.node1 0002835701 0002 0001
在两边节点分别启动一个sleep进程
1
sanlock client command -c /usr/bin/sleep 3000 &
节点1获取锁
注:58521是节点1上sleep的进程号
这里offset设置成0,如果和host_lease共用一个文件时可以指定offset,在offset后面支持指定锁的类型,默认是互斥锁,也支持共享锁(SH)
1
2
3
[root@node2 ~]# sanlock client acquire -r test:testres:/data/resource_lease:0 -p 58521
acquire pid 58521
acquire done 0
节点2获取锁失败
返回值:-243
1
2
3
4
5
6
[root@node1 ~]# ps -ef|grep sleep
root 34426 91554 0 11:05 pts/0 00:00:00 /usr/bin/sleep 3000
root 34615 91554 0 11:05 pts/0 00:00:00 grep --color=auto sleep
[root@node1 ~]# sanlock client acquire -r test:testres:/data/resource_lease:0 -p 34426
acquire pid 34426
acquire done -243
节点1手动释放锁后节点2再获取锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 节点1释放锁
[root@node1 ~]# sanlock client release -r test:testres:/data/resource_lease:0 -p 58521
release pid 58521
release done 0
# 节点2成功获取锁
[root@node2 ~]# sanlock client acquire -r test:testres:/data/resource_lease:0 -p 34426
acquire pid 34426
acquire done 0
# 锁被node2获取
[root@node2 ~]# sanlock direct dump /data/resource_lease
offset lockspace resource timestamp own gen lver
00000000 test testres 0002836575 0002 0001 5
节点2 kill sleep进程(模拟进程异常)后,节点1再获取锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# kill sleep进程后,resource_lease中own还是node2但是timestmap变成了0,锁被释放了
[root@node2 ~]# ps -ef|grep sleep
root 34426 91554 0 11:05 pts/0 00:00:00 /usr/bin/sleep 3000
root 42242 91554 0 11:14 pts/0 00:00:00 grep --color=auto sleep
[root@node2 ~]# kill -9 34426
[root@node2 ~]# sanlock direct dump /data/resource_lease
offset lockspace resource timestamp own gen lver
00000000 test testres 0000000000 0002 0001 5
# 节点1获取锁
[root@node1 ~]# ps -ef|grep sleep
root 58521 117744 0 11:03 pts/0 00:00:00 /usr/bin/sleep 3000
root 69528 117744 0 11:16 pts/0 00:00:00 grep --color=auto sleep
[root@node1 ~]# sanlock client acquire -r test:testres:/data/resource_lease:0 -p 58521
acquire pid 58521
acquire done 0
[root@node1 ~]# sanlock client status
daemon 0b8392d3-094f-42af-905d-86514f0d61a7.node1
p -1 helper
p -1 listener
p 58521
p -1 status
s test:1:/data/hosts_lease:0
s LS:2:/data/nfs/test_lockspace:0
r test:testres:/data/resource_lease:0:6 p 58521
同一个节点两个不同进程获取锁类似
参考
- https://pagure.io/sanlock
- https://cloud.tencent.com/developer/article/1651000
- https://blog.csdn.net/maokexu123/article/details/40790939