Sanlock是一个基于共享存储的分布式锁管理器,集群中的每个节点都各自运行sanlock服务,锁的状态都被写到了共享存储上,所有节点都能访问共享存储,共同维护锁的状态。
Sanlock的锁是名义锁,并非强制锁,被sanlock保护的对象,即使不上锁也能直接访问,完全由程序员自由决定sanlock的锁保护的是何种资源,以及何时上锁和释放。sanlock不适用于对性能要求高的场景,它主要用于实现主机高可用的场景,比如实现虚拟机高可用。
sanlock原理
参考资料
- 基本原理: https://www.ibm.com/developerworks/cn/linux/1404_zhouzs_sanlock
- Disk Paxos算法: https://www.microsoft.com/en-us/research/uploads/prod/2016/12/Disk-Paxos-1.pdf
- Delta Lease算法: http://publications.csail.mit.edu/lcs/pubs/pdf/MIT-LCS-TR-934.pdf
- Libvirt使用sanlock参考: https://libvirt.org/locking-sanlock.html
Sanlock使用的Disk Paxos算法和Delta Lease算法理解起来比较困难,但对于使用sanlock而言,只需要记住下面几个点即可,无需对这两个算法进行深入的研究
- 集群中的所有主机都需要挂载同一个共享存储
- 集群中的每个主机都需要一个唯一的编号,编号需要人为分配(编号范围为1到2000)
- sanlock通过对共享存储上的2个文件的读写来实现分布式锁的管理
- 其中一个文件用于管理加入到sanlock集群中的主机,确保集群中没有编号重复的主机,我们将用于管理主机的文件命名为hosts_lease
- 另外一个文件用于管理分布式锁,确保在任意时刻只能有一个进程能够得到锁,获取到对资源的操作权限。在sanlock中,一个进程只能对锁占用一段时间,超时后会自动释放,如果要继续持有锁,则必须续租(sanlock自动续租),在sanlock中称为租期,所以后续将该文件命名为resource_lease
sanlock安装
在centos7上安装sanlock
代码语言:txt复制yum install -y sanlock sanlock-python
关闭firewall和selinux
代码语言:txt复制# 关闭防火墙
systemctl stop firewalld
systemctl disable firewalld
# 关闭selinux
setenforce 0
sed -i 's/SELINUX=enforcing/SELINUX=disabled/g' /etc/selinux/config
启动sanlock和wdmd服务
代码语言:txt复制# 启动sanlock
systemctl enable sanlock
systemctl start sanlock
# 启动wdmd
systemctl enable wdmd
systemctl start wdmd
sanlock命令行使用
参考资料: https://blog.didiyun.com/index.php/2018/12/27/sanlock-kvm
通过sanlock命令行来学习sanlock的使用,此处采用两个主机进行验证,两个主机需要挂载共享存储,共享存储的搭建此处不再赘述,网上已经有很多的搭建教程,这里采用NFS来搭建共享存储,共享存储路径为/data。
sanlock初始化
在任意一个节点上执行下面的步骤即可。
创建sanlock所需要的两个文件,两个文件的大小均为1MB
代码语言:txt复制[root@sanlock01 ~]# dd if=/dev/zero bs=1048576 count=1 of=/data/hosts_lease
1 0 records in
1 0 records out
1048576 bytes (1.0 MB) copied, 0.00145441 s, 721 MB/s
[root@sanlock01 ~]# dd if=/dev/zero bs=1048576 count=1 of=/data/resource_lease
1 0 records in
1 0 records out
1048576 bytes (1.0 MB) copied, 0.00121022 s, 866 MB/s
初始化用于主机管理的文件 hosts_lease
代码语言:txt复制# 命令格式为
# sanlock direct init -s <name>:<host_id>:<path>:<offset>
# <name>: sanlock的lockspace名字,任意字符串即可
# <host_id>: 主机id,前面提到的必须唯一的主机编号,初始化时用0替代
# <path>: 主机管理文件的具体路径,这里是/data/hosts_lease
# <offset>: 偏移量,始终提供0即可
[root@sanlock01 ~]# sanlock direct init -s test:0:/data/hosts_lease:0
init done 0
[root@sanlock01 ~]# chown sanlock:sanlock /data/hosts_lease
初始化用于分布式锁管理的文件 resource_lease
代码语言:txt复制# 命令格式为
# sanlock direct init -r <name>:<res_name>:<path>:<offset>
# <name>: sanlock的lockspace名字,任意字符串即可
# <res_name>:要管理的资源名称,任意字符串即可
# <path>: 分布式锁管理文件的具体路径,这里是/data/resource_lease
# <offset>: 偏移量,始终提供0即可
[root@sanlock01 ~]# sanlock direct init -r test:testres:/data/resource_lease:0
init done 0
[root@sanlock01 ~]# chown sanlock:sanlock /data/resource_lease
将主机加入到sanlock集群中
前面提到sanlock有一个用于主机管理的文件,即我们前面创建的/data/hosts_lease,将主机加入到sanlock集群,也就是将主机写入到/data/hosts_lease文件中。
将主机1加入到sanlock集群中,我们为主机1编号为1
代码语言:txt复制# 命令格式为
# 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即可
[root@sanlock01 ~]# sanlock client add_lockspace -s test:1:/data/hosts_lease:0
add_lockspace
add_lockspace done 0
将主机2加入到sanlock集群中,我们为主机2编号为2
代码语言:txt复制[root@sanlock02 ~]# sanlock client add_lockspace -s test:2:/data/hosts_lease:0
add_lockspace
add_lockspace done 0
查看 hosts_lease 文件中记录的注册到sanlock集群的主机
代码语言:txt复制# 下面命令输出的结果中
# offset 为自动计算的偏移量,主机1的数据记录在第1个512字节的磁盘中,主机2的数据记录在第2个512字节的磁盘中
# owner 0001即对应主机sanlock01,owner 0002即对应主机sanlock02
[root@sanlock01 ~]# sanlock direct dump /data/hosts_lease
offset lockspace resource timestamp own gen lver
00000000 test 00f18893-078c-49bc-a36c-c9d780576ae9.centos-san 0000000162 0001 0001
00000512 test 8ad72230-d8fd-48eb-ba99-0c245bc39d56.centos-san 0000023015 0002 0001
注册服务
将主机加入到sanlock集群后,就可以利用sanlock来管理主机上的服务了。如果两个主机上有相同的服务,这两个主机上的服务会对共享存储上的同一个文件执行写操作,如果不对其加以控制,就会导致数据损坏,sanlock存在的目的就是限制在任意时刻,都只会有一个服务能够对共享资源进行操作。
服务(对应的就是进程)必须要注册到sanlock服务后,sanlock才能为其提供锁机制。下面采用sleep命令来模拟服务的运行。
在主机1上执行下面的命令,创建一个sleep进程,进程运行3000秒后退出,并将该进程的pid注册到sanlock中
代码语言:txt复制# 创建sleep进程,并将其注册到sanlock,从输出可以知道,pid号为11404
[root@sanlock01 ~]# sanlock client command -c /bin/sleep 3000 &
[1] 11404
...
# 查看本机上的sanlock状态
[root@sanlock01 ~]# sanlock client status
daemon 00f18893-078c-49bc-a36c-c9d780576ae9.centos-san
p -1 helper
p -1 listener
p 11404
p -1 status
s test:1:/data/hosts_lease:0
在主机2上执行下面的命令,创建一个sleep进程,进程运行3000秒后退出,并将该进程的pid注册到sanlock中
代码语言:txt复制[root@sanlock01 ~]# sanlock client command -c /bin/sleep 3000 &
[1] 4481
...
[root@sanlock02 ~]# sanlock client status
daemon 8ad72230-d8fd-48eb-ba99-0c245bc39d56.centos-san
p -1 helper
p -1 listener
p 4481
p -1 status
s test:2:/data/hosts_lease:0
获取sanlock租期(即分布式锁)
主机加入集群,并将主机上的进程注册到sanlock后,就可以通过sanlock为注册的进程分配锁了。
前面提到sanlock通过共享存储上的一个文件来管理锁,即前面创建的/data/resource_lease文件,首先看一下该文件中的内容,在任意主机上执行下面的命令即可
代码语言:txt复制# 命令显示own为0000,即此时没有主机获得锁
[root@sanlock01 ~]# sanlock direct dump /data/resource_lease
offset lockspace resource timestamp own gen lver
00000000 test testres 0000000000 0000 0000 0
在主机1上执行下面的命令尝试获取sanlock租期,如果获取租期成功,则表示此时共享资源只能由当前获得租期的进程访问,如果获取租期失败,则不能访问共享资源
代码语言:txt复制# -p参数指定进程的pid,表示为进程11404获取sanlock租期,返回状态码为0表示获取成功
[root@sanlock01 ~]# sanlock client acquire -r test:testres:/data/resource_lease:0 -p 11404
acquire pid 11404
acquire done 0
# 再次查看resource_lease中的内容,可以看到此时own为0001,且timestamp不为0,即主机1获取到了租期
[root@sanlock01 ~]# sanlock direct dump /data/resource_lease
offset lockspace resource timestamp own gen lver
00000000 test testres 0000049743 0001 0001 1
在主机2上执行下面的命令尝试获取sanlock租期,由于上一步主机1已经获取到了租期,所以主机2获取租期会失败
代码语言:txt复制# 为进程4481获取sanlock租期,返回状态码不为0,即获取租期失败
[root@sanlock02 ~]# sanlock client acquire -r test:testres:/data/resource_lease:0 -p 4481
acquire pid 4481
acquire done -243
为了让主机2能够获取到sanlock租期,需要在主机1上释放sanlock租期,在主机1上执行下面的命令执行释放操作
代码语言:txt复制 [root@sanlock01 ~]# sanlock client release -r test:testres:/data/resource_lease:0 -p 11404
release pid 11404
release done 0
# 释放后查看resource_lease文件内容,可以看到timestamp变为0,表示释放成功
[root@sanlock01 ~]# sanlock direct dump /data/resource_lease
offset lockspace resource timestamp own gen lver
00000000 test testres 0000000000 0001 0001 1
再次在主机2上尝试获取sanlock租期
代码语言:txt复制# 为进程4481获取sanlock租期,返回状态码为0,即获取租期成功
[root@sanlock02 ~]# sanlock client acquire -r test:testres:/data/resource_lease:0 -p 4481
acquire pid 4481
acquire done 0
# resource_lease文件中,own已变为0002,且timestamp不为0,表示主机2获得了运行的租期
[root@sanlock02 ~]# sanlock direct dump /data/resource_lease
offset lockspace resource timestamp own gen lver
00000000 test testres 0000074135 0002 0001 2
前面5个步骤演示了两个主机,两个主机上分别有一个进程时,进程之间获取租期的操作。下面演示一个主机上有2个进程需要竞争共享资源的情况。
在主机1上新增一个sleep进程,进程执行3000秒后退出
代码语言:txt复制# 新增一个sleep进程,加上前面创建的sleep进程,主机1上一共有2个sleep进程
[root@sanlock01 ~]# sanlock client command -c /bin/sleep 3000 &
[2] 11461
...
# 查看本机上的sanlock状态
[root@sanlock01 ~]# sanlock client status
daemon 00f18893-078c-49bc-a36c-c9d780576ae9.centos-san
p -1 helper
p -1 listener
p 11404
p 11461
p -1 status
s test:1:/data/hosts_lease:0
在主机1上,为进程11404获取租期
代码语言:txt复制[root@sanlock01 ~]# sanlock client acquire -r test:testres:/data/resource_lease:0 -p 11404
acquire pid 11404
acquire done 0
# 再次查看resource_lease中的内容,可以看到此时own为0001,且timestamp不为0,即主机1获取到了租期
[root@sanlock01 ~]# sanlock direct dump /data/resource_lease
offset lockspace resource timestamp own gen lver
00000000 test testres 0000052511 0001 0001 4
在主机1上,为进程11461获取租期,由于前面进程11404已获取到租期,所以进程11461获取租期会失败
代码语言:txt复制[root@sanlock01 ~]# sanlock client acquire -r test:testres:/data/resource_lease:0 -p 11461
acquire pid 11461
acquire done -17
在主机1上,释放进程11404获取到的租期
代码语言:txt复制[root@sanlock01 ~]# sanlock client release -r test:testres:/data/resource_lease:0 -p 11404
release pid 11404
release done 0
# 释放后查看resource_lease文件内容,可以看到timestamp变为0,表示释放成功
[root@sanlock01 ~]# sanlock direct dump /data/resource_lease
offset lockspace resource timestamp own gen lver
00000000 test testres 0000000000 0001 0001 5
在主机1上,重新为进程11461获取租期,此时获取租期成功,说明sanlock可以管理同一个主机上多个进程的并发行为,但从resource_lease的文件内容中,无法辨别出当前是哪个进程获取到了租期,此时可以通过查询sanlock的状态知道是哪个进程获取到了租期
代码语言:txt复制[root@sanlock01 ~]# sanlock client acquire -r test:testres:/data/resource_lease:0 -p 11461
acquire pid 11461
acquire done 0
# 再次查看resource_lease中的内容,可以看到此时own为0001,且timestamp不为0,即主机1获取到了租期
[root@sanlock01 ~]# sanlock direct dump /data/resource_lease
offset lockspace resource timestamp own gen lver
00000000 test testres 0000056678 0001 0001 6
# 通过sanlock client status命令可以知道占用租期的进程pid,最后一行显示当前进程11461占用了testres资源的租期
[root@sanlock01 ~]# sanlock client status
daemon 00f18893-078c-49bc-a36c-c9d780576ae9.centos-san
p -1 helper
p -1 listener
p 11404
p 11461
p -1 status
s test:1:/data/hosts_lease:0
r test:testres:/data/resource_lease:0:6 p 11461
如果获取到租期的进程异常退出,没有正常的释放租期,会如何呢?
前面在主机1上启动了2个sleep进程,分别是11404和11461进程,此时由进程11461获取到了sanlock租期,为了演示进程异常退出,这里直接将进程11461杀死
代码语言:txt复制[root@sanlock01 ~]# kill -9 11461
[1]- Killed sanlock client command -c /bin/sleep 3000
# kill执行后,查看sanlock状态,可以看到11461进程已经不在sanlock中了,且租期已经释放
[root@sanlock01 ~]# sanlock client status
daemon 00f18893-078c-49bc-a36c-c9d780576ae9.centos-san
p -1 helper
p -1 listener
p -1 status
p 11404
s test:1:/data/hosts_lease:0
# 查看resource_lease文件,可以发现timestamp已经被设置为0,即目前没有进程获取到租期
[root@sanlock01 ~]# sanlock direct dump /data/resource_lease
offset lockspace resource timestamp own gen lver
00000000 test testres 0000000000 0001 0001 9
从上一步可知,kill掉获取到租期的进程后,sanlock会自动释放该进程占用的租期,以便其它进程可以正常获取租期
代码语言:txt复制# 11404进程获取租期成功
[root@sanlock01 ~]# sanlock client acquire -r test:testres:/data/ resource_lease:0 -p 11404
acquire pid 11404
acquire done 0
# resource_lease文件中租约设置成功
[root@sanlock01 ~]# sanlock direct dump /data/resource_lease
offset lockspace resource timestamp own gen lver
00000000 test testres 0000058712 0001 0001 10
# 从sanlock状态可以看到此时11404获取到了资源testres的租约
[root@sanlock01 ~]# sanlock client status
daemon 00f18893-078c-49bc-a36c-c9d780576ae9.centos-san
p -1 helper
p -1 listener
p -1 status
p 11404
s test:1:/data/hosts_lease:0
r test:testres:/data/resource_lease:0:10 p 11404
代码使用sanlock
前面使用sanlock命令行学习了sanlock的基本使用,主要步骤有:
准备sanlock需要的共享存储
以及主机租期文件
和资源租期文件
;
- 为主机分配一个1到2000的唯一编号,并通过该编号将主机注册到sanlock集群;
- 将要运行的服务进程注册到sanlock中;
- 服务进程获取资源租期,获取成功则可执行,否则等待一定时间,再次尝试获取资源租期;
在代码中使用sanlock的流程和前面的4个步骤是一致的,下面采用python来模拟真实的业务场景。
假设我们有一个服务需要向一个文件不停地写入随机数字,但是该服务所在主机很不稳定,随时可能崩溃。为了让我们的业务尽可能的一直运行下去,我们需要在多个服务器上都部署上我们的服务。但我们只有一个位于共享存储上的文件,多个服务进程不能够同时向该文件写入随机数,任意时刻都只能有一个服务进程在执行写操作,所以我们这里就需要使用sanlock来实现这个同步功能了。
代码语言:txt复制[root@sanlock01 data]# tree
.
├── randb
│ └── resfile # 服务进程要操作的文件
└── sanlock
├── randb_hosts_lease # sanlock用于主机管理的文件
└── randb_resource_lease # sanlock用于资源管理的文件
2 directories, 3 files
对sanlock路径下的文件进行初始化操作,我们将lockspace命名为randb,将resource_name命名为resfile(即我们的服务要操作的资源对象),初始化命令如下
代码语言:txt复制[root@sanlock01 ~]# sanlock direct init -s randb:0:/data/sanlock/randb_hosts_lease:0
init done 0
[root@sanlock01 ~]# sanlock direct init -r randb:resfile:/data/sanlock/randb_resource_lease:0
init done 0
[root@sanlock01 ~]# chown sanlock:sanlock /data/sanlock/randb_hosts_lease
[root@sanlock01 ~]# chown sanlock:sanlock /data/sanlock/randb_resource_lease
将下面的代码拷贝到主机1和主机2的/root路径下,并命名为 simpleha.py,需要修改代码中的host_id为sanlock集群中1到2000的唯一值
代码语言:txt复制# coding: utf-8
import time
import random
import socket
import sanlock
# 修改host_id为sanlock集群中1到2000之间的唯一值
host_id = 1
hosts_lease = '/data/sanlock/randb_hosts_lease'
resource_lease = '/data/sanlock/randb_resource_lease'
# 若尚未加入 sanlock 集群,则尝试获取对 host_id 的 Delta Lease
if not sanlock.inq_lockspace("randb", host_id, hosts_lease):
print "Try to acquire host id randb:%s:%s:0" % (host_id, hosts_lease)
# 并尝试加入 sanlock 集群
sanlock.add_lockspace("randb", host_id, hosts_lease)
# 把本进程注册到 sanlock 服务上
sfd = sanlock.register()
while True:
try:
# 尝试获取 Delta Lease
print "Try to acquire resfile lease randb:resfile:%s:0" % resource_lease
sanlock.acquire("randb", "resfile", [(resource_lease, 0)], sfd)
except sanlock.SanlockException:
# 无法获取资源租期, 10秒后重试
print "Failed to acquire leases, try again in 10s."
time.sleep(10)
else:
# 成功获取资源租期,不再重试
break
# 成功获取到了资源租期,开始向/data/randb/resfile写入随机数
hostname = socket.gethostname()
while True:
with open("/data/randb/resfile", "a") as randf:
randnum = random.randint(0, 100)
msg = "%s %d" % (hostname, randnum)
randf.write(msg "n")
print msg, ">> /data/randb/resfile"
time.sleep(1)
在主机1上修改 simpleha.py 中的host_id变量,设置为1,并执行该python文件
代码语言:txt复制[root@sanlock01 ~]# python simpleha.py
Try to acquire host id randb:1:/data/sanlock/randb_hosts_lease:0
Try to acquire resfile lease randb:resfile:/data/sanlock/randb_resource_lease:0
sanlock01.novalocal 48 >> /data/randb/resfile
sanlock01.novalocal 61 >> /data/randb/resfile
sanlock01.novalocal 84 >> /data/randb/resfile
...
# 查看randb_hosts_lease,主机1已加入sanlock集群
[root@sanlock01 ~]# sanlock direct dump /data/sanlock/randb_hosts_lease
offset lockspace resource timestamp own gen lver
00000000 randb 00f18893-078c-49bc-a36c-c9d780576ae9.centos-san 0000171028 0001 0001
# 查看randb_resource_lease,主机1已获取到对资源resfile的租期
[root@sanlock01 ~]# sanlock direct dump /data/sanlock/randb_resource_lease
offset lockspace resource timestamp own gen lver
00000000 randb resfile 0000170665 0001 0001 4
# sanlock状态显示当前获得资源resfile租期的进程号为12999
[root@sanlock01 ~]# sanlock client status
daemon 00f18893-078c-49bc-a36c-c9d780576ae9.centos-san
p -1 helper
p -1 listener
p 12999
p -1 status
s randb:1:/data/sanlock/randb_hosts_lease:0
r randb:resfile:/data/sanlock/randb_resource_lease:0:4 p 12999
在主机2上修改 simpleha.py 中的host_id变量,设置为2,并执行该python文件
代码语言:txt复制# 从输出可以看出,主机2上的simpleha.py获取资源resfile的租期失败,每隔10秒重试一次
[root@sanlock02 ~]# python simpleha.py
Try to acquire host id randb:2:/data/sanlock/randb_hosts_lease:0
Try to acquire resfile lease randb:resfile:/data/sanlock/randb_resource_lease:0
Failed to acquire leases, try again in 10s.
Try to acquire resfile lease randb:resfile:/data/sanlock/randb_resource_lease:0
Failed to acquire leases, try again in 10s.
...
# 通过randb_hosts_lease文件可以主机2已经加入到了sanlock集群中
[root@sanlock02 ~]# sanlock direct dump /data/sanlock/randb_hosts_lease
offset lockspace resource timestamp own gen lver
00000000 randb 00f18893-078c-49bc-a36c-c9d780576ae9.centos-san 0000171438 0001 0001
00000512 randb 8ad72230-d8fd-48eb-ba99-0c245bc39d56.centos-san 0000194296 0002 0001
# 查看sanlock状态可以看到,simpleha服务已经注册到sanlock中了
[root@sanlock02 ~]# sanlock client status
daemon 8ad72230-d8fd-48eb-ba99-0c245bc39d56.centos-san
p -1 helper
p -1 listener
p 5555
p -1 status
s randb:2:/data/sanlock/randb_hosts_lease:0
通过直接kill主机1上的simpleha.py进程12999,模拟主机1服务故障。将主机1上的simpleha.py进程kill后,主机2上的服务进程很快就进入了写文件的流程,可见由sanlock控制的HA服务是有效的
代码语言:txt复制[root@sanlock01 ~]# kill -9 12999
[root@sanlock02 ~]# python simpleha.py
...
Try to acquire resfile lease randb:resfile:/data/sanlock/randb_resource_lease:0
Failed to acquire leases, try again in 10s.
Try to acquire resfile lease randb:resfile:/data/sanlock/randb_resource_lease:0
sanlock02.novalocal 34 >> /data/randb/resfile
sanlock02.novalocal 75 >> /data/randb/resfile
sanlock02.novalocal 14 >> /data/randb/resfile
...
# 主机2获取到了对资源resfile的租期
[root@sanlock02 ~]# sanlock direct dump /data/sanlock/randb_resource_lease
offset lockspace resource timestamp own gen lver
00000000 randb resfile 0000194546 0002 0001 5
# 服务进程5555已获取到资源resfile的租期
[root@sanlock02 ~]# sanlock client status
daemon 8ad72230-d8fd-48eb-ba99-0c245bc39d56.centos-san
p -1 helper
p -1 listener
p 5555
p -1 status
s randb:2:/data/sanlock/randb_hosts_lease:0
r randb:resfile:/data/sanlock/randb_resource_lease:0:5 p 5555