服务注册与发现 — 选择 CP 还是 AP?
概述为什么要使用服务发现假设您正在编写一些代码,这些代码将调用具有 REST API 的服务。为了发出请求,您的代码需要知道服务实例的网络位置(IP 地址和端口)。在物理硬件上运行的传统应用程序中,服务实例的网络位置是相对静态的。例如,您的代码可以从偶尔更新的配置文件中读取网络位置。但是,在现代的基于云的微服务应用程序中,这是一个要解决的难题,如下图所示。服务实例具有动态分配的网络位置。...
概述
为什么要使用服务发现
假设您正在编写一些代码,这些代码将调用具有 REST API 的服务。为了发出请求,您的代码需要知道服务实例的网络位置(IP 地址和端口)。在物理硬件上运行的传统应用程序中,服务实例的网络位置是相对静态的。例如,您的代码可以从偶尔更新的配置文件中读取网络位置。
但是,在现代的基于云的微服务应用程序中,这是一个要解决的难题,如下图所示:
服务实例具有动态分配的网络位置。而且,服务实例集会由于自动缩放,故障和升级而动态更改。因此,您的客户端代码需要使用更复杂的服务发现机制。
客户端查询服务注册表,该服务注册表是可用服务实例的数据库。然后,客户端使用负载平衡算法来选择可用的服务实例之一并发出请求。
服务实例的网络位置在启动时会在服务注册表中注册。实例终止时,将从服务注册表中将其删除。通常使用心跳机制定期刷新服务实例的注册。
CAP 理论
CAP 理论的诞生
在分布式系统的发展中,影响最大最广泛的莫过于 CAP 理论了,可以说 CAP 理论是分布式系统发展的理论基石。早在 1998 年,加州大学的计算机科学家 Eric Brewer ,就提出分布式系统的三个指标。在此基础上,2 年后,Eric Brewer 进一步提出了 CAP 猜想。又过了 2 年,到了 2002 年,麻省理工学院的 Seth Gilbert 和 Nancy Lynch 从理论上证明了 CAP 猜想。CAP 猜想成为了 CAP 定理,也称为布鲁尔定理。从此,CAP 定理成为分布式系统发展的理论基石,广泛而深远的影响着分布式系统的发展。
CAP 理论指标
CAP 理论,简单的说就是分布式系统不可能同时满足 Consistency 一致性、Availability 可用性、Partition Tolerance 分区容错性三个要素。因为 Consistency、Availability 、Partition Tolerance 这三个单词的首字母分别是 C、A、P,所以这个结论被称为 CAP 理论。
Consistency 一致性
CAP 理论的第一个要素是 Consistency 一致性。一致性的英文含义是指“all nodes see the same data at the same time”。即所有节点在任意时间,被访问返回的数据完全一致。CAP 作者 Brewer 的另外一种解释是在写操作之后的读指令,必须得到的是写操作写入的值,或者写操作之后新更新的值。从服务端的视角来看,就是在 Client 写入一个更新后,Server 端如何同步这个新值到整个系统,从而保证整个系统的这个数据都相同。而从客户端的视角来看,则是并发访问时,在变更数据后,如何获取到最新值。
Availability 可用性
CAP 理论的第二个要素是 Availability 可用性。可用性的英文含义是指“Reads and writes always succeed”。即服务集群总能够对用户的请求给予响应。Brewer 的另外一个种解释是对于一个没有宕机或异常的节点,总能响应用户的请求。也就是说当用户访问一个正常工作的节点时,系统保证该节点必须给用户一个响应,可以是正确的响应,也可以是一个老的甚至错误的响应,但是不能没有响应。从服务端的视角来看,就是服务节点总能响应用户请求,不会吞噬、阻塞请求。而从客户端视角来看,发出的请求总有响应,不会出现整个服务集群无法连接、超时、无响应的情况。
Partition Tolerance 分区容错性
第三个要素是 Partition Tolerance 分区容错性。分区容错的英文含义是指“The system continues to operate despite arbitrary message loss or failure of part of the system”。即出现分区故障或分区间通信异常时,系统仍然要对外提供服务。在分布式环境,每个服务节点都不是可靠的,不同服务节点之间的通信有可能出现问题。当某些节点出现异常,或者某些节点与其他节点之间的通信出现异常时,整个系统就产生了分区问题。从服务端的视角来看,出现节点故障、网络异常时,服务集群仍然能对外提供稳定服务,就是具有较好的分区容错性。从客户端视角来看,就是服务端的各种故障对自己透明。
正常服务场景
如图所示,网络上有 2 个服务节点 Node1 和 Node2,它们之间通过网络连通组成一个分布式系统。在正常工作的业务场景,Node1 和 Node2 始终正常运行,且网络一直良好连通。
假设某初始时刻,两个节点中的数据相同,都是 V0,用户访问 Nodel 和 Node2 都会立即得到 V0 的响应。当用户向 Node1 更新数据,将 V0 修改为 V1时,分布式系统会构建一个数据同步操作 M,将 V1 同步给 Node2,由于 Node1 和 Node2 都正常工作,且相互之间通信良好,Node2 中的 V0 也会被修改为 V1。此时,用户分别请求 Node1 和 Node2,得到的都是 V1,数据保持一致性,且总可以都得到响应。
网络异常场景
作为一个分布式系统,总是有多个分布的、需要网络连接的节点,节点越多、网络连接越复杂,节点故障、网络异常的情况出现的概率就会越大。要完全满足 CAP 三个元素。就意味着,如果节点之间出现了网络异常时,需要支持网络异常,即支持分区容错性,同时分布式系统还需要满足一致性和可用性。我们接下来看是否可行。
现在继续假设,初始时刻,Node1 和 Node2 的数据都是 V0,然后此时 Node1 和 Node2 之间的网络断开。用户向 Node1 发起变更请求,将 V0 变更为 V1,分布式系统准备发起同步操作 M,但由于 Node1 和 Node2 之间网络断开,同步操作 M 无法及时同步到 Node2,所以 Node2 中的数据仍然是 V0。
此时,有用户向 Node2 发起请求,由于 Node2 与 Node1 断开连接,数据没有同步,Node2 无法立即向用户返回正确的结果 V1。那怎么办呢?有两种方案。
第一种方案,是牺牲一致性,Node2 向请求用户返回老数据 V0 的响应。
第二种方案,是牺牲可用性,Node2 持续阻塞请求,直到 Node1 和 Node2 之间的网络连接恢复,并且数据更新操作 M 在 Node2 上执行完毕,Node2 再给用户返回正确的 V1 操作。
至此,简要证明过程完毕。整个分析过程也就说明了,分布式系统满足分区容错性时,就无法同时满足一致性和可用性,只能二选一,也就进一步证明了分布式系统无法同时满足一致性、可用性、分区容错性这三个要素。
CAP 权衡
在通常的分布式系统中,为了保证数据的高可用,通常会将数据保留多个副本(Replica),网络分区是既成的现实,于是只能在可用性和一致性两者间做出选择。CAP 理论关注的是在绝对情况下,在工程上,可用性和一致性并不是完全对立的,我们关注的往往是如何在保持相对一致性的前提下,提高系统的可用性。
- CA 架构:不支持分区容错,只支持一致性和可用性。
不支持分区容错性,也就意味着不允许分区异常,设备、网络永远处于理想的可用状态,从而让整个分布式系统满足一致性和可用性。
但由于分布式系统是由众多节点通过网络通信连接构建的,设备故障、网络异常是客观存在的,而且分布的节点越多,范围越广,出现故障和异常的概率也越大,因此,对于分布式系统而言,分区容错 P 是无法避免的,如果避免了 P,只能把分布式系统回退到单机单实例系统。
- CP 架构:因为分区容错 P 客观存在,即相当于放弃系统的可用性,换取一致性。
系统在遇到分区异常时,会持续阻塞整个服务,直到分区问题解决,才恢复对外服务,这样可以保证数据的一致性。选择 CP 的业务场景比较多,特别是对数据一致性特别敏感的业务最为普遍。比如在支付交易领域,Hbase 等分布式数据库领域,都要优先保证数据的一致性,在出现网络异常时,系统就会暂停服务处理。分布式系统中,用来分发及订阅元数据的 Zookeeper,也是选择优先保证 CP 的。因为数据的一致性是这些系统的基本要求,否则,银行系统 余额大量取现,数据库系统访问,随机返回新老数据都会引发一系列的严重问题。
- AP 架构:由于分区容错 P 客观存在,即相当于放弃系统数据的一致性,换取可用性。
在系统遇到分区异常时,节点之间无法通信,数据处于不一致的状态,为了保证可用性,服务节点在收到用户请求后立即响应,那只能返回各自新老不同的数据。这种舍弃一致性,而保证系统在分区异常下的可用性,在互联网系统中非常常见。比如微博多地部署,如果不同区域的网络中断,区域内的用户仍然发微博、相互评论和点赞,但暂时无法看到其他区域用户发布的新微博和互动状态。对于微信朋友圈也是类似。还有如 12306 的火车购票系统,在节假日高峰期抢票时,偶尔也会遇到,反复看到某车次有余票,但每次真正点击购买时,却提示说没有余票。这样,虽然很小一部分功能受限,但系统整体服务稳定,影响非常有限,相比 CP,用户体验会更佳。
CAP 问题及误区
CAP 理论极大的促进了分布式系统的发展,但随着分布式系统的演进,大家发现,其实 CAP 经典理论其实过于理想化,存在不少问题和误区。
首先,以互联网场景为例,大中型互联网系统,主机数量众多,而且多区域部署,每个区域有多个 IDC。节点故障、网络异常,出现分区问题很常见,要保证用户体验,理论上必须保证服务的可用性,选择 AP,暂时牺牲数据的一致性,这是最佳的选择。
但是,当分区异常发生时,如果系统设计的不够良好,并不能简单的选择可用性或者一致性。例如,当分区发生时,如果一个区域的系统必须要访问另外一个区域的依赖子服务,才可以正常提供服务,而此时网络异常,无法访问异地的依赖子服务,这样就会导致服务的不可用,无法支持可用性。同时,对于数据的一致性,由于网络异常,无法保证数据的一致性,各区域数据暂时处于不一致的状态。在网络恢复后,由于待同步的数据众多且复杂,很容易出现不一致的问题,同时某些业务操作可能跟执行顺序有关,即便全部数据在不同区域间完成同步,但由于执行顺序不同,导致最后结果也会不一致。长期多次分区异常后,会累积导致大量的数据不一致,从而持续影响用户体验。
其次,在分布式系统中,分区问题肯定会发生,但却很少发生,或者说相对于稳定工作的时间,会很短且很小概率。当不存在分区时,不应该只选择 C 或者 A,而是可以同时提供一致性和可用性。
再次,同一个系统内,不同业务,同一个业务处理的不同阶段,在分区发生时,选择一致性和可用性的策略可能都不同。比如前面讲的 12306 购票系统,车次查询功能会选择 AP,购票功能在查询阶段也选择 AP,但购票功能在支付阶段,则会选择 CP。因此,在系统架构或功能设计时,并不能简单选择 AP 或者 CP。
而且,系统实际运行中,对于 CAP 理论中的每个元素,实际并不都是非黑即白的。比如一致性,有强一致性,也有弱一致性,即便暂时大量数据不一致,在经历一段时间后,不一致数据会减少,不一致率会降低。又如可用性,系统可能会出现部分功能异常,其他功能正常,或者压力过大,只能支持部分用户的请求的情况。甚至分区也可以有一系列中间状态,区域网络完全中断的情况较少,但网络通信条件却可以在 0~100% 之间连续变化,而且系统内不同业务、不同功能、不同组件对分区还可以有不同的认知和设置。
最后,CAP 经典理论,没有考虑实际业务中网络延迟问题,延迟自始到终都存在,甚至分区异常P都可以看作一种延迟,而且这种延迟可以是任意时间,1 秒、1 分钟、1 小时、1 天都有可能,此时系统架构和功能设计时就要考虑,如何进行定义区分及如何应对。
基于 ZooKeeper 的服务发现(CP)
流程分析
1、服务平台管理端先在 ZooKeeper 中创建一个服务根路径,可以根据接口名命名(例如: /dubbo/com.foo.BarService),在这个路径再创建服务提供方目录与服务调用方目录(例如:providers、consumers),分别用来存储服务提供方的节点信息和服务调用方的节点信息。
2、当服务提供方发起注册时,会在服务提供方目录中创建一个临时节点,节点中存储该服务提供方的注册信息。
创建临时节点是因为临时节点的生命周期与客户端会话相关,所以一旦提供者所在的机器出现故障导致提供者无法提供服务,该临时节点就会自动从 Zookeeper 删除。
3、当服务调用方发起订阅时,则在服务调用方目录中创建一个临时节点,节点中存储该服务调用方的信息,同时服务调用方 watch 该服务的服务提供方目录 (/dubbo/com.foo.BarService/providers)中所有的服务节点数据。
4、当服务提供方目录下有节点数据发生变更时,ZooKeeper 就会通知给发起订阅的服务调用方。
存在的问题
问题一:
ZooKeeper 的一大特点就是强一致性,ZooKeeper 集群的每个节点的数据每次发生更新操作,都会通知其它 ZooKeeper 节点同时执行更新。
当连接到 ZooKeeper 的节点数量特别多,对 ZooKeeper 读写特别频繁,且 ZooKeeper 存储的目录达到一定数量的时候,ZooKeeper 将不再稳定,CPU 持续升高,最终宕机。而宕机之后,由于各业务的节点还在持续发送读写请求,刚一启动,ZooKeeper 就因无法承受瞬间的读写压力,马上宕机。
问题二:
ZooKeeper 无法正确处理服务发现的网络分区。在 ZooKeeper 中,无法达到仲裁数量的分区的节点客户端完全无法与ZooKeeper 及其服务发现机制进行通信。
基于 Eureka 的服务发现(AP)
流程分析
1、Eureka-Client 在初始化时会将服务实例信息注册到任意一个 Eureka-Server,并且每隔 30 秒发送心跳请求。
2、该 Eureka-Server 会将注册、心跳的请求,批量打包同步到其他 Eureka-Server。
存在的问题
问题一:
订阅端拿到的是服务的全量的地址:这个对于客户端的内存是一个比较大的消耗,特别在多数据中心部署的情况下,某个数据中心的订阅端往往只需要同数据中心的服务提供端即可。
问题二:
客户端采用周期性向服务端主动 pull 服务数据的模式(也就是客户端轮训的方式),这个方式存在实时性不足以及无谓的拉取性能消耗的问题。
问题三:
Eureka 集群的多副本的一致性协议采用类似“异步多写”的 AP 协议,每一个 server 都会把本地接收到的写请求发送给组成集群的其他所有的机器(Eureka 称之为 peer),特别是 hearbeat 报文是周期性持续不断的在 client->server->all peers 之间传送;这样的一致性算法,导致了如下问题
- 每一台Server都需要存储全量的服务数据,Server 的内存明显会成为瓶颈。
- 当订阅者却来越多的时候,需要扩容 Eureka 集群来提高读的能力,但是扩容的同时会导致每台 server 需要承担更多的写请求,扩容的效果不明显。
- 组成 Eureka 集群的所有 server 都需要采用相同的物理配置,并且只能通过不断的提高配置来容纳更多的服务数据。
扩展:Eureka 2.0
Eureka 2.0主要就是为了解决上述问题而提出的,主要包含了如下的改进和增强:
- 数据推送从 pull 走向 push 模式,并且实现更小粒度的服务地址按需订阅的功能。
- 读写分离:写集群相对稳定,无需经常扩容;读集群可以按需扩容以提高数据推送能力。
- 新增审计日志的功能和功能更丰富的 Dashboard。
阿里 Nacos
与 Eureka 1.x 对比增强的地方如下:
- 去除了基于客户端的同步模式,采用了批量的基于长连接级别的数据同步+周期性的 renew 的方案来保证数据的一致性;
- 客户端通过订阅感兴趣的服务信息,服务端只会"推送"客户端感兴趣的少量数据给客户端。
- 集群拆分成 session 和 data 两个集群,客户端分片的把服务数据注册到 session 集群中,session 集群会把数据异步的写到 data 集群,data 集群完成服务数据的聚合后,把压缩好的服务数据推送到 session 层缓存下来,客户端可以直接从session 层订阅到所需要的服务数据。
注:Nacos 服务信息"推送"本质还是拉模式,具体流程为:
1、Nacos 客户端会循环请求服务端变更的数据,并且超时时间设置为 30s,当配置发生变化时,请求的响应会立即返回;
2、服务端数据变更后,找到具体的客户端请求中的 response,然后直接将结果写入 response 中;
3、Nacos 客户端就能够实时感知到服务端配置发生了变化。
总结
对于服务发现而言,拥有可能包含虚假信息的信息要比根本不拥有任何信息更好,所以个人认为 AP 优于 CP。
在 AP 模式下,如果请求到不准确的服务实例信息,导致请求发送到一个宕机的服务端,只要做好失败重试机制和负载均衡,这次请求能够顺利的进行。
扩展阅读
CP vs AP for service discovery(consul)
Why not use Curator/Zookeeper as a service registry?
Eureka! Why You Shouldn’t Use ZooKeeper for Service Discovery
参考资料
Service Discovery in a Microservices Architecture
极客时间专栏:RPC实战与核心原理
极客时间专栏:分布式技术原理与算法解析
更多推荐
所有评论(0)