You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

111 lines
13 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 08 | 服务发现到底是要CP还是AP
你好我是何小锋。在上一讲中我讲了“怎么设计一个灵活的RPC框架”总结起来就是怎么在RPC框架中应用插件用插件方式构造一个基于微内核的RPC框架其关键点就是“插件化”。
今天我要和你聊聊RPC里面的“服务发现”在超大规模集群的场景下所面临的挑战。
## 为什么需要服务发现?
先举个例子,假如你要给一位以前从未合作过的同事发邮件请求帮助,但你却没有他的邮箱地址。这个时候你会怎么办呢?如果是我,我会选择去看公司的企业“通信录”。
同理为了高可用在生产环境中服务提供方都是以集群的方式对外提供服务集群里面的这些IP随时可能变化我们也需要用一本“通信录”及时获取到对应的服务节点这个获取的过程我们一般叫作“服务发现”。
对于服务调用方和服务提供方来说其契约就是接口相当于“通信录”中的姓名服务节点就是提供该契约的一个具体实例。服务IP集合作为“通信录”中的地址从而可以通过接口获取服务IP的集合来完成服务的发现。这就是我要说的RPC框架的服务发现机制如下图所示
![](https://static001.geekbang.org/resource/image/51/5d/514dc04df2b8b2f3130b7d44776a825d.jpg?wh=2746*1445 "RPC服务发现原理图")
1. 服务注册在服务提供方启动的时候将对外暴露的接口注册到注册中心之中注册中心将这个服务节点的IP和接口保存下来。
2. 服务订阅在服务调用方启动的时候去注册中心查找并订阅服务提供方的IP然后缓存到本地并用于后续的远程调用。
## 为什么不使用DNS
既然服务发现这么“厉害”那是不是很难实现啊其实类似机制一直在我们身边我们回想下服务发现的本质就是完成了接口跟服务提供者IP的映射。那我们能不能把服务提供者IP统一换成一个域名啊利用已经成熟的DNS机制来实现
先带着这个问题简单地看下DNS的流程
![](https://static001.geekbang.org/resource/image/3b/18/3b6a23f392b9b8d6fcf31803a5b4ef18.jpg?wh=5273*1884 "DNS查询流程")
如果我们用DNS来实现服务发现所有的服务提供者节点都配置在了同一个域名下调用方的确可以通过DNS拿到随机的一个服务提供者的IP并与之建立长连接这看上去并没有太大问题但在我们业界为什么很少用到这种方案呢不知道你想过这个问题没有如果没有现在可以停下来想想这样两个问题
* 如果这个IP端口下线了服务调用者能否及时摘除服务节点呢
* 如果在之前已经上线了一部分服务节点,这时我突然对这个服务进行扩容,那么新上线的服务节点能否及时接收到流量呢?
这两个问题的答案都是“不能”。这是因为为了提升性能和减少DNS服务的压力DNS采取了多级缓存机制一般配置的缓存时间较长特别是JVM的默认缓存是永久有效的所以说服务调用者不能及时感知到服务节点的变化。
这时你可能会想我是不是可以加一个负载均衡设备呢将域名绑定到这台负载均衡设备上通过DNS拿到负载均衡的IP。这样服务调用的时候服务调用方就可以直接跟VIP建立连接然后由VIP机器完成TCP转发如下图所示
![](https://static001.geekbang.org/resource/image/d8/b9/d8549f6069a8ca5bd1012a0baf90f6b9.jpg?wh=2553*1299 "VIP方案")
这个方案确实能解决DNS遇到的一些问题但在RPC场景里面也并不是很合适原因有以下几点
* 搭建负载均衡设备或TCP/IP四层代理需求额外成本
* 请求流量都经过负载均衡设备,多经过一次网络传输,会额外浪费些性能;
* 负载均衡添加节点和摘除节点,一般都要手动添加,当大批量扩容和下线时,会有大量的人工操作和生效延迟;
* 我们在服务治理的时候,需要更灵活的负载均衡策略,目前的负载均衡设备的算法还满足不了灵活的需求。
由此可见DNS或者VIP方案虽然可以充当服务发现的角色但在RPC场景里面直接用还是很难的。
## 基于ZooKeeper的服务发现
那么在RPC里面我们该如何实现呢我们还是要回到服务发现的本质就是完成接口跟服务提供者IP之间的映射。这个映射是不是就是一种命名服务当然我们还希望注册中心能完成实时变更推送是不是像开源的ZooKeeper、etcd就可以实现我很肯定地说“确实可以”。下面我就来介绍下一种基于ZooKeeper的服务发现方式。
整体的思路很简单就是搭建一个ZooKeeper集群作为注册中心集群服务注册的时候只需要服务节点向ZooKeeper节点写入注册信息即可利用ZooKeeper的Watcher机制完成服务订阅与服务下发功能整体流程如下图
![](https://static001.geekbang.org/resource/image/50/75/503fabeeae226a722f83e9fb6c0d4075.jpg?wh=4214*1803 "基于ZooKeeper服务发现结构图")
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那种数据强一致的数据更新机制呢
因为要求最终一致性,我们可以考虑采用消息总线机制。注册数据可以全量缓存在每个注册中心内存中,通过消息总线来同步数据。当有一个注册中心节点接收到服务节点注册时,会产生一个消息推送给消息总线,再通过消息总线通知给其它注册中心节点更新数据并进行服务下发,从而达到注册中心间数据最终一致性,具体流程如下图所示:
![](https://static001.geekbang.org/resource/image/73/ff/73b59c7949ebed2903ede474856062ff.jpg?wh=4256*2276 "流程图")
* 当有服务上线,注册中心节点收到注册请求,服务列表数据发生变化,会生成一个消息,推送给消息总线,每个消息都有整体递增的版本。
* 消息总线会主动推送消息到各个注册中心,同时注册中心也会定时拉取消息。对于获取到消息的在消息回放模块里面回放,只接受大于本地版本号的消息,小于本地版本号的消息直接丢弃,从而实现最终一致性。
* 消费者订阅可以从注册中心内存拿到指定接口的全部服务实例,并缓存到消费者的内存里面。
* 采用推拉模式,消费者可以及时地拿到服务实例增量变化情况,并和内存中的缓存数据进行合并。
为了性能,这里采用了两级缓存,注册中心和消费者的内存缓存,通过异步推拉模式来确保最终一致性。
另外你也可能会想到服务调用方拿到的服务节点不是最新的所以目标节点存在已经下线或不提供指定接口服务的情况这个时候有没有问题这个问题我们放到了RPC框架里面去处理在服务调用方发送请求到目标节点后目标节点会进行合法性验证如果指定接口服务不存在或正在下线则会拒绝该请求。服务调用方收到拒绝异常后会安全重试到其它节点。
通过消息总线的方式我们就可以完成注册中心集群间数据变更的通知保证数据的最终一致性并能及时地触发注册中心的服务下发操作。在RPC领域精耕细作后你会发现服务发现的特性是允许我们在设计超大规模集群服务发现系统的时候舍弃强一致性更多地考虑系统的健壮性。最终一致性才是分布式系统设计中更为常用的策略。
## 总结
今天我分享了RPC框架服务发现机制以及如何用ZooKeeper完成“服务发现”还有ZooKeeper在超大规模集群下作为注册中心所存在的问题。
通常我们可以使用ZooKeeper、etcd或者分布式缓存如Hazelcast来解决事件通知问题但当集群达到一定规模之后依赖的ZooKeeper集群、etcd集群可能就不稳定了无法满足我们的需求。
在超大规模的服务集群下,注册中心所面临的挑战就是超大批量服务节点同时上下线,注册中心集群接受到大量服务变更请求,集群间各节点间需要同步大量服务节点数据,最终导致如下问题:
* 注册中心负载过高;
* 各节点数据不一致;
* 服务下发不及时或下发错误的服务节点列表。
RPC框架依赖的注册中心的服务数据的一致性其实并不需要满足CP只要满足AP即可。我们就是采用“消息总线”的通知机制来保证注册中心数据的最终一致性来解决这些问题的。
另外在今天的内容中很多知识点不只可以应用到RPC框架的“服务发现”中。例如服务节点数据的推送采用增量更新的方式这种方式提高了注册中心“服务下发”的效率而这种方式你还可以利用在其它地方比如统一配置中心用此方式可以提升统一配置中心下发配置的效率。
## 课后思考
目前服务提供者上线后会自动注册到注册中心,服务调用方会自动感知到新增的实例,并且流量会很快打到该新增的实例。如果我想把某些服务提供者实例的流量切走,除了下线实例,你有没有想到其它更便捷的办法呢?
欢迎留言和我分享你的思考和疑惑,也欢迎你把文章分享给你的朋友,邀请他加入学习。我们下节课再见!