Skip to content
Published on

分布式系统完全指南: 从 CAP theorem 到分布式 ML 训练、Kafka

分享
Authors

引言

分布式系统是现代 AI 基础设施的骨架。要训练 GPT-4 这样的大模型,需要数千个 GPU 协同工作;要每秒处理数百万次推理请求,Kafka、Cassandra 这样的分布式系统必不可少。

本指南系统梳理 AI 工程师所需的分布式系统知识 — 从 CAP theorem 这样的理论基础,一直到实际的 PyTorch 分布式训练代码。


1. 分布式系统基础: CAP theorem 与一致性模型

CAP Theorem

2000 年 Eric Brewer 提出的 CAP theorem,描述了分布式系统的根本性限制。

┌─────────────────────────────────────────────┐
CAP Triangle│                                             │
Consistency (C)│               △                             │
/ \                            │
/   \                           │
/     \                          │
/  CA   \                         │
/  (RDBMS)\                        │
/───────────\                       │
/  CPAP  \                      │
/ (HBase│(Cassandra)│      ▽────────┼────────▽                    │
Availability  Partition Tolerance     (A)              (P)└─────────────────────────────────────────────┘

三项属性无法同时全部满足:

  • C (Consistency): 所有节点读到相同的数据
  • A (Availability): 所有请求都能得到响应
  • P (Partition Tolerance): 在网络分区的情况下依然能运作

由于网络分区在真实的分布式环境中必然发生,实际的选择只能是 CP 还是 AP

系统选择案例
HBase, Zookeeper, etcdCP分区时放弃可用性
Cassandra, DynamoDBAP分区时放弃一致性
传统 RDBMSCA不容许分区

PACELC: CAP 的扩展

CAP 只讨论分区状态,而 PACELC 还纳入了正常运行时 Latency 与 Consistency 之间的权衡。

PA or C  (发生分区时)
EL or C  (正常运行时)

Cassandra: PA/EL  (优先可用性 + 低延迟)
etcd/Raft: PC/EC  (优先一致性)

一致性模型

按从强到弱排列:

Linearizability (线性一致性)
  └── 最强。所有操作即时可见
  └── 例: etcd, Zookeeper

Sequential Consistency (顺序一致性)
  └── 所有节点看到相同的操作顺序
  └── 例: CPU 内存模型

Causal Consistency (因果一致性)
  └── 存在因果关系的操作保证顺序
  └── 例: MongoDB causally consistent sessions

Eventual Consistency (最终一致性)
  └── 终将达到一致
  └── 例: Cassandra, DNS, S3

2. 共识算法: Paxos 与 Raft

Paxos 的问题

Leslie Lamport 于 1989 年提出的 Paxos,理论上无可挑剔,但极难理解。Diego Ongaro 以「Paxos 太难理解了」为题发表 Raft,原因正在于此。

Raft 算法

Raft 把分布式共识拆解为三个独立的问题:

  1. 领导者选举 (Leader Election)
  2. 日志复制 (Log Replication)
  3. 安全性 (Safety)

节点状态

┌──────────┐  timeout   ┌──────────────┐  majority   ┌────────┐
│          │──────────▶│   Candidate  │────────────▶│        │
Follower │            └──────────────┘             │ Leader│          │◀─────────────────────────────────────── │        │
└──────────┘          receive heartbeat              └────────┘
     ▲                                                    │
     └────────────────── heartbeat ──────────────────────┘

领导者选举过程

  1. Follower 在 election timeout(150-300ms)内没有收到 heartbeat,就转为 Candidate
  2. term 编号加 1,先投票给自己,再向其他节点发送 RequestVote RPC
  3. 获得过半数投票即当选 Leader
  4. Leader 定期发送 AppendEntries(heartbeat)

日志复制

# 用 etcd 实现分布式锁
import etcd3
import time

def distributed_lock(etcd_client, lock_name, ttl=10):
    """基于 Raft 的 etcd 分布式锁实现"""
    lease = etcd_client.lease(ttl)
    lock_key = f"/locks/{lock_name}"

    # atomic CAS (Compare-And-Swap)
    success, _ = etcd_client.transaction(
        compare=[
            etcd3.transactions.create(lock_key, '==', 0)
        ],
        success=[
            etcd3.transactions.put(lock_key, 'locked', lease=lease)
        ],
        failure=[]
    )

    if success:
        print(f"Lock acquired: {lock_name}")
        return lease
    else:
        print(f"Lock already held: {lock_name}")
        return None

def release_lock(etcd_client, lease):
    """释放锁"""
    if lease:
        lease.revoke()
        print("Lock released")

# 使用示例
client = etcd3.client(host='localhost', port=2379)

lease = distributed_lock(client, 'my-resource')
if lease:
    try:
        # 在临界区执行操作
        time.sleep(5)
    finally:
        release_lock(client, lease)

etcd 实战: Kubernetes 使用 etcd 的方式

Kubernetes 把整个集群状态都存在 etcd 里。etcd 集群的写入性能受 Raft 共识限制,因此通常建议使用 3~5 个节点。

┌─────────────────────────────────────────────────────┐
│                  etcd Cluster (3 nodes)│                                                     │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐      │
│  │  Leader  │───▶│Follower 1│    │Follower 2│      │
│  │  node-1  │───▶│  node-2  │    │  node-3  │      │
│  └──────────┘    └──────────┘    └──────────┘      │
│       ▲               │               │             │
│       └───────────────┴───────────────┘             │
Raft Consensus└─────────────────────────────────────────────────────┘

3. 分布式事务: 2PC、Saga、CQRS

2PC (Two-Phase Commit)

Phase 1 (Prepare):
  Coordinator ──▶ Participant A: "准备好了吗?"
  Coordinator ──▶ Participant B: "准备好了吗?"
  Participant A ──▶ Coordinator: "YES"
  Participant B ──▶ Coordinator: "YES"

Phase 2 (Commit):
  Coordinator ──▶ Participant A: "提交!"
  Coordinator ──▶ Participant B: "提交!"

2PC 的问题: Coordinator 故障时,各 Participant 会陷入阻塞状态。

Saga 模式

Saga 把一个长事务拆分为多个本地事务,失败时执行补偿事务。

订单 Saga:
  1. 创建订单 → 失败时: 取消订单
  2. 扣减库存 → 失败时: 恢复库存
  3. 处理支付 → 失败时: 退款
  4. 开始配送 → 失败时: 取消配送
from typing import Callable, List
from dataclasses import dataclass
import logging

logger = logging.getLogger(__name__)

@dataclass
class SagaStep:
    name: str
    action: Callable
    compensating_action: Callable

class SagaOrchestrator:
    """Saga 编排器模式实现"""

    def __init__(self, steps: List[SagaStep]):
        self.steps = steps
        self.completed_steps = []

    def execute(self, context: dict) -> bool:
        for step in self.steps:
            try:
                logger.info(f"Executing step: {step.name}")
                step.action(context)
                self.completed_steps.append(step)
                logger.info(f"Step completed: {step.name}")
            except Exception as e:
                logger.error(f"Step failed: {step.name}, error: {e}")
                self._compensate(context)
                return False
        return True

    def _compensate(self, context: dict):
        """按逆序执行补偿事务"""
        logger.info("Starting compensation...")
        for step in reversed(self.completed_steps):
            try:
                logger.info(f"Compensating: {step.name}")
                step.compensating_action(context)
            except Exception as e:
                logger.error(f"Compensation failed: {step.name}, error: {e}")
                # 补偿事务失败需要人工介入
                raise RuntimeError(f"Saga compensation failed at {step.name}")

# 定义订单 Saga
def create_order(ctx):
    ctx['order_id'] = 'ORD-001'
    print(f"Order created: {ctx['order_id']}")

def cancel_order(ctx):
    print(f"Order cancelled: {ctx['order_id']}")

def deduct_inventory(ctx):
    ctx['inventory_reserved'] = True
    print("Inventory deducted")

def restore_inventory(ctx):
    print("Inventory restored")

def process_payment(ctx):
    # 模拟支付失败
    raise Exception("Payment gateway timeout")

def refund_payment(ctx):
    print("Payment refunded")

order_saga = SagaOrchestrator([
    SagaStep("create_order", create_order, cancel_order),
    SagaStep("deduct_inventory", deduct_inventory, restore_inventory),
    SagaStep("process_payment", process_payment, refund_payment),
])

context = {"user_id": "user-123", "amount": 50000}
result = order_saga.execute(context)
print(f"Saga result: {'success' if result else 'failed with compensation'}")

事件溯源与 CQRS

CQRS 架构:

  Command Side ()            Query Side ()
  ─────────────────            ────────────────
  POST /orders          ──▶    GET /orders/123
  PUT  /inventory       ──▶    GET /inventory/stats
       │                              ▲
       ▼                              │
  Event Store ──── Projection ────────┘
  (append-only)    (更新 read model)

事件溯源的核心: 不存储当前状态,而是存储事件的序列。由此可以还原任意时刻的状态、生成审计日志、重放事件。


4. 消息队列: Kafka 架构

Kafka 核心概念

┌─────────────────────────────────────────────────────────────┐
Kafka Cluster│                                                             │
Topic: orders (4 partitions, replication factor: 3)│                                                             │
Broker 1      Broker 2      Broker 3│  ┌──────┐      ┌──────┐      ┌──────┐                       │
│  │ P0 L │      │ P0 F │      │ P0 FL=Leader, F=Follower│  │ P1 F │      │ P1 L │      │ P1 F │                       │
│  │ P2 F │      │ P2 F │      │ P2 L │                       │
│  │ P3 L │      │ P3 F │      │ P3 F │                       │
│  └──────┘      └──────┘      └──────┘                       │
│                                                             │
Consumer Group A          Consumer Group BConsumer 1: P0, P1        Consumer X: P0, P1, P2, P3Consumer 2: P2, P3└─────────────────────────────────────────────────────────────┘
  • Partition: 保证顺序、也是并行处理的单位
  • Replication: 把分区复制到多个 broker,保证高可用
  • Consumer Group: 每个分区在同一个组内只由一个 consumer 处理

Kafka Producer/Consumer 实战

from confluent_kafka import Producer, Consumer, KafkaError
import json
import time

# Producer 设置
producer_config = {
    'bootstrap.servers': 'localhost:9092',
    'acks': 'all',          # 所有 ISR 复制完成后再确认
    'retries': 3,
    'enable.idempotence': True,  # 保证精确一次发送
    'compression.type': 'snappy',
}

def delivery_report(err, msg):
    if err is not None:
        print(f'Message delivery failed: {err}')
    else:
        print(f'Message delivered to {msg.topic()} [{msg.partition()}] @ {msg.offset()}')

producer = Producer(producer_config)

# 发布订单事件
for i in range(10):
    order_event = {
        'order_id': f'ORD-{i:04d}',
        'user_id': f'user-{i % 5}',
        'amount': 10000 * (i + 1),
        'timestamp': time.time()
    }
    producer.produce(
        topic='orders',
        key=order_event['user_id'],   # 相同 user 落到同一个分区
        value=json.dumps(order_event),
        callback=delivery_report
    )
    producer.poll(0)

producer.flush()
print("All messages sent")

# Consumer 设置
consumer_config = {
    'bootstrap.servers': 'localhost:9092',
    'group.id': 'order-processor-group',
    'auto.offset.reset': 'earliest',
    'enable.auto.commit': False,   # 手动提交 offset,保证 at-least-once
}

consumer = Consumer(consumer_config)
consumer.subscribe(['orders'])

try:
    while True:
        msg = consumer.poll(timeout=1.0)
        if msg is None:
            continue
        if msg.error():
            if msg.error().code() == KafkaError._PARTITION_EOF:
                continue
            else:
                raise KafkaError(msg.error())

        event = json.loads(msg.value().decode('utf-8'))
        print(f"Processing order: {event['order_id']}, amount: {event['amount']}")

        # 处理完成后提交 offset
        consumer.commit(asynchronous=False)

except KeyboardInterrupt:
    pass
finally:
    consumer.close()

Kafka Topic YAML 配置

# Kafka Topic 配置(基于 Strimzi Operator)
apiVersion: kafka.strimzi.io/v1beta2
kind: KafkaTopic
metadata:
  name: orders
  labels:
    strimzi.io/cluster: my-cluster
spec:
  partitions: 12 # 分区数 = 最大并行 consumer 数
  replicas: 3 # 复制因子
  config:
    retention.ms: '604800000' # 保留 7 天
    min.insync.replicas: '2' # 至少 2 个 ISR 复制后再确认
    compression.type: snappy
    cleanup.policy: delete

Pulsar vs Kafka

项目KafkaPulsar
存储直接在 Broker分离到 Apache BookKeeper
多租户有限原生支持
地域复制MirrorMaker2内置 Geo-Replication
延迟~5ms~1ms
运维复杂度
成熟度非常高

5. 分布式存储: Consistent Hashing 与 Cassandra

Consistent Hashing

普通哈希的问题: 节点增减时,几乎所有的 key 都要重新分布。 Consistent Hashing 的解法: 节点变化时,只有最少量的 key 需要重新分布。

import hashlib
from sortedcontainers import SortedDict

class ConsistentHashRing:
    """Consistent Hashing 环实现"""

    def __init__(self, virtual_nodes=150):
        self.virtual_nodes = virtual_nodes
        self.ring = SortedDict()
        self.nodes = set()

    def _hash(self, key: str) -> int:
        return int(hashlib.md5(key.encode()).hexdigest(), 16)

    def add_node(self, node: str):
        """把节点加入环(含虚拟节点)"""
        self.nodes.add(node)
        for i in range(self.virtual_nodes):
            virtual_key = f"{node}#{i}"
            hash_val = self._hash(virtual_key)
            self.ring[hash_val] = node
        print(f"Added node: {node} with {self.virtual_nodes} virtual nodes")

    def remove_node(self, node: str):
        """从环中移除节点"""
        self.nodes.discard(node)
        keys_to_remove = [k for k, v in self.ring.items() if v == node]
        for k in keys_to_remove:
            del self.ring[k]
        print(f"Removed node: {node}")

    def get_node(self, key: str) -> str:
        """返回 key 对应的节点"""
        if not self.ring:
            raise Exception("Ring is empty")

        hash_val = self._hash(key)
        # 找到第一个大于等于该哈希值的虚拟节点
        idx = self.ring.bisect_left(hash_val)
        if idx == len(self.ring):
            idx = 0  # 环形结构,需要 wrap around

        return self.ring.values()[idx]

    def get_nodes_for_replication(self, key: str, n: int) -> list:
        """返回用于复制的 n 个节点"""
        if len(self.nodes) < n:
            raise Exception(f"Not enough nodes: {len(self.nodes)} < {n}")

        hash_val = self._hash(key)
        idx = self.ring.bisect_left(hash_val)

        result_nodes = []
        seen_nodes = set()
        total = len(self.ring)

        for i in range(total):
            curr_idx = (idx + i) % total
            node = self.ring.values()[curr_idx]
            if node not in seen_nodes:
                seen_nodes.add(node)
                result_nodes.append(node)
                if len(result_nodes) == n:
                    break

        return result_nodes

# 使用示例
ring = ConsistentHashRing(virtual_nodes=150)
for i in range(5):
    ring.add_node(f"cassandra-node-{i}")

# 测试 key 分布
for key in ["user:1001", "order:5555", "product:abc", "session:xyz"]:
    node = ring.get_node(key)
    replicas = ring.get_nodes_for_replication(key, 3)
    print(f"Key '{key}' -> Primary: {node}, Replicas: {replicas}")

Apache Cassandra 架构

┌─────────────────────────────────────────────────────────────────┐
Cassandra Ring (6 nodes)│                                                                 │
Node 1/      \                                  │
Node 6      Node 2|            |Node 5      Node 3│                       \      /Node 4│                                                                 │
Write Path:ClientCoordinator → 对 Partition Key 哈希 → 相应节点        │
│                       → CommitLog (WAL)MemtableSSTable│                                                                 │
Read Path:ClientCoordinator → 相应节点 → Row Cache / Bloom Filter│                       → 合并 SSTable → 返回最新数据             │
└─────────────────────────────────────────────────────────────────┘

Cassandra 的核心特性:

  • Tunable Consistency: 可选择 QUORUMONEALL 等一致性级别
  • Gossip Protocol: 节点间状态传播使用 epidemic 算法
  • Compaction: 定期合并 SSTable,提升读取性能

6. 分布式 ML 训练: Ring-AllReduce 与 PyTorch Distributed

Parameter Server vs All-Reduce

Parameter Server 方式:
  ─────────────────────
  Worker 1 ─┐
  Worker 2 ─┤── PS (Parameter Server) ──▶ 聚合梯度后再分发
  Worker 3 ─┘

  问题: PS 成为瓶颈。带宽随节点数以 O(n) 增长

Ring-AllReduce 方式:
  ────────────────────
  Worker 1 ──▶ Worker 2 ──▶ Worker 3
     ▲                          │
     └──────────────────────────┘

  每个 Worker 只与相邻节点通信。
  总通信量与节点数无关,恒为 O(n-1)/n * data_size

Ring-AllReduce 的两个阶段:

  1. Reduce-Scatter: 每个节点对自己的分片做 reduce,同时传给下一个节点
  2. All-Gather: 把 reduce 后的结果传播给所有节点

PyTorch DistributedDataParallel (DDP)

import os
import torch
import torch.nn as nn
import torch.optim as optim
import torch.distributed as dist
from torch.nn.parallel import DistributedDataParallel as DDP
from torch.utils.data import DataLoader, DistributedSampler

def setup(rank, world_size):
    """初始化分布式训练"""
    os.environ['MASTER_ADDR'] = 'localhost'
    os.environ['MASTER_PORT'] = '12355'

    # NCCL 后端: GPU 间高速通信(利用 NVLink、InfiniBand)
    dist.init_process_group(
        backend='nccl',
        rank=rank,
        world_size=world_size
    )
    torch.cuda.set_device(rank)

def cleanup():
    dist.destroy_process_group()

class SimpleModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.layers = nn.Sequential(
            nn.Linear(1024, 2048),
            nn.ReLU(),
            nn.Linear(2048, 1024),
            nn.ReLU(),
            nn.Linear(1024, 10)
        )

    def forward(self, x):
        return self.layers(x)

def train(rank, world_size, epochs=5):
    setup(rank, world_size)

    # 把模型放到各个 GPU 上,再用 DDP 包装
    model = SimpleModel().to(rank)
    ddp_model = DDP(model, device_ids=[rank])

    # DistributedSampler: 每个进程处理不同的数据
    dataset = torch.utils.data.TensorDataset(
        torch.randn(1000, 1024),
        torch.randint(0, 10, (1000,))
    )
    sampler = DistributedSampler(dataset, num_replicas=world_size, rank=rank)
    dataloader = DataLoader(dataset, batch_size=32, sampler=sampler)

    optimizer = optim.Adam(ddp_model.parameters(), lr=1e-3)
    criterion = nn.CrossEntropyLoss()

    for epoch in range(epochs):
        sampler.set_epoch(epoch)  # 每个 epoch 重设 shuffle
        total_loss = 0.0

        for batch_x, batch_y in dataloader:
            batch_x = batch_x.to(rank)
            batch_y = batch_y.to(rank)

            optimizer.zero_grad()
            output = ddp_model(batch_x)
            loss = criterion(output, batch_y)
            loss.backward()  # 自动执行梯度 AllReduce
            optimizer.step()
            total_loss += loss.item()

        if rank == 0:  # 只在主进程打印日志
            print(f"Epoch {epoch}: Loss = {total_loss/len(dataloader):.4f}")

    cleanup()

# 运行: torchrun --nproc_per_node=4 train.py
if __name__ == '__main__':
    world_size = torch.cuda.device_count()
    torch.multiprocessing.spawn(
        train,
        args=(world_size,),
        nprocs=world_size,
        join=True
    )

NCCL 与通信后端

NCCL (NVIDIA Collective Communications Library):

  单节点(NVLink):
  GPU0 ←──── NVLink ────→ GPU1
    │                       │
   600 GB/s 双向带宽

  多节点(InfiniBand + RDMA):
  Node 1 ←── IB (200 Gb/s) ──→ Node 2
    │                              │
  RDMA: 无需 CPU 介入的 GPU-to-GPU 直接通信

后端选择:
  - nccl: GPU 分布式训练(推荐)
  - gloo: CPU 训练或调试
  - mpi: HPC 环境

与 Horovod 的比较

项目PyTorch DDPHorovod
集成方式PyTorch 内置需单独安装
算法Bucketed AllReduceRing-AllReduce
框架支持仅 PyTorchTF/PyTorch/MXNet
易用性略复杂简单(hvd.init())
性能相近相近

7. 可观测性: 分布式追踪、日志、指标

分布式追踪 (Distributed Tracing)

微服务环境下,一个请求会经过多个服务,分布式追踪用来记录它的完整路径。

请求流程 (Jaeger/Zipkin 追踪):

  BrowserAPI GWOrder SvcInventory SvcDB
    │           │          │            │          │
TraceID: abc123     │            │          │
SpanID: 001    SpanID: 002  SpanID: 003    │                      │            │          │
    └──────────────────────┴────────────┴──────────┘
             总延迟: 120ms
             Order Svc: 80ms (瓶颈!)
OpenTelemetry 分层:
  - API: 编写埋点代码
  - SDK: 数据处理与导出
  - Exporter: 发送到 Jaeger/Zipkin/OTLP

ELK Stack 日志聚合

┌─────────────────────────────────────────────────────┐
ELK Stack│                                                     │
App Logs ──▶ Filebeat ──▶ Logstash ──▶ Elasticsearch│
K8s Logs ──▶ Fluentd ──/             ──▶ Kibana                                         (可视化)│                                                     │
│  或者更现代的方案:App Logs ──▶ Promtail ──▶ Loki ──▶ Grafana└─────────────────────────────────────────────────────┘

Prometheus + Grafana 指标

# Prometheus 抓取配置
scrape_configs:
  - job_name: 'kafka'
    static_configs:
      - targets: ['kafka-broker-1:9090', 'kafka-broker-2:9090']
    metrics_path: '/metrics'
    scrape_interval: 15s

  - job_name: 'pytorch-training'
    static_configs:
      - targets: ['trainer-0:8080', 'trainer-1:8080']

核心 Kafka 指标(Grafana 仪表盘):

  • kafka_consumer_lag: consumer 延迟(核心 SLA 指标)
  • kafka_network_request_rate: broker 请求处理速率
  • kafka_log_size: 分区日志大小
  • kafka_under_replicated_partitions: 复制滞后的分区数

小测验

Q1. CAP theorem 中,发生网络分区时 CP 系统与 AP 系统的权衡差异是什么?

答案: CP 系统在分区状态下放弃可用性、维持一致性。AP 系统则相反,放弃一致性、维持可用性。

说明: 网络分区发生时,被隔离的节点之间无法通信。CP 系统(例如 etcd、HBase)此时会拒绝请求或返回错误,以避免提供过时的数据。AP 系统(例如 Cassandra、DynamoDB)在分区的节点上仍会继续响应,但不同节点可能返回不同的数据。分区解除后,再通过最终一致性(eventual consistency)把数据对齐。

Q2. Raft 算法中,领导者故障时如何选出新的领导者?

答案: Follower 在 election timeout 内没有收到 heartbeat,就转为 Candidate 并发送 RequestVote RPC,获得过半数投票的节点成为新 Leader。

说明: 领导者故障时,各 Follower 会在 election timeout(通常 150-300ms)内等待 heartbeat。超时后转为 Candidate,把 term 加 1,先给自己投票。随后向其他节点发送 RequestVote RPC。每个节点在同一个 term 内只能投一次票,获得过半数投票的 Candidate 成为 Leader。若出现 split vote,则所有节点在各自超时后重新发起选举。

Q3. Kafka 中,当 consumer group 的 consumer 数量多于分区数时会出现什么问题?

答案: 多出来的 consumer 分不到任何分区,处于 idle 状态。

说明: 在 Kafka 的消费模型中,一个分区在同一个 consumer group 内只能由一个 consumer 处理。因此如果有 4 个 consumer 而只有 3 个分区,就会有 1 个 consumer 分不到任何分区,处理不了任何消息。这属于资源浪费,所以通常设计上会让 consumer 数量不超过分区数。要提升吞吐量,应先增加分区数。

Q4. 为什么 Ring-AllReduce 比 Parameter Server 方式的通信效率更高?

答案: Ring-AllReduce 中每个节点只与相邻节点通信,总带宽与节点数无关、保持恒定;而 Parameter Server 中所有 Worker 都要与 PS 通信,PS 的带宽因而成为瓶颈。

说明: 在 Parameter Server 方式下,如果有 n 个 Worker,PS 就必须与全部 n 个 Worker 通信。Worker 越多,PS 所需的网络带宽就线性增长。而在 Ring-AllReduce 中,每个 Worker 只向两侧相邻的节点发送数据。n 个节点的总数据传输量为 2*(n-1)/n * data_size,均等地利用了所有节点的带宽。因此即便增加节点,每个节点的通信负担也不会增加。

Q5. Saga 模式中,什么情况下需要补偿事务(compensating transaction)?

答案: 当 Saga 的中间步骤失败时,为撤销之前已完成步骤的效果,会执行补偿事务。

说明: Saga 模式用于分布式环境下无法使用 ACID 事务的场景。每个步骤都作为本地事务立即提交。例如,在创建订单 → 扣减库存 → 处理支付的顺序中,如果支付处理失败,就会按逆序执行「恢复库存」事务来撤销已完成的库存扣减,再执行「取消订单」事务来撤销订单创建。补偿事务本身也可能失败,因此幂等性(idempotency)保证和重试逻辑非常重要。


结语

分布式系统不是一朝一夕能够掌握的领域。理解 CAP theorem 的权衡、亲手实现 Raft 共识、把 Kafka 分区策略应用到实际服务中,这个过程会一点点积累出深度。

对 AI 工程师而言,Ring-AllReduce 和 NCCL 这类分布式 ML 训练技术正变得越来越重要。要训练 GPT-4 规模的模型,就必须优化数千个 GPU 之间的网络通信,而这最终归结为分布式系统的核心原理。

核心学习路径: 理解 CAP theorem → 阅读 Raft 论文 → 亲自运维 etcd → 积累 Kafka 生产经验 → 用 PyTorch DDP 做多 GPU 训练