TiDB PD v8.5.5 Coordinator 深度学习03

2 阅读8分钟

TiDB PD Coordinator - 源码实战手册

通过对比、代码示例和最佳实践,展示如何学习并应用PD的设计


第1部分:核心代码对比

为什么心跳间隔和决策间隔要分开?

问题场景:负载均衡决策
// 场景:处理100个Store上的10000个Region
// 问题:决策成本高(O(n²)扫描),不能每次心跳都做

// ❌ 错误做法:心跳到就决策
for hb := range heartbeats {  // 可能1秒多个心跳
    decision := makeDecision(hb)  // 扫描所有region
    if decision != nil {
        push(decision)
    }
}
// 结果:决策频繁波动、CPU高、延迟高

// ✅ PD做法:分离心跳和决策
// 线程1: 心跳接收(快速)
for hb := range heartbeats {
    updateClusterState(hb)  // O(1)更新
}

// 线程2: 周期决策(拉长)
ticker := time.NewTicker(5 * time.Minute)
for range ticker.C {
    decision := makeDecision()  // 一次O(n²)扫描
    if decision != nil {
        push(decision)
    }
}

对比分析:

维度错误做法PD做法
决策频率10/秒1/300秒
CPU成本
反应延迟中等
抖动程度严重轻微

第2部分:四个工作线程的职责划分

为什么要4个线程并发而不是1个?

// 线程分工:专一原则(Unix哲学)

// ┌─────────────────────────────────────────┐
// │ Coordinator                             │
// │ ├─ PatrolRegions()                      │
// │ │  └─ 循环扫描所有region状态            │
// │ ├─ checkSuspectRanges()                 │
// │ │  └─ 检测热点key范围,触发分裂         │
// │ ├─ drivePushOperator()                  │
// │ │  └─ 500ms推送所有pending operator    │
// │ └─ driveSlowNodeScheduler()             │
// │    └─ 检测慢节点,触发evict             │
// └─────────────────────────────────────────┘

线程1:PatrolRegions(定期巡检)

func (c *Coordinator) PatrolRegions() {
    // 周期:可配置,默认10秒
    ticker := time.NewTicker(c.GetPatrolInterval())
    
    for range ticker.C {
        // 遍历所有region,运行checkers
        // ├─ HealthChecker: 副本数是否充足
        // ├─ LeaderChecker: leader是否健康
        // ├─ MergeChecker: 是否可以合并
        // └─ SplitChecker: 是否需要分裂
        
        // 作用:发现需要修复的问题
    }
}

职责:主动发现问题

  • 副本不足 → 触发AddPeer operator
  • 无leader → 触发选举
  • 过小region → 触发Merge
  • 过大region → 触发Split

线程2:checkSuspectRanges(热点检测)

func (c *Coordinator) checkSuspectRanges() {
    // 周期检测:可配置
    
    for {
        // 扫描HotRegions,找到未分裂的热点范围
        // 生成SplitRegion operator
        
        hotRegions := c.cluster.GetHotRegions()
        for _, region := range hotRegions {
            if shouldSplit(region) {
                op := NewSplitRegionOperator(region)
                c.opController.AddOperator(op)
            }
        }
    }
}

职责:处理热点范围问题

  • 计算热点key分布
  • 建议分裂位置
  • 生成SplitRegion operator

线程3:drivePushOperator(决策推送)★★★重要

// 这是最关键的心跳+决策融合点!

func (c *Coordinator) drivePushOperator() {
    ticker := time.NewTicker(500 * time.Millisecond)  // 固定500ms
    
    for range ticker.C {
        // 关键调用链:
        c.opController.PushOperators(c.RecordOpStepWithTTL)
        
        // 内部流程:
        // 1. 遍历operator队列
        // 2. 对每个operator,找到affected region
        // 3. 生成RegionHeartbeatResponse
        // 4. 推送给HeartbeatStreams
    }
}

职责:连接决策和执行

  • 将内存中的operator转换为gRPC响应
  • 通过heartbeat流推送
  • 支持批量推送(500ms内生成的所有operator统一推送)

线程4:driveSlowNodeScheduler(慢节点处理)

