基于raft一致性算法的casbin-dispatcher

1,029 阅读7分钟

1.背景

casbin多节点之间的策略数据同步,可以通过watcher机制来实现(关于watcher的具体实现可以看我之前分享的文章:juejin.cn/post/709050… ),也可通过调度器来实现,官方更推荐的做法是通过调度器来实现。因此这里就跟大家分享一下基于raft一致性算法的casbin-Dispatcher的具体实现

2.raft一致性算法

最早的分布式一致性算法是莱斯利·兰伯特(Leslie Lamport)1990年提出的Paxos算法,但是Paxos算法是公认的最难理解的算法,这就导致普通人很难将其应用到实际的项目中。raft算法提供了和 Paxos 算法相同的功能和性能,但是它的算法结构和 Paxos 不同,使得 Raft 算法更加容易理解并且更容易应用到实际项目中。casbin-Dispatcher其实就是raft算法一种实际应用,所以在通读本篇文章之前建议先搞懂raft算法,理解了raft算法对我们理解其他中间件如redis,zooKeeper等是怎么做集群的一致性的也有帮助,比如etcd,consul就是基于raft算法。raft算法论文 原稿raft.github.io/raft.pdf ,如果英文不好的读者也可以看github上关于raft算法论文的 中文翻译github.com/maemual/raf…

3.casbin-Dispatcher

Dispatcher的作用其实就是,当集群中一个节点的策略数据发生变动时,如何通知其他节点,保证整个分布式系统的数据一致性,下面这张架构图 展示了当客户端的写请求通过nginx负载之后,打到后端节点时 整个集群 的交互流程。为便于画出简洁的示意图,这里后端服务我只画了三个节点分别为node1、node2、node3,每个节点占用三个端口,其中port1为接收业务请求的端口,port2和port3共用一个物理端口,但通过cmux实现端口复用,只是port2和port3分别接收http和rpc协议的请求,所以port2和port3也可以理解为两个独立的端口(:实际qps高的情况下整个集群至少是有几十个节点的,而且也不可能仅通过nginx做负载。raft算法要求集群最少有三个节点,为避免leader挂掉之后,选举过程中出现均分选票的情况,后台节点数最好为奇数个)。由于raft算法的强领导特性,整个集群任意时刻只会有一个leader,当follower收到写请求后会路由给leader的端口2进行处理,当leader收到写请求时也把写操作转给自身的port2端口进行处理,然后leader处理完成后再通过port3端口将自身的日志同步给集群中的其余follower(也即raft算法中的AppendEntries操作),follower完成AppendEntries操作之后,就应答给leader。最后当leader发现一半以上的follower完成了AppendEntries操作后,就会触发commit操作,通知follower将Entry-Log应用到自身(也即raft的FSM机制),在我们这个例子中就是将Entry-Log中携带的策略数据同步到casbin的Enforcer句柄中。完成这一系列操作之后,集群中所有节点的casbin策略数据就保持了最终一致,读操作就可以打到集群的任一节点上。 未命名文件.png

4.实战展示

4.1 构建raft集群

为了方便演示,这里在本地就仅启动三个节点,leader占用的业务端口为6770,follower1和follower2分别占用6780、6790,然后leader在6771端口监听节点加入集群的操作,以及监听follower节点转发的策略数据变更操作(见上图蓝色线条的指示)。先启动leader后启动两个follower。follower通过leader成功加入集群,集群成功建立 1651920190(1).png 1651920113(1).png 1651920234(1).png

4.2 集群间数据同步

4.2.1 往集群添加数据

先往leader添加策略数据,leader成功收到PUT请求,并把策略数据通过AppendEntries操作同步给了两个follower节点,如下: 1651920780(1).png 1651920864(1).png 1651920894(1).png 然后往follower1添加策略数据,follower1收到PUT操作后,将添加操作路由给leader处理,然后leader添加完后将自身数据同步到集群中的各个节点 1651921747(1).png 1651921849(1).png 1651921822(1).png

4.2.2 查询数据

先调用leader的enforcer接口,传入上述添加的策略数据,执行casbin Enforce操作,成功返回200。说明策略数据已成功通过FSM机制同步到应用层 1651922664(1).png 再调用follower2的enforcer接口,同样传入上述添加的策略数据,执行casbin Enforce操作,成功返回200。说明策略数据已成功通过leader同步到follower,并且leader commit之后follower的FSM机制被处罚,成功将raft的Entry Log数据同步到应用层,同理follower1也必定成功同步到数据的 1651922873(1).png 同样的往集群中添加数据,再调remove接口删除策略数据,然后再查询会发现集群的中各节点的状态也是一致的,这里我就不再一一演示了。这就验证了,借助raft算法,不管何种操作下来最终我们的集群都是保持了高度的一致。具体的代码demo我已放到个人github上 github.com/KDKYG/casbi… ,demo比较简单,关键是要搞懂raft算法的机制,以及结合下面的架构图搞懂demo所依赖的hraft-dispatcher。关于raft集群的机制,也可以依据这个demo做更多试验,比如启动5个节点,当我们的leader节点挂点之后,哪个节点会被选为leader?raft节点中sanpshots具体存放的是些什么数据等

5.问题

上面我们在测试环境中验证了casbin-dispatcher借助raft算法,可以保持集群中所有节点之间数据的一致性,但当我们想把casbin-dispatcher应用到线上环境会发现还有其他问题需要解决。最大的问题就是,如何在线上环境构建raft集群。在这里我们是一个一个节点的启动,我们先规定了哪个节点为leader并且通过bootstrap机制先把leader启动起来,leader启动之后才能启动follower,通过leader把follower加入集群中,并且这里我们的follower和leader所依赖的配置文件还不一样,甚至follower与follower之间都不一样。但实际的线上环境我们的服务各节点之间的配置文件肯定是一样的(线上通常是配置中心,配置热更新),并且线上假如我们通过Jenkins流水线发布的时候各节点是同时启动的。如下给大家举个简单的例子:先手动的构建具有五个节点的raft集群,leader跟之前的保持一致,新加入两个followe分别占用6761和6751端口 1651926472(1).png 现在我们手动的把leader关掉,按照raft算法,当follower在短暂的超时时间内没监听到leader的心跳时,就会认为leader挂掉了,然后开启一轮leader选举,最终占用6750端口的follower4被选为新的leader,如下所示: 1651926991(1).png 这时假设我们又想把上一任leader启动起来,并加入到集群中成为新leader的follower,这时就改动之前leader的配置文件,将joinAddress改为127.0.0.1:6751,也即当前新leader监听新follower请求的端口 image.png 以上这个操作,在测试环境我们可以这样搞。但是在线上环境当leader挂掉之后,新选出来的leader可能是几十个节点中的任意一个我们很难知道,而且当我们需要扩容的时候,也不可能改动配置,将新节点加入到集群中来。

6.解决方案

众所周知etcd也是基于raft算法的,关于etcd集群的构建方式有三种(具体请看:doczhcn.gitbook.io/etcd/index/… ),那么我们也可以参照etcd的解决方案去构建我们的线上raft集群。限于篇幅和时间有限这里我就不详细介绍了,后面有机会再写文章分享我的解决思路及代码实现吧

7.总结

总的来说基于raft一致性算法的casbin-Dispatcher,可以很好的解决casbin集群的一致性问题,但真正要把这套东西应用到线上去使用,实际还有很多问题需要考虑的。所幸的是raft已经有了相对广泛的应用了,如etcd,consul,InfluxDB、IPFS等。