python 64式: 第26式、分布式锁与群组管理__1、 tooz介绍

引言: 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
 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值