func (c *Coordinator) driveSlowNodeScheduler() {
    // 周期:可配置
    
    for {
        // 检测:哪些store响应慢
        slowStores := detectSlowStores()
        
        // 决策:自动disable掉慢store或触发evict
        for _, store := range slowStores {
            if shouldEvict(store) {
                evictLeaderAndRegions(store)
            }
        }
    }
}

职责:故障处理

  • 自动检测网络慢或硬件故障的节点
  • 触发驱逐operator

第3部分:心跳流消息设计

ReportOptions 的两层时间设计

// 配置示例(TiDB默认推荐)
const (
    // 心跳间隔(TiKV→PD)
    RegionHeartBeatTickInterval = 10 * time.Second
    LeaderHeartBeatTickInterval = 50 * time.Millisecond
    
    // 决策周期(PD内部)
    BalanceLeaderInterval = 300 * time.Second   // 5分钟
    BalanceRegionInterval = 600 * time.Second   // 10分钟
)

// 为什么不同步?
// TiKV定期汇报状态(心跳)
// PD不一定每次心跳都要做决策
// 决策是独立的周期任务

一次完整的决策流程(时间轴)

时刻      事件                          线程
────────────────────────────────────────────────
T0        Region 123心跳到达              PatrolRegions
          ├─更新region元数据 O(1)
          └─放入待检查列表

T+0.1s    Leader决策周期触发              ScheduleController
          ├─扫描所有region
          ├─计算leader分布
          └─生成transfer operator

T+0.3s    operator入队                   drivePushOperator
          ├─选择target store
          ├─生成RegionHeartbeatResponse
          └─放入msgCh

T+0.5s    心跳推送(500ms触发)          HeartbeatStreams
          ├─从msgCh消费operator
          ├─通过gRPC发送给TiKV
          └─TiKV接收并执行

T+0.6s    TiKV下一次心跳                  TiKV
          ├─执行TransferLeader
          └─心跳上报进度

关键发现:

  • 心跳汇报(T0)到决策生成(T+0.1s):100ms
  • 决策生成到推送(T+0.5s):500ms(等待推送周期)
  • 总延迟:600ms

第4部分:Operator 的完整生命周期

状态转移图(源码视角)

// pkg/operator/operator.go
type OperatorStatus int

const (
    CREATED  = 0   // 刚创建
    WAITING  = 1   // 等待前置条件
    STARTED  = 2   // 已开始执行(已推送给TiKV)
    SUCCESS  = 3   // 成功完成
    TIMEOUT  = 4   // 超时放弃
    CANCELED = 5   // 被取消
)

// 状态转移规则
func (op *Operator) Transition() {
    switch op.status {
    case CREATED:
        // 1. 检查前置条件(其他operator冲突?)
        // 2. 如果满足 → WAITING
        // 3. 否则等待
        
    case WAITING:
        // 1. 前置条件满足? → STARTED
        // 2. 超时? → CANCELED
        
    case STARTED:
        // 1. 检查TiKV执行进度(心跳报告)
        // 2. 完成? → SUCCESS
        // 3. 超时? → TIMEOUT
        // 4. 冲突? → 删除(TiKV自动撤销)
    }
}

与Heartbeat的交互

PD内存中:
┌──────────────────────────────────┐
│ Operator Queue                   │
│ ├─ Op1: TransferLeader(STARTED)  │
│ ├─ Op2: AddPeer(STARTED)         │
│ └─ Op3: Merge(WAITING)           │
└────────────┬─────────────────────┘
             │ 每500ms推送
             ⬇️

┌──────────────────────────────────┐
│ drivePushOperator()              │
│ ├─ 检查Op1进度:还没完成→继续推  │
│ ├─ 检查Op2进度:预提交成功→下一步│
│ └─ 检查Op3:前置不满足→暂不推    │
└────────────┬─────────────────────┘
             │
             ⬇️

┌──────────────────────────────────┐
│ HeartbeatStreams                 │
│ msgCh: [Op1, Op2, Op3_diff]      │
└────────────┬─────────────────────┘
             │ 异步gRPC发送
             ⬇️
           TiKV

第5部分:缓冲机制与背压

