只要理解了共识算法,其实集成 raft 很简单,由于共识算法优秀文章已经很多了,本章就不做过多赘述了;
这里我们重点是讨论如何在我们的实际工程中集成 raft 协议,来实现分布式一致性;
前面我们只会简单的介绍分布式共识算法以及学习 Nacos 如何来集成分布式算法,重点在第4小节;
看完本章,大家可以根据自己项目的实际情况来决定是否需要集成 raft 协议来实现分布式一致性。
https://raft.github.io/
Raft is a consensus algorithm that is designed to be easy to understand. It’s equivalent to Paxos in fault-tolerance and performance. The difference is that it’s decomposed into relatively independent subproblems, and it cleanly addresses all major pieces needed for practical systems. We hope Raft will make consensus available to a wider audience, and that this wider audience will be able to develop a variety of higher quality consensus-based systems than are available today.
分布式共识算法
SOFAJRaft 是一个基于 RAFT 一致性算法的生产级高性能 Java 实现,支持 MULTI-RAFT-GROUP,适用于高负载低延迟的场景。 使用 SOFAJRaft 你可以专注于自己的业务领域,由 SOFAJRaft 负责处理所有与 RAFT 相关的技术难题,并且 SOFAJRaft 非常易于使用,你可以通过几个示例在很短的时间内掌握它。
github 地址:https://github.com/sofastack/sofa-jraft
一致性协议顶层接口
CP 协议接口
AP 协议接口
Distro 其实是一种 AP 协议的实现,类似的有 Eureka,Consul Gossip 等等
Distro 协议的主要设计思想如下:
Nacos 每个节点是平等的都可以处理写请求,同时把新数据同步到其他节点。
每个节点只负责部分数据,定时发送自己负责数据的校验值到其他节点来保持数据⼀致性。
每个节点独立处理读请求,及时从本地发出响应。
我们的项目为 data-porter,是一个分布式的数据迁移工具,采用配置文件的方式来确定集群中谁是 leader 节点,在某一个服务实例的配置中增加以下环境变量
data.porter.flow.client.tag=leader
leader 节点的功能
follower 节点功能
主要是为了 leader 选举功能。之前的 leader 是通过配置文件来控制,有明显的缺陷
本项目只需要 leader 选举功能,所以并未实现日志复制功能,在最后的总结有提及原因
将官网示例中的 com.alipay.sofa.jraft.example.election 包下面的所有代码拷贝到我们的项目中
以 data-porter 项目为例,我们可以新增 core 模块,再将代码复制过来
只需要改造 com.xxx.xxx.dp.core.election.DataPorterElection 这一个类即可
官网示例代码如下:
改造的思路如下
类上增加 @Component 注解即可
DataPorterServiceProperties 对象
@ConfigurationProperties("data.porter.service")
@Data
public class DataPorterServiceProperties {
private String dataPath = "/tmp/data-porter";
private String groupId = "data-porter";
private String serverAddress = "127.0.0.1:8081";
/**
* 集群 127.0.0.1:8081,127.0.0.1:8082,127.0.0.1:8083
*/
private String serverAddressList = "127.0.0.1:8081,127.0.0.1:8082,127.0.0.1:8083";
}
DataPorterElection 增加 @EnableConfigurationProperties(DataPorterServiceProperties.class) 激活配置;properties.getDataPath() 获取属性
@Component
@Slf4j
@EnableConfigurationProperties(DataPorterServiceProperties.class)
public class DataPorterElection {
@Autowired
private DataPorterServiceProperties properties;
@PostConstruct
public void init() {
final ElectionNodeOptions electionOpts = new ElectionNodeOptions();
electionOpts.setDataPath(properties.getDataPath());
electionOpts.setGroupId(properties.getGroupId());
electionOpts.setServerAddress(properties.getServerAddress());
electionOpts.setInitialServerAddressList(properties.getServerAddressList());
...
}
}
在 spring 容器启动 Ready 之后,再初始化 JRaft 组件
实现 ApplicationListener
@Component
@Slf4j
@EnableConfigurationProperties(DataPorterServiceProperties.class)
public class DataPorterElection implements ApplicationEventPublisherAware, ApplicationListener<ApplicationReadyEvent> {
...
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
...
}
}
这一段的代码就是参考 nacos 中的实现
官网示例还有一个问题就是,集群配置是在启动的时候,配置好的,如下
serverAddressList = "127.0.0.1:8081,127.0.0.1:8082,127.0.0.1:8083";
如果此时集群新增一个节点,其他节点不会感知到这个新的节点,会抛出 WARN 信息,提示接收到的 request 请求不是集群 conf 中的节点,如下。
Node <data-porter/127.0.0.1:19212> ignore PreVoteRequest from 127.0.0.1:19215 as it is not in conf <ConfigurationEntry [id=LogId [index=1, term=1], conf=127.0.0.1:19212,127.0.0.1:19213,127.0.0.1:19214, oldConf=]>.
那么如何让新增的节点添加到集群中呢?
在启动过程中,将本地节点注册到集群即可,核心代码如下
localPeerId = PeerId.parsePeer(properties.getServerAddress());
Configuration configuration = new Configuration();
for (String address : properties.getServerAddressList().split(",")) {
PeerId peerId = PeerId.parsePeer(address);
configuration.addPeer(peerId);
}
executorService.execute(()->{
// 动态将本地服务注册到 raft 实例集群
registerSelfToCluster(properties.getGroupId(), localPeerId, configuration);
});
registerSelfToCluster 方法
void registerSelfToCluster(String groupId, PeerId selfIp, Configuration conf) {
for (; ; ) {
try {
List<PeerId> peerIds = cliService.getPeers(groupId, conf);
if (peerIds.contains(selfIp)) {
return;
}
Status status = cliService.addPeer(groupId, conf, selfIp);
if (status.isOk()) {
return;
}
log.warn("Failed to join the cluster, retry...");
} catch (Exception e) {
log.error("Failed to join the cluster, retry...", e);
}
try {
Thread.sleep(1_000L);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
增加此段代码之后,日志会打印,表示新增的 config 被 Committed,可以看到 127.0.0.1:19215 被成功添加到集群中
c.a.sofa.jraft.core.StateMachineAdapter : onConfigurationCommitted: 127.0.0.1:19212,127.0.0.1:19213,127.0.0.1:19214,127.0.0.1:19215.
在服务实例 leader 节点发生变动的时候,将变动信息通知到业务侧,方便业务侧做后续处理
发送 spring event 事件
@Slf4j
public class DataPorterLeaderStateListener implements LeaderStateListener {
private ApplicationEventPublisher applicationEventPublisher;
public DataPorterLeaderStateListener(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
@Override
public void onLeaderStart(Node node, long leaderTerm) {
if (null == node) {
log.warn("node is null,term: " + leaderTerm);
return;
}
PeerId serverId = node.getLeaderId();
String ip = serverId.getIp();
int port = serverId.getPort();
log.info("[DataPorterElection] Leader's ip is: " + ip + ", port: " + port);
log.info("[DataPorterElection] Leader start on term: " + leaderTerm);
applicationEventPublisher.publishEvent(new LeaderStartedEvent(ip, port));
}
@Override
public void onLeaderStop(Node node, long leaderTerm) {
System.out.println("[DataPorterElection] Leader stop on term: " + leaderTerm);
}
}
监听 LeaderStartedEvent 事件,重新进行注册
@Component
public class LeaderStartedEventListener implements ApplicationListener<LeaderStartedEvent> {
@Override
public void onApplicationEvent(LeaderStartedEvent event) {
// 业务处理
}
}
以 capture 为例
pom 新增依赖
<dependency>
<groupId>com.megvii.cbggroupId>
<artifactId>data-porter-coreartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
配置文件增加
data.porter.service.dataPath=/tmp/data-porter-capture1
data.porter.service.serverAddress=127.0.0.1:19212
data.porter.service.serverAddressList=127.0.0.1:19212,127.0.0.1:19213,127.0.0.1:19214
需要注意这个里面的端口号是 GRPC 服务端的端口号,不能和 web 服务 server.port 一样。
这里用了5个节点来进行测试。分别是 CaptureApplication1 - CaptureApplication5
环境变量如下
文件存储路径
1.类似 Raft 这一类的分布式共识算法,在我们的平常业务开发中是很难用到的
2.Raft 更多使用是在一些分布式中间件的工具,这类工具使用 Raft 可以很容易的解决分布式中的各种问题。