服务注册与发现 — 选择 CP 还是 AP?

概述

为什么要使用服务发现

假设您正在编写一些代码,这些代码将调用具有 REST API 的服务。为了发出请求,您的代码需要知道服务实例的网络位置(IP 地址和端口)。在物理硬件上运行的传统应用程序中,服务实例的网络位置是相对静态的。例如,您的代码可以从偶尔更新的配置文件中读取网络位置。

但是,在现代的基于云的微服务应用程序中,这是一个要解决的难题,如下图所示:
服务注册与发现 — 选择 CP 还是 AP?_第1张图片
服务实例具有动态分配的网络位置。而且,服务实例集会由于自​​动缩放,故障和升级而动态更改。因此,您的客户端代码需要使用更复杂的服务发现机制。

客户端查询服务注册表,该服务注册表是可用服务实例的数据库。然后,客户端使用负载平衡算法来选择可用的服务实例之一并发出请求。

服务注册与发现 — 选择 CP 还是 AP?_第2张图片
服务实例的网络位置在启动时会在服务注册表中注册。实例终止时,将从服务注册表中将其删除。通常使用心跳机制定期刷新服务实例的注册。

CAP 理论

什么是 CAP?

在一个分布式系统(指互相连接并共享数据的节点的集合)中,当涉及读写操作时,只能保证一致性(Consistence)、可用性(Availability)、分区容错性(Partition Tolerance)三者中的两个,另外一个必须被牺牲。

  • Consistency:对某个指定的客户端来说,读操作保证能够返回最新的写操作结果。
  • Availability:非故障的节点在合理的时间内返回合理的响应(不是错误和超时的响应)。
  • Partition Tolerance:当出现网络分区后,系统能够继续“履行职责”。

CAP 为什么不能同时满足?

因为网络本身无法做到 100% 可靠,有可能出故障,所以分区是一个必然的现象。如果我们选择了 CA 而放弃了 P,那么当发生分区现象时,为了保证 C,系统需要禁止写入,当有写入请求时,系统返回 error(例如,当前系统不允许写入),这又和 A 冲突了,因为 A 要求返回 no error 和 no timeout。因此,分布式系统理论上不可能选择 CA 架构,只能选择 CP 或者 AP 架构。

CP - Consistency/Partition Tolerance:

出现网络分区时,往节点 A 插入新数据,但是由于分区故障导致数据无法同步,此时节点 A 和节点 B 数据不一致,为了保证一致性。客户端查询时只能返回 error,违背了 Availability。

AP - Availability/Partition Tolerance:

出现网络分区时,往节点 A 插入新数据,但是由于分区故障导致数据无法同步,此时节点 A 和节点 B 数据不一致,为了保证可用性,客户端查询时可能看到不同的两份数据,违背了 Consistency。

CAP 选择策略

对于涉及钱的交易时,数据的一致性至关重要,因此保 CP 弃 A 应该是最佳选择。 2015 年发生的支付宝光纤被挖断的事件,就导致支付宝就出现了不可用的情况。显然,支付宝当时的处理策略就是,保证了 CP 而牺牲了 A。

而对于其他场景,大多数情况下的做法是选择 AP 而牺牲 C,因为很多情况下不需要太强的一致性(数据始终保持一致),只要满足最终一致性即可。

保 CP 弃 A

如果一个分布式场景需要很强的数据一致性,或者该场景可以容忍系统长时间无响应的情况下,保 CP 弃 A 这个策略就比较适合。

一个保证 CP 而舍弃 A 的分布式系统,一旦发生网络分区会导致数据无法同步情况,就要牺牲系统的可用性,降低用户体验,直到节点数据达到一致后再响应用户。

保证 CP 的系统有很多,典型的有 Redis、HBase、ZooKeeper 等。

在 ZooKeeper 集群中,Leader 节点之外的节点被称为 Follower 节点,Leader 节点会专门负责处理用户的写请求:

  • 当用户向节点发送写请求时,如果请求的节点刚好是 Leader,那就直接处理该请求;
  • 如果请求的是 Follower 节点,那该节点会将请求转给 Leader,然后 Leader 会先向所有的 Follower 发出一个 Proposal,等超过一半的节点同意后,Leader 才会提交这次写操作,从而保证了数据的强一致性。

当出现网络分区时,如果其中一个分区的节点数大于集群总节点数的一半,那么这个分区可以再选出一个Leader,仍然对用户提供服务,但在选出 Leader 之前,不能正常为用户提供服务;如果形成的分区中,没有一个分区的节点数大于集群总节点数的一半,那么系统不能正常为用户提供服务,必须待网络恢复后,才能正常提供服务。

这种设计方式保证了分区容错性,但牺牲了一定的系统可用性。

CAP 是忽略网络延迟的,理论中的 C 在实践中是不可能完美实现的,在数据复制的过程中,节点 A 和节点 B 的数据并不一致。

保 AP 弃 C

如果一个分布式场景需要很高的可用性,或者说在网络状况不太好的情况下,该场景允许数据暂时不一致,那这种情况下就可以牺牲一定的一致性了。

网络分区出现后,各个节点之间数据无法马上同步,为了保证高可用,分布式系统需要即刻 响应用户的请求。但此时可能某些节点还没有拿到最新数据,只能将本地旧的数据返回给用户,从而导致数据不一致的情况。

适合保证 AP 放弃 C 的场景有很多。比如,很多查询网站、电商系统中的商品查询等,用户体验非常重要,所以大多会保证系统的可用性,而牺牲一定的数据一致性。

目前,采用保 AP 弃 C 的系统也有很多,比如 CoachDB、Eureka、Cassandra、DynamoDB 等。

对比分析:

CP 模型,采用 CP 模型的分布式系统,一旦因为消息丢失、延迟过高发生了网络分区,就影响用户的体验和业务的可用性。因为为了防止数据不一致,集群将拒绝新数据的写入。

AP 模型,采用 AP 模型的分布式系统,实现了服务的高可用。用户访问系统的时候,都能得到响应数据,不会出现响应错误,但当出现分区故障时,相同的读操作,访问不同的节点,得到响应数据可能不一样。

CAP 理论的延伸:Base 理论

BASE 理论是 CAP 理论中的 AP 的延伸,是对互联网大规模分布式系统的实践总结,强调可用性。

它的核心就是基本可用(Basically Available)、软状态( Soft State)、最终一致性( Eventual Consistency),核心思想是即使无法做到强一致性(CAP 的一致性就是强一致性),但应用可以采用适合的方式达到最终一致性。

  • Basically Available:分布式系统在出现故障时,允许损失部分可用性,即保证核心可用。
  • Soft State:允许系统存在中间状态,而该中间状态不会影响系统整体可用性。这里的中间状态就是 CAP 理论中的数据不一致。
  • Eventual Consistency:系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。

基于 ZooKeeper 的服务发现(CP)

流程分析

服务注册与发现 — 选择 CP 还是 AP?_第3张图片
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)

流程分析

服务注册与发现 — 选择 CP 还是 AP?_第4张图片
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

服务注册与发现 — 选择 CP 还是 AP?_第5张图片
与 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

阿里巴巴为什么不用 ZooKeeper 做服务发现?

参考资料

Service Discovery in a Microservices Architecture

极客时间专栏:RPC实战与核心原理

极客时间专栏:分布式技术原理与算法解析

阿里巴巴服务注册中心产品ConfigServer 10年技术发展回顾

你可能感兴趣的:(微服务)