msgCh 的缓冲设计

// 1024缓冲的含义
type HeartbeatStreams struct {
    msgCh chan core.RegionHeartbeatResponse  // 缓冲1024
}

// 实际大小计算:
// 1024条消息 × 平均500字节 ≈ 512KB内存
// 500ms推送周期 → 吞吐 = 1024 / 0.5s = 2048 ops/s

// 演算例子:
// - 100个store,每个store缓冲不超过30条等待消息
// - 集群稳定时消耗:100 * 30 = 3000条位置,超过1024缓冲
// → 需要gRPC发送速度 > 生成速度

背压机制(流量控制)

// 当msgCh满时会发生什么?

func (s *HeartbeatStreams) SendMsg(region *core.RegionInfo, op *Operation) {
    resp := buildResponse(op)
    
    select {
    case s.msgCh <- resp:        // ✅ 成功放入
    case <-s.hbStreamCtx.Done(): // ❌ ctx取消
    // ⚠️ 没有default,意味着会阻塞!
    }
}

// 阻塞链路:
// msgCh满
// ├─ SendMsg() 阻塞
// ├─ PushOperators() 阻塞
// ├─ drivePushOperator() 阻塞
// └─ 新operator不能加入(直到有空位)

// 这是自然的背压:生成速度会被推送速度限制

背压的价值:

  • 防止operator队列无限增长
  • 自动流量控制(不需要手动限流)
  • 内存可预测

第6部分:实战代码模板

模板1:构建自己的Coordinator

package mycoordinator

import (
    "context"
    "sync"
    "time"
)

type MyCoordinator struct {
    mu      sync.RWMutex
    ctx     context.Context
    cancel  context.CancelFunc
    wg      sync.WaitGroup
    
    // CP层:持久化存储
    store Store  // etcd/raft
    
    // 内存协调
    decisions chan Decision  // 1024缓冲
    
    // AP层:心跳推送
    streams *HeartbeatStreams
}

func (c *MyCoordinator) Run() {
    // 第一阶段:加载持久化配置
    if err := c.LoadConfig(); err != nil {
        // 从store恢复配置
    }
    
    // 第二阶段:启动工作线程
    c.wg.Add(3)
    go c.patrolResources()        // 线程1:资源巡检
    go c.periodicDecision()       // 线程2:周期决策
    go c.driveHeartbeat()         // 线程3:心跳推送
}

// 线程1:主动发现问题
func (c *MyCoordinator) patrolResources() {
    defer c.wg.Done()
    ticker := time.NewTicker(10 * time.Second)
    defer ticker.Stop()
    
    for range ticker.C {
        c.mu.RLock()
        resources := c.GetAllResources()
        c.mu.RUnlock()
        
        for _, res := range resources {
            problem := c.CheckHealth(res)
            if problem != nil {
                decision := c.MakeDecision(problem)
                select {
                case c.decisions <- decision:
                case <-c.ctx.Done():
                    return
                }
            }
        }
    }
}

// 线程2:周期决策(与巡检独立)
func (c *MyCoordinator) periodicDecision() {
    defer c.wg.Done()
    ticker := time.NewTicker(5 * time.Minute)
    defer ticker.Stop()
    
    for range ticker.C {
        c.mu.RLock()
        state := c.GetClusterState()
        c.mu.RUnlock()
        
        // 复杂的负载均衡决策(O(n²)不需要每次都做)
        decisions := c.BalanceLoad(state)
        
        for _, decision := range decisions {
            select {
            case c.decisions <- decision:
            case <-c.ctx.Done():
                return
            }
        }
    }
}

// 线程3:心跳推送(连接所有决策和执行)
func (c *MyCoordinator) driveHeartbeat() {
    defer c.wg.Done()
    ticker := time.NewTicker(500 * time.Millisecond)
    defer ticker.Stop()
    
    for range ticker.C {
        // 批量推送所有pending决策
        for {
            select {
            case decision := <-c.decisions:
                c.streams.SendMessage(decision)
            case <-c.ctx.Done():
                return
            default:
                // 非阻塞,继续到下一个推送周期
                return
            }
        }
    }
}

