分布式系统之Raft共识原理解析

203 阅读4分钟

我正在参与掘金创作者训练营第4期,点击了解活动详情,一起学习吧!

业务系统复杂度的增长

  1. 我们常见的单体结构的集中式系统,一般整个项目就是一个独立的应用,所有的模块都聚合在一起。
  2. 稍微复杂一点的系统,可能拆分了不同的模块,一个项目分了多个独立应用,但是还是有统一的数据库。
  3. 再复杂一点的系统,可能由于业务或者由于数量过大做了,拆分了很多系统,又做了分库分表。
  4. 更复杂的系统,还做了区域机房,异地多活

系统复杂了最明显的变化是什么?

是机器变得越来越多,服务越来越多。节点多了故障的概率就会显著增加,为了保证服务的高可用,我门又要部署更多的节点来提高容错。 相同的节点多了,如果他们没有相同的数据库或者说持久化对象,数据的一致性是分布式系统的最大挑战。

CAP理论

CAP 定理(也称为 Brewer 定理),指的是在分布式计算环境下,有3个核心的需求:

  1. 一致性(Consistency):再分布,所有实例节点同一时间看到是相同的数据。
  2. 可用性(Availability):不管是否成功,确保每一个请求都能接收到响应。
  3. 分区容错性(Partition Tolerance):系统任意分区后,在网络故障时,仍能操作。

CAP理论告诉我们,分布式系统不可能同时满足以下三种。最多只能同时满足其中的两项,因为很多时候P是必须的, 因此往往选择就在CP或者AP中

  1. AP:放弃强一致性。追求最终一致性,类似的场景比如转账,可以接受两小时后到账,Eureka的注册也是类似的做法。
  2. CP:放弃可用性。zookeeper在leader宕机后,选举期间是不提供服务的。类似的场景比如支付完成之后出订单,必须一进一出都完成才行。

重点来了

分布式系统最关键的一致性问题,无论你是 强一致性弱一致性,还是最终一致性,如何让分布式系统达成系统间的一致性呢?

嗯,好吧!用通俗的方式来解决这个问题,就是:首先我们要选出一个负责人来管理所有的分布式系统,然后在这些分布式系统中同步数据。

完美,现在就差把人找到,在让他来对比分布式系统中的各个系统中的数据再调度这些数据完成各个系统间的数据复制同步。

1.共识算法Raft

找系统负责人,其实就是让所有系统达成共识找到一个Leader,大家都听Leader的就行了。共算法有很多,这里介绍比较容易理解的一种Raft算法。

  1. 选举Leader

    Raft的选举很简单,就是我要当Leader然后找所有节点拉票,票数过半就当选Leader。有个问题,大家一起选,大家都拿不到选票咋办? 简单选举的时候大家错开时间就行了。

vote.jpg B要当Leader->向A和C拉票->A和C给B投票->B当选Leader

  1. raft核心源代码
# raft 状态机核心逻辑
func (n *node) run(r *raft) {
	for {
		select {
		case m := <-n.recvc:
			// filter out response message from unknown From. 
			if pr := r.getProgress(m.From); pr != nil || !IsResponseMsg(m.Type) {
				r.Step(m) // raft never returns an error
			}
		case <-n.tickc:
			r.tick() 
		case <-n.stop:
			close(n.done)
			return
		}
	}
}

  // tickElection is run by followers and candidates after r.electionTimeout.
func (r *raft) tickElection() {
	r.electionElapsed++

	if r.promotable() && r.pastElectionTimeout() {
		r.electionElapsed = 0
		r.Step(pb.Message{From: r.id, Type: pb.MsgHup})
	}
}
  // 状态机应答响应
func (r *raft) Step(m pb.Message) error {
	// ...
	switch m.Type {
	case pb.MsgHup:
		if r.state != StateLeader {
			ents, err := r.raftLog.slice(r.raftLog.applied+1, r.raftLog.committed+1, noLimit)
			if err != nil {
				r.logger.Panicf("unexpected error getting unapplied entries (%v)", err)
			}
			if n := numOfPendingConf(ents); n != 0 && r.raftLog.committed > r.raftLog.applied {
				r.logger.Warningf("%x cannot campaign at term %d since there are still %d pending configuration changes to apply", r.id, r.Term, n)
				return nil
			}

			r.logger.Infof("%x is starting a new election at term %d", r.id, r.Term)
			if r.preVote {
				r.campaign(campaignPreElection)
			} else {
				r.campaign(campaignElection)
			}
		} else {
			r.logger.Debugf("%x ignoring MsgHup because already leader", r.id)
		}

                 // ...
	default:
		r.step(r, m)
	}
	return nil
}

2.数据同步

  1. 数据写入leader主写

    有了leader之后数据的变更就可由leader来负责调度,A,B,C上任意节点发生数据变化时都要首先告知leader,leader会把变化通知所有节点,这个过程中不需要同步数据,只要 同步数据指令即可,告知每个节点数据在哪里从哪里同步即可,之后不管是通过binlog还是队列都可以完成数据同步做到数据的一致性。

follow.jpg

写在最后

  1. raft算法动画 这个一定要看,里面通过动画介绍了raft的选举和log同步。

参考资料

  1. 30种共识算法
  2. ETCD背后的raft算法
  3. raft算法动画
  4. raft golang源码
  5. raft关键源码分析