pysyncobj源码剖析和raft协议理解

发布时间 2023-12-31 00:02:24作者: zhang--yd

 

什么是PySyncObj 

源代码地址:https://github.com/bakwc/PySyncObj

PySyncObj是一个python库,可以辅助去搭建一个可容错的分布式系统,通过复制备份你的应用数据在多个服务器上来达到。

实现的功能:基于raft协议的leader选举和日志复制;日志的压缩和落盘;动态成员变动支持;内存和主存的数据序列化存储。

 

为什么需要raft
分布式需要一致性,任意一个节点都可以挂掉,需要做到去中心化
安全性: 网络波动,网络延迟,丢包,乱序等问题
高可用:只要集群中的服务器超过半数是可用的,系统就是可用的
不影响时序保证日志的一致性

 

raft是怎么做到的
一致性算法,是在复制状态机的原理来实现的,核心有两个部分:leader选举日志复制
复制状态机通过复制日志来实现的,每个节点都会存储一份日志,日志存储的是一系列命令。节点的状态机会按照顺序执行这些日志中的命令,从而实现多个节点的状态同步

 

leader选举过程
1,节点有三种状态:follower状态,candidate状态,leader状态
class _RAFT_STATE:
    FOLLOWER = 0
    CANDIDATE = 1
    LEADER = 2

  

2,在协议启动的开始,所有的节点都是follower状态,follower状态的节点存在一个选举超时时间,
如 syncobj.py 的 SyncObj 类的 _onTick 函数所示 ,__raftElectionDeadline 过了超时时间,则认为是集群失去了leader
则自我申请成为candidate节点,增加一个term,进入下一个select周期,向其他节点申请本节点成为leader
        if self.__raftState in (_RAFT_STATE.FOLLOWER, _RAFT_STATE.CANDIDATE) and self.__selfNode is not None:
            if self.__raftElectionDeadline < monotonicTime() and self.__connectedToAnyone():
                self.__raftElectionDeadline = monotonicTime() + self.__generateRaftTimeout()
                self.__raftLeader = None
                self.__setState(_RAFT_STATE.CANDIDATE)
                self.__raftCurrentTerm += 1
                self.__votedForNodeId = self.__selfNode.id
                self.__votesCount = 1
                self.__onLeaderChanged()

  

3 当前节点如果在follower或者candidate状态,如果获得了超过半数的投票,则可以直接成为leader状态
开始向其他节点同步消息 (见 函数 __sendAppendEntries 的内容 )
   def __onBecomeLeader(self):
        self.__raftLeader = self.__selfNode
        self.__setState(_RAFT_STATE.LEADER)
        self.__lastResponseTime.clear()

        # No-op command after leader election.
        idx, term = self.__getCurrentLogIndex() + 1, self.__raftCurrentTerm
        self.__raftLog.add(_bchr(_COMMAND_TYPE.NO_OP), idx, term)
        self.__noopIDx = idx
        if not self.__conf.appendEntriesUseBatch:
            self.__sendAppendEntries()

        self.__sendAppendEntries()

  

4 当前节点在leader状态,会定期向所有的follower发送消息
        if self.__raftState == _RAFT_STATE.LEADER:
            if monotonicTime() > self.__newAppendEntriesTime or needSendAppendEntries:
                self.__sendAppendEntries()

  

5 当前节点在leader状态,如果收到了其他follower节点的下一任term的选举消息,说明当前节点已经失去了leader状态了,因为其没能规定时间内让follower节点都稳定下来
        if self.__raftState == _RAFT_STATE.LEADER:
            commitIdx = self.__raftCommitIndex
            nextCommitIdx = self.__raftCommitIndex
            deadline = monotonicTime() - self.__conf.leaderFallbackTimeout
            count = 1
            for node in self.__otherNodes:
                if self.__lastResponseTime[node] > deadline:
                    count += 1
            if count <= (len(self.__otherNodes) + 1) / 2:
                self.__setState(_RAFT_STATE.FOLLOWER)
                self.__raftLeader = None

  

 

