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/

浙公网安备 33010602011771号