先举个例子,假如你要给一位以前从未合作过的同事发邮件请求帮助,但你却没有他的邮箱地址。这个时候你会怎么办呢?如果是我,我会选择去看公司的企业“通信录”。
同理,为了高可用,在生产环境中服务提供方都是以集群的方式对外提供服务,集群里面的 这些 IP 随时可能变化,我们也需要用一本“通信录”及时获取到对应的服务节点,这个获 取的过程我们一般叫作“服务发现”,或者叫服务
对于服务调用方和服务提供方来说,其契约就是接口,相当于“通信录”中的姓名,服务节 点就是提供该契约的一个具体实例。服务 IP 集合作为“通信录”中的地址,从而可以通过 接口获取服务 IP 的集合来完成服务的发现。这就是我要说的 PRC 框架的服务发现机制, 如下图所示:
-
服务注册:在服务提供方启动的时候,将对外暴露的接口注册到注册中心之中,注册中 心将这个服务节点的 IP 和接口保存下来。
-
服务订阅:在服务调用方启动的时候,去注册中心查找并订阅服务提供方的 IP,然后缓 存到本地,并用于后续的远程调用。
- 从服务提供者的角度看:当提供者服务启动时,需要自动向注册中心注册服务;
- 当提供者服务停止时,需要向注册中心注销服务;
- 提供者需要定时向注册中心发送心跳,一段时间未收到来自提供者的心跳后,认为提供者已经停止服务,从注册中心上摘取掉对应的服务。
- 从调用者的角度看:调用者启动时订阅注册中心的消息并从注册中心获取提供者的地址;
- 当有提供者上线或者下线时,注册中心会告知到调用者;
- 调用者下线时,取消订阅。
整体的思路很简单,就是搭建一个 ZooKeeper 集群作为注册中心集群,服务注册的时候只 需要服务节点向 ZooKeeper 节点写入注册信息即可,利用 ZooKeeper 的 Watcher 机制 完成服务订阅与服务下发功能,整体流程如下图:
- 服务平台管理端先在 ZooKeeper 中创建一个服务根路径,可以根据接口名命名(例 如:/service/com.demo.xxService),在这个路径再创建服务提供方目录与服务调用方目录(例如:provider、consumer),分别用来存储服务提供方的节点信息和服务调 用方的节点信息。
- 当服务提供方发起注册时,会在服务提供方目录中创建一个临时节点,节点中存储该服务提供方的注册信息。
- 当服务调用方发起订阅时,则在服务调用方目录中创建一个临时节点,节点中存储该服 务调用方的信息,同时服务调用方 watch 该服务的服务提供方目录 (/service/com.demo.xxService/provider)中所有的服务节点数据。
- 当服务提供方目录下有节点数据发生变更时,ZooKeeper 就会通知给发起订阅的服务调 用方。
我们知道,ZooKeeper 的一大特点就是强一致性,ZooKeeper 集群的每个节点的数据每 次发生更新操作,都会通知其它 ZooKeeper 节点同时执行更新。它要求保证每个节点的数 据能够实时的完全一致,这也就直接导致了 ZooKeeper 集群性能上的下降。这就好比几个 人在玩传递东西的游戏,必须这一轮每个人都拿到东西之后,所有的人才能开始下一轮,而 不是说我只要获得到东西之后,就可以直接进行下一轮了。
而 RPC 框架的服务发现,在服务节点刚上线时,服务调用方是可以容忍在一段时间之后 (比如几秒钟之后)发现这个新上线的节点的。毕竟服务节点刚上线之后的几秒内,甚至更 长的一段时间内没有接收到请求流量,对整个服务集群是没有什么影响的,所以我们可以牺 牲掉 CP(强制一致性),而选择 AP(最终一致),来换取整个注册中心集群的性能和稳 定性。
那么是否有一种简单、高效,并且最终一致的更新机制,能代替 ZooKeeper 那种数据强一 致的数据更新机制呢?
因为要求最终一致性,我们可以考虑采用消息总线机制。注册数据可以全量缓存在每个注册 中心内存中,通过消息总线来同步数据。当有一个注册中心节点接收到服务节点注册时,会 产生一个消息推送给消息总线,再通过消息总线通知给其它注册中心节点更新数据并进行服 务下发,从而达到注册中心间数据最终一致性,具体流程如下图所示:


