服务发现:到底是要 CP 还是 AP?

简介: 本文探讨RPC框架中服务发现机制,重点分析超大规模集群下基于ZooKeeper的局限性,并提出基于消息总线的最终一致性方案。通过推拉结合、增量更新与两级缓存,实现高性能、高可用的服务注册与发现,提升系统稳定性和扩展性。

在上一讲中,我讲了「怎么设计一个灵活的 RPC 框架」,总结起来,就是怎么在 RPC 框架中应用插件,用插件方式构造一个基于微内核的 RPC 框架,其关键点就是「插件化」。
今天,我要和你聊聊 RPC 里面的「服务发现」在超大规模集群的场景下所面临的挑战。
为什么需要服务发现?
先举个例子,假如你要给一位以前从未合作过的同事发邮件请求帮助,但你却没有他的邮箱地址。这个时候你会怎么办呢?如果是我,我会选择去看公司的企业「通信录」。
同理,为了高可用,在生产环境中服务提供方都是以集群的方式对外提供服务,集群里面的这些 IP 随时可能变化,我们也需要用一本通信录 及时获取到对应的服务节点,这个获取的过程我们一般叫作 服务发现。
对于服务调用方和服务提供方来说,其契约就是接口,相当于通信录中的姓名,服务节点就是提供该契约的一个具体实例。服务 IP 集合作为通信录中的地址,从而可以通过接口获取服务 IP 的集合来完成服务的发现。这就是我要说的 PRC 框架的服务发现机制,如下图所示:
注册中心服务接口注册服务接口订阅远程方法调用服务提供方服务调用方

1
服务注册:在服务提供方启动的时候,将对外暴露的接口注册到注册中心之中,注册中心将这个服务节点的 IP 和接口保存下来。
2
服务订阅:在服务调用方启动的时候,去注册中心查找并订阅服务提供方的 IP,然后缓存到本地,并用于后续的远程调用。
为什么不使用 DNS?
既然服务发现这么「厉害」,那是不是很难实现啊?其实类似机制一直在我们身边,我们回想下服务发现的本质,就是完成了接口跟服务提供者 IP 的映射。那我们能不能把服务提供者 IP 统一换成一个域名啊,利用已经成熟的 DNS 机制来实现?
好,先带着这个问题,简单地看下 DNS 的流程:
PC客户机本地DNS服务器DNS服务器JVM区域q3DNS服务器a3q592q1.1q1.2.comJVM缓存域名浏览本地DNS缓存DNS服务器a5a1.2a1.1a2netq4cna4DNS缓存hosts文件

如果我们用 DNS 来实现服务发现,所有的服务提供者节点都配置在了同一个域名下,调用方的确可以通过 DNS 拿到随机的一个服务提供者的 IP,并与之建立长连接,这看上去并没有太大问题,但在我们业界为什么很少用到这种方案呢?不知道你想过这个问题没有,如果没有,现在可以停下来想想这样两个问题:

如果这个 IP 端口下线了,服务调用者能否及时摘除服务节点呢?

如果在之前已经上线了一部分服务节点,这时我突然对这个服务进行扩容,那么新上线的服务节点能否及时接收到流量呢?
这两个问题的答案都是:不能。这是因为为了提升性能和减少 DNS 服务的压力,DNS 采取了多级缓存机制,一般配置的缓存时间较长,特别是 JVM 的默认缓存是永久有效的,所以说 服务调用者不能及时感知到服务节点的变化。
这时你可能会想,我是不是可以加一个负载均衡设备呢?将域名绑定到这台负载均衡设备上,通过 DNS 拿到负载均衡的 IP。这样服务调用的时候,服务调用方就可以直接跟 VIP 建立连接,然后由 VIP(虚拟 IP:Virtual IP) 机器完成 TCP 转发,如下图所示:
DNS服务器服务提供方服务提供方服务调用方VIP服务提供方

这个方案确实能解决 DNS 遇到的一些问题,但在 RPC 场景里面也并不是很合适,原因有以下几点:

搭建负载均衡设备或 TCP/IP 四层代理,需求额外成本;

请求流量都经过负载均衡设备,多经过一次网络传输,会额外浪费些性能;

负载均衡添加和摘除节点一般都要手动添加,当大批量扩容和下线时,会有大量的人工操作和生效延迟;