日志复制
强leader机制的要求是日志只能由leader复制到其他follower节点
 
日志的成员如下,记录所有的日志,带有持久化到磁盘的功能,启动的时候,会加入一个初始的无操作的日志
      self.__raftLog = createJournal(self.__conf.journalFile)
        if len(self.__raftLog) == 0:
            self.__raftLog.add(_bchr(_COMMAND_TYPE.NO_OP), 1, self.__raftCurrentTerm)

  

每次add日志的接口如下, 日志项包括index日志索引,term选举任期,command具体的日志命令三个元素
    def add(self, command, idx, term):
        self.__journal.append((command, idx, term))
        cmdData = struct.pack('<QQ', idx, term) + to_bytes(command)
        cmdLenData = struct.pack('<I', len(cmdData))
        cmdData = cmdLenData + cmdData + cmdLenData
        self.__journalFile.write(self.__currentOffset, cmdData)
        self.__currentOffset += len(cmdData)
        self.__setLastRecordOffset(self.__currentOffset)

  

其中日志的命令有四类,如下
class _COMMAND_TYPE:
    REGULAR = 0         # 常规的业务命令
    NO_OP = 1           # 无操作
    MEMBERSHIP = 2      # 有节点加入或者退出
    VERSION = 3         # 代码版本号

  

每次外来的压入命令会经过缓存队列,在每次tick的时候从队列中拿到命令进行执行和存盘,所以压入的命令不一定能够得到保障
    def _applyCommand(self, command, callback, commandType = None):
        try:
            if commandType is None:
                self.__commandsQueue.put_nowait((command, callback))
            else:
                self.__commandsQueue.put_nowait((_bchr(commandType) + command, callback))
            if not self.__conf.appendEntriesUseBatch and PIPE_NOTIFIER_ENABLED:
                self.__pipeNotifier.notify()
        except Queue.Full:
            self.__callErrCallback(FAIL_REASON.QUEUE_FULL, callback)

  

每帧都是进行日志的序列号存盘检查,如果是成功序列化存盘了,则会删除raftlog队列中已经落盘的内容
    def __tryLogCompaction(self):
        currTime = monotonicTime()
        serializeState, serializeID = self.__serializer.checkSerializing()

        if serializeState == SERIALIZER_STATE.SUCCESS:
            self.__lastSerializedTime = currTime
            self.__deleteEntriesTo(serializeID)
            self.__lastSerializedEntry = serializeID

  

从消息收发来看下该架构的通信设计
通过message的type来区分不同类型的消息
1 request_vote 是 失联的节点进入candidate状态后广播的消息,请求本节点成为leader状态
如果其他节点收到此类消息,且该消息的term比当前节点的term更高的时候,该节点退化为follower节点,向发来的节点承认其leader的response_vote消息
2 response_vote 如果本节点为candidate,则收集被承认本节点的数量,超过半数则成为leader状态
3 append_entries 收到了来自leader节点的日志增长的消息。本节点也进行存储新的日志
4 next_node_idx 只有leader状态节点才会收到
5 apply_command 收到到了来自leader节点的执行命令的消息,表示日志被半数以上节点同步,在本节点进行执行操作
6 apply_command_response 由leader节点对follower节点回复
 
    def __onMessageReceived(self, node, message):
        pass

  

 

Consumer是消费者,raftcluster会添加consumer,对consumer中的某些方法进行一致性同步操作,等满足了raft协议的要求之后再执行
class SyncObjConsumer(object):
    def __init__(self):
        self._syncObj = None
        self.__properies = set()
        for key in self.__dict__:
            self.__properies.add(key)

    def _destroy(self):
        self._syncObj = None

    def _serialize(self):
        return dict([(k, v) for k, v in iteritems(self.__dict__) if k not in self.__properies])

    def _deserialize(self, data):
        for k, v in iteritems(data):
            self.__dict__[k] = v

  

