为什么要聊分布式
单机时代其实挺好的。一个数据库实例,所有数据都在本地磁盘,事务ACID四个字母背后有成熟的实现,不用操心数据一致性------因为只有一份数据。
但业务不允许我们一直待在单机的舒适区。当QPS从几百涨到几万,当数据量从几个G涨到几个T,当一台服务器挂掉意味着全部业务停摆------你不得不走向分布式。
走向分布式的那一刻,一切都变了。数据有了多个副本,节点之间要通过网络通信,网络会延迟、会丢包、会断开。你突然发现,曾经在单机上理所当然的事情------"写进去的数据一定能读到""两个操作要么都成功要么都失败"------在分布式环境下变成了需要付出巨大代价才能勉强保证的东西。
这篇文章想把分布式系统的核心问题讲透:从最底层的理论约束(CAP),到解决这些约束的算法设计(Paxos、Raft、Gossip等),再到这些算法在主流中间件里的工程落地(ZooKeeper、etcd、Redis Cluster、Kafka等)。
CAP:分布式的不可能三角
2000年,Eric Brewer提出了CAP猜想(后被证明为定理):
- C(Consistency):一致性------所有节点在同一时刻看到的数据是相同的
- A(Availability):可用性------每个请求都能在合理时间内收到非错误的响应
- P(Partition Tolerance):分区容忍性------网络分区发生时系统仍能继续运行
定理的结论:三者最多只能同时满足两个。
为什么P是必选项
很多人第一次听到CAP时会想:"那我选CA不就好了?"
不行。在分布式系统中,网络分区不是"要不要支持"的问题,而是"一定会发生"的事实。网线会被挖断、交换机会故障、机房之间的专线会抖动。你没法选择"不允许网络分区",只能选择在分区发生时,是牺牲一致性(AP)还是牺牲可用性(CP)。
所以CAP的真实选择是:CP还是AP。
CP系统:宁可不服务,也不给错数据
典型代表:ZooKeeper、etcd、HBase。
当网络分区发生时,如果Leader节点无法跟多数派通信,它会主动放弃领导权,停止对外服务。少数派的节点也不会响应写请求。系统在分区期间可能部分或完全不可用,但保证一旦恢复,数据一定是一致的。
适用场景:配置中心、分布式锁、元数据管理------这些场景容不得半点错误,宁可等一等,也不能给出一个错误的响应。
AP系统:宁可给旧数据,也不拒绝你
典型代表:Cassandra、DynamoDB、DNS。
当网络分区发生时,每个节点仍然接受读写请求。不同分区的节点可能会产生数据冲突,等分区恢复后再通过冲突解决机制(如向量时钟、Last-Write-Wins)来合并。
适用场景:购物车、用户画像、社交动态------这些场景对实时一致性要求不高,但必须随时可用。
CAP的现实:不是非黑即白
实际工程中,很少有系统是纯粹的CP或AP。更多是在正常情况下提供强一致性,在网络分区时做降级:
- 允许读请求走本地副本(牺牲一致性,保证读可用)
- 写请求仍然要求多数派确认(保证写一致性)
- 分区恢复后做数据修复
这就引出了一个比CAP更贴近工程实际的理论------BASE(Basically Available, Soft state, Eventually consistent),本质上是对AP的工程化表达:基本可用、软状态、最终一致。
共识算法:分布式的"宪法"
CAP告诉我们约束在哪,共识算法告诉我们怎么在约束下工作。
分布式系统最核心的问题是:多个节点如何对同一个值达成一致? 这就是共识问题。
Paxos:理论上的完美,工程上的噩梦
Leslie Lamport在1990年提出Paxos算法,这是第一个被严格证明正确的共识算法。
Paxos的核心角色:
- Proposer:提案者,提出一个值
- Acceptor:接受者,投票决定是否接受
- Learner:学习者,学习最终被选定的值
算法分两个阶段:
Phase 1(Prepare):Proposer选一个提案编号n,向多数派Acceptor发送Prepare(n)请求。Acceptor如果没见过比n更大的编号,就承诺不再接受编号小于n的提案,并返回它之前已接受的最高编号提案。
Phase 2(Accept):Proposer收到多数派的承诺后,用自己的值(或Phase 1中返回的已接受值)发送Accept(n, v)请求。Acceptor如果没有在此期间承诺过更高编号,就接受这个提案。
当多数派Acceptor接受了同一个提案,共识就达成了。
Paxos的问题是什么?太难实现了。 原始Paxos论文描述的是单次共识,要做成连续的状态机复制(Multi-Paxos)需要大量工程扩展。Google的Chubby内部实现了Paxos,据说代码量和复杂度远超预期。Lamport自己都说:"Paxos算法的描述方式让很多人觉得它很难理解。"
Raft:为可理解性而生
2014年Diego Ongaro提出Raft,明确目标就是做一个跟Paxos同等正确但更容易理解和实现的共识算法。
Raft把共识问题分解为三个相对独立的子问题:
1. Leader选举
集群中始终只有一个Leader。每个节点有三种状态:Leader、Follower、Candidate。
- 正常情况下,Leader定期发送心跳给Follower
- 如果Follower超过一段时间(Election Timeout)没收到心跳,就变成Candidate,发起选举
- Candidate向所有节点请求投票,获得多数派投票则成为新Leader
- 每次选举,任期号(Term)递增。节点发现更高Term时,自动退为Follower
为什么这个设计很巧妙?Election Timeout是随机的(比如150ms~300ms之间随机),这使得多个节点同时发起选举的概率很低,大多数情况下一轮就能选出Leader。
2. 日志复制
客户端所有写操作都发给Leader。Leader将操作追加到本地日志,然后并行发送给所有Follower。当多数派Follower确认收到后,Leader将这条日志标记为committed,应用到状态机并返回客户端。
关键点:日志的顺序是严格一致的。Term + Index唯一标识每条日志。如果Follower的日志跟Leader不一致,Leader会回溯找到分歧点,用自己的日志覆盖Follower的。
3. 安全性保证
- 选举限制:只有日志至少跟多数派一样新的节点才能当选Leader(保证新Leader包含所有committed的日志)
- 提交限制:Leader只能提交当前Term的日志(避免覆盖前任已提交的日志)
Raft相比Paxos的工程优势非常明显:角色划分清晰(Leader/Follower/Candidate)、日志只从Leader流向Follower(单向数据流)、新Leader一定包含所有已提交数据(不需要回放历史)。这就是为什么etcd、Consul、TiKV都选择了Raft而不是Paxos。
ZAB:ZooKeeper的变种Paxos
ZooKeeper用的是ZAB(ZooKeeper Atomic Broadcast)协议,本质上是Multi-Paxos的一个工程简化版。跟Raft很像,也是Leader-based,也要求多数派确认。
ZAB的特殊之处在于它保证了全序广播:所有事务按照Leader分配的zxid(递增事务ID)严格有序地应用到所有节点。这使得ZooKeeper可以提供比普通共识更强的顺序性保证。
Gossip:去中心化的"八卦"协议
前面的算法都是Leader-based的,需要一个中心节点协调。Gossip协议走的是另一条路------完全去中心化。
灵感来自流行病传播模型。每个节点定期随机选择几个邻居,把自己知道的信息"八卦"给它们。收到信息的节点再继续传播。最终所有节点都会收到所有信息------虽然不能保证何时,但能保证最终一定会。
Gossip的特点:
- 去中心化:没有Leader,所有节点平等
- 最终一致:不保证实时一致,但保证最终所有节点状态相同
- 容错性极强:任何节点挂了都不影响整体,因为没有单点
- 扩展性好:新增节点只需要知道任意一个现有节点即可加入
代价是什么?收敛时间不可控。在一个N节点的集群中,一条信息传播到所有节点大约需要O(logN)轮通信------但这是概率性的,实际时间取决于网络延迟和选择策略。
一致性Hash:数据分片的基础
严格来说一致性Hash不是共识算法,但它解决了分布式系统另一个核心问题:数据分片------如何决定一个Key应该存在哪个节点上?
普通Hash的问题:hash(key) % N。当节点数N变化时,几乎所有Key的映射都会改变------这在线上环境意味着大规模的数据迁移。
一致性Hash的思路:把Hash空间组织成一个环(0到2^32-1),节点和Key都通过Hash映射到环上。Key属于它顺时针方向遇到的第一个节点。这样当新增或移除一个节点时,只有相邻节点间的数据需要迁移,其他不受影响。
进一步优化:虚拟节点。每个物理节点映射为多个虚拟节点分散在环上,解决数据倾斜问题。
主流中间件的分布式实现
理论终归要落地。我们来看看日常打交道的中间件,底层都是怎么实现分布式的。
ZooKeeper:CP路线的典型
算法:ZAB协议(类Multi-Paxos)
架构:一个Leader + 多个Follower + 可选的Observer
写流程:
- 客户端写请求必须转发给Leader
- Leader生成Proposal(带有递增的zxid),广播给所有Follower
- Follower将Proposal写入本地日志后返回ACK
- Leader收到多数派ACK后,广播Commit
- 所有节点将Proposal应用到内存数据树
读流程 :默认Follower可以直接响应读请求(可能读到旧数据)。如果需要强一致读,客户端可以在读之前调用sync()强制跟Leader同步。
选举机制:当Leader挂了或网络分区时,Follower检测到心跳超时,进入选举状态。选举规则是:优先选zxid最大的节点(数据最新),zxid相同则选myid最大的。
典型用途:分布式锁、配置中心、服务注册发现、Master选举。
etcd:Raft的标杆实现
算法:Raft
架构:对等节点,通过Raft选举产生Leader
存储:底层用BoltDB(B+树),所有数据以Key-Value形式存储,支持MVCC(多版本并发控制)
关键特性:
- Watch机制:客户端可以监听Key的变化,基于HTTP/2 + gRPC长连接实现
- Lease机制:Key可以绑定租约,租约过期自动删除------这是实现服务注册/心跳的基础
- 事务:支持CAS(Compare-And-Swap)事务,基于Revision做乐观锁
跟ZooKeeper的对比:
- etcd用Raft更易理解和实现,ZooKeeper用ZAB历史包袱重
- etcd原生支持gRPC,性能更好;ZooKeeper基于TCP自定义协议
- etcd的Watch基于Revision线性有序,ZooKeeper的Watch是一次性的(触发后需要重新注册)
典型用途:Kubernetes的所有状态存储就在etcd里------Pod定义、Service配置、ConfigMap、Secret......K8s的API Server本质上是etcd的一个客户端。
Redis Cluster:AP路线的工程妥协
数据分片 :Hash Slot方案。总共16384个Slot,每个Key通过CRC16(key) % 16384映射到一个Slot,每个节点负责一部分Slot。
为什么不用一致性Hash?Redis官方的解释是:Hash Slot方案在Slot迁移时更加可控------可以精确地把某些Slot从一个节点移到另一个节点,不需要重新计算整个环。
故障检测 :Gossip协议。每个节点每秒随机Ping几个节点,如果某个节点超过cluster-node-timeout未响应,标记为PFAIL(疑似下线)。当超过半数的master节点都认为某个节点PFAIL时,标记为FAIL(确认下线),触发故障转移。
故障转移:当master被标记为FAIL,它的slave发起选举。选举需要获得多数派master的投票。当选后slave提升为新master,接管原master的Slot。
一致性保证 :Redis Cluster默认是异步复制------master写入成功就返回客户端,然后异步同步给slave。这意味着如果master在同步完成前挂了,数据会丢。可以通过WAIT命令要求同步复制,但会牺牲性能。
典型用途:缓存、会话存储、排行榜、消息队列(Stream)。
Kafka:分区级别的一致性
架构:Topic被分为多个Partition,每个Partition有一个Leader和多个Follower(ISR集合)。
ISR(In-Sync Replicas):与Leader保持同步的副本集合。只有ISR内的副本才有资格被选为新Leader。
写流程:
- Producer发消息到Partition的Leader
- Leader写入本地日志
- Follower从Leader拉取数据(Pull模式)
- 当ISR中所有副本都确认写入后,该消息被标记为committed
acks参数的权衡:
acks=0:发出去就不管了,最快但可能丢acks=1:Leader确认就返回,Leader挂了可能丢acks=all:ISR全部确认才返回,不丢但最慢
Leader选举:由Controller节点(集群中一个特殊的Broker)负责。Controller本身通过ZooKeeper(老版本)或KRaft(新版本)选举产生。
KRaft:Kafka 3.x开始引入的内置Raft实现,目标是去除对ZooKeeper的依赖。元数据管理(Broker注册、Partition分配、Leader选举)全部用内置的Raft集群完成。
典型用途:消息队列、事件流、日志收集、流计算数据源。
MySQL:主从到组复制的进化
传统主从复制 :异步或半同步。Master写Binlog,Slave拉取Binlog重放。半同步(rpl_semi_sync_master_wait_for_slave_count)要求至少N个Slave确认收到后才返回客户端,但不保证Slave已经执行。
MGR(MySQL Group Replication):MySQL 5.7+引入的基于Paxos的组复制方案。
- 单主模式:一个Primary处理写,其他Secondary自动同步
- 多主模式:所有节点都能写,通过Paxos协议做冲突检测和事务排序
MGR的冲突检测基于Write Set:每个事务提交前,把它修改的行的主键信息作为Write Set,通过Paxos广播给所有节点。如果两个并发事务的Write Set有交集(修改了同一行),则后提交的事务被回滚。
典型用途:金融级数据存储、OLTP业务。
TiDB/CockroachDB:NewSQL的Raft实践
TiDB是目前Raft在数据库领域最大规模的工程实践之一。
架构:
- TiDB:无状态SQL层,负责解析SQL、生成执行计划
- TiKV:分布式存储层,数据按Range分片,每个Range(Region)是一个Raft Group
- PD:调度器,负责Region的分裂、合并、迁移
核心设计:每个Region(默认96MB)是一个独立的Raft组。当Region数据量超阈值时自动分裂为两个Region。PD根据各TiKV节点的负载情况,调度Region的Leader分布,实现负载均衡。
这意味着一个TiDB集群可能有上万个Raft Group同时运行。每个Group独立选举、独立复制,互不干扰。
分布式事务:基于Percolator模型(Google论文),使用两阶段提交(2PC)+ 全局时间戳(TSO)实现跨Region的ACID事务。
分布式系统设计的核心权衡
聊了这么多理论和中间件,最后说说工程实践中的几个核心权衡:
强一致vs最终一致
没有"最好的一致性级别",只有"最适合场景的一致性级别"。
- 转账:必须强一致,不能出现两边余额对不上
- 商品库存:可以短暂不一致(超卖后补偿),但不能长期不一致
- 点赞数:最终一致就够了,晚几秒刷新不影响体验
同步复制vs异步复制
同步复制(所有副本确认):数据不丢,但延迟高、可用性低(一个副本挂了全部写入阻塞)。
异步复制(Leader确认即返回):延迟低、可用性高,但Leader挂了可能丢数据。
大多数系统选择了折中方案:多数派确认。3副本只要2个确认就行。这在延迟、持久性、可用性之间取了一个工程上可接受的平衡。
Leader-based vs Leaderless
Leader-based(Raft、ZAB):实现简单,线性一致性好保证,但Leader是性能瓶颈和单点。
Leaderless(Gossip、DynamoDB):无单点、扩展性好,但只能做最终一致,冲突解决复杂。
没有银弹。选什么架构,取决于你要解决的核心矛盾是什么。
写在最后
分布式系统的核心挑战,从30年前Lamport写Paxos论文到今天,本质上没有变:在不可靠的网络上,让多个独立的节点表现得像一个整体。
CAP告诉我们天花板在哪,共识算法告诉我们如何在天花板下做到最好,工程实践告诉我们理论和现实之间永远有Gap------而这个Gap,就是分布式系统工程师存在的价值。
理解这些底层原理,不是为了面试时背八股,而是为了在做技术选型时能问出正确的问题:这个中间件在网络分区时会怎么表现?它的一致性保证到底是什么级别?故障转移需要多长时间?数据会不会丢?
能回答这些问题,才算真正理解了分布式。