引言
分布式系统的本质挑战在于:多个独立节点通过网络协作,而网络是不可靠的。当请求跨越多个数据中心、多个服务器、多次网络跳转时,你会面临单机系统中不存在的问题——节点可能宕机、网络可能分区、消息可能延迟或丢失、时钟可能不同步。
从 Google 三驾马车(GFS、MapReduce、Bigtable)到 Kubernetes、TiDB、CockroachDB,分布式系统的理论基础始终围绕几个核心概念:一致性、可用性、分区容错、共识。本文将系统梳理这些概念,从 CAP 定理出发,深入 Raft/Paxos 共识算法、分布式事务、最终一致性和幂等性设计。
目录
1. CAP 定理
CAP 定理由 Eric Brewer 在 2000 年提出,2002 年被形式化证明。任何分布式系统只能在以下三个特性中选择两个:
- Consistency:所有节点同一时刻看到相同数据
- Availability:每个请求能在合理时间内收到响应(不保证最新)
- Partition Tolerance:网络分区发生时系统仍能运行
由于网络分区不可避免(P 是必选项),实际只能在 CP 和 AP 之间权衡:
┌───── 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 对比
| 特性 | Paxos | Raft |
|---|---|---|
| 可理解性 | 低(学术晦涩) | 高(设计目标易懂) |
| 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、Session | AP(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 可视化
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。