分布式系统一致性与可用性:CAP定理、共识算法与容错设计详解

从CAP定理出发,深入理解分布式系统的一致性与可用性权衡,详解Raft/Paxos共识算法、分布式事务、最终一致性等核心概念,构建可靠的分布式系统。

引言

分布式系统的本质挑战在于:多个独立节点通过网络协作,而网络是不可靠的。当请求跨越多个数据中心、多个服务器、多次网络跳转时,你会面临单机系统中不存在的问题——节点可能宕机、网络可能分区、消息可能延迟或丢失、时钟可能不同步。

从 Google 三驾马车(GFS、MapReduce、Bigtable)到 Kubernetes、TiDB、CockroachDB,分布式系统的理论基础始终围绕几个核心概念:一致性、可用性、分区容错、共识。本文将系统梳理这些概念,从 CAP 定理出发,深入 Raft/Paxos 共识算法、分布式事务、最终一致性和幂等性设计。


目录


1. CAP 定理

CAP 定理由 Eric Brewer 在 2000 年提出,2002 年被形式化证明。任何分布式系统只能在以下三个特性中选择两个:

  • Consistency:所有节点同一时刻看到相同数据
  • Availability:每个请求能在合理时间内收到响应(不保证最新)
  • Partition Tolerance:网络分区发生时系统仍能运行

由于网络分区不可避免(P 是必选项),实际只能在 CPAP 之间权衡:

               ┌───── Consistency ─────┐
               │                        │
               ▼                        ▼
         ┌─────────┐              ┌─────────┐
         │   CP    │              │   AP    │
         │ System  │              │ System  │
         └─────────┘              └─────────┘
               ▲                        ▲
               └──── Availability ──────┘
                  (Partition Tolerance 必须)

CP 系统:ZooKeeper、etcd、HBase、MongoDB(强一致模式)——网络分区时拒绝写入。

AP 系统:Cassandra、DynamoDB、DNS——网络分区时仍接受写入,稍后同步。


2. 一致性模型

模型含义实现
强一致性写入后立即对所有读可见ZooKeeper、etcd
顺序一致性所有操作按某种全局顺序执行Java volatile
因果一致性有因果关系的事件保证顺序Facebook Messenger
最终一致性没有新写入时,最终所有副本一致Cassandra、DynamoDB

选择建议:金融账户、库存扣减用强一致性;社交 feed、日志收集用最终一致性。


3. Raft 共识算法

Raft 由 Diego Ongaro 和 John Ousterhout 在 2014 年提出,目标是比 Paxos 更易理解。etcd、Consul、TiKV 都使用 Raft。

3.1 Leader 选举

集群中只有一个 Leader 处理写入,其余为 Follower:

T0: [Follower] [Follower] [Follower]     所有节点等待选举超时
T1: [Candidate] [Follower] [Follower]    Node 1 超时,发起选举
T2: [Leader]    [Follower] [Follower]    Node 1 收到多数票,成为 Leader
T3: [Leader]    [Follower] [Follower]    Node 1 故障,心跳停止
T4: [Dead]      [Candidate] [Follower]   Node 2 发起新选举(term+1)

3.2 日志复制

func (r *RaftNode) HandleClientRequest(cmd Command) {
    if r.role != Leader {
        r.redirectToLeader()
        return
    }

    // 1. 追加到本地日志
    entry := LogEntry{Term: r.currentTerm, Index: r.lastLogIndex() + 1, Command: cmd}
    r.log.Append(entry)

    // 2. 并行发送到所有 Follower
    for _, peer := range r.peers {
        go r.sendAppendEntries(peer, entry)
    }

    // 3. 等待多数派确认(quorum)
    r.waitForCommit(entry.Index) // 需要 (N/2)+1 个确认

    // 4. 应用到状态机并响应
    r.stateMachine.Apply(entry.Command)
    r.respondToClient(entry.Result)
}

安全性保证

  • Election Safety:每个 term 最多一个 Leader
  • Leader Append-Only:Leader 只追加日志
  • Log Matching:同一索引处 term 相同,则之前所有日志相同
  • Leader Completeness:被 commit 的日志会出现在之后的所有 Leader 中

4. Paxos vs Raft 对比

特性PaxosRaft
可理解性低(学术晦涩)高(设计目标易懂)
Leader 选举隐式显式 term-based
日志复制复杂(Multi-Paxos)Leader 统一管理
成员变更需额外协议支持 Joint Consensus
工程实现难(Chubby 复杂)易(etcd 数万行)

结论:新系统优先 Raft,除非特殊理由(如 Google Spanner 的 TrueTime)。


5. 分布式事务

5.1 两阶段提交(2PC)

Coordinator          A          B          C
    │── PREPARE ──▶ │          │          │
    │── PREPARE ──▶ │ ───────▶│          │
    │── PREPARE ──▶ │ ───────▶│ ───────▶│
    │◀── VOTE YES ─ │          │          │
    │◀── VOTE YES ─ │ ───────▶│          │
    │◀── VOTE NO ── │ ───────▶│ ───────▶│
    │── ABORT ────▶ │          │          │  (任一投 NO 则中止)

