简单共识选主实现自定义分布式锁
一些任务只需要一个实例执行,由于高可用要求,需要多台实例。那么多实例通信就成问题,而一些情况下环境比较苛刻,没有组件可以借用,简直为难老实人。
共识算法
有多个实例想要达成共识,那么可分为两个阵营:拜占庭将军问题和非拜占庭将军问题。由于咱们环境比较苛刻,换句话就是都可信,那么就是非拜占庭节点,降低思考难度。接着常见的分布式一致性协议有:
- Paxos
- Bully
- Raft
- Zab
- Gossip
这里 Paxos(难实现),Zab(不如Raft通用),这样又可以缩减下分析目标。接着考虑Gossip,实际上很快,不过理论上可以暂时不一致,所以也移除。分析下bully的选举:长者为尊,leader挂了,节点通知长者进行选举,没回应轮到自己。听起来就简单霸道易实现,缺点也能感觉到,长者加入退出都要触发选举,速度也有点慢。再看看历史,Mongo以及ES早期都是用Bully,后期都转成了类Raft,基本筛选完毕。
Raft简介
Raft算法选主中集群各个节点的角色,一共有3中角色:
- Leader: 为主节点,同一时刻只有一个Leader节点,负责整个集群的节点间的协调和管理。
- Candidate: 候选节点,只有角色为候选者的节点才可以被选为新的Leader,每个节点 都有可以成为候选者。
- Follower: Leader的跟随者,这个角色的时候不可以发起选主。
选举流程:
- 初始化时,各个节点均为Follower状态。
- 开始选主时,所有节点的Follower状态转为Candidate状态,并向其他节点发送选主请求。
- 其他节点根据收到的选主请求的先后顺序,进行回复是否同意其成为主节点;每个节点只能投一张票。
- 发起选主的节点如果得到一半以上的投票,则会成为主节点,状态变为Leader,其他几点则会由Candidate转为Follower状态,此时Leader和Follower将保持心跳检测。
- 如果Leader节点的任期到了,Leader则会降为Follower,进行新一轮选主。
总结与剪裁
咱们所有节点每个都是平等的,不存在状态问题,因为任何时候任何节点都时可以当选主;那么Raft中需要多数人的投票就可以参考bully算法,通过任期与自身ID判定,且任期永远有效。就这么直接,那么步骤3的回复也可以省略,消息结构体也只需要一种,结构如下:
- Term 节点毛遂自荐时候的时间戳,当做任期
- Current 节点发送心跳时候的时间
- Id 节点ID
选举流程:
- 初始化时,各个节点均为Follower状态。
- 开始选主时,所有节点的Follower状态转为Candidate状态,并向其他节点发送自身心跳。
- 其他节点收到心跳,对比自己心跳,Term比自身小,或者相同是ID比自身大,则节点降为Follower,不在发送心跳。
- 当Candidate持续心跳有效期时间N内未收到其他节点的心跳,则晋升为Leader,周期发送心跳。
新节点加入:
- 节点加入一个检测周期后,收到有效心跳,则沉默为Follower,否则进入Candidate。
- 参考初始选举步骤3,新加入任期一定更大,所以不会有波动。
Leader挂了:
- Follower节点经过多个检测周期,直到有效期N失效后,各自进入Candidate,开始互发心跳。
- 参考初始选举步骤3。
还有其他场景,咱们也理理:非Leader节点挂了不影响;Leader假死则回归那就是上任归来,现任让位;如果脑裂回归,那也是直接PK,谁老谁连任。这样简单的选择,大部分场景都满足了。
代码实现
RPC选择
心跳带有有效期,因此过期的没意义,且Leader会一直发,则丢了一两个也行。总结下咱们采用UDP,逻辑更简单,效率更高。第二个问题就是Socket Recv阻塞,有两个简单方法:丢个回调函数,起个线程recv然后callback;第二个来个Queue,起个线程recv然后压入。资源都是一个线程,不过Queue的话,框架只是数据读取不处理,逻辑更清晰,所以咱们选择后者。
import marshal
import socket
import threading
class PeerSocket(object):
def __init__(self, endpoint, queue):
self.endpoint = endpoint
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.queue = queue
self.start()
def listen(self):
self.sock.bind(self.endpoint)
while True:
content = self.sock.recv(256)
data = marshal.loads(content)
self.queue.put(data)
def send(self, data, addr):
content = marshal.dumps(data)
self.sock.sendto(content, addr)
def start(self):
task = threading.Thread(target=self.listen)
task.setDaemon(True)
task.start()
接着咱们设计需要共享的心跳以及状态:
def reset(self):
self.heartbeat = {
"term": float('inf'),
"current_ts": float('-inf'),
'id': -255
}
self.status = Role.follower.value
基于流程,咱们合并所有操作,得出主逻辑如下:
def watch(self):
while True:
with self.lock:
if self.status == Role.leader.value:
self.send_followers()
elif self.status == Role.candidate.value:
self.recv_leader()
self.send_followers()
else:
self.recv_leader()
self.clean()
time.sleep(0.5)
咱们也可以换个写法,概括一下就是(通常在不影响性能的情况下,咱们更喜欢直白的写法,后期回看好看懂):
- 当角色不是Follower,则需要发送心跳
- 当角色不是Leader,则需要接收心跳
- 每个检测周期,都是检测心跳是否过期,更新自身状态
这样花费2个线程,少量代码,老实人也能在苛刻环境下实现节点高可用了。源代码不多,参考地址: https://github.com/maycap/simpleleader