TiDB PD v8.5.5 Coordinator 深度学习02

3 阅读13分钟

TiDB PD Coordinator - 快速参考卡片

核心概念、关键代码和设计模式的速查表


🎯 核心概念 30 秒速览

Coordinator = ReportOptions + Operator队列 + HeartbeatStreams

ReportOptions(两个关键时间)
├─ Interval:心跳汇报周期(10-30s,快)
└─ Balance*Interval:决策周期(300-600s,慢)
   ↓
Operator生成(决策)
├─ TransferLeader(转移leader)
├─ ChangePeer(添加/删除副本)
├─ Merge(合并region)
├─ Split(分裂region)
└─ ChangePeerV2(原子配置变更)
   ↓
HeartbeatStreams推送(执行)
└─ 500ms周期推送给TiKV(缓冲1024

📋 四大工作线程

线程周期职责关键方法
PatrolRegions10s巡检资源,发现问题Run() → PatrolRegions()
checkSuspectRanges可配检测热点范围Run() → checkSuspectRanges()
drivePushOperator500ms推送决策到TiKVRun() → drivePushOperator()
driveSlowNodeScheduler可配处理故障节点Run() → driveSlowNodeScheduler()

🔄 Operator 状态机

CREATED → WAITING → STARTED → SUCCESS/TIMEOUT/CANCELED
           ↑          ↓
           └──────────┘ (冲突时退回)

关键:STARTED后通过heartbeat推送,TiKV自治执行

💡 核心代码片段速查

1️⃣ Coordinator.Run() - 启动流程

// 第一阶段:等待集群信息充分
for {
    if c.ShouldRun(collectWaitTime...) {
        break
    }
    select {
    case <-ticker.C:
    case <-c.ctx.Done():
        return
    }
}

// 第二阶段:启动4个工作线程
c.InitSchedulers(true)
c.wg.Add(4)
go c.PatrolRegions()
go c.checkSuspectRanges()
go c.drivePushOperator()          // ⭐ 心跳推送线程
go c.driveSlowNodeScheduler()

2️⃣ drivePushOperator() - 决策推送引擎

func (c *Coordinator) drivePushOperator() {
    ticker := time.NewTicker(500 * time.Millisecond)
    defer ticker.Stop()
    for {
        select {
        case <-c.ctx.Done():
            return
        case <-ticker.C:
            // 每500ms推送一批operator
            c.opController.PushOperators(c.RecordOpStepWithTTL)
        }
    }
}

// PushOperators 内部干什么?
// 1. 遍历所有STARTED operator
// 2. 检查执行进度(心跳反馈)
// 3. 生成RegionHeartbeatResponse
// 4. 放入msgCh(1024缓冲)

3️⃣ HeartbeatStreams.run() - 异步推送

func (s *HeartbeatStreams) run() {
    keepAliveTicker := time.NewTicker(1 * time.Minute)
    for {
        select {
        case update := <-s.streamCh:         // 优先级1:流更新
            s.streams[update.storeID] = update.stream
            
        case msg := <-s.msgCh:               // 优先级2:operator消息
            if stream, ok := s.streams[storeID]; ok {
                stream.Send(msg)  // gRPC异步发送
            }
            
        case <-keepAliveTicker.C:            // 优先级3:keepalive
            for storeID, stream := range s.streams {
                stream.Send(keepAlive)
            }
            
        case <-s.hbStreamCtx.Done():
            return
        }
    }
}

4️⃣ SendMsg() - 推送Operator

func (s *HeartbeatStreams) SendMsg(region *core.RegionInfo, op *Operation) {
    resp := &RegionHeartbeatResponse{
        RegionId:      region.GetID(),
        RegionEpoch:   region.GetRegionEpoch(),
        TargetPeer:    region.GetLeader(),
        TransferLeader: op.TransferLeader,  // ← 指令
        ChangePeer:    op.ChangePeer,       // ← 指令
        Merge:         op.Merge,            // ← 指令
        SplitRegion:   op.SplitRegion,      // ← 指令
    }
    
    select {
    case s.msgCh <- resp:           // 放入缓冲
    case <-s.hbStreamCtx.Done():   // ctx取消
        // 没有default,意味着会阻塞!(背压机制)
    }
}

🎨 关键设计模式

模式1:两层时间分离

心跳间隔(快)          决策间隔(慢)
    ↓                      ↓
10-30 seconds    ←        300-600 seconds
(感知延迟)              (避免波动)

比例:1 : 30 (决策是心跳的30倍周期)

为什么?

每次决策成本高 O(n²)    心跳必须快 O(1)
       ↓                  ↓
    拉长周期          保持高频
    减少CPU           快速更新状态

模式2:缓冲异步处理(无锁并发)

// 生产者(多线程)
c.decisions.SendMsg(msg)     // 放入msgCh不阻塞

// 消费者(单线程)
case msg := <-msgCh:         // 顺序处理
    stream.Send(msg)         // 异步gRPC

// 优势:
// - 无锁(channel自己同步)
// - 背压自动(缓冲满了生产者会阻塞)
// - 顺序保证(单消费者)

模式3:CP + AP融合

CP层(Raft/etcd)              内存决策              AP层(Heartbeat)
┌──────────────┐          ┌──────────┐        ┌──────────────┐
│ Scheduler配置 │  读取    │ 生成Op   │  推送  │ 推送给TiKV   │
│ RegionState  ├─────────→│ (快速)   ├──────→│ (异步)       │
│ ClusterVer   │          └──────────┘        └──────────────┘
└──────────────┘
 重启不丢失      几毫秒延迟      500ms推送周期

结果:
- 决策延迟:100ms
- 集群可用性:99.9%(PD故障时TiKV继续执行)
- 一致性:CP + 最终一致性混合

🚀 快速实现 Checklist

如果你要从 0 开始构建 Coordinator

□ 第1步:设计CP层持久化
  └─ 用 etcd 保存 Scheduler配置、边界信息
  
□ 第2步:设计内存决策层
  └─ Operator 队列 + 决策算法
  
□ 第3步:设计AP层推送
  └─ HeartbeatStreams + msgCh(1024缓冲)
  
□ 第4步:启动4个工作线程
  ├─ 巡检线程(发现问题)
  ├─ 决策线程(生成指令)
  ├─ 推送线程(下发指令)✅ 最关键
  ├─ 故障线程(处理异常)
  
□ 第5步:处理通信
  └─ gRPC heartbeat流 + 缓冲消息
  
□ 第6步:监控和告警
  └─ operator队列深度/延迟/失败率

📊 性能指标参考

指标目标来源
决策延迟<1000msPatrolRegions + drivePushOperator
operator推送吞吐>1000 ops/smsgCh缓冲 / 推送周期
heartbeat丢失率<0.1%gRPC重试机制
Coordinator内存<1GBoperator队列深度

🔍 关键参数调优表

// 参数                 默认值        调小(低延迟)     调大(省资源)
// ─────────────────────────────────────────────────────────
BalanceLeaderInterval  = 300s       → 180s           → 600s
BalanceRegionInterval  = 600s       → 300s           → 1200s
pushOperatorTickInterval = 500ms    → 200ms          → 1000ms
PatrolInterval         = 10s        → 5s             → 30s

⚡ Troubleshooting 速查表

问题:Operator队列堆积

症状:opController.OperatorCount() 不断增加
原因:
  1. 前置条件冲突多(如副本数不足状态下加副本)
  2. TiKV执行慢(硬件、网络)
  3. drivePushOperator频率不够

诊断:
  metrics: operator_waiting_count / operator_running_count
  
  if 等待数 > 运行数 × 10:
      → 说明前置条件冲突过多
      → 调整冲突检测逻辑
  
  if 运行数 ≈ 等待数:
      → 说明TiKV执行慢
      → 检查TiKV日志/硬件

问题:决策响应慢(>1s)

症状:从Region心跳到operator下发超过1秒
原因:
  1. 决策算法复杂(巡检扫描慢)
  2. msgCh缓冲不足
  3. gRPC吞吐不足

诊断:
  分别测量时间:
  1. PatrolRegions(): 应该 <100ms
  2. PushOperators(): 应该 <200ms
  3. HeartbeatStreams.Send(): 应该 <100ms
  
  加起来应该 <500ms(等一个推送周期)

问题:Heartbeat推送失败

症状:metrics heartbeat_stream xxx err 率高
原因:
  1. gRPC连接断开
  2. TiKV故障
  3. 网络波动

处理:
  自动:stream.Send()失败 → 删除stream → 下次心跳重连
  手动:检查TiKV日志和网络

📚 相关源文件导航

pkg/schedule/
├─ coordinator.go ⭐⭐⭐
│  └─ Run()、PatrolRegions()、drivePushOperator()
│
├─ hbstream/
│  └─ heartbeat_streams.go ⭐⭐⭐
│     └─ run()、SendMsg()
│
├─ operator/
│  └─ controller.go ⭐⭐
│     └─ PushOperators()、AddOperator()
│
└─ schedulers/
   ├─ scheduler_controller.go ⭐⭐
   │  └─ 周期执行各scheduler
   │
   ├─ balance_leader.go ⭐
   │  └─ Leader均衡决策
   │
   └─ balance_region.go ⭐
      └─ Region均衡决策

🎓 学习路线图

Day 1-2:理论基础

  • 两层架构(CP + AP)理解
  • ReportOptions 参数作用
  • Operator 状态机

Day 3-4:代码走读

  • Run() 启动流程
  • drivePushOperator() 推送引擎
  • HeartbeatStreams 消息流

Day 5-6:深度理解

  • 四个工作线程分工
  • msgCh 缓冲机制
  • Operator 生命周期

Day 7+:实战应用

  • 构建简单 Coordinator
  • 性能调优
  • 故障排查

💬 核心问答集

Q: 为什么不直接用Raft推送决策?

A: 延迟太高
   Raft同步 = 主-从-多数派 → 10s+
   直接推送 = PD→TiKV异步 → <1s
   
   而且TiKV缓存operator,PD故障后继续执行

Q: msgCh缓冲为什么是1024?

A: 权衡内存和吞吐
   1024条消息 × 500字节  512KB可接受
   500ms推送周期 = 1024/0.5s = 2048 ops/s吞吐足够

Q: 为什么要1分钟keepalive?

A: 防止gRPC连接自动关闭
   TiKV可能长时间不心跳(如稳定集群)
   1分钟keepalive = 确保连接活跃

Q: 决策冲突了怎么办?

A: TiKV自动处理
   如果Region已经在执行另一个operator
   TiKV忽略新的冲突operator
   下次PD推送时会检测到旧operator已完成

🎯 核心洞察(务必记住)

  1. 分离是关键

    • 心跳汇报(快)vs 决策生成(慢)
    • CP层存储(重)vs AP层推送(快)
    • 状态收集vs决策生成线程独立
  2. 缓冲是价值

    • msgCh 1024缓冲 = 自动背压
    • TiKV缓存已下发operator = 高可用
    • 500ms推送周期 = 批量优化
  3. Raft不对等

    • Raft保证配置一致(持久化)
    • 但执行指令的一致性靠心跳+重试
    • 这不是bug,这是分布式系统的艺术

TiDB PD 通过 Raft 构建分布式协调器系统 - 深度解读

本文基于 TiDB PD v8.5.5 源码深度剖析,展示如何用 Raft 作为 CP 层构建高效的分布式资源调度系统


核心架构:两层融合(CP + AP)

架构全景图

┌─────────────────────────────────────────────────────────────┐
│  Coordinator(PD所有决策的中枢)                             │
│                                                              │
│  CP层(Raft)                    内存协调                       │
│  ├─ 持久化存储配置             ├─ 生成Operator              │
│  ├─ Scheduler持久化配置         ├─ Operator生命周期管理       │
│  └─ RegionState边界信息        └─ 驱动周期决策              │
│                                  ↓                           │
│                             AP层(Heartbeat)                  │
│                             ├─ 缓冲通道(1024)                │
│                             ├─ 500ms推送间隔                 │
│                             ├─ 1分钟keepalive保活            │
│                             └─ TiKV自治执行机制              │
└────────────────────────────────────────────────────────────┘
                              ↓
                    ┌──────────────────┐
                    │   TiKV Store     │
                    │ 自治执行Operator │
                    │ 心跳上报进度     │
                    └──────────────────┘

1. CP 层:Raft 持久化决策基础

为什么需要 Raft?

PD 原生 embedded etcd(基于 Raft),存储:

  1. Scheduler 配置 - 哪些调度器启用,参数是什么
  2. ClusterVersion - 当前集群支持的特性版本
  3. RegionState - Region 边界、元数据、热点信息
// pkg/schedule/coordinator.go - InitSchedulers 方法
// 从 Raft 存储加载持久化配置
func (c *Coordinator) InitSchedulers(shouldCheckPersist bool) {
    if shouldCheckPersist {
        // 从etcd(Raft存储)加载所有scheduler配置
        // 确保集群重启后调度策略不丢失
    }
}

关键特性:

  • ✅ 集群重启不丢失调度策略
  • ✅ 多副本Raft保证强一致性
  • ✅ 支持配置动态更新

2. 内存协调层:四大工作线程体系

2.1 启动流程(Run 方法)

// 第一阶段:等待集群信息充分准备
for {
    if c.ShouldRun(collectWaitTime...) {  // 等待足够的Region信息
        log.Info("coordinator has finished cluster information preparation")
        break
    }
    // 3秒检查一次
}

// 第二阶段:初始化调度器并启动4个工作线程
c.InitSchedulers(true)
c.wg.Add(4)
go c.PatrolRegions()           // 线程1:区域巡检
go c.checkSuspectRanges()      // 线程2:可疑范围检查
go c.drivePushOperator()       // 线程3:心跳推送驱动(500ms周期)
go c.driveSlowNodeScheduler()  // 线程4:慢节点调度

工作线程分职责:

线程周期职责
PatrolRegions可配置扫描所有region状态
checkSuspectRanges可配置检查未分裂的大key范围
drivePushOperator500ms推送运算指令到TiKV
driveSlowNodeScheduler可配置检测慢节点并触发 evict

3. AP 层:心跳驱动的决策下发机制

3.1 心跳驱动的核心原理

TiKV Region Heartbeat
    ↓
+─────────────────────────────────────┐
│  HeartbeatStreams.run()             │  
│  ├─ 接收 stream 绑定更新            │
│  ├─ 接收 msgCh 中的Operator        │
│  └─ 1分钟keepalive保活              │
└──────────────┬──────────────────────┘
               ↓
        ┌──────────────────┐
        │  gRPC流发送      │
        │  (async)         │
        └─────────┬────────┘
                  ↓
           TiKV执行Operator

3.2 ReportOptions:两个关键的时间参数

type ReportOptions struct {
    // 心跳收集周期 - 快速感知集群状态变化
    Interval time.Duration              // 通常 10-30 秒
    
    // 决策周期 - 避免频繁波动
    BalanceLeaderInterval time.Duration // Leader 均衡周期
    BalanceRegionInterval time.Duration // Region 均衡周期
}

为什么分开两个周期?

  • 心跳(Interval)短 ✅ 快速感知热点、故障
  • 决策(Balance)长* ✅ 避免抖动、汇总更多信息

实际配置示例(TiKV推荐):

心跳间隔:    10s  (快速感知)
Leader决策:  300s (5分钟一次均衡)
Region决策:  600s (10分钟一次均衡)

3.3 HeartbeatStreams 的消息通道设计

// 缓冲大小为 1024,异步处理
msgCh chan core.RegionHeartbeatResponse  // 1024缓冲

// 三层事件循环
for {
    select {
    case update := <-s.streamCh:        // 高优先级:流更新
        s.streams[update.storeID] = update.stream
        
    case msg := <-s.msgCh:              // 正常优先级:Operator消息
        if stream, ok := s.streams[storeID]; ok {
            stream.Send(msg)  // 异步gRPC发送
        }
        
    case <-keepAliveTicker.C:           // 低优先级:keepalive
        for storeID, stream := range s.streams {
            stream.Send(keepAlive)
        }
    }
}

风险处理:

  • 如果 msgCh 满了? → 新的Operator会被块(PD不会丢消息)
  • 如果 gRPC 发送失败? → 删除该stream,触发重连
  • 如果 TiKV 长期离线? → 1分钟keepalive失败后清理stream

4. Operator 生命周期:从生成到执行

4.1 完整状态转移

创建 (Create)
    ↓
等待 (Waiting) ← 检查前置条件
    ↓   ↑
开始 (Started) ← 可能转移回等待(如冲突)
    ↓
进行中 (Running) ← 通过heartbeat推送给TiKV
    ↓   ↑
    ├─→ TiKV执行(自治)
    ↓
成功 (Success) / 超时 (Timeout) / 取消 (Canceled)

4.2 drivePushOperator:500ms 周期推送

func (c *Coordinator) drivePushOperator() {
    ticker := time.NewTicker(pushOperatorTickInterval)  // 500ms
    defer ticker.Stop()
    for {
        select {
        case <-c.ctx.Done():
            return
        case <-ticker.C:
            // 关键调用:推送所有未完成的operator
            c.opController.PushOperators(c.RecordOpStepWithTTL)
        }
    }
}

每个周期做什么?

  1. 遍历所有运行中的 Operator
  2. 生成 RegionHeartbeatResponse(含diff)
  3. 放入 msgCh 通道
  4. HeartbeatStreams.run() 异步发送给 TiKV

5. 核心数据结构:Operation(调度指令)

5.1 Operation 包含的决策类型

type RegionHeartbeatResponse struct {
    RegionId      uint64
    RegionEpoch   *RegionEpoch
    TargetPeer    *Peer              // 哪个store是leader
    
    // 六大类决策
    ChangePeer    *ChangePeer        // 添加/删除副本
    TransferLeader *TransferLeader   // 转移leader
    Merge         *Merge             // 合并两个region
    SplitRegion   *SplitRegion       // 分裂region
    ChangePeerV2  *ChangePeerV2      // 原子配置变更
    SwitchWitnesses *SwitchWitnesses // 见证者模式
}

为什么用heartbeat推送而不是主动推送?

  • ✅ 天然的流量控制(TiKV决定汇报频率)
  • ✅ 快速反馈(同一连接中)
  • ✅ 减少网络往返(piggyback到heartbeat上)
  • ✅ TiKV缓存已下发operator,PD故障不影响执行

6. 两层架构的巧妙平衡

为什么 CP + AP 要分离?

问题场景1:纯CP系统(强一致)
PD决策 → 写入Raft → 等待确认 → 响应TiKV
                                  
问题:延迟高、吞吐低、可用性差(PD故障集群停滞)
问题场景2:纯AP系统(高可用)
PD决策 → 直接响应TiKV → 集群执行
                        
问题:PD重启数据丢失、配置波动、不一致
PD 的解决方案(CP + AP融合)
CP层(Raft):
  ├─ 存储Scheduler配置        ← 保证重启不丢失
  ├─ 存储ClusterVersion       ← 保证版本一致
  └─ 存储RegionState          ← 保证边界准确

  ⬇️
  
内存决策:快速生成Operator(无需等Raft)

AP层(Heartbeat):
  ├─ 缓冲发送(1024缓冲)     ← 高吞吐
  ├─ TiKV自治执行            ← PD故障不影响
  └─ 多次重试(500ms推送)    ← 最终一致

效果:

  • ✅ 决策延迟:100ms 级别(无需等Raft同步)
  • ✅ 集群可用性:PD故障500ms内TiKV继续执行已下发operator
  • ✅ 一致性:关键配置通过Raft保证,执行指令通过心跳重试保证

7. 关键设计模式详解

模式1:缓冲异步处理(无锁并发)

msgCh chan core.RegionHeartbeatResponse  // 1024缓冲

// 发送端(多个scheduler并发生成operator)
select {
case s.msgCh <- resp:
case <-s.hbStreamCtx.Done():
}

// 接收端(单一协程消费)
for msg := <-s.msgCh {
    if stream, ok := s.streams[storeID]; ok {
        stream.Send(msg)
    }
}

优势:

  • 零锁竞争(channel内部自己同步)
  • 自动背压(缓冲满了新scheduler会阻塞)
  • 顺序保证(单消费者)

模式2:事件驱动的优先级队列

for {
    select {
    case update := <-s.streamCh:      // 最高优先级:流更新
        // 立即处理
        
    case msg := <-s.msgCh:            // 中优先级:Operator消息
        // 按来的顺序处理
        
    case <-keepAliveTicker.C:         // 低优先级:超时事件
        // 周期处理
    }
}

这是Go 原生的优先级队列实现(比传统堆更清晰)。

模式3:两层缓冲(gRPC流 + 通道)

┌──────────────────┐
│  Operator Queue  │  (内存Operator队列)
│  (opController)  │
└────────┬─────────┘
         │ 推送到
         ⬇️
┌──────────────────┐
│   msgCh (1024)   │  (通道缓冲)
└────────┬─────────┘
         │ 异步发送
         ⬇️
┌──────────────────┐
│  gRPC流 Send()   │  (系统缓冲,通常64KB)
└────────┬─────────┘
         │
         ⬇️
      TiKV

为什么三层?

  • 解耦heartbeat发送速度和operator生成速度
  • 缓冲突刺流量
  • 支持背压机制

8. 实战经验:如何应用到自己的系统

场景1:构建数据库集群协调器

原则:

CP核心        AP传递
├─ 配置存储   ├─ 快速下发
├─ 元数据     ├─ 缓冲重试
└─ 关键决策   └─ TiKV自执行

实现步骤:

  1. 用 etcd 存储不可变的配置和边界信息
  2. 在内存中快速生成执行指令(无需Raft同步)
  3. 通过心跳流缓冲推送,支持重试
  4. 客户端自治执行(不依赖PD连接保活)

场景2:优化决策延迟

关键参数:

type ReportOptions struct {
    Interval                   // 心跳汇报间隔(10-30s)
    BalanceLeaderInterval      // Leader决策周期(300s)
    BalanceRegionInterval      // Region决策周期(600s)
}

调优思路:

  • 增加心跳间隔 → 减少CPU成本,但感知延迟增加
  • 减少决策周期 → 快速响应,但可能频繁波动

推荐配置:

心跳: 10s  (感知热点)
决策: 300-600s (避免抖动)

场景3:处理高并发operator生成

PD的做法:

// 多个scheduler并发push到msgCh(1024缓冲)
// 单个consumer从msgCh pop,通过gRPC发送

// 容错:
// - msgCh满 → scheduler阻塞(背压)
// - gRPC失败 → stream删除,触发重连
// - TiKV离线 → 1分钟后清理stream

9. 源码地图

pkg/schedule/
├─ coordinator.go           ← 四大线程启动
├─ operator/
│  └─ controller.go         ← Operator生命周期
├─ schedulers/
│  ├─ scheduler_controller.go  ← 周期执行
│  ├─ balance_leader.go     ← Leader均衡决策
│  └─ balance_region.go     ← Region均衡决策
└─ hbstream/
   └─ heartbeat_streams.go  ← 心跳推送机制

总结:Raft 在分布式协调器中的角色

层次技术作用特点
CP层Raft(etcd)配置+元数据强一致, 重启不丢
内存层Lock-free决策生成毫秒延迟
AP层Heartbeat推送执行高可用, 最终一致

关键洞察:

Raft不是用来保证执行一致性的,而是保证决策配置一致性
执行的一致性通过"心跳+客户端缓存+重试"实现的
这是分布式系统的大智慧:分离存储一致性和执行可用性

推荐深读

  1. coordinator.go - 架构全景
  2. heartbeat_streams.go - AP层消息流
  3. operator/controller.go - Operator管理
  4. balance_leader.go - 具体决策算法示例