一文详解 RPC 中的注册中心
为什么需要服务发现
为了高可用,在生产环境中服务提供方都是以集群的方式对外提供服务,集群里面的这些 IP 随时可能会变化,我们需要一本“通信录”即时获取到对应的服务节点,这个获取的过程我们一般叫做“服务发现”。
对于服务调用方和服务提供方来说,其契约就是接口。服务 IP 集合作为“通信录”中的地址,从而可以通过接口获取服务 IP 的集合来完成服务的发现,这就是 RPC 框架的服务发现机制。
1.服务注册:在服务提供方启动的时候,将对外暴露的接口注册到注册中心之中,注册中心将这个服务节点的 IP 和接口保存下来。
2.服务订阅:在服务调用方启动的时候,去注册中心查找并订阅服务提供方的 IP,然后缓存到本地,并用于后续的远程调用。
为什么不使用 DNS
服务发现的本质,就是完成了接口跟服务提供者的 IP 的映射,那能不能把服务提供者 IP 同一换成一个域名,利用已经成熟的 DNS 机制来实现?
先简单看一下 DNS的流程:
如果我们用 DNS 来实现服务发现,所有的服务提供者节点都同一配置在了同一个域名下,调用方的确可以通过 DNS 拿到随机的一个服务提供者的 IP,并与之建立长连接,这看上去并没有太大问题,但是为啥很少使用这种方案呢?
我们先来想一下这两个问题:
- 如果这个 IP 端口下线了,服务调用者能否即时摘除服务节点呢?
- 如果在之前已经上线了一部分服务节点,这时我突然对这个服务进行扩容,那么新上线的服务节点能否即时收到流量呢?
答案是“不能”,因为为了提升性能和减少 DNS 服务的压力,DNS 采取了多级缓存机制,一把配置的缓存时间比较长,特别是对 JVM的默认缓存是永久的,所以说服务调用者不能即时感知到服务节点的变化。
那是不是可以加一个负载均衡设备呢? 将域名绑定到这台负载均衡设备上,通过 DNS 拿到负载均衡的IP。这样服务调用的时候,服务调用方就可以直接跟 VIP 建立连接,然后由 VIP 机器完成 TCP 转发,如图:
这个方案确实能解决 DNS 遇到的一些问题,但是在 RPC 场景里面也并不是很合适,原因如下:
- 搭建负载均衡设备或 TCP/IP 四层代理,需要额外成本
- 请求流量都经过负载均衡设备,多经过一次网络传输,会额外浪费些性能
- 负载均衡添加节点和摘除节点,一般都要手动添加,当大批量扩容下线时,会有大量的人工操作和生效延迟
- 在服务治理的时候,需要更灵活的负载均衡策略,目前的负载均衡策略设备还无法满足不了灵活的需求。
可见,DNS 或者 VIP 方案虽可以充当服务发现的角色,但在 RPC 场景里面直接用还是很难的。
基于 ZooKeeper 的服务发现
那在 RPC 里面该如何实现呢,还是要回到服务发现的本质,就是完成接口跟服务提供者 IP 之间的映射,这个映射不就是一种命名服务,还希望注册中心能完成实时变更推送,那 ZooKeeper 就能完成。
搭建一个 ZooKeeper 集群作为注册中心集群,服务注册的时候只需要服务节点向 ZooKeeper 节点写入注册信息即可,利用 ZooKeeper 的 Watcher 机制完成服务订阅与服务下发功能。
1.服务管理平台先在 ZooKeeper 中创建一个服务根路径,可以根据接口命名,在这个路径再创建服务提供方目录与服务调用方的节点信息,分别用来存储服务提供方的节点信息和服务调用方的节点信息。
2.当服务提供方发起注册时,会在服务提供方目录中创建一个临时节点,节点中存储的该服务提供方的注册信息。
3.当服务调用方发起订阅时,则在服务提供方目录中创建一个临时节点,节点中存储该服务调用方的信息,同时服务调用方 watch 该服务的服务提供方目录。
4.当服务提供方目录下有节点数据发生变更时,ZooKeeper就会通知给发起订阅的服务调用方。
但是使用 ZooKeeper一定没有问题了吗?
当连接到ZooKeeper 的节点数量特别多,对 ZooKeeper 读写特别频繁,且 ZooKeeper 存储的目录达到了一定数量的时候,ZooKeeper 将不再稳定,CPU 持续身高,最终宕机。而宕机之后,由于各业务的节点还在持续发送读写请求,刚一启动,ZooKeeper 就因无法承受瞬间的读写压力,马上宕机。
那有没有其他的方案呢?
基于消息总线的最终一致性的注册中心
ZooKeeper 是强一致性,ZooKeeper 集群的每个节点的数据每次发生更新操作,都会通知其他 ZooKeeper 节点同时执行更新,要求保证每个节点的数据能够实时的完全一致,也就直接导致了 ZooKeeper 集群性能上的下降。
而 RPC 框架的服务发现,在服务节点刚上线的时候,服务调用方是可以容忍在一段时间之后(比如几秒钟之后)发现这个新上线的节点的,毕竟服务节点刚上线之后的几秒内,甚至更长的一段时候内没有接收到流量请求,对整个服务集群是没有什么影响的,所以我们可以牺牲的 CP,而选择 AP,来换取整个注册中心集群的性能和稳定性。
那么是否有一种简单、高效,并且最终一致性的更新机制,能代替 ZooKeeper 那种数据强一致的数据更新机制呢?
因为要求最终一致性,我们可以采用消息总线机制。注册数据可以全量缓存在每个注册中心内存中,通过消息总线来同步数据。当有一个注册中心节点接收到服务节点注册时,会产生一个消息推送给消息总线,再通过消息总线通知给其他注册中心节点更新数据并进行服务下发,从而达到注册中心间数据的最终一致性,如下:
- 当有服务上线时,注册中心节点收到注册请求,服务列表数据发生变化,会生成一个消息,推动给消息总线,每个消息都有整体递增的版本。
- 消息总线会主动推动消息到各个注册中心,同时注册中心也会定时拉取消息。对于获取到消息的在消息回放模块里面回放,只接受大于本地版本号的消息,小于本地版本号的消息直接丢弃,从而实现最终一致性。
- 消费者定于可以从注册中心内存拿到指定接口的全部服务实例,并缓存到消费者的内存里面
- 采用推拉模式,消费者可以及时地拿到服务实例增量变化的情况,并和内存中的缓存数据进行合并
为了性能,这里采用两级缓存,注册中心和消费者的内存缓存,通过异步推拉的方式来确保最终一致性。
那服务调用方拿到的服务节点不是最新的,目标节点存在已经下线或不提供指定接口服务的情况,这个时候有没有问题。
在服务调用方发送请求到目标节点后,目标节点会进行合法性验证,如果指定接口服务不存在或正式下线,则会拒绝该请求,服务调用方收到拒绝异常后,会安全重试到其他节点。
通过消息总线的方式,可以完成注册中心集群间数据变更的通知,保证数据的最终一致性,并能即时地触发注册中心的服务下发操作。最终一致性才是分布式系统设计中更为常用的策略。
巨人的肩膀:
https://time.geekbang.org/column/intro/100046201?tab=comment