总结和延伸一下CAP理论。强烈建议先看一下之前翻译的一篇博客,对CAP理论先有一个基本的认识:
常见的分类
我们首先对常用的技术框架,进行一个CAP上的分类
CA | CP | AP |
---|---|---|
MySQL | Zookeeper | Eureka |
PostgreSQL | ||
Oracle |
聊聊CA,数据库
其实一定程度来来说,满足CA的都是单点系统,这个在证明中其实可以看出来,满足持久化+可用性,就意味着节点必须不满足分区容忍性,那么就必定不支持分布式必须的场景:多节点组成集群,所以满足CA的系统基本都是单点系统,即一些传统数据库
传统数据库的CA显而易见,写入后立即读取到的就是新值(事务、写盘保证),并且系统保证高可用,对于任何请求都会在处理后马上返回。
实际上,如果不具备任何分区容忍性的系统,可以说根本不具备形成分布式系统的条件,因为都没有分区容忍性了,node之间可以认为全部都是割裂的,只有单点最好满足CA。
CA系统一般有以下特点:
- 单机系统(没有P的容忍,何谈分布式集群,不过也有方法,后面会说)
- 一般多是数据库系统,系统稳定性非常高
聊聊CP,强一致性
典型的CP就是Zookeeper,我们看看Zookeepr如何满足CP的:
以下内容来自GPT:
1. 一致性(Consistency):
Zookeeper 保证了强一致性(linearizability)对于写操作的顺序和结果是严格的,遵循以下原则:
•线性一致性:Zookeeper 保证所有写操作以相同的顺序提交到各个节点。这意味着所有客户端无论连接到哪个节点,看到的写操作顺序都是相同的。
•多数派机制:Zookeeper 使用多数派(quorum)来确保数据的一致性。写操作必须在集群中超过一半的节点上提交成功,才能视为写成功。即使部分节点宕机,只要有超过半数的节点正常,Zookeeper 依然能保证数据一致性。
•事务ID(ZXID):每个写操作在 Leader 节点上会分配一个全局唯一的事务ID(ZXID),并按照严格的顺序分发给 Follower 节点。Follower 在收到 Commit 消息后,会按照 ZXID 的顺序应用这些事务,确保数据一致。
2. 分区容忍性(Partition Tolerance):
在分布式系统中,网络分区是不可避免的。Zookeeper 通过以下机制来确保即使在网络分区的情况下,依然能提供一致性:
•多数派机制:即使部分节点因网络故障失联,只要集群中有超过半数的节点能彼此通信,Zookeeper 就能继续工作。不能与大多数节点通信的节点将停止处理请求,避免返回不一致的数据。
•Leader选举:当网络分区或 Leader 宕机时,Zookeeper 会通过选举机制确保集群中的存活节点选出一个新的 Leader。只有新的 Leader 才能处理写请求。网络分区导致的少数派节点无法选出新的 Leader,从而不会处理写请求,保证一致性。
Zookeeper通过选举来确认leader,然后使用一主多从的方式来保障一致性,每次写只能在主节点上,此时主节点对从节点发起一次全局的commit以尝试在所有从节点上也写入新数据,只有在包含从节点以内半数以上节点写入成功,才认为这次写入成功了
总结来说,Zookeeper通过以下途径保障了C(一致性):
- 只在主节点写入,从节点只能读(从节点可能有延迟,因为半数以上写入成功就写成功,失败的有追赶机制)
- 写入时,半数以上节点写入才算写入成功
- 如果有节点crash,从新根据最新数据的事务id的多数派去选举新leader(写入需要半数以上,那么有节点crash以后,必定有一半是最新状态)
- 选举之类的通过zookeeper自己实现的类似paxios的分布式选举协议实现。
可以看到,zookeeper的一致性好像没有想象那么强,半数节点写入成功就算写成功,那么只针对从节点来说,有可能读到的不是实时的数据。
接下来我们来看下ZK的P(分区容忍性),其实也和选举有关,ZK具备一定的分区容忍性。通过选举的机制,那些有网络故障的节点因为失去leader的心跳退出集群、亦或者leader已经crash了,剩下的从节点们也会发起选举选出新的leader。在集群还保持在开始数量的一半以上时,ZK遭遇网络故障可以通过选举来确定新的leader,来保障整个集群具备分区容忍性。
脑裂问题。之所以说ZK具备一定的分区容忍性,是因为ZK在遭遇极端网络故障的情况下,可能会产生脑裂,即网络状况由一个集群分裂为了两个集群,出现了各自两个leader。
所以,zookeeper的集群部署一般为奇数。比如部署5台与4台,此时半数以上决策都需要至少3台存活节点,但是5台可以允许2个节点挂掉。同时,奇数可以避免脑裂问题,当集群划为两个分区:
- 5台的话至少会划分为3台与2台的各自2个分区,其中2台的分区无法选出主会宕机,3台的依旧可以达成共识继续提供服务。
- 如果是4台,那么可能划分为2台与2台2个分区,这两个分区分别无法完成选举,集群宕机
我们看看ZK的可用性如何:
ZK无法保证可用性,我们先看看读场景。虽然从节点可能读到旧数据,但是在需求强一致的情况下,可以使用sync()API来保障从节点同步了leader节点的数据,此时在强一致下就无法提供可用性了,因为需要从leader同步数据这一步,此时发生网络中断,那么就无论如何也不可能读到最新值了。
写也是,在写的时候,如果发生了网络故障,半数以上节点无法确认写,或者集群正在进行选举,那么请求就不能被正确响应。
CP系统一般都有以下特点:
- 主从模式
- 具备相应的选举以及事务机制
聊聊AP
比较典型的AP应该就是Spring Cloud的Eureka注册中心了。Eureka可以在CP和AP之间横跳,取决于它的保护模式。在保护模式打开的情况下,如果丢失了一些实例的心跳,那么它会保留一段时间实例的信息(偏向于认为是子暂时网络故障,而不是马上认为实例不可用剔除掉),来实现AP。
但是显然,此时返回的数据会各不相同,比如实例A注册到了注册中心E1和E2,实例B去E1查询实例A时发现实例A已下线,而去E2查询时却发现实例A又在线。这显然不满足一致性:
当一个读操作发生在写操作完成之后,那么这个读操作返回的必须是这个写操作的值,或者是最后一个写操作值
AP系统一般有以下特点:
- 无主模式
- 实例间一般通过类似广播的模式通信(注册中心之间),信息冗余
- 存在重试机制(例如eureka,在请求的某台实例不可用时,会自动重试几次,实际就是考虑了AP的问题)
加深理解
不要把CAP的概念搞混淆
CAP中因为所谓的持久性、可用性这两个特性,经常会在其他场景用到,所以很容易搞混淆。我们要记住分布式CAP讨论的这两个定义:
当一个读操作发生在写操作完成之后,那么这个读操作返回的必须是这个写操作的值,或者是最后一个写操作值
每个对于正常节点(non-failing node )的请求,都可以收到正常的响应
如果抛开这两个定义,经常就会讨论错重点,比如一致性,只表达分布式系统响应成功了就应该具备一致性。但是很容易混淆其他概念,就混淆到了数据是在内存还是落盘了,还是写日志了。进入到这种更细的讨论,往往混淆了持久化与一致性在ACID和分布式集群中表达的不同含义的概念。
CAP的结合
CAP不能同时完美存在,但是经常的设计是让那个不能存在的属性以较弱的方式去呈现。
CA弱P
比如前面讨论CA时说的,CA基本是单机系统,但是也可以集群,只是分区容忍很差。比如古早使用MySQL的binlog订阅来实现主从,以及使用keepalived+MySQL多主互相订阅来实现集群的HA(高可用)。
这些方案可以让CA实现集群,但是显而易见的是,通过binlog(实质还是数据复制)的方式,分区容忍性及其的低。但是也可以通过代理层+心跳等方式,使这样的集群具备更好的分区容忍性。
CP弱A
CP弱A一般是通过对CP,尤其是其中C的牺牲,来换来一定的A。比如写操作可以开启纯异步同步(没具体查ZK支不支持,但是包括MQ,ES,都是这个套路),来保障leader节点写入即可,其他节点交给集群内部异步同步。读操作可以通过允许读到旧数据,来达成弱A。
AP弱C
这里说到之前待过的一个架构组的一个方案,之前待得某家公司用Spring Cloud时,不太能忍受发布时节点下线但是调用端延迟感知的问题(我寻思这本身不是AP的优点吗),所以架构组修改了实例销毁的钩子,增加了一个发送MQ的功能,然后由另外的应用去消费MQ再去Eureka注册中心调用api下线该实例。
这里不谈这个方案的好坏,但是可以看到纯AP的环境下,也可以通过一些其他方式来保障一定的C。
AP弱C-最终一致性
还有就是经典的最终一致性场景,我们的应用和分布式事务在考虑高并发和高可用的情况下,大多使用消息来实现最终一致性,现在你可以很轻松的分析了。
举例说明,比如下单和支付场景,假如其中使用了消息来保障最终一致性,那么在消息有一定积压的情况下,就可能会出现存在支付订单已支付但是购物订单还是待支付状态的情况,此时显然不满足一致性。
这是允许一致性的延迟,来提供高可用(实例处理完后丢mq同步即可,不需要等待下游业务系统状态一致)。并且通过中间件的解耦,来提升分区容忍性(依旧是丢mq即可,下游系统哪怕5分钟内不可用,也可以在恢复后通过mq达成最终一致)。
具体情况具体分析
CAP表明不能同时满足这三个属性,但是不代表一个系统不能一定程度上具备这三个属性,只是无法完美的对标上定义,所以具体情况要具体分析。
举一个具体情况具体分析的经典例子:ES满足CAP中的哪几个?
我们看看stackoverflow上的经典回答,可以看一下其他人讨论CAP问题的方式:
The answer is not so straightforward. It depends upon how the system is configured and how you want to use it. I will try to go into the details.
答案并不简单。这取决于系统的配置方式以及你打算如何使用它。我会尝试详细说明。
Paritioning in ElasticSearch
- Each index is partitioned in shards, meaning data in each shard is mutually exclusive to other shards. Each shard further has multiple Lucence indices, which are not in the scope of this answer.
ElasticSearch中的分片 每个索引都被分成多个分片,这意味着每个分片中的数据是彼此独立的。每个分片还有多个Lucene索引,但这不在本文讨论范围内。)
- Each shard can have a replica running (most setups have) and in an event of a failure, the replica can be promoted to a master. Let\'s call a shard that has a primary working and is reachable from the ES node that our application server is hitting as an Active shard. Thus, a shard with no copies in primary and is not reachable is considered as failed shard. (Eg: An error saying all shards failed means no primaries in that index are available)
每个分片都可以有一个副本在运行(大多数设置都有),如果发生故障,副本可以被提升为主分片。我们将一个主分片正常工作且可以从应用服务器连接的ES节点访问的分片称为“活跃分片”。因此,主分片没有副本且无法访问的分片被认为是“失效分片”。(例如,显示“所有分片失效”的错误意味着该索引中没有可用的主分片)
- ES has a feature to have multiple primaries (divergent shards). It is not a good situation as we lose both read/write consistencies.
ES有一个功能可以允许多个主分片共存(分裂分片)。这不是一种理想的情况,因为我们会失去读写一致性。
读场景:
By default reads will continue to happen on shards that are active. Thus, the data from failed shards will be excluded from our search queries. In this context, we consider the system to be AP. However, the situation is temporary and does not require manual effort to synchronize shard when the cluster is connected again.
默认情况下,读取操作将继续在活跃的分片上进行。因此,失效分片中的数据将不会包含在我们的搜索查询中。在这种情况下,我们认为系统是AP的。然而,这种情况是暂时的,并且在集群重新连接时,无需手动同步分片。
By setting a search option allow_partial_search_results [1] to false, we can force the system to error when some of the shards have failed, guaranteeing consistent results. In this context, we consider the system to be CP.
通过将搜索选项 allow_partial_search_results [1] 设置为 false,我们可以在某些分片失效时强制系统报错,从而保证结果的一致性。在这种情况下,我们认为系统是CP的。
In case no primaries are reachable from the node(s) that our application server is connecting to, the system will completely fail. Even if we say that our partition tolerance has failed, we also see that availability has taken a hit. This situation can be called be just C or CP.
如果应用服务器所连接的节点无法访问任何主分片,系统将完全失效。即使我们说分区容忍性失效了,我们也可以看到可用性受到了影响。这种情况可以称为C或CP。
There can be cases where the team has to anyhow bring up the shards and their out of sync replica(s) were reachable. So they decide to make it a primary (manually). Note that there can be some un-synced data resulting in divergent shards. This results in the AP situation. Consistency will be hard to restore when the situation normalizes (sync shards manually).
在某些情况下,团队必须强行启动分片,而它们的不同步副本是可访问的。因此,团队决定手动将副本提升为主分片。需要注意的是,可能存在一些未同步的数据,导致分片分裂。这会导致AP的情况。当情况恢复正常时,一致性将很难恢复(需要手动同步分片)。
写场景:
Only if all shards fail, writes will stop working. But even if one shard is active, writes will work and are consistent (by default). This will be CP.
只有当所有分片失效时,写入操作才会停止。但是即使有一个分片是活跃的,写入操作仍然可以进行并且是一致的(默认情况下)。这将是CP的情况。
However, we can set option index-wait-for-active-shards [2] as all to ensure writes only happen when all shards in the index are active. I only see a little advantage of the flag, which would be to keep all shards balanced at any cost. This will be still CP (but lesser availability than the previous case).
然而,我们可以将选项 index-wait-for-active-shards [2] 设置为 all,以确保只有当索引中的所有分片都是活跃状态时才会进行写入。我认为这个选项的好处很有限,只是为了不惜一切代价保持所有分片的平衡。这样系统仍然是CP的(但比前一种情况的可用性更低)。
Like in the last read network partition case, if we make un-synced replicas as primary (manually) there can be some data loss and divergent shards. The situation will be AP here and consistency will be hard to restore when the situation normalizes (sync shards manually).
就像上一个读取网络分区的情况一样,如果我们手动将未同步的副本提升为主分片,可能会出现数据丢失和分片分裂的情况。在这里,系统将处于AP状态,并且在情况恢复正常时,一致性将很难恢复(需要手动同步分片)。
结合前文就讨论过的场景,许多时候你开启某些配置,他就更加CP了,关闭了,则更加AP,这取决于你更希望你的系统在哪方面具备更好的适应性。分布式系统与操作系统一样,因为CAP理论存在,分布式系统重永远在trade-off,没有银弹。
总结
通过本文和之前翻译的CAP理论的证明,希望大家都对CAP理论和分布式系统设计中对CAP的trade-off有更深的理解,也可以提升分析分布式系统中CAP三个要素的能力。