replicated装饰器,被replicated修饰过的函数,会被注册进入集群,当被调用的时候,需要先进行一致性同步操作之后,才会去真正调用该函数的内容
def replicated(*decArgs, **decKwargs):
    """Replicated decorator. Use it to mark your class members that modifies
    a class state. Function will be called asynchronously. Function accepts
    flowing additional parameters (optional):
        'callback': callback(result, failReason), failReason - `FAIL_REASON <#pysyncobj.FAIL_REASON>`_.
        'sync': True - to block execution and wait for result, False - async call. If callback is passed,
            'sync' option is ignored.
        'timeout': if 'sync' is enabled, and no result is available for 'timeout' seconds -
            SyncObjException will be raised.
    These parameters are reserved and should not be used in kwargs of your replicated method.

    :param func: arbitrary class member
    :type func: function
    :param ver: (optional) - code version (for zero deployment)
    :type ver: int
    """
    def replicatedImpl(func):
        def newFunc(self, *args, **kwargs):

            if kwargs.pop('_doApply', False):
                return func(self, *args, **kwargs)
            else:
                if isinstance(self, SyncObj):
                    applier = self._applyCommand
                    funcName = self._getFuncName(func.__name__)
                    funcID = self._methodToID[funcName]
                elif isinstance(self, SyncObjConsumer):
                    consumerId = id(self)
                    funcName = self._syncObj._getFuncName((consumerId, func.__name__))
                    funcID = self._syncObj._methodToID[(consumerId, funcName)]
                    applier = self._syncObj._applyCommand
                else:
                    raise SyncObjException("Class should be inherited from SyncObj or SyncObjConsumer")

                callback = kwargs.pop('callback', None)
                if kwargs:
                    cmd = (funcID, args, kwargs)
                elif args and not kwargs:
                    cmd = (funcID, args)
                else:
                    cmd = funcID
                sync = kwargs.pop('sync', False)
                if callback is not None:
                    sync = False

                if sync:
                    asyncResult = AsyncResult()
                    callback = asyncResult.onResult

                timeout = kwargs.pop('timeout', None)
                applier(pickle.dumps(cmd), callback, _COMMAND_TYPE.REGULAR)

                if sync:
                    res = asyncResult.event.wait(timeout)
                    if not res:
                        raise SyncObjException('Timeout')
                    if not asyncResult.error == 0:
                        raise SyncObjException(asyncResult.error)
                    return asyncResult.result

        func_dict = newFunc.__dict__ if is_py3 else newFunc.func_dict
        func_dict['replicated'] = True
        func_dict['ver'] = int(decKwargs.get('ver', 0))
        func_dict['origName'] = func.__name__

        callframe = sys._getframe(1 if decKwargs else 2)
        namespace = callframe.f_locals
        newFuncName = func.__name__ + '_v' + str(func_dict['ver'])
        namespace[newFuncName] = __copy_func(newFunc, newFuncName)
        functools.update_wrapper(newFunc, func)
        return newFunc

  

其他的数据结构

网络接口层面,本项目用的是TCP协议,其中的有多个数据结构来封装该功能

tcp connection 负责tcp连接的建立和socket的管理
tcp server 负责 tcp socket 和 连接池的桥梁
 
 
在往磁盘序列化和存储数据的过程中,其中负责的模块是 class Serializer(object)。
存储方式可以分为同步和异步的方式,同步即是在同一个进程内进行存储操作,因为Python的GIL,所以存储的IO过程会阻塞整个进程
也可以通过fork的方式,创建一个新的进程来完成存储功能,和原进程才有的管道通知,从一个进程向另一个进程发通知,其中负责的模块是 class PipeNotifier(object)
 
 

 

参考内容

raft的官方论文 https://raft.github.io/raft.pdf
raft协议的演示 https://thesecretlivesofdata.com/raft/
知乎上的介绍文章https://zhuanlan.zhihu.com/p/32052223