背景
raft算法原理,建议参考raft官网:https://raft.github.io/
强烈推荐观看raft的流程动画,方便直观理解算法过程
动画地址:http://thesecretlivesofdata.com/raft/
本系列通过阅读SofaJRaft源码,并在本地运行SofaJRaft自带的Counter演示程序,学习了解raft算法在工程中的具体实现。
SofaJRaft源码地址:https://github.com/sofastack/sofa-jraft
对应的文档地址:https://www.sofastack.tech/projects/sofa-jraft/jraft-user-guide/
本项目阅读和演示的代码版本:
<dependencies>
<!-- jraft -->
<dependency>
<groupId>com.alipay.sofa</groupId>
<artifactId>jraft-core</artifactId>
<version>1.3.9</version>
</dependency>
<dependency>
<groupId>com.alipay.sofa</groupId>
<artifactId>jraft-rheakv-core</artifactId>
<version>1.3.9</version>
</dependency>
<!-- jsr305 -->
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
<version>3.0.2</version>
</dependency>
<!-- bolt -->
<dependency>
<groupId>com.alipay.sofa</groupId>
<artifactId>bolt</artifactId>
<version>1.6.4</version>
</dependency>
<dependency>
<groupId>com.alipay.sofa</groupId>
<artifactId>hessian</artifactId>
<version>3.3.6</version>
</dependency>
<!-- log -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.21</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.21</version>
</dependency>
<!-- disruptor -->
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>3.3.7</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.8.0</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
<!-- protobuf -->
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.5.1</version>
</dependency>
<!-- protostuff -->
<dependency>
<groupId>io.protostuff</groupId>
<artifactId>protostuff-core</artifactId>
<version>1.6.0</version>
</dependency>
<dependency>
<groupId>io.protostuff</groupId>
<artifactId>protostuff-runtime</artifactId>
<version>1.6.0</version>
</dependency>
<!-- rocksdb -->
<dependency>
<groupId>org.rocksdb</groupId>
<artifactId>rocksdbjni</artifactId>
<version>6.22.1.1</version>
</dependency>
<!-- java thread affinity -->
<dependency>
<groupId>net.openhft</groupId>
<artifactId>affinity</artifactId>
<version>3.1.7</version>
</dependency>
<!-- metrics -->
<dependency>
<groupId>io.dropwizard.metrics</groupId>
<artifactId>metrics-core</artifactId>
<version>4.0.2</version>
</dependency>
</dependencies>
选主
通过raft算法的原理可以知道,raft集群是一主(learder)多从(follower)的结构,leader负责接收客户端(client)的请求,处理并记录日志,然后leader将日志复制到所有的follower,达到数据冗余备份的目的。
当集群中有follower宕机,在不超过半数的情况下,集群仍然可以正常提供服务,follower恢复后,可以从leader复制快照(snapshot),以及快照点之后的所有日志记录,用于恢复内存中的数据,以追赶上集群状态。
当集群中的leader宕机后,所有的follower将选举出一名新的leader替代原有的leader,此过程即“选主”。
Counter演示程序
在源码分析之前,先演示一遍raft集群的启动及选主程序,直观的感受一下流程。
该程序来源于SofaJRaft源码,业务功能很简单,就是一个计数器,从零开始,每次客户端调用,都可以自增服务端内存中的整数值。
服务器启动
服务器执行入口是源码中的CounterServer的main方法
我这里会在idea中启动3个本地服务host,模拟一主两从的集群结构。
CounterServer1、 CounterServer2、CounterServer3 是3个服务器,执行入口都是上面说的同一个main方法,但是启动参数有细微差别:
CounterServer1的启动参数:
/tmp/server1 counter 127.0.0.1:8181 127.0.0.1:8181,127.0.0.1:8182,127.0.0.1:8183
CounterServer2的启动参数:
/tmp/server2 counter 127.0.0.1:8182 127.0.0.1:8181,127.0.0.1:8182,127.0.0.1:8183
CounterServer3的启动参数:
/tmp/server3 counter 127.0.0.1:8183 127.0.0.1:8181,127.0.0.1:8182,127.0.0.1:8183
逗号分隔的4个参数,第一个是服务目录,第二个是group名称,第三个是服务ip,第四个是group配置。
其中,group名称需要一样,作为3个服务器加入的同一个集群的标志;因为是本机演示,所以服务ip都是一样的,靠端口区分;最后,group配置,其实就是一串服务ip列表,保存该集群的所有ip地址。
接下来,我们依次启动CounterServer1(端口8181)、 CounterServer2(端口8182)、CounterServer3(端口8183)
可以看到“Start the RaftGroupService successfully.”,表示服务器启动完成。
随后,CounterServer1开始发起选举,投自己一票,并向CounterServer2和CounterServer3要选票。 从上图可以看到,CounterServer3返回了granted=true,表示CounterServer3同意了CounterServer1的自荐请求。加上CounterServer1自己投自己的一票,2票同意超过了集群3台服务器的半数,所以最后CounterServer1成功成为了集群的leader:“become leader of group”。
我们再看看CounterServer3的日志情况:
可以看到,CounterServer3收到了来自CounterServer1的preVote和Vote请求,并没有表示异议。最后开始成为集群的follower,认可CounterServer1为leader。
再看看CounterServer2的日志:
也有类似的收到 CounterServer1的preVote和Vote请求,以及认可CounterServer1为leader。
服务器重新选主
这个时候,我们的3台服务器的小集群已经到达了可对外服务的稳定状态,CounterServer1作为leader,CounterServer2和CounterServer3作为follower。
我们接下来模拟一下leader挂掉的场景,把CounterServer1的进程终止掉
然后发现CounterServer3开始选举自己为leader,并发送选举请求给 CounterServer2,且收到了认可回复:
最后,CounterServer3成为了集群的新领导者,然后不断尝试重新连接已经挂掉的CounterServer1:
再看看CounterServer2,可以看到其认可CounterServer3成为新leader的日志:
以上就是Counter演示程序的启动和选主流程简要过程,更多复杂的场景,比如网络分裂合并等,需要起更多的进程进行演示,可以自己试试。
下一篇将对这个过程,进行简单的源码流程阅读分析。