引言: python中分布式锁与群组管理系列
最近有接触到分布式锁的相关问题。
基于openstack相关组件源码, tooz官网文档和自己对组件使用的一点点心得,
想整理一下这部分的内容。
主要想分为四个部分介绍:
分布式锁与群组管理 1、 tooz介绍
分布式锁与群组管理 2、 tooz应用之负载均衡
分布式锁与群组管理 3、 tooz应用之分布式锁
分布式锁与群组管理 4、 tooz源码分析
本文是第1部分的内容
1 Tooz基础
作用:
1) 提供分布式协调API来管理群组和群组中的成员
2) 提供分布式锁从而允许分布式节点获取和释放锁来实现同步
解决的问题: 多个分布式进程同步问题
2 Tooz架构
本质: Tooz是zookeeper, Raft consensus algorithm, Redis等方案的抽象,
通过驱动(driver)形式来提供后端功能
驱动分类:
zookeeper, Zake, memcached, redis,
SysV IPC(只提供分布式锁功能), PostgreSQL(只提供分布式锁功能), MySQL(只提供分布式锁功能)
驱动特点:
所有驱动都支持分布式进程, Tooz API完全异步,更高校。
3 Tooz功能
3.1 群组管理
管理群组成员。
操作: 群组创建,加入群组,离开群组,查看群组成员,有成员加入或离开群组时通知的功能
应用场景:
ceilometer-notification服务利用群组管理实现负载均衡和真正意义上的服务水平扩展。
3.2 领导选取
每个群组都有领导,所有节点可决定是否参与选举;
领导消失则选取新领导节点;
领导被选取其他成员可能得到通知;
各节点可随时获取当前组的领导。
感悟:
考虑可以使用tooz实现自己的leader选举算法和服务高可用。
3.3 分布式锁
应用场景:
原来ceilometer中通过RPC检测alarm evaluator进程是否存活。
后来ceilometer通过Tooz群组管理来协调多个alarm evaluator进程。
应用场景2:
gnocchi中利用分布式锁操作监控项与监控数据
4 在你的应用中使用Tooz
4.1 创建一个Coordinator
tooz提供的最基本的对象就是coordinator。它允许你使用不同的特性,例如
组成员,leader选举或者分布式锁。
tooz coordinator提供的特性被不同的驱动实现了。当创建一个coordinator,你需要指定
它使用的是哪一个后台驱动。不同的驱动可能提供不同的能力。
如果一个驱动没有实现一个特性,它将会抛出一个NotImplemented异常。
这个样例项目加载了使用ZooKeeper作为驱动的基本coordinator。
from tooz import coordination
coordinator = coordination.get_coordinator('zake://', b'host-1')
coordinator.start()
coordinator.stop()
传递给coordinator的第二个参数必须是一个唯一的标识符来识别运行的程序。
在coordinator被创建后,它可以被用来使用提供的各种特性。
为了保持连接到coordination的server是存活的,方法heartbeat()
必须被周期性调用。这将确保某个coordinator不会被参与到coordination中
的其他程序认为它已经宕机。除非你想要自己手动调用它,你可以通过传递
start_heart参数来使用tooz内置的heartbeat管理器。
from tooz import coordination
coordinator = coordination.get_coordinator('zake://', b'host-1')
coordinator.start(start_heart=True)
coordinator.stop()
heartbeat在不同时刻或者间隔。
注意某些驱动,例如memcached很大成都上是基于timeout的,因此用于运行
heartbeat的interval是重要的。
4.2 组成员
4.2.1 基本操作
coordinator提供的一个特性就是管理组成员。一旦一个成员被创建了,
任何coordinator可以加入到group并变成该组的一个成员。任何
coordinator可以在一个成员加入或者离开组的时候被通知。
import uuid
import six
from tooz import coordination
coordinator = coordination.get_coordination('zake://', b'host-1')
coordinationr.start()
# create a group
group = six.binary_type(six.text_type(uuid.uuid4()).encode('ascii'))
request = coordinator.create_group(group)
request.get()
# join a group
request = coordinator.join_group(group)
request.get()
coordinator.stop()
注意所有的操作都是异步的。这意味这你不能确保在你调用
tooz.coordination.CoordAsyncResult.get()方法的时候,你的组已经创建了或者加入了。
你也可以使用tooz.coordination.CoordinationDriver.leave_group()方法来离开一个组。
可以通过tooz.coordination.CoordinationDriver.get_groups()方法被查询所有可以获得的组。
4.2.2 监视组的改变
可以在组中成员发生变化的时候监视到和被通知到。
这在组中一些事情发生后可以运行一些回调方法。
import uuid
import six
from tooz import coordination
coordinator = coordination.get_coordinator('zake://', b'host-1')
coordinator.start()
# create a group
group = six.binary_type(six.text_type(uuid.uuid4()).encode('ascii'))
request = coordinator.create_group(group)
request.get()
def group_joined(event):
# event is an instance of tooz.coordination.MemberJoinedGroup
print "group id: {groupId}, member id: {memberId}".format(groupId=event.group_id, memberId=event.member_id)
coordinator.watch_join_group(group, group_joined)
coordinator.stop()
使用tooz.coordination.CoordinationDriver.watch_join_group()和
tooz.coordination.CoordinationDriver.watch_leave_group(),你的应用可以在每次有成员加入组或者
离开组的时候被通知到。为了监视一个事件,两个方法:
tooz.coordination.CoordinationDriver.watch_join_group()和
tooz.coordination.CoordinationDriver.watch_leave_group()允许注销一个特定的回调。
参考:
https://docs.openstack.org/tooz/latest/user/tutorial/group_membership.html
5 leader选举
每个组可以选出自己的leader。一个组中在一个时间段只能有一个leader。
只有那些运行的成员才可以在选举中被选举。一旦leader挂了,一个正在运行的新成
员在选举中会被选举。
import time
import uuid
import six
from tooz import coordination
ALIVE_TIME = 1
coordinator = coordination.get_coordinator('zake://', b'host-1')
coordinator.start()
group = six.binary_type(six.text_type(uuid.uuid4()).encode('ascii'))
request = coordinator.create_group(group)
request.get()
request = coordinator.join_group(group)
request.get()
def beLeader(event):
print "group id: {groupId}, member id: {memberId}".format(groupId=event.group_id, memerId=event.member_id)
coordinator.watch_elected_as_leader(group, beLeader)
start = time.time()
while time.time() - start < ALIVE_TIME:
coordinator.heartbeat()
coordinator.run_watchers()
time.sleep(0.1)
coordinator.stop()
方法
tooz.coordination.CoordinatonDriver.watch_elected_as_leader()允许注册
一个方法,该方法可以在成员被选举为leader的时候被回调。使用这个功能意味这
运行用于选举。成员可以通过注销所有的回调:
tooz.coordination.CoordinationDriver.unwatch_elected_as_leader()
来停止运行。
它也可以在成为leader的时候临时运行
tooz.coordination.CoordinatonDriver.stand_down_group_leader()来辞去
leader的职位。如果另一个成员正处与选举中,它可能会取而代之。
为了查找一个组中的leader,可以使用:
tooz.coordination.CoordinationDriver.get_leader()。
参考:
https://docs.openstack.org/tooz/latest/user/tutorial/leader_election.html
6 Lock
Tooz提供了分布式锁。一个锁被一个名字所标识,
并且一个锁仅仅可以被一个coordinator在一个时间获取到。
from tooz import coordination
coordinator = coordination.get_coordinator('zake://', b'host-1')
coordinator.start()
# create a lock
lock = coordination.get_lock('foobar')
with lock:
print "Do some thing which is distributed"
coordinator.stop()
方法:
tooz.coordination.CoordinationDriver.get_lock()允许创建一个用名称标识的锁。
一旦你查询到这个锁,你可以在一个上下文管理或者tooz.locking.Lock.acquire()和
tooz.locking.Lock.release()方法中获取和释放锁。
参考:
https://docs.openstack.org/tooz/latest/user/tutorial/lock.html
7 Hash ring(一致性哈希)
Tooz提供了一个一致性哈希实现。它可以被用于将objects(通过二进制键表示)映射为
一个或者多个节点。当节点列表发生改变,可以通过ring实现再平衡。
from tooz import hashring
def getHashNode():
hashringObj = hashring.HashRing({'node1', 'node2', 'node3'})
nodeForFoo = hashringObj[b'foo']
print nodeForFoo
nodes = hashringObj.get_nodes(b'foo', replicas=2)
print nodes
nodes = hashringObj.get_nodes(b'foo', replicas=2, ignore_nodes={'node2'})
print nodes
参考:
https://docs.openstack.org/tooz/latest/user/tutorial/hashring.html
https://bugzilla.redhat.com/show_bug.cgi?id=1630445
https://review.opendev.org/#/c/399022/
注意:
hashring的实现是在1.47.0中的
tooz>=1.47.0 # Apache-2.0
8 分区器Partitioner
Tooz基于它的一致性哈希实现提供了一个partitioner对象。
它可以被用于映射Python对象到一个或几个节点。这个partitioner对象自动跟踪
节点加入组和离开组,因此再平衡也被管理了。
from tooz import coordination
coordinator = coordination.get_coordinator('zake://', b'host-1')
coordinator.start()
partitioner = coordinator.join_partitioned_group("group1")
# Returns {'host-1'}
member = partitioner.members_for_object(object())
coordinator.leave_partitioned_group(partitioner)
coordinator.stop()
参考:
https://docs.openstack.org/tooz/latest/user/tutorial/partitioner.html
9 API接口
9.1 组管理相关api
watch_join_group()
unwatch_join_group()
watch_leave_group()
unwatch_leave_group()
create_group()
get_groups()
join_group()
leave_group()
delete_group()
get_members()
get_member_capabilities()
update_capabilities()
9.2 leader选举相关api
watch_elected_as_leader()
unwatch_elected_as_leader()
stand_down_group_leader()
get_leader()
9.3 分布式锁相关api
get_lock()
参考:
https://docs.openstack.org/tooz/latest/user/compatibility.html
10 源码demo
基于tooz的分布式锁、群组管理、一致性哈希使用的demo实现如下
# -*- encoding: utf-8 -*-
import bisect
import hashlib
import struct
import time
import uuid
import six
from tooz import coordination
class PartionCoordinator(object):
def __init__(self, memberId=None):
self._coordinator = None
self._groups = set()
self._memberId = memberId or str(uuid.uuid1())
self._backendUrl = None
self._group = None
def createCoordinator(self, backendUrl, member=""):
try:
self._memberId = member if member else self._memberId
self._backendUrl = backendUrl
self._coordinator = coordination.get_coordinator(backendUrl, self._memberId)
return self._coordinator
except Exception as ex:
info = "createCoordinator exception: {expe}, message: {mess}".format(
expe=ex.__class__.__name__,
mess=ex
)
print info
def startCoordinator(self, startHeart=False):
try:
self._coordinator.start(start_heart=startHeart)
info = "start coordination backend: {backendUrl} sucesssfully".format(
backendUrl=self._backendUrl
)
print info
except Exception as ex:
info = "start coordination backend: {backendUrl}, exception: {expe}, message: {mess}".format(
expe=ex.__class__.__name__,
mess=ex,
backendUrl=self._backendUrl
)
print info
def stopCoordinator(self):
try:
self._coordinator.stop()
info = "stop coordination backend: {backendUrl} sucesssfully".format(
backendUrl=self._backendUrl
)
print info
except Exception as ex:
info = "stop Ccoordination backend: {backendUrl}, exception: {expe}, message: {mess}".format(
expe=ex.__class__.__name__,
mess=ex,
backendUrl=self._backendUrl
)
print info
def createGroup(self, group=''):
group = group if group else six.binary_type(six.text_type(uuid.uuid1()).encode('ascii'))
self._group = group
try:
request = self._coordinator.create_group(group)
result = request.get()
info = "create coordination group: {group}, result: {result}, type: {resultType}".format(
result=result,
resultType=type(result),
group=group
)
print info
except Exception as ex:
info = "create coordination group: {group}, exception: {expe}, message: {mess}".format(
group=group,
expe=ex.__class__.__name__,
mess=ex
)
print info
def joinGroup(self, group):
if not group:
info = "group is empty, can not join group"
print info
return
try:
request = self._coordinator.join_group(group)
self._groups.add(group)
result = request.get()
info = "join coordination group: {group}, result: {result}, type: {resultType}".format(
result=result,
resultType=type(result),
group=group
)
print info
except Exception as ex:
info = "join coordination group: {group}, exception: {expe}, message: {mess}".format(
group=group,
expe=ex.__class__.__name__,
mess=ex
)
print info
def watchJoinGroup(self, group, callback):
try:
self._coordinator.watch_join_group(group, callback)
print "watch join coordination group: {group} successfully".format(
group=group
)
except Exception as ex:
info = "watch join coordination group: {group}, exception: {expe}, message: {mess}".format(
group=group,
expe=ex.__class__.__name__,
mess=ex
)
print info
def watchBeLeader(self, group, callback):
try:
self._coordinator.watch_elected_as_leader(group, callback)
print "watch be leader, coordination group: {group} successfully".format(
group=group
)
except Exception as ex:
info = "watch be leader, coordination group: {group}, exception: {expe}, message: {mess}".format(
group=group,
expe=ex.__class__.__name__,
mess=ex
)
print info
def runWatchers(self):
try:
self._coordinator.run_watchers()
print "run watchers successfully"
except Exception as ex:
info = "run watchers exception: {expe}, message: {mess}".format(
expe=ex.__class__.__name__,
mess=ex
)
print info
def processWithLock(self, lockName):
try:
lock = self._coordinator.get_lock(lockName)
info = "get lock name: {lockName} successfully, lock is: {lock}".format(
lockName=lockName,
lock=lock
)
print info
with lock:
print "do something which is distributed"
except Exception as ex:
info = "get lock: {lockName}, exception: {expe}, message: {mess}".format(
lockName=lockName,
expe=ex.__class__.__name__,
mess=ex
)
print info
def beLeader(self, event):
info = "########## be leader, group id: {groupId}, member id: {memberId}".format(
groupId=event.group_id,
memberId=event.member_id
)
print info
def groupJoined(self, event):
info = "########## group joined, id: {groupId}, member id: {memberId}".format(
groupId=event.group_id,
memberId=event.member_id
)
print info
def joinPartitionedGroup(self, group):
try:
partitioner = self._coordinator.join_partitioned_group(group)
member = partitioner.members_for_object(object())
info = "member: {member}, type: {memberType}".format(
member=member,
memberType=type(member)
)
print info
self._coordinator.leave_partitioned_group(partitioner)
except Exception as ex:
info = "join partitioned group: {group}, exception: {expe}, message: {mess}".format(
group=group,
expe=ex.__class__.__name__,
mess=ex
)
print info
def getMembers(self, group):
if not self._coordinator:
return [self._memberId]
while True:
request = self._coordinator.get_members(group)
try:
result = request.get()
return result
except tooz.coordination.GroupNotCreated():
self.joinGroup(group)
# TODO()
def joinGroupWithRetry(self, group):
pass
'''
作用: 抽取出当前节点上对应的对象
主要思想: 从tooz中获取出一组活跃的组成员,
然后将锁有对象映射到buckets桶中,并只返回
在我们这个bucket中的锁有对象
'''
def extractMySubset(self, group, iterable):
if not group:
return iterable
if group not in self._groups:
self.joinGroup(group)
try:
members = self.getMembers(group)
info = "members: {members}, group: {group}".format(
members=members,
group=group
)
print info
if self._memberId not in members:
info = "current member: {member} not in members: {members}, group: {group}".format(
member=self._memberId,
members=members,
group=group
)
print info
# it needs to rejoin the group
self.joinGroup(group)
if self._memberId not in members:
raise Exception(info)
# 初始化一致性哈希环
hashRing = ConsistentHashRing(members)
# 从给定的队列数组(例如: 0~9总共10个队列中)中抽取出落在当前节点上的队列编号列表
iterable = list(iterable)
results = []
for i in iterable:
expectedMember = hashRing.getNode(i)
if expectedMember == self._memberId:
results.append(i)
info = "all queue numbers: {allQueues}, located in " \
"current member: {member} are these queue " \
"numbers: {filterQueues}".format(allQueues=iterable,
member=self._memberId,
filterQueues=results)
print info
return results
except Exception as ex:
info = "extractMySubset exception: {expe}, message: {mess}".format(
expe=ex.__class__.__name__,
mess=ex
)
print info
class ConsistentHashRing(object):
'''
作用: 设置节点到其哈希值的映射
具体处理步骤:
步骤1: 遍历群组中的成员名称列表(可以认为是节点列表),对每个节点
1.1 遍历每个副本值
1.2 拼接哈希名称为: 节点名称-副本值,并计算该哈希名称的哈希值
计算哈希值的步骤具体如下:
1.2.1 先将输入数据转换为unicode字符串,并用utf-8编码为字节流
1.2.2 对字节流用hashlib.md5处理,并获取处理后的二进制摘要字符串
1.2.3 对二进制摘要字符串采用struct.unpack_from处理,设置格式为大端模式,转换为python
的整型数据,该整型数据即为输入数据对应的哈希值。
1.3 建立映射: <哈希值,节点名称>
1.3 将哈希值存入哈希值数组中
步骤2: 对哈希值数组排序
'''
def __init__(self, nodes, replicas=100):
self._ring = {}
self._sortedKeys = []
for node in nodes:
for i in range(replicas):
key = '{node}-{index}'.format(
node=node,
index=i
)
hashKey = self.getHash(key)
self._ring[hashKey] = node
self._sortedKeys.append(hashKey)
self._sortedKeys.sort()
'''
作用: 获取输入数据对应的哈希值
处理过程:
步骤1: 将数据转换为unicode字符串,然后编码为utf-8的二进制字节流
步骤2: 将该数据的二进制字节流通过hashlib.md5的digest摘要算法获取其二进制字节流摘要
步骤3: 对二进制字节流采用大端模式利用struct的unpack_from进行拆包,转换为python中的整型数据
该整型数据就为输入数据的哈希值。
整体过程:
数据->unicode字符串->编码为utf-8->md5获取其二进制摘要->拆包该二进制摘要转换为python整型数据
样例哈希值: 1984516612
'''
@staticmethod
def getHash(data):
tempData = six.text_type(data)
# print tempData
realData = decodeUnicode(tempData)
# print realData
digestResult = hashlib.md5(realData).digest()
# print digestResult
# print type(digestResult)
'''
1) > 表示大端模式,即高字节对应起始地址
例子: 0x12345678
12在高字节,大端存入后的结果如下:
12 34 56 78
----> 内存增长方向
即大端模式是符合正常思维, 网络传输默认是大端模式
2) I 表示 C语言中的unsigned int, python中的integer 或者 long
3) >I 表示按照大端模式,对二进制字节流进行拆包,得到对应python中的整型数据
ref:
https://blog.csdn.net/qq_32446743/article/details/80163845
https://www.cnblogs.com/tonychopper/archive/2010/07/23/1783501.html
'''
result = struct.unpack_from('>I', digestResult)[0]
return result
'''
作用: 获取数据数据在ring中的位置
主要思想: 获取该输入数据对应的哈希值(
md5获取数据的二进制摘要字符串,struct对二进制摘要字符串拆包得到
对应python中的整型数据);
然后用二分查找bisect查找到之前哈希值数组中当前输入数据哈希值所在的位置
'''
def getPositionInRing(self, data):
hashKey = self.getHash(data)
position = bisect.bisect(self._sortedKeys, hashKey)
result = position if position < len(self._sortedKeys) else 0
return result
'''
作用: 获取数据数据对应的哈希值(整型数据)
'''
def getNode(self, data):
if not self._ring:
return
'''
excellent: 之所以这里不用字典根据键查找到对应的节点,是因为
我们需要根据任意给定输入数据,推断出它离哪个节点的哈希值较近,
从而将该输入数据分配到这个节点上。
'''
position = self.getPositionInRing(data)
result = self._ring[self._sortedKeys[position]]
# if hashKey in self._ring:
# result = self._ring[hashKey]
# else:
# result = None
return result
def decodeUnicode(data):
if isinstance(data, dict):
temp = {}
# 通过排序
tempDict = sorted(six.iteritems(data))
print tempDict
for key, value in tempDict:
print "key: {key}, value: {value}".format(
key=key,
value=value
)
# 递归对键和递归对值处理
tempKey = decodeUnicode(key)
tempValue = decodeUnicode(value)
temp[tempKey] = tempValue
return temp
elif isinstance(data, (tuple, list)):
results = []
for value in data:
result = decodeUnicode(value)
results.append(result)
return results
elif isinstance(data, six.text_type):
result = data.encode('utf-8')
return result
elif six.PY3 and isinstance(data, six.binary_type):
result = data.decode('utf-8')
return result
else:
return data
def useCoordinator():
backendUrl = 'redis://localhost:6379/'
'''
注意: 这里使用redis作为tooz的后端,必须配置好url,并且启动redis
1 安装redis:
sudo yum install redis -y
systemctl enable redis
systemctl start redis
systemctl status redis
[root@localhost .pip]# ps -ef|grep 6379
redis 15347 1 0 16:47 ? 00:00:00 /usr/bin/redis-server 127.0.0.1:6379
发现用 pip install redis,redis启动不了
'''
member = b'node-1'
# member = str(uuid.uuid4())
coordinator = PartionCoordinator()
coordinator.createCoordinator(backendUrl, member=member)
coordinator.startCoordinator()
# group = six.binary_type(six.text_type(uuid.uuid1()).encode('ascii'))\
group = "ceilometer.notification"
coordinator.createGroup(group)
callback = coordinator.groupJoined
coordinator.watchJoinGroup(group, callback)
callback = coordinator.beLeader
coordinator.watchBeLeader(group, callback)
coordinator.joinGroup(group)
coordinator.runWatchers()
lockName = "hello"
coordinator.processWithLock(lockName)
members = coordinator.getMembers(group)
print "members: {members}, members type: {membersType}".format(
members=members,
membersType=type(members)
)
iterable = range(10)
queueNumbers = coordinator.extractMySubset(group, iterable)
# coordinator.joinPartitionedGroup(group)
coordinator.stopCoordinator()
def process():
useCoordinator()
if __name__ == "__main__":
process()
参考总结:
[1]https://docs.openstack.org/tooz/latest/
[2]https://docs.openstack.org/tooz/latest/user/tutorial/index.html
[3]https://docs.openstack.org/tooz/latest/user/tutorial/group_membership.html
[4]https://docs.openstack.org/tooz/latest/user/tutorial/leader_election.html
[5]https://docs.openstack.org/tooz/latest/user/tutorial/lock.html
[6]https://docs.openstack.org/tooz/latest/user/tutorial/hashring.html
[7]https://bugzilla.redhat.com/show_bug.cgi?id=1630445
[8]https://review.opendev.org/#/c/399022/
[9]https://docs.openstack.org/tooz/latest/user/tutorial/partitioner.html
[10]https://docs.openstack.org/tooz/latest/user/compatibility.html