// 持久化关键配置
func (c *MyCoordinator) SaveConfig(cfg Config) error {
    data := serialize(cfg)
    return c.store.Put("/config", data)  // Raft写入
}

// 恢复关键配置
func (c *MyCoordinator) LoadConfig() error {
    data, err := c.store.Get("/config")
    if err != nil {
        return err
    }
    return deserialize(data, &c.config)
}

模板2:ReportOptions 的最佳实践

type ClusterConfig struct {
    // 心跳配置(快速感知)
    HeartbeatInterval time.Duration  // 10-30s(由客户端决定)
    
    // 决策配置(拉长周期避免波动)
    Schedulers []SchedulerConfig
}

type SchedulerConfig struct {
    Name     string
    Interval time.Duration  // 300-600s
    Enabled  bool
    Params   map[string]interface{}
}

// 配置示例
var DefaultConfig = ClusterConfig{
    HeartbeatInterval: 10 * time.Second,
    Schedulers: []SchedulerConfig{
        {
            Name:     "balance-leader",
            Interval: 300 * time.Second,  // 5分钟
            Enabled:  true,
        },
        {
            Name:     "balance-region",
            Interval: 600 * time.Second,  // 10分钟
            Enabled:  true,
        },
    },
}

// 调优建议:
// 低吞吐场景:HeartbeatInterval=30s, 决策Interval=600s
// 高吞吐场景:HeartbeatInterval=10s, 决策Interval=300s
// 频繁扩容:HeartbeatInterval=10s, 决策Interval=180s

模板3:operator 缓冲与推送

type OperatorController struct {
    mu        sync.RWMutex
    operators map[uint64]*Operator  // regionID -> operator
    opCount   int
    maxOps    int  // 最多保留多少个operator
}

func (c *OperatorController) PushOperators(
    record func(*Operator) error) {
    
    c.mu.RLock()
    pending := make([]*Operator, 0, len(c.operators))
    for _, op := range c.operators {
        if op.status == STARTED {
            pending = append(pending, op)
        }
    }
    c.mu.RUnlock()
    
    // 批量推送(可以分组优化gRPC)
    for _, op := range pending {
        // 检查进度
        if op.IsCompleted() {
            c.mu.Lock()
            delete(c.operators, op.RegionID)
            c.mu.Unlock()
            continue
        }
        
        // 记录历史(用于debug)
        if err := record(op); err != nil {
            log.Warn("record operator failed", err)
        }
        
        // 推送给heartbeat流
        // streams.SendMsg(op.Region, op)
    }
}

第7部分:性能调优检查单

诊断命令(假设有metrics)

# 查看决策延迟分布
curl http://pd:2379/metrics | grep 'schedule_duration'

# 查看operator队列深度
curl http://pd:2379/metrics | grep 'operator_waiting_count'

# 查看heartbeat吞吐
curl http://pd:2379/metrics | grep 'region_heartbeat'

# 查看推送失败率
curl http://pd:2379/metrics | grep 'heartbeat_stream.*err'

常见问题排查

问题症状原因解决
决策延迟高900ms+msgCh缓冲不足或gRPC慢增加缓冲或优化gRPC
频繁波动频繁转移leader决策周期太短增加BalanceLeaderInterval
operator堆积waiting队列深度高前置条件冲突过多检查冲突检测逻辑
内存增长Coordinator内存持续增operator未及时清理检查COMPLETED状态清理

总结

PD Coordinator的三大设计原则

  1. 分离关注点

    • 心跳:快速感知状态
    • 决策:周期生成指令
    • 推送:异步缓冲重试
  2. 强弱一致性分离

    • 配置存储:Raft强一致
    • 指令执行:最终一致
  3. 事件驱动架构

    • 通道缓冲:无锁并发
    • 细粒度锁:只保护状态
    • 优先级队列:自然背压

学习路径

  1. ✅ 理解两层架构(CP + AP)
  2. ✅ 理解四线程分工
  3. ✅ 理解operator生命周期
  4. ✅ 理解heartbeat推送机制
  5. 实现一个简单的Coordinator原型
  6. ✅ 理解缓冲和背压
  7. ✅ 性能调优和故障排查