我们在服务治理时需要更灵活的负载均衡策略,目前的负载均衡设备的算法还满足不了灵活的需求。
由此可见,DNS 或者 VIP 方案虽然可以充当服务发现的角色,但在 RPC 场景里面直接用还是很难的。
基于 ZooKeeper 的服务发现
那么在 RPC 里面我们该如何实现呢?我们还是要回到服务发现的本质,就是 完成接口跟服务提供者 IP 之间的映射。这个映射是不是就是一种命名服务?当然,我们还希望注册中心能完成实时变更推送,是不是像开源的 ZooKeeper、etcd 就可以实现?我很肯定地说「确实可以」。下面我就来介绍下一种基于 ZooKeeper 的服务发现方式。
整体的思路很简单,就是搭建一个 ZooKeeper 集群作为注册中心集群,服务注册的时候只需要服务节点向 ZooKeeper 节点写入注册信息即可,利用 ZooKeeper 的 Watcher 机制完成服务订阅与服务下发功能,整体流程如下图:
2.服务注册:1.创建服务目录创建临时节点数据服务提供者ZooKeeper集群服务平台管理端3.服务订阅:3.1创建临时节点数据4.服务节点变更通知3.2拉取服务全量数据3.3watch服务全部调用者节点数据服务调用者

1
服务平台管理端先在 ZooKeeper 中创建一个服务根路径,可以根据接口名命名(例如:/service/com.demo.xxService),在这个路径再创建服务提供方目录与服务调用方目录(例如:provider、consumer),分别用来存储服务提供方的节点信息和服务调用方的节点信息。
2
当服务提供方发起注册时,会在服务提供方目录中创建一个临时节点,节点中存储该服务提供方的注册信息。
3
当服务调用方发起订阅时,则在服务调用方目录中创建一个临时节点,节点中存储该服务调用方的信息,同时服务调用方 watch 该服务的服务提供方目录(/service/com.demo.xxService/provider)中所有的服务节点数据。
4
当服务提供方目录下有节点数据发生变更时,ZooKeeper 就会通知给发起订阅的服务调用方。
我所在的技术团队早期使用的 RPC 框架服务发现就是基于 ZooKeeper 实现的,并且还平稳运行了一年多,但后续团队的微服务化程度越来越高之后,ZooKeeper 集群整体压力也越来越高,尤其在集中上线的时候越发明显。「集中爆发」是在一次大规模上线的时候,当时有超大批量的服务节点在同时发起注册操作,ZooKeeper 集群的 CPU 突然飙升,导致 ZooKeeper 集群不能工作了,而且我们当时也无法立马将 ZooKeeper 集群重新启动,一直到 ZooKeeper 集群恢复后业务才能继续上线。
经过我们的排查,引发这次问题的根本原因就是 ZooKeeper 本身的性能问题,当连接到 ZooKeeper 的节点数量特别多,对 ZooKeeper 读写特别频繁,且 ZooKeeper 存储的目录达到一定数量的时候,ZooKeeper 将不再稳定,CPU 持续升高,最终宕机。而宕机之后,由于各业务的节点还在持续发送读写请求,刚一启动,ZooKeeper 就因无法承受瞬间的读写压力,马上宕机。
这次意外让我们意识到,ZooKeeper 集群性能显然已经无法支撑我们现有规模的服务集群了,我们需要重新考虑服务发现方案。
基于消息总线的最终一致性的注册中心
我们知道,ZooKeeper 的一大特点就是强一致性,ZooKeeper 集群的每个节点的数据每次发生更新操作,都会通知其它 ZooKeeper 节点同时执行更新。它要求保证每个节点的数据能够实时的完全一致,这也就直接导致了 ZooKeeper 集群性能上的下降。这就好比几个人在玩传递东西的游戏,必须这一轮每个人都拿到东西之后,所有的人才能开始下一轮,而不是说我只要获得到东西之后,就可以直接进行下一轮了。
而 RPC 框架的服务发现,在服务节点刚上线时,服务调用方是可以容忍在一段时间之后(比如几秒钟之后)发现这个新上线的节点的。毕竟服务节点刚上线之后的几秒内,甚至更长的一段时间内没有接收到请求流量,对整个服务集群是没有什么影响的,所以我们可以牺牲掉 CP(强制一致性),而选择 AP(最终一致),来换取整个注册中心集群的性能和稳定性。
那么是否有一种简单、高效,并且最终一致的更新机制,能代替 ZooKeeper 那种数据强一致的数据更新机制呢?
因为要求最终一致性,我们可以考虑采用消息总线机制。注册数据可以全量缓存在每个注册中心内存中,通过消息总线来同步数据。当有一个注册中心节点接收到服务节点注册时,会产生一个消息推送给消息总线,再通过消息总线通知给其它注册中心节点更新数据并进行服务下发,从而达到注册中心间数据最终一致性,具体流程如下图所示:
消息总线接口:com.test.Order地址:192.168.1.9:9080注册中心注册中心版本:2019113589接口:com.test.Hello生成注册消息服务注册地址:192.168.1.2:9080服务注册版本:2019103243消息回放消息回放接口:com.test.Hello推拉结合先进先出地址:192.168.1.2:9080接受大于本地版本消息版本:2019093143版本递增放弃小于本地版本消息