缺点:阻塞(Prepare 后参与者持锁)、Coordinator 单点故障、两次网络往返延迟高。

3PC 在 2PC 基础上加 Pre-Commit 和超时,减少阻塞但未完全解决网络分区问题。

5.2 TCC 模式

业务层分布式事务,每个操作分三步:Try(预留)、Confirm(确认)、Cancel(取消)。

type InventoryService struct{ db *sql.DB }

func (s *InventoryService) Try(txID, skuID string, qty int) error {
    _, err := s.db.Exec(`
        UPDATE inventory SET frozen = frozen + ?, available = available - ?
        WHERE sku_id = ? AND available >= ?`, qty, qty, skuID, qty)
    return err
}

func (s *InventoryService) Confirm(txID, skuID string, qty int) error {
    _, err := s.db.Exec(`
        UPDATE inventory SET frozen = frozen - ?, sold = sold + ?
        WHERE sku_id = ?`, qty, qty, skuID)
    return err
}

func (s *InventoryService) Cancel(txID, skuID string, qty int) error {
    _, err := s.db.Exec(`
        UPDATE inventory SET frozen = frozen - ?, available = available + ?
        WHERE sku_id = ?`, qty, qty, skuID)
    return err
}

6. 最终一致性实践

6.1 CRDTs

CRDTs(Conflict-free Replicated Data Types)无需协调即可自动合并。

G-Counter(只增计数器)

type GCounter struct{ counts map[string]uint64 }

func (c *GCounter) Increment(nodeID string) { c.counts[nodeID]++ }

func (c *GCounter) Value() (total uint64) {
    for _, v := range c.counts { total += v }
    return
}

func (c *GCounter) Merge(other *GCounter) {
    for id, v := range other.counts {
        if v > c.counts[id] { c.counts[id] = v }
    }
}

适用:在线协作文档、分布式计数器、聊天消息状态。

6.2 Vector Clocks

追踪事件因果关系:

Node A: [A:1, B:0, C:0]  -- 事件 1
Node A: [A:2, B:0, C:0]  -- 事件 2
Node B: [A:2, B:1, C:0]  -- 收到 A 后写入
Node C: [A:2, B:1, C:1]  -- 收到 A、B 后写入

VC(a) < VC(b)(所有分量都不大于,至少一个严格小于),则事件 a 在 b 之前发生。


7. 幂等性设计

幂等性:同一操作执行多次,结果与执行一次相同。分布式系统中重试是常态,幂等性是必须。

幂等键(Idempotency Key)

func (s *PaymentService) ProcessPayment(ctx context.Context, req PaymentRequest) (*Payment, error) {
    // 检查是否已处理
    if existing, err := s.repo.GetByIdempotencyKey(req.IdempotencyKey); err == nil {
        return existing, nil // 返回缓存结果
    }

    payment, err := s.charge(req.Amount, req.Currency)
    if err != nil { return nil, err }

    s.repo.SaveIdempotencyKey(req.IdempotencyKey, payment)
    return payment, nil
}

其他方法

  • 数据库唯一约束防止重复插入
  • 状态机限制合法转换(PENDING → PAID,不能 PAID → PAID)
  • 乐观锁使用版本号

8. 故障检测

心跳机制

func (m *Monitor) StartHeartbeat() {
    for range time.NewTicker(1 * time.Second).C {
        m.sendHeartbeat()
    }
}

func (m *Monitor) CheckHealth(nodeID string) bool {
    m.mu.RLock()
    defer m.mu.RUnlock()
    lastSeen, ok := m.lastHeartbeat[nodeID]
    return ok && time.Since(lastSeen) < 5*time.Second
}

Phi Accrual Failure Detector:Akka 和 Cassandra 使用的概率型检测器,基于心跳间隔的统计分布计算"怀疑程度":

phi = -log10(P(now - last_heartbeat < T))
当 phi > threshold(通常 8-12)时判定故障

优点:自适应不同网络条件,避免误判。


9. 系统设计决策矩阵

场景推荐方案原因
配置中心、分布式锁CP(etcd/ZooKeeper)需要强一致
社交 Feed、SessionAP(Cassandra/Redis)高可用优先
金融账户、库存强一致 + TCC不能容忍不一致
日志、监控最终一致 + Kafka高吞吐
协作编辑、聊天CRDTs + 最终一致无中心协调
Leader 选举Raft易理解易实现
跨数据中心复制Gossip + Vector Clocks低延迟
微服务调用幂等 + 重试 + 断路器网络不可靠

核心原则

  • 没有"最好"的方案,只有针对场景最合适的权衡
  • 先明确业务对一致性和可用性的真实要求
  • 默认假设网络会失败、节点会宕机、时钟会漂移
  • 为每个远程调用设计超时、重试和降级策略

延伸阅读

  • Leslie Lamport, Paxos Made Simple
  • Diego Ongaro, In Search of an Understandable Consensus Algorithm(Raft 论文)
  • Eric Brewer, CAP Twelve Years Later: How the “Rules” Have Changed
  • Martin Kleppmann, Designing Data-Intensive Applications, O’Reilly
  • etcd 官方文档
  • The Secret Lives of Data - Raft 可视化

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页