⽆法加⼊nacos服务列表_Nacos注册中⼼落地实践
前⾔
公司在19年开始推进同城双活架构,未来规划是在南汇机房出现故障时能把所有读流量切到宝⼭机房,这样⾄少保证读请求是没问题的;我们的微服务使⽤的zookeeper来做服务发现, zk由于它的强⼀致性模型不适合多机房部署, 由于zk的服务发现模型是基于会话机制创建的临时节点, 就算两个机房各部署⼀套zk, 再部署⼀个sync服务两边同步,也会因为跨机房⽹络不稳定导致连接断开, zk会因此⼀开始就没有考虑继续⽤zk作为下⼀代服务发现的注册中⼼.
zk基于ZAB协议实现了强⼀致性,在出现⽹络分区时有可能因集⽆法选出Leader导致服务不可⽤, 这时候不可以新部署,重新启动,扩容或者缩容,这对于服务发现场景来讲是不能接受的, 在实践中,注册中⼼不能因为⾃⾝的任何原因破坏服务之间本⾝的可连通性, 数据的短期不⼀致对客户端来说就是多⼀个或者少⼀个节点,对于业务层⾯完全是可以接受的, 因此注册中⼼在设计时应偏向AP架构.
背景故事
我刚⼊职的时候便接⼿了⼀个任务:把zk注册中⼼替换成consul,因为⽹上的介绍都说consul是AP架构, ⽽且consul在⽣产环境已经完全落地, 运维已经⽤了consul来做物理机的发现和监控, 因此当时的做法就
是研究consul的接⼊⽂档和API, 完成了基于consul的开发和验证,上线了⼀个注册中⼼同步的服务,⽤于把zk注册中⼼和consul双向同步, 上线后很多服务开始报no active server的错误,最后排查下来发现我们线上的consul版本并不⽀持tag过滤的功能, 导致consul返回了在测试环境不会返回的服务实例信息, 最后导致反复触发服务上线下线事件,进⽽引起了故障,最后把sync服务强制下线才解决; 这次事故还发现个问题是线上的prometheus会给所有thrift端⼝发送/metric http请求,导致m2服务解析时直接内存溢出了, 需要重启服务才能解决.
这次事故我们吸取了深刻的教训, 由于没有深⼊了解Consul的原理, 导致解决问题花了较长时间,后⾯我们便把进度放慢, 先去熟悉Consul的技术架构, 深⼊Consul的源码才发现,Consul并不是AP架构, Consul的服务注册模型和KV模型都是基于Raft实现的强⼀致性,因此对于服务发现它是⼀个CP的架构, ⽽Consul内部Agent的发现和事件机制是基于Gossip流⾔协议实现的, 这个协议是最终⼀致性的,所以⽹上很多⽂章把Consul归类为AP架构, 这个是有问题的.
还有⼀个暴露出来的问题是运维⽤Consul来管理物理机,他们对可⽤性,⼀致性,性能这些指标并没有太多的要求, 因此业务和运维共⽤⼀套服务发现系统是有很⼤的风险的, 运维测试环境和⽣产环境安装的consul版本不⼀致就是如此, 因此基于⼀致性模型和数据共享的风险我们最终弃⽤了Consul.
注册中⼼选型
Eureka调研
基于我们在服务发现领域的积累, 最终⼀致性的架构⽐较适合注册中⼼的场景,因此我们调研了业界使⽤⽐较⼴泛的2款开源框架Eureka和Nacos.
Eureka是Netflix开源的⼀款提供服务注册和发现的产品,基于Java语⾔开发,在它的实现中,节点之间相互平等,部分注册中⼼的节点挂掉也不会对集造成影响,即使集只剩⼀个节点存活,也可以正常提供发现服务。哪怕是所有的服务注册节点都挂了,Eureka Clients上也会缓存服务调⽤的信息。这就保证了我们微服务之间的互相调⽤⾜够健壮.
看完前⾯的介绍可能会觉得Eureka的实现⾮常适合服务发现的场景,在SpringCloud体系下Eureka在很多公司都有成功的落地经验, 但深⼊Eureka源码层⾯进⾏分析时,我们发现了Eureka的⼀个巨⼤隐患, Eureka每个服务节点会保存所有的服务实例数据, 同时每个Eureka
会作为其他节点的Client, Eureka-Client也会保存⼀份全量的数据, 这就导致每个Eureka Server保存了2份全量的数据,因此当集规模⽐较⼤时Eureka Server的压⼒会⾮常⼤,⽽且⽆法通过⽔平扩展来分担压⼒.
贴⼀张Eureka的官⽅架构图:
总结了⼀下Eureka存在的问题
订阅端拿到的是所有服务的全量地址,这个对于客户端的内存是⼀个⽐较⼤的消耗(不使⽤官⽅客户端可以解决),每个server节点会在内存⾥保存2份全量的注册信息
pull模式:客户端采⽤周期性pull⽅式存在实时性不⾜以及拉取性能消耗的问题(开发长轮询功能)
⼀致性协议:Eureka集的多副本的⼀致性协议采⽤类似“异步多写”的AP协议,每⼀个 server都会把本地接收到的写请求(register/heartbeat/unregister/update)发送给组成集的其他所有的机器,特别是hearbeat报⽂是周期性持续不断的在client->server->all other server之间传送
当读请求增多的时候集需要扩容,但扩容⼜会导致每台server承担更多的写请求,扩容效果并不明显,⽽且每个server内存中保存2份全量的数据,内存会成为瓶颈
1.x版本更新频率很低,
2.x版本闭源, 问题修复频率低
Nacos调研
除了Eureka之外,我们还调研了另⼀款采⽤AP架构的Nacos注册中⼼,Nacos由阿⾥开源,在国内很多公司有成功落地经验,由于Nacos包含注册中⼼和配置中⼼两部分功能,以下内容只涉及注册中⼼.
nacos总体思路和eureka差不多,但是解决了eureka存在的⼀些问题:
pull时效性问题:采⽤udpPush+ack的⽅式, 并且⽀持定时pull
⼼跳请求在server间来回转发的问题:采⽤分⽚⽅式每个server负责⼀部分service的状态检查,并定时向其他server同步
扩容效果不明显问题:采⽤hash分⽚机制,每个server处理⼀部分service, 虽然每个server存储的数据量是⼀样的,但扩容可以解决写⼊的瓶颈
除此之外Nacos的其他⼏个关键特性:
⽀持多个namespace,实现租户隔离
⽀持CP和AP两种⼀致性模式, CP模式也是采⽤的Raft协议
⽀持主动和被动的⼼跳检测⽅式, 主动检测⽀持TCP,HTTP和MySQL协议, 由Nacos Server主动向服务实例发送探测请求来更新节点的健康状态,主要⽤来监控那些持久的节点,⽐如数据库,Redis实例等, 被动检测是由节点主动通过HTTP接⼝向Nacos Server发送⼼跳请求
下⾯这个是Nacos Server的请求处理流转图
综合对⽐后,我们决定⽤Nacos来作为我们微服务的下⼀代注册中⼼.
Nacos开发篇
官⽅的Nacos还⽆法直接在我们的⽣产环境上使⽤, 因为Nacos采⽤的Udp Push的⽅式来更新服务实例, ⽽Udp Push需要每个服务实例多监听⼀个端⼝,我们线上基本都是⼀个物理机部署好⼏个服务, 采⽤监听端⼝的⽅式很容易就端⼝冲突了, 因此我们需要将Udp Push改造成HTTP 长轮询的⽅式;
Nacos Server验证和改造
HTTP长轮询改造
Http长轮询功能是基于Servlet 3.0 提供的AsyncContext的能⼒, nacos-client收到订阅请求后会和nacos-server建⽴HTTP长连接, nacos只有在服务实例有变更时才向客户端返回最新的数据, 延迟基本在ms级别.
MetaServer改造
Nacos Server之间互相发现是通过配置⽂件⾥配置服务列表实现的,如果要新增⼀个节点, 需要修改配置⽂件并重启所有节点; 因此为了后⾯⽅便快速扩容, 我们把Server列表接⼊了配置中⼼,可以动态更新server集的地址;
Nacos-Client默认也是通过配置的⽅式获取获取server集地址, 同时也⽀持通过vipServer的⽅式来动态刷新服务列表, 需要客户端配置⼀个http url, 由于我们线上物理机都安装了Consul Agent, 因此我们直接把Consul当做MetaServer, 当health server列表有变更时把变更后的地址写到Consul的KV⾥, Nacos-Client动态的从Consul KV获取healthy集的地址.
压测和断⽹测试
Nacos官⽹给出的压测结果是⾮常优秀的, 我们压测下来也发现线上只需要部署三个节点就能满⾜要求,Nacos的代码质量也⽐较⾼, 服务GC ⼀直都⽐较稳定, 最后我们线上是部署了5个节点.
由于Nacos采⽤的是AP架构, 我们需要重点关注出现⽹络分区或机器故障的场景下Nacos的实际表现, 因此针对各种可能出现的场景都做了验证,下⾯列出来的是发现问题的⼏个场景:
测试场景1:⽹络分区5分钟,⽹络恢复后2个server数据⼀直会不⼀致
最后发现是Nacos的同步任务有Bug,该Bug已提交PR并采纳,参考issue1665
测试场景2: 两个实例⽹络分区后服务实例的变化过程
由于采⽤的分⽚处理的模型, 当某⼀个节点和其他节点⽹络分区了并且15s内没有恢复, 这个节点上的实例会集体下线⼏秒钟,客户端会出现⼤量的no active server, 这个问题我在Github与Nacos团队有讨论过, 参考issue1873,他们给出的解决⽅案是Nacos Server之间剔除过期节点的时间间隔要⼩于 (服务实例超时时间-服务实例⼼跳间隔)
后⾯我们修改了Nacos Server之间的⼼跳配置,改为每秒发送⼀次⼼跳, 超过10s没有收到⼼跳就摘除该Server.
测试场景3: Nacos Server重启后服务实例的变化
Nacos Server启动后会从其他Server同步全量的数据, 但如果同步过来的部分服务原先是由⾃⼰负责处理⼼跳的, ⽽这些实例的⼼跳时间戳是⽐较早的(Nacos-Server之间并不会同步服务实例最新的heartbeat时间戳), 这些服务会被每5s执⾏的ClientBeatCheckTask删除掉,但很快会重新注册上.
如果不在业务⾼峰期重启⼀般问题不⼤, 但想了下还是加了个保护模式, 当Nacos-Server集地址有变更时, 暂停主动的ClientBeatCheckTask 30s, 这样Nacos Server可以⽆损的发布和重启, 重启Nacos Server对业务完全⽆影响.
双注册中⼼架构
Nacos注册中⼼上线后, 我们需要考虑的是和现有zk注册中⼼的兼容问题, 由于我们线上还有很多使⽤服务还在使⽤⽼版本微服务sdk, 这些服务很多都不到维护的⼈, 因此我们预计到未来很长⼀段时间内需要保留2个注册中⼼, 因此需要有个服务对2个注册中⼼的数据进⾏双向同步.
Nacos官⽅提供了Nacos-Sync来做注册中⼼数据的迁移, 但调研后发现这是⼀个单机版的实现,主要⽤于⼀次性迁移数据, ⽆法达到⾼可⽤,因此我们需要⾃⼰开发⼀个Sync服务,⽤于zk和nacos双向同步数据.
Sync服务处理的原则:
微服务注册中心有哪些
1. ⽆状态, 服务实例⽆论是注册在zk还是nacos,两个注册中⼼的数据必须是⼀致的, ⽽且允许中间状态的存在(在升级过程中⼀个服务可能
部分实例⽤zk,部分⽤nacos)
2. ⼀个业务服务在绝⼤多数情况下,⼀般只存在⼀个双向同步任务, 在sync服务上下线过程中可能reHash出现多个sync节点都存在同⼀
个服务的sync任务,但结果必须是⼀致的
3. ⼀个业务服务的同步⽅向,是根据业务服务实例元数据( Metadata )的标记 fromSync 来决定的,⽐如服务实例是注册在zk, 该实例同
步到nacos后会加⼀个fromSync的
MetaData, 这样从Nacos同步到zk时会忽略fromSync=true的实例,避免来回同步
我们采⽤了⼀致性 Hash ⽅式来解决任务分配的问题,当⼀台或者⼏台同步服务器挂掉后,采⽤ Zookeeper 临时节点的 Watch 机制监听同步服务器挂掉情况,通知剩余同步服务器执⾏ reHash,挂掉服务的⼯作由剩余的同步服务器来承担,通过⼀致性 Hash 实现被同步的业务服务列表的平均分配,基于对业务服务名的⼆进制转换作为 Hash 的 Key 实现⼀致性 Hash 的算法.
Sync服务在给Nacos发送⼼跳时, 将⼼跳上报请求放⼊队列,以固定线程消费,当⼀个sync节点处理的服务实例数超过⼀定的阈值会造成业务服务实例的⼼跳发送不及时,从⽽造成业务服务实例的意外丢失。其实对于sync过去的服务来说, ⼼跳的间隔可以设置的长⼀些, 因为服务实例⼀旦在⼀个注册中⼼下线了, 会被sync服务监听到然后主动下线,这些实例并不需要nacos server频繁的主动剔除检查, 因此我们实际采⽤了⼀个15~45s的随机值作为⼼跳间隔, 极⼤的减少了sync服务发送⼼跳的压⼒,同⼀个服务的所有实例也不会出现集体下线的情况.
Sync服务平滑上下线
由于我们依赖zk的临时节点来做⼀致Hash, 当某个sync节点下线时,其他节点监听到该节点下线时有延迟的, 这样就会出现sync过去的实例会有1s左右的下线, 针对这个场景,我们实现了主动下线的逻辑, 在重启sync节点前发送主动下线命令, 该节点主动把⾃⼰从zk上摘除,并执⾏慢清理操作, 这样在该节点停⽌之前其他节点能把对应的syncTask接管过去, 实现⽆损下线.
最后我们的双注册中⼼架构如下:

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。