在RocketMQ Connect遵循Message Connector的设计理念,依托RocketMQ实现。主要包括Source Connector、Sink Connector、Runtime。RocketMQ Connect Runtime 是 Source、Sink Connector的运行时环境,同时,它也是一个分布式系统,支持集群节点之间的服务发现、配置同步、负载均衡等功能。
原Connect Runtime设计思路
首先,关于集群配置相关的实现都在ConfigManagementService中,当配置变化时主要涉及两种ConfigChangeKey,分别是ONLINE_KEY和CONFIG_CHANG_KEY
public void onCompletion(Throwable error, String key, ConnAndTaskConfigs result) {
boolean changed = false;
switch (ConfigChangeEnum.valueOf(key)) {
case ONLINE_KEY:
mergeConfig(result);
changed = true;
sendSynchronizeConfig();
break;
case CONFIG_CHANG_KEY:
changed = mergeConfig(result);
break;
default:
break;
}
if (changed) {
triggerListener();
}
}
其中ONLINE_KEY只会在ConfigManagementService开启时会执行一次,表示有新的节点上线了并将其配置发送到RocketMQ,以便其他节点获取并更新配置,同时,从代码中我们可以看到,在节点收到ONLINE_KEY后,会merge自己的配置并发送到RocketMQ中,这里有两个问题:一是会造成信息冗余,因为当集群中节点过多时,当一个节点收到ONLINE_KEY后,继而继续发送自己的CONFIG_CHANG_KEY到RocketMQ中,然后其他节点又会收到它的消息。通常情况下,我们认为集群的配置是同步的,所以每个节点在收到ONLINE_KEY都接收、merge、并发送消息到RocketMQ是没有必要的,造成了信息的浪费。二是如果在merge后发送CONFIG_CHANG_KEY到集群的过程失败时,本地的配置和其他节点的配置就不一致了。
private void sendOnlineConfig() {
ConnAndTaskConfigs configs = new ConnAndTaskConfigs();
configs.setConnectorConfigs(connectorKeyValueStore.getKVMap());
configs.setTaskConfigs(taskKeyValueStore.getKVMap());
dataSynchronizer.send(ConfigChangeEnum.ONLINE_KEY.name(), configs);
}
private void sendSynchronizeConfig() {
ConnAndTaskConfigs configs = new ConnAndTaskConfigs();
configs.setConnectorConfigs(connectorKeyValueStore.getKVMap());
configs.setTaskConfigs(taskKeyValueStore.getKVMap());
dataSynchronizer.send(ConfigChangeEnum.CONFIG_CHANG_KEY.name(), configs);
}
当用户通过RESTful接口创建新的connector时,通过putConnectorConfig处理新的Connector并分配task,然后通过sendSynchronizeConfig()向RocketMQ中发送CONFIG_CHANG_KEY,这里同样,当发送CONFIG_CHANG_KEY消息到RocketMQ集群失败或者发送过程中该worker挂掉时,会产生一致性问题。
问题描述&目标
通过上述问题可以看出,我们需要一种方案来解决当发送消息到RocketMQ失败时产生的一致性问题。解决方案如下:
- 把RocketMQ本身当作持久化日志存储
- 增加leader、follower的概念,只允许leader往RocketMQ中写入消息,其他follower从topic消费数据
- 将follower的RESTful接口请求将直接重定向到leader
总而言之,就是只允许一个worker往RocketMQ中发送消息,其他worker消费RocketMQ中的消息并进行connector/task的更新。注意,和Raft中的leader不同,RocketMQ Connect Runtime中的leader的功能只有把config update写入RocketMQ消息队列,所以我们称之为轻量化leader的概念。
当某个follower挂掉并重启后,重新从头到尾读一遍topic里的所有消息,把最新的config构建起来,保证配置与RocketMQ中的消息一致,就实现了配置的全局一致性。
另外,由于目前没有leader选举算法,所以我们采用直接指派leader,并指定某个follower在leader挂掉后进行主从切换,目前这种方式会丧失一定的可用性,所以打算后续会实现类似Raft/Dledger中的强一致性leader选举算法
配置同步解决方案架构
下面是该配置同步优化的解决方案架构图
Class Design
- ConnectorConfig
- 增加leaderID、workerID
- ConfigConverter/ConfigWrapper
- ConfigWrapper将ConnAndTaskConfig和leaderID封装
- ConfigConverter实现了将leader、connentor、task转化为json以及从json解析为config的方法
- ConfigManagementService
- checkLeaderState() 方法进行leader指定并检查
- ClusterManagementService
- 在onWorkerChangeListener中实现主从切换的逻辑
- MQConnectRuntimeAdminStartup
- 实现与RESTful相同功能的命令行工具(CLI),优化用户体验
- RestServer
- 增加对follower进行请求重定向的方法
- RestSender
- 实现对在CLI接口中请求的转发功能
Leader选举
首先,在配置文件connect.conf中设置leader以及主从切换
# Is the current node a Leader ("1" is a leader,null or "0" is a follower)
isLeader=1
# Is the current node a Candidate (When the leader down, it automatically becomes the leader)
isCandidate=0
重构了ConfigManagementService中的设计,取消worker启动时会发送ONLINE_KEY的设定,而是在开启ConfigManagementService时进行判断,如果是leader才会发送ONLINE_KEY
/**
* Check if Cluster has leader
*
* @return workerID if Cluster has leader or this worker is leader
*/
public void checkLeaderState() throws ConnectException {
if (connectConfig.getIsLeader() == 1){
log.info("This worker is a leader, leaderID is " + connectConfig.getWorkerID());
connectConfig.setLeaderID(connectConfig.getWorkerID());
sendOnlineConfig();
}
else {
//因为正常情况下,follow已经通过leader的CONFIG_CHANG_KEY设置了leader
if (connectConfig.getLeaderID() != null){
log.info("This worker is a follower, leader is " + connectConfig.getLeaderID());
connectConfig.setLeaderID(connectConfig.getLeaderID());
}
else
throw new ConnectException("leader status error");
}
}
另外,由于只要leader才会发送ONLINE_KEY,所以在收到ONLINE_KEY后,不再向集群中发送配置更新消息,而是只有当收到关于更新配置的RESTful请求,才会发送CONFIG_CHANG_KEY到RocketMQ中
public void onCompletion(Throwable error, String key, ConfigWrapper result) {
boolean changed = false;
switch (ConfigChangeEnum.valueOf(key)) {
case ONLINE_KEY:
log.info("Receive ON_LINE key, leader ip is {}", result.getLeader());
mergeConfig(result);
changed = true;
break;
case CONFIG_CHANG_KEY:
if (!checkLeaderState(result.getLeader()))
break;
changed = mergeConfig(result);
break;
default:
break;
}
if (changed) {
triggerListener();
}
}
原有的配置信息是ConnAndTaskConfigs,包含所有connector和task的配置信息,现在我们将其与leader信息封装到一起,作为配置信息转化为json并发送到RocketMQ中
主从切换
在ClusterManagementService中,每个worker都subscribe了cluster-topic,当其中的consumer(worker)数量减少时,每个worker的WorkerChangeListener的onWorkerChange就会触发,然后唤醒负载均衡线程,所以就会感知到集群中节点的变化。
/**
* Check whether the leader is down and the master-slave switch occurs
*
*/
private void checkClusterLeader(){
if (connectConfig.getLeaderID() == null) return;
List<String> workers = getAllAliveWorkers();
if (connectConfig.getIsLeader() == 1 && !workers.contains(connectConfig.getLeaderID() + "")){
log.error("This condition should not happen!");
}
if (connectConfig.getIsLeader() == 0 && connectConfig.getIsCandidate() == 1 && !workers.contains(connectConfig.getLeaderID())){
for (LeaderStatusListener leaderStatusListener : leaderStatusListeners){
leaderStatusListener.onLeaderChange();
}
log.info("Finish the master-slave switch");
}
}
这里,我们在WorkerChangeListener加入判断主从切换的逻辑,当follower检测到leader挂掉后,立刻通过LeaderStatusListener的onLeaderChange方法触发主从切换。在主从切换中,首先将当前worker设置为leader,然后发送CONFIG_CHANG_KEY到RocketMQ中。
一个leader挂掉,另外一个follower切换成leader。当原leader重启后,WorkerChangeListener会检测到集群中有leader冲突,然后返回错误。
消息转发
当leader接收到RESTful请求后,直接处理请求。当follower接收到RESTful请求后,将请求重定向到leader中。那么是如何获得leader的IP地址的呢?目前我们是通过ClusterManagementService中的getAllAliveWorkers获取到leader的ClientID,而ClientID是由ip地址+端口组成。
if (this.connectController.getConnectConfig().getIsLeader() != 1){
app.get("/*", this::RedirectionToLeader);
}
/**
* Redirect to the Leader
*
* @param context
*/
private void RedirectionToLeader(Context context) {
String address = this.connectController.getConnectConfig().getLeaderID().replace(":", "@");
String parm = context.queryString() == null ? "" : "?" + context.queryString();
String url = "http://" + address + context.path() + parm;
context.redirect(url);
}
在RESTHandler接收到RESTful请求后,会判断当前worker是leader还是follower,如果不是leader,就将请求重定向到leader中。
问题
- 目前这种获取leader的ip地址的方式在云原生环境显地不太优雅,后续我们也会寻找更好的解决方案
- 关于集群中中,当主从切换后原leader又开启、以及两个leader woker同时开启的逻辑部分目前还没有测试,还应该多加考虑
- 强一致性leader选举算法