当有服务上线,注册中心节点收到注册请求,服务列表数据发生变化,会生成一个消息,推送给消息总线,每个消息都有整体递增的版本。

消息总线会主动推送消息到各个注册中心,同时注册中心也会定时拉取消息。对于获取到消息的在消息回放模块里回放,只接受大于本地版本号的消息,小于本地版本号的消息直接丢弃,从而实现最终一致性。

消费者订阅可以从注册中心内存拿到指定接口的全部服务实例,并缓存到消费者的内存里面。

采用推拉模式,消费者可以及时地拿到服务实例增量变化情况,并和内存中的缓存数据进行合并。
为了性能,这里采用了两级缓存,注册中心和消费者的内存缓存,通过异步推拉模式来确保最终一致性。
另外,你也可能会想到,服务调用方拿到的服务节点不是最新的,所以目标节点存在已经下线或不提供指定接口服务的情况,这个时候有没有问题?这个问题我们放到了 RPC 框架里面去处理,在服务调用方发送请求到目标节点后,目标节点会进行合法性验证,如果指定接口服务不存在或正在下线,则会拒绝该请求。服务调用方收到拒绝异常后,会安全重试到其它节点。
通过消息总线的方式,我们就可以完成注册中心集群间数据变更的通知,保证数据的最终一致性,并能及时地触发注册中心的服务下发操作。在 RPC 领域精耕细作后,你会发现,服务发现的特性是允许我们在设计超大规模集群服务发现系统的时候,舍弃强一致性,更多地考虑系统的健壮性。最终一致性才是分布式系统设计中更为常用的策略。
总结
今天我分享了 RPC 框架服务发现机制,以及如何用 ZooKeeper 完成「服务发现」,还有 ZooKeeper 在超大规模集群下作为注册中心所存在的问题。
通常我们可以使用 ZooKeeper、etcd 或者分布式缓存(如 Hazelcast)来解决事件通知问题,但当集群达到一定规模之后,依赖的 ZooKeeper 集群、etcd 集群可能就不稳定了,无法满足我们的需求。
在超大规模的服务集群下,注册中心所面临的挑战就是超大批量服务节点同时上下线,注册中心集群接受到大量服务变更请求,集群间各节点间需要同步大量服务节点数据,最终导致如下问题:

注册中心负载过高;

各节点数据不一致;

服务下发不及时或下发错误的服务节点列表。
RPC 框架依赖的注册中心的服务数据的一致性其实并不需要满足 CP,只要满足 AP 即可。我们就是采用消息总线的通知机制,来保证注册中心数据的最终一致性,来解决这些问题的。
另外,在今天的内容中,很多知识点不只可以应用到 RPC 框架的服务发现中。例如服务节点数据的推送采用增量更新的方式,这种方式提高了注册中心服务下发的效率,而这种方式,你还可以利用在其它地方,比如统一配置中心,用此方式可以提升统一配置中心下发配置的效率。
课后思考
目前服务提供者上线后会自动注册到注册中心,服务调用方会自动感知到新增的实例,并且流量会很快打到该新增的实例。如果我想把某些服务提供者实例的流量切走,除了下线实例,你有没有想到其它更便捷的办法呢?
笔者认为:可以给这些负责提供者配置权重,通过负载均衡的方式来偏移流量
若有收获,就点个赞吧

