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的三大设计原则
-
分离关注点
- 心跳:快速感知状态
- 决策:周期生成指令
- 推送:异步缓冲重试
-
强弱一致性分离
- 配置存储:Raft强一致
- 指令执行:最终一致
-
事件驱动架构
- 通道缓冲:无锁并发
- 细粒度锁:只保护状态
- 优先级队列:自然背压
学习路径
- ✅ 理解两层架构(CP + AP)
- ✅ 理解四线程分工
- ✅ 理解operator生命周期
- ✅ 理解heartbeat推送机制
- ✅ 实现一个简单的Coordinator原型
- ✅ 理解缓冲和背压
- ✅ 性能调优和故障排查