pysyncobj源码剖析和raft协议理解
什么是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/