联席作者:吴毅挺 任浩军 童子龙
郑重鸣谢:Nacos – 彦林,Spring Cloud Alibaba – 小马哥、洛夜,Nacos 社区 – 张龙(pader)、春少(chuntaojun)
掌门教育自 2014 年正式转型在线教育以来,秉承“让教育共享智能,让学习高效高兴”的主旨和愿景,经验云计算、大数据、人工智能、AR / VR / MR 以及现今最火的 5G,始终保持用科技赋能教育。掌门教育的业务近几年失去了疾速倒退,特地是往年的疫情,使在线教育成为了新的风口,也给掌门教育新的时机。
随着业务规模进一步扩充,流量进一步暴增,微服务数目进一步增长,使老的微服务体系所采纳的注册核心 Eureka 不堪重负,同时 Spring Cloud 体系曾经演进到第二代,第一代的 Eureka 注册核心曾经不大适宜当初的业务逻辑和规模,同时它目前被 Spring Cloud 官网置于保护模式,将不再向前倒退。如何抉择一个更为优良和实用的注册核心,这个课题就摆在了掌门人的背后。通过对 Alibaba Nacos、HashiCorp Consul 等开源注册核心做了深刻的调研和比拟,最终选定 Alibaba Nacos 做微服务体系 Solar 中的新注册核心。
背景故事
两次 Eureka 引起业务服务大面积解体后,尽管通过降级硬件和优化配置参数的形式得以解决,Eureka 服务器目前运行安稳,但咱们仍旧放心此类事变在将来会再次发生,最终抉择落地 Alibaba Nacos 作为掌门教育的新注册核心。
Nacos 开发篇
Nacos Eureka Sync 计划演进
Sync 官网计划
通过钻研,咱们采取了官网的 Nacos Eureka Sync 计划,在小范畴试用了一下,成果良好,但一部署到 FAT 环境后,发现基本不行,一台同步服务器无奈抗住将近 660 个服务(非实例数)的频繁心跳,同时该计划不具备高可用特点。
Sync 高可用一致性 Hash + Zookeeper 计划
既然一台不行,那么就多几台,但如何做高可用呢?
咱们率先想到的是一致性 Hash 形式。当一台或者几台同步服务器挂掉后,采纳 Zookeeper 长期节点的 Watch 机制监听同步服务器挂掉状况,告诉残余同步服务器执行 reHash,挂掉服务的工作由残余的同步服务器来承当。通过一致性 Hash 实现被同步的业务服务列表的平均分配,基于对业务服务名的二进制转换作为 Hash 的 Key 实现一致性 Hash 的算法。咱们自研了这套算法,发现平均分配的很不现实,第一工夫狐疑是否算法有问题,于是找来 Kafka 自带的算法(见 Utils.murmur2),发现成果仍旧不现实,起因还是业务服务名的自身散布就是不均匀的,于是又回到自研算法上进行了优化,根本达到预期,下文会具体讲到。但说实话,直到现在仍旧无奈做到十分良好的相对均匀。
Sync 高可用主备 + Zookeeper 计划
这个计划是个小插曲,当一台同步服务器挂掉后,由它的“备”顶上,当然主备切换也是基于 Zookeeper 长期节点的 Watch 机制来实现的。前面探讨下来,主备计划,机器的老本很高,实现也不如一致性 Hash 优雅,最初没采纳。
Sync 高可用一致性 Hash + Etcd 计划
折腾了这么几次后,发现同步业务服务列表是长久化在数据库,同步服务器挂掉后 reHash 告诉机制是由 Zookeeper 来负责,两者是否能够合并到一个中间件上以降低成本?于是咱们想到了 Etcd 计划,即通过它实现同步业务服务列表长久化 + 业务服务列表增减的告诉 + 同步服务器挂掉后 reHash 告诉。至此计划最终确定,即两个注册核心(Eureka 和 Nacos)的双向同步计划,通过第三个注册核心(Etcd)来做桥梁。
Sync 业务服务名列表定时更新优化计划
解决了一致性 Hash 的问题后,还有一个潜在危险,即官网计划每次定时同步业务服务的时候,都会去读取全量业务服务名列表,对于业务服务数较少的场景应该没问题,但对于咱们这种场景下,这么频繁的全量去拉业务服务列表,会不会对 Nacos 服务器的性能有所冲击呢?接下去咱们对此做了优化,勾销全量定时读取业务服务名列表,通过 DevOps 的公布零碎平台施行判断,如果是迁徙过去的业务服务或者新上 Nacos 的业务服务,由公布平台对立调用 Nacos 接口来减少新的待同步业务服务 Job,当该业务服务全副迁徙结束后,在官网同步界面上删除该同步业务服务 Job 即可。
Sync 服务器两次扩容
计划实现后,上了 FAT 环境上后没发现问题(此环境,很多业务服务只部署一个实例),而在 PROD 环境上发现存在双向同步丢心跳的问题,起因是同步服务器来不及执行排队的心跳线程,导致 Nacos 服务器无奈及时收到心跳而把业务服务踢下来。咱们从 8 台 4C 8G 同步服务器扩容到 12 台,状况好了很多,但察看下来,还是存在一天内一些业务服务失落心跳的状况,于是咱们再次从 12 台 4C 8G 同步服务器扩容到 20 台,状况失去了大幅改善,但仍旧存在某个同步服务器上个位数失落心跳的状况,察看下来,那台同步服务器接受的某几个业务服务的实例数特地多的状况,咱们在那台同步服务器调整了最大同步线程数,该问题失去了修复。咱们将持续察看,如果该问题仍旧复现,不排除降级机器配置到 8C16G 来确保 PROD 环境的相对平安。
至此,通过 2 个月左右的致力付出,Eureka 和 Nacos 同步运行稳固,PROD 环境上同步将近 660 个服务(非实例数),状况良好。
十分重要的揭示:一致性 Hash 的虚构节点数,在所有的 Nacos Sync Server 上必须保持一致,否则会导致一部分业务服务同步的时候会被脱漏。
Nacos Eureka Sync 落地实际
Nacos Eureka Sync 指标准则
注册核心迁徙指标
1、过程并非欲速不达的,业务服务逐渐迁徙的过程要保障线上调用不受影响,例如,A 业务服务注册到 Eureka 上,B 业务服务迁徙到 Nacos,A 业务服务和 B 业务服务的相互调用必须失常。
2、过程必须保障双注册核心都存在这两个业务服务,并且指标注册核心的业务服务实例必须与源注册核心的业务服务实例数目和状态放弃实时严格统一。
注册核心迁徙准则
1、一个业务服务只能往一个注册核心注册,不能同时双向注册。
2、一个业务服务无论注册到 Eureka 或者 Nacos,最终后果都是等效的。
3、一个业务服务在绝大多数状况下,个别只存在一个同步工作,如果是注册到 Eureka 的业务服务须要同步到 Nacos,那就有一个 Eureka -> Nacos 的同步工作,反之亦然。在平滑迁徙中,一个业务服务一部分实例在 Eureka 上,另一部分实例在 Nacos 上,那么会产生两个双向同步的工作。
4、一个业务服务的同步方向,是依据业务服务实例元数据(Metadata)的标记 syncSource 来决定。
Nacos Eureka Sync 问题痛点
Nacos Eureka Sync 同步节点须要代理业务服务实例和 Nacos Server 间的心跳上报。
Nacos Eureka Sync 将心跳上报申请放入队列,以固定线程生产,一个同步业务服务节点解决的服务实例数超过肯定的阈值会造成业务服务实例的心跳发送不及时,从而造成业务服务实例的意外失落。
Nacos Eureka Sync 节点宕机,下面解决的心跳工作会全副失落,会造成线上调用大面积失败,结果不堪设想。
Nacos Eureka Sync 曾经开始工作的时候,从 Eureka 或者 Nacos 上,新上线或者下线一个业务服务(非实例),都须要让 Nacos Eureka Sync 实时感知。
Nacos Eureka Sync 架构思维
1、从各个注册核心获取业务服务列表,初始化业务服务同步工作列表,并长久化到 Etcd 集群中。
2、后续迁徙过程增量业务服务通过 API 接口长久化到 Etcd 集群中,业务服务迁徙过程整合 DevOps 公布平台。整个迁徙过程全自动化,躲避人为操作造成的脱漏。
3、同步服务订阅 Etcd 集群获取工作列表,并监听同步集群的节点状态。
4、同步服务依据存活节点的一致性 Hash 算法,找到解决工作节点,后端接口通过 SLB 负载平衡,删除工作指令轮询到的节点。如果是本人解决工作则移除心跳,否则找到解决节点,代理进来。
5、同步服务监听源注册核心每个业务服务实例状态,将失常的业务服务实例同步到指标注册核心,保障单方注册核心的业务服务实例状态实时同步。
6、业务服务所有实例从 Eureka 到 Nacos 后,须要业务部门告诉基础架构部手动从 Nacos Eureka Sync 同步界面摘除该同步工作。
Nacos Eureka Sync 计划实现
基于官网的 Nacos Sync 做工作分片和集群高可用,指标是为了反对大规模的注册集群迁徙,并保障在节点宕机时,其它节点能疾速响应,转移故障。技术点如下,文中只列出局部源码或者以伪代码示意:
** 具体代码,请参考:
https://github.com/zhangmen-tech/nacos**
服务一致性 Hash 分片路由:
依据如图 1 多集群部署,为每个节点设置可配置的虚构节点数,使其在 Hash 环上能均匀分布。
// 虚构节点配置
sync.consistent.hash.replicas = 1000;
// 存储虚构节点
SortedMap circle = new TreeMap();
// 循环增加所有节点到容器,构建 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(SLASH).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), option).get();
long delay = ttl / 6;
// 定时续约
scheduledExecutorService.schedule(new BeatTask(leaseId, delay), delay, TimeUnit.SECONDS);
// 续约工作
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, TimeUnit.SECONDS);
}
}
节点宕机 。其中某个节点宕机,其工作转移到其它节点,因为有虚构节点的缘故,所以此节点的工作会平衡 ReSharding 到其它节点,那么,集群在任何时候,工作解决都是分片平衡的,如图 2 中,B 节点宕机,##1、##2 虚构节点的工作会别离转移到 C 和 A 节点,这样防止一个节点承当宕机节点的所有工作造成残余节点间断雪崩。
节点复原 。如图 3,节点的虚构节点从新增加到 Hash 环中,Sharding 规定变更,复原的节点会依据新的 Hash 环规定承当其它节点的一部分工作。心跳工作一旦在节点产生都不会主动隐没,这时须要清理其它节点的多余工作(即重新分配给复苏节点的工作),给其它节点减负(这一步十分要害,不然也可能会引发集群的间断雪崩),保障集群复原到最后失常工作同步状态。
// 找到此节点解决的心跳同步工作
Map finishedTaskMap = skyWalkerCacheServices.getFinishedTaskMap();
// 存储非此节点解决工作
Map unBelongTaskMap = Maps.newHashMap();
// 找到集群复苏后,Rehash 后不是此节点解决的工作
if (!shardingEtcdProxy.isProcessNode(taskDO.getServiceName()) && TaskStatusEnum.SYNC.getCode().equals(taskDO.getTaskStatus())) {
unBelongTaskMap.put(operationId, entry.getValue());
}
unBelongTaskMap for loop {
// 删除多余的节点同步
specialSyncEventBus.unsubscribe(taskDO);
// 删除多余的节点解决工作数
proxy.deleteEtcdValueByKey(PER_WORKER_PROCESS_SERVICE.concat(SLASH).concat(IPUtils.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());
}
节点容灾 。如果 Etcd 集群连贯不上,则存活节点从配置文件中获取,集群失常运作,然而会失去容灾能力。
// 配置所有解决节点的机器 IP,用于构建 Hash 环
sync.worker.address = ip1, ip2, ip3;
// 从配置文件获取所有解决工作节点 IP
List ips = getWorkerIps();
ConsistentHash consistentHash = new ConsistentHash(replicas, ips);
// 如果从 Etcd 中获取不到以后解决节点,则构建 Hash 环用配置文件中的 IP 列表,且列表不会动态变化
if (CollectionUtils.isNotEmpty(nodeCaches)) {
consistentHash = new ConsistentHash(replicas, nodeCaches);
}
return consistentHash;
Nacos Eureka Sync 保障伎俩
Nacos Eureka Sync 同步界面
从如下界面能够保障,从 Eureka 或者 Nacos 上,新上线或者下线一个业务服务(非实例),都能让 Nacos Eureka Sync 实时感知。但咱们做了更进一层的智能化和自动化:
1、新增同步。联合 DevOps 公布平台,当一个业务服务(非实例)新上线的时候,智能判断它是从哪个注册核心上线的,而后回调 Nacos Eureka Sync 接口,主动增加同步接口,例如,A 业务服务注册到 Eureka 上,DevOps 公布平台会主动增加它的 Eureka -> Nacos 的同步工作,反之亦然。当然从如下界面的操作也可实现该性能。
2、删除同步。因为 DevOps 公布平台无奈判断一个业务服务(非实例)下线,或者曾经迁徙到另一个注册核心,曾经全副结束(有同学会反诘,能够判断的,即查看那个业务服务的实例数是否是零为规范,但咱们应该思考,实例数为零在网络故障的时候也会产生,即心跳全副失落,所以这个判断根据是不谨严的),交由业务人员来判断,同时配合钉钉机器人告警揭示,由基础架构部同学从如下界面的操作实现该性能。
Nacos Eureka Sync Etcd 监控
从如下界面能够监控到,业务服务列表是否在同步服务的集群上出现一致性 Hash 平衡散布。
Nacos Eureka Sync 告警
Nacos Eureka Sync 告警:
业务服务同步结束告警:
Nacos Eureka Sync 降级演练
1、7 月某天早晨 10 点开始,FAT 环境进行演练,通过自动化运维工具 Ansible 两次执行一键降级和回滚均没问题。
2、早晨 11 点 30 开始,执行灾难性操作,察看智能复原情况,9 台 Nacos Eureka Sync 挂掉 3 台的操作,只失落一个实例,但 5 分钟后复原(经考察,问题定位在 Eureka 上某个业务服务实例状态异样)。
3、早晨 11 点 45 开始,持续挂掉 2 台,只剩 4 台,故障转移,同步失常。
4、早晨 11 点 52 开始,复原 2 台,Nacos Eureka Sync 集群从新平衡 ReHash,同步失常。
5、早晨 11 点 55 开始,全副复原,Nacos Eureka Sync 集群从新平衡 ReHash,同步失常。
6、12 点 14 分,极限劫难演练,9 台挂掉 8 台,剩 1 台也能抗住,故障转移,同步失常。
7、凌晨 12 点 22 分,降级 UAT 环境顺利。
8、凌晨 1 点 22,降级 PROD 环境顺利。
容灾复原中的 ReHash 工夫小于 1 分钟,即 Nacos Eureka Sync 服务大面积故障产生时,复原工夫小于 1 分钟。
作者信息:
吴毅挺,掌门技术副总裁,负责技术中台和少儿技术团队。曾就任于百度、eBay、携程,曾任携程高级研发总监,负责从零打造携程公有云、容器云、桌面云和 PaaS 平台。
任浩军,掌门基础架构部负责人。曾就任于安全银行、万达、惠普,曾负责安全银行平台架构部 PaaS 平台 Halo 根底服务框架研发。10 多年开源经验,Github ID:@HaojunRen,Nepxion 开源社区创始人,Nacos Group Member,Spring Cloud Alibaba & Nacos & Sentinel& OpenTracing Committer。
原文链接:https://developer.aliyun.com/…
本文为阿里云原创内容,未经容许不得转载。