相关文章
|
8月前
|
机器学习/深度学习 搜索推荐 算法
广告系统:广告引擎如何做到在 0.1s 内返回广告信息?
广告系统是互联网核心营收支柱,支撑Google、Facebook等公司超80%收入。其本质是高并发、低延迟的实时检索系统,需在0.1秒内完成百万级广告匹配。本文详解广告引擎架构:通过标签过滤、树形分片优化索引;引入向量检索实现智能匹配;采用非精准打分预筛+深度学习精排的混合排序策略;并在离线索引构建时前置过滤无效广告,压缩检索空间。结合业务特点,从索引、召回到排序全方位提升性能,保障高效精准投放。
|
人工智能 运维 自然语言处理
电力+AI,「国网云智」重构电网运维的"超级大脑"
在深夜,当城市楼宇渐入梦乡时,在国网信通公司云运营中心还有一批运维工程师默默守护着大家微弱的灯光、此起彼伏的沟通声、咔咔的键盘敲击声响彻着每个工位。 当某系统的异常警报亮起时,工程师迅速利用「国网云智」定位问题,屏幕上即刻弹出详尽的排查方案及解决方案;而另一侧的监控员框选闪烁的告警区域,系统已自动锁定故障点,并在生成检修方案上标注了对应的工具清单和操作优先级。
1378 0
|
JSON Java 测试技术
面向初学者的JMeter实战手册:从环境搭建到组件解析
Apache JMeter 是 Apache 组织基于 Java 开发的压力测试工具,用于对软件做压力测试。JMeter 最初被设计用于 Web 应用测试,但后来扩展到了其他测试领域,可用于测试静态和动态资源,如静态文件、Java 小服务程序、CGI 脚本、Java 对象、数据库和 FTP 服务器等等。JMeter 可对服务器、网络或对象模拟巨大的负载,在不同压力类别下测试它们的强度和分析整体性能。另外,JMeter 能够对应用程序做功能/回归测试,通过创建带有断言的脚本来验证程序是否返回了期望结果。
1265 0
|
11月前
|
数据采集 网络协议 API
协程+连接池:高并发Python爬虫的底层优化逻辑
协程+连接池:高并发Python爬虫的底层优化逻辑
|
8月前
|
存储 编解码 JSON
RPC 实战:剖析 gRPC 源码,动手实现一个完整的 RPC
本讲通过剖析gRPC源码,实战实现RPC框架。利用Protocol Buffer定义接口,生成客户端和服务端代码,结合HTTP/2多路复用与PB序列化,详解请求发送、接收及编解码流程,揭示动态代理、序列化等技术在gRPC中的落地应用,帮助读者掌握RPC核心原理与实现。
|
8月前
|
存储 缓存 NoSQL
《神领物流》
本项目为基于微服务架构的智能物流系统,涵盖用户端、快递员端、司机端及管理端。采用GitFlow协作开发,结合Jenkins实现持续集成。通过Redis优化运费模板查询,利用Neo4j实现路线规划,MongoDB存储作业范围与物流轨迹,结合RabbitMQ保障消息可靠传输,使用Seata解决分布式事务,并引入多级缓存与布隆过滤器应对高并发场景,提升系统性能与稳定性。
什么叫高内聚,低耦合 超简单生活例子,让你一看就明白
本文通过生活化的例子解释了高内聚和低耦合的概念,强调了在编写代码时应该追求高内聚(相关功能紧密组合)和低耦合(功能间相互独立,减少相互影响),以提高代码质量和可维护性。
1967 0
|
XML 机器学习/深度学习 人工智能
CLaMP 3:音乐搜索AI革命!多模态AI能听懂乐谱/MIDI/音频,用27国语言搜索全球音乐
CLaMP 3是由清华大学团队开发的多模态、多语言音乐信息检索框架,支持27种语言,能够进行跨模态音乐检索、零样本分类和音乐推荐等任务。
1445 1
CLaMP 3:音乐搜索AI革命!多模态AI能听懂乐谱/MIDI/音频,用27国语言搜索全球音乐
|
JSON 小程序 数据格式
微信小程序 - 设置单个页面/导航栏/TabBar等背景颜色
微信小程序 - 设置单个页面/导航栏/TabBar等背景颜色
1851 0
微信小程序 - 设置单个页面/导航栏/TabBar等背景颜色
|
PyTorch 算法框架/工具
Pytorch学习笔记(七):F.softmax()和F.log_softmax函数详解
本文介绍了PyTorch中的F.softmax()和F.log_softmax()函数的语法、参数和使用示例,解释了它们在进行归一化处理时的作用和区别。
1943 1
Pytorch学习笔记(七):F.softmax()和F.log_softmax函数详解