Nacos Eureka Sync 架构思想

1、从各个注册中心获取业务服务列表,初始化业务服务同步任务列表,并持久化到Etcd 集群中。
2、后续迁移过程增量业务服务通过API 接口持久化到Etcd 集群中,业务服务迁移过程整合
DevOps 发布平台。整个迁移过程全自动化,规避人为操作造成的遗漏。
3、同步服务订阅Etcd 集群获取任务列表,并监听同步集群的节点状态。
4、同步服务根据存活节点的⼀致性Hash 算法,找到处理任务节点,后端接口通过SLB 负载均,
删除任务指令轮询到的节点。如果是自己处理任务则移除心跳,否则找到处理节点,代理出去。
5、同步服务监听源注册中心每个业务服务实例状态,将正常的业务服务实例同步到目标注册中心,保证双方注册中心的业务服务实例状态实时同步。
6、业务服务所有实例从Eureka 到Nacos 后,需要业务部门通知基础架构部手动从Nacos Eureka Sync 同步界面摘除该同步任务。

基于官方的Nacos Sync 做任务分片和集群高可用,目标是为了支持大规模的注册集群迁移,并保
障在节点宕机时,其它节点能快速响应,转移故障。技术点如下,文中只列出部分源码或者以伪代
码表示:

服务⼀致性Hash 分片路由:
根据如图1 多集群部署,为每个节点设置可配置的虚拟节点数,使其在Hash 环上能均匀分布。
// 虚拟节点配置
sync.consistent.hash.replicas = 1000;
// 存储虚拟节点
SortedMap<Integer, T> circle = new TreeMap<Integer, T>();
// 循环添加所有节点到容器,构建Hash 环
replicas for loop {
// 为每个物理节点设置虚拟节点
String nodeStr = node.toString().concat("##").concat(Integer.toString(replica));
// 根据算法计算出虚拟节点的Hash 值

int hashcode = getHash(nodeStr);
// 将虚拟节点放入Hash 环中
circle.put(hashcode, node);

// 异步监听节点存活状态
etcdManager.watchEtcdKeyAsync(REGISTER_WORKER_PATH, true, response -> {
for (WatchEvent event : response.getEvents()) {
// 删除事件,从内存中剔除此节点及Hash 中虚拟节点
if (event.getEventType().equals(WatchEvent.EventType.DELETE)) {
String key = Optional.ofNullable(event.getKeyValue().getKey()).map(bs -> bs.toString
(Charsets.UTF_8)).orElse(StringUtils.EMPTY);
//获取Etcd 中心跳丢失的节点
String[] ks = key.split(SLASH);
log.info("{} lost heart beat", ks[3]);
// 自身节点不做判断
if (!IPUtils.getIpAddress().equalsIgnoreCase(ks[3])) {
// 监听心跳丢失,更显存货节点缓存,删除Hash 环上节点
nodeCaches.remove(ks[3]);
try {
// 心跳丢失,清除etcd 上该节点的处理任务
manager.deleteEtcdValueByKey(PER_WORKER_PROCESS_SERVICE.concat(SL
ASH).concat(ks[3]), true);
} catch (InterruptedException e) {
log.error("clear {} process service failed,{}", ks[3], e);
} catch (ExecutionException e) {
log.error("clear {} process service failed,{}", ks[3], e);
}
}
}
}根据业务服务名的FNV1_32_HASH 算法计算每个业务服务的哈希值,计算该Hash 值顺时针最近
的节点,将任务代理到该节点。
// 计算任务的Hash 值
int hash = getHash(key.toString());
if (!circle.containsKey(hash)) {
SortedMap<Integer, T> tailMap = circle.tailMap(hash);
// 找到顺势针最近节点
hash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey();
}
// 得到Hash 环中的节点位置
circle.get(hash);
// 判断任务是否自己的处理节点
if (syncShardingProxy.isProcessNode(taskDO.getServiceName())) {
//如果任务属于该节点,则进行心跳同步处理
processTask(Task);
}
//删除心跳同步任务
if (TaskStatusEnum.DELETE.getCode().equals(taskUpdateRequest.getTaskStatus())) {
// 通过Etcd 存活节点的⼀致性Hash 算法,获取此任务所在的处理节点
Node processNode = syncShardingProxy.fetchProcessNode(Task);
if (processNode.isMyself()) {
// 如果是自己的同步任务,发布删除心跳事件
eventBus.post(new DeleteTaskEvent(taskDO));
} else {
// 如果是其他节点,则通过Http 代理到此节点处理
httpClientProxy.deleteTask(targetUrl,task);
}
}

同步节点宕机故障转移:
节点监听。监听其它节点存活状态,配置Etcd 集群租约TTL , TTL 内至少发送5 个续约心跳
以保证⼀旦出现网络波动避免造成节点丢失。

// 心跳TTL 配置
sync.etcd.register.ttl = 30;
// 获取租约TTL 配置
String ttls = environment.getProperty(ETCD_BEAT_TTL);
long ttl = NumberUtils.toLong(ttls);
// 获取租约ID
long leaseId = client.getLeaseClient().grant(ttl).get().getID();
PutOption option = PutOption.newBuilder().withLeaseId(leaseId).withPrevKV().build();
client.getKVClient().put(ByteSequence.from(key, UTF_8), ByteSequence.from(value, UTF_8), o
ption).get();
long delay = ttl / 6;
// 定时续约
scheduledExecutorService.schedule(new BeatTask(leaseId, delay), delay, TimeUnit.SECOND
S);
// 续约任务
private class BeatTask implements Runnable {
long leaseId;
long delay;
public BeatTask(long leaseId, long delay) {this.leaseId = leaseId;
this.delay = delay;
}
public void run() {
client.getLeaseClient().keepAliveOnce(leaseId);
scheduledExecutorService.schedule(new BeatTask(this.leaseId, this.delay), delay, Ti
meUnit.SECONDS);
}
}

节点宕机。其中某个节点宕机,其任务转移到其它节点,因为有虚拟节点的缘故,所以此节点的任
务会均衡ReSharding 到其它节点,那么,集群在任何时候,任务处理都是分片均衡的,如图2 中,
B 节点宕机, ##1 、##2 虚拟节点的任务会分别转移到C 和A 节点,这样避免⼀个节点承担宕
机节点的所有任务造成剩余节点连续雪崩。
节点恢复。如图3,节点的虚拟节点重新添加到Hash 环中, Sharding 规则变更,恢复的节点会
根据新的Hash 环规则承担其它节点的⼀部分任务。心跳任务⼀旦在节点产生都不会自动消失,这
时需要清理其它节点的多余任务(即重新分配给复苏节点的任务),给其它节点减负(这⼀步非常
关键,不然也可能会引发集群的连续雪崩),保障集群恢复到最初正常任务同步状态。

// 找到此节点处理的心跳同步任务
Map<String, FinishedTask> finishedTaskMap = skyWalkerCacheServices.getFinishedTaskMap
();
// 存储非此节点处理任务
Map<String, FinishedTask> unBelongTaskMap = Maps.newHashMap();
// 找到集群复苏后,Rehash 后不是此节点处理的任务

if (!shardingEtcdProxy.isProcessNode(taskDO.getServiceName()) && TaskStatusEnum.SYNC.ge
tCode().equals(taskDO.getTaskStatus())) {
unBelongTaskMap.put(operationId, entry.getValue());
}
unBelongTaskMap for loop {
// 删除多余的节点同步
specialSyncEventBus.unsubscribe(taskDO);
// 删除多余的节点处理任务数
proxy.deleteEtcdValueByKey(PER_WORKER_PROCESS_SERVICE.concat(SLASH).concat(IPU
tils.getIpAddress()).concat(SLASH).concat(taskDO.getServiceName()), false);
// 根据不同的同步类型,删除多余的节点心跳
if (ClusterTypeEnum.EUREKA.getCode().equalsIgnoreCase(clusterDO.getClusterType())) {
syncToNacosService.deleteHeartBeat(taskDO);
}
if (ClusterTypeEnum.NACOS.getCode().equalsIgnoreCase(clusterDO.getClusterType())) {
syncToEurekaService.deleteHeartBeat(taskDO);
}
// 删除多余的finish 任务
finishedTaskMap.remove(val.getKey());
}

// 配置所有处理节点的机器IP,用于构建Hash 环
sync.worker.address = ip1, ip2, ip3;
// 从配置文件获取所有处理任务节点IP
List<String> ips = getWorkerIps();
ConsistentHash<String> consistentHash = new ConsistentHash(replicas, ips);
// 如果从Etcd 中获取不到当前处理节点,则构建Hash 环用配置文件中的IP 列表,且列表不会动
态变化
if (CollectionUtils.isNotEmpty(nodeCaches)) {
consistentHash = new ConsistentHash(replicas, nodeCaches);
}
return consistentHash;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值