运维常把 PD(Placement Driver)想象成一个实时指挥中心:一发现某个 TiKV 节点快满了,立刻把数据挪走。真实情况是异步、心跳驱动、建议式的控制循环——PD 不直接搬数据,也不保证调度立刻执行。它周期性收集 Store 心跳和 Region 心跳,在内存里维护一份集群视图,用若干独立的调度器算出该做什么,把结果封装成 Operator(算子),塞进下一次心跳的响应里”建议”给 Region Leader;Leader 可以执行,也可以因为自身状态(例如正在处理其他变更)跳过它。
第 3 篇 定义了 Region、peer、epoch;第 6 篇 讲了副本怎么用 Snapshot 迁移、日志怎么截断。本篇钉住 PD 怎么知道该调度、算出什么、怎么落地——即 heartbeat → scheduler → operator → raftstore 执行的完整链路,并说明它和 distributed/27 再平衡叙事 的分工差异。
本文是「TiKV / HTAP 内核」系列第 7 篇(共 18 篇)。上一篇:Snapshot、log GC 与副本迁移;下一篇:Split / Merge / 热点;→ 系列目录
版本锚定:TiKV / PD 7.1–8.4。8.4.0 前后部分默认值不同的地方文内单独标注;不覆盖云托管版本的托管调度策略。
一、PD 的信息来源:两种心跳
PD 集群本身是一组基于 etcd Raft 的节点(复用 etcd 的 Raft 实现管理自己的元数据,不是 TiKV 的 Multi-Raft),对外只有 leader 处理调度请求。PD 对集群状态一无所知,除非 TiKV 主动上报。上报靠两种心跳:
| 心跳类型 | 发起者 | 默认周期 | 核心字段 |
|---|---|---|---|
| Store heartbeat | 每个 TiKV
节点(pd-store-heartbeat-tick-interval) |
10s | capacity、available、region_count、is_busy、bytes/keys
written/read、SlowScore(≥ v6.2) |
| Region heartbeat | 每个 Region 的
Leader(pd-heartbeat-tick-interval) |
60s(或状态变化时立即触发) | region_id、leader、peers、start/end key、approximate_size/keys、term、down_peers、pending_peers |
来源:TiKV
etc/config-template.toml(pd-heartbeat-tick-interval = "60s"、pd-store-heartbeat-tick-interval = "10s");PD
Wiki Metadata Management(region heartbeat 上报
RegionHeartbeatRequest 字段)。
两种心跳的粒度不同:Store 心跳给 PD
容量与整体负载的宏观视图;Region 心跳给 PD
每个 Raft 组的精确位置与大小。PD
用后者重建一份全局的 Region 路由表——这份表也是
TiDB/客户端通过 GetRegion
拿到路由信息的来源。Region
状态变化(split、merge、副本变更、leader
切换)会立即触发一次心跳,不必等 60
秒周期,这是调度算子能较快确认执行结果的原因。
flowchart LR
subgraph TiKV["TiKV nodes"]
S1["Store 1"]
S2["Store 2"]
RL["Region leader"]
end
subgraph PDL["PD leader"]
CI["Cluster info<br/>(in-memory)"]
SCH["Schedulers"]
OC["Operator controller"]
end
S1 -- "store heartbeat, 10s" --> CI
S2 -- "store heartbeat, 10s" --> CI
RL -- "region heartbeat, 60s or on change" --> CI
CI --> SCH
SCH -- "candidate operator" --> OC
OC -- "operator in heartbeat response" --> RL
RL -- "execute or skip" --> S1
PD 对 TiKV 没有推送通道:算子永远是心跳响应的”顺风车”,PD 不会主动建立连接去命令某个 Region。这意味着调度的最短延迟下限约等于该 Region 的心跳周期(心跳变化触发时可以更快),不是”毫秒级指挥”。
二、调度的基本单元:Operator 与 OperatorStep
PD 生成的调度决策叫 Operator,一个 Operator 由若干 OperatorStep 组成,且都要落到三个 Raft 原语上——TiDB Docs Scheduling 称之为 basic scheduling operators:
- Add a
replica(
AddReplica/AddLearner+PromoteLearner) - Remove a
replica(
RemoveReplica) - Transfer
leader(
TransferLeader)
一次 Region 迁移(move-peer)通常拼成:
// PD pd/schedule 包,CreateMovePeerOperator 简化示意
// 来源:tikv/pd wiki《Balance Scheduling》
Operator{
RegionID: regionID,
Kind: OpRegion,
Steps: []OperatorStep{
{Type: AddLearner, StoreID: targetStore}, // 先加 Learner,避免立即变成投票副本
{Type: PromoteLearner, StoreID: targetStore}, // Learner 追上日志后提升为 Voter
{Type: RemovePeer, StoreID: sourceStore}, // 删除旧副本
},
}先加 Learner 而不是直接加 Voter,是因为 Learner 不参与选举和多数派计算——新副本还没追上日志时,把它算进多数派会威胁可用性(如果它和另一个节点同时故障,恰好凑够”多数派缺失”)。这个”先 Learner 再 Promote”路径与 第 6 篇 的 Snapshot 传输机制直接对接:新 Learner 需要一份 Snapshot 才能追上日志。
Leader
转移(transfer-leader)只需要一次 Raft
内部协商,通常几毫秒完成,不涉及任何数据搬迁——这是它和
move-peer
代价差异的关键:前者只挪”处理请求的权利”,后者要挪整个
Region 的数据。
PD Docs 明确指出算子只是”建议”:
Note that here “operators” are only suggestions to the Region leader, which can be skipped by Regions. Leader of Regions can decide whether to skip a scheduling operator or not based on its current status.
—— TiDB Docs, TiDB Scheduling
这条边界经常被忽略:运维手动
operator add
下发的算子同样可能被跳过,不是”强制执行”。
三、默认调度器:balance-leader / balance-region / hot-region
PD 默认启用三个调度器(server/schedulers
包),每个调度器独立算分、独立生成候选算子:
| 调度器 | 目标 | 打分依据 | 产出算子类型 |
|---|---|---|---|
| balance-leader-scheduler | 均衡各 Store 的 Leader 数量(Leader 承担全部读写请求) | 该 Store 上以其为 Leader 的 Region 大小之和 | transfer-leader |
| balance-region-scheduler | 均衡各 Store 的 副本存储量,避免个别节点写满 | 综合容量、已用空间、Region 数计算的 score | move-peer(AddLearner→Promote→RemovePeer) |
| balance-hot-region-scheduler | 分散读写热点,避免少数 Store 成为瓶颈 | 各 Region 的字节/QPS 速率,超过
hot-region-cache-hits-threshold 判定为热 |
写热点:move-peer +
transfer-leader;读热点:仅
transfer-leader |
来源:TiKV Development Guide Scheduling;TiDB Docs Best Practices for PD Scheduling(v3.0 起写热点会同时挪副本和 Leader,读热点只挪 Leader——因为读只经过 Leader,挪 Leader 就能分散读压力,不必搬数据)。
balance-leader 与
balance-region 的评分方法不同:前者只看”该
Store 上 Leader 对应 Region
的大小总和”,后者要考虑每个节点的实际存储容量(high-space-ratio
/ low-space-ratio
等阈值),逼近满盘的节点会被优先卸载,即便它的 Leader
数量并不多。
调度并发与限流
单靠打分算法不足以保证系统稳定——如果同时对几百个 Region
发起迁移,Snapshot 传输会打满网络。PD 用一组
*-schedule-limit 参数控制窗口大小:
| 参数 | 默认值 | 含义 |
|---|---|---|
leader-schedule-limit |
4 | 同时进行的 leader 调度任务数 |
region-schedule-limit |
2048 | 同时进行的 OpRegion 类算子数 |
replica-schedule-limit |
64 | 同时进行的副本补齐/修复任务数 |
merge-schedule-limit |
8 | 同时进行的 Region Merge 任务数 |
max-snapshot-count |
64 | 单个 Store 同时接收/发送的 Snapshot 数上限 |
max-pending-peer-count |
64 | 单个 Store 上待追日志的 pending peer 数上限 |
来源:PD Control User
Guide(v6.5)config show
输出。这些限制维护的是”窗口”而非硬速率——每种算子类型有独立窗口,region-schedule-limit
管的是 OpRegion 这一类算子,不影响
transfer-leader(OpLeader)的并发。
优先级同样有区分:修复副本缺失/位置违规(高)> 热点平衡(中)> 常规 leader/region 平衡(低)。当 PD 同时发现”某 Region 少了一个副本”和”集群负载略微不均”,前者会先被调度——可用性修复优先于负载均衡。
四、心跳驱动的调度循环
把 §一到§三 串起来看一次完整的调度往返:
sequenceDiagram
participant Leader as Region leader (TiKV)
participant PD as PD leader
participant Sched as Schedulers
participant OC as Operator controller
Leader->>PD: region heartbeat (size, qps, peers)
PD->>PD: update in-memory cluster info
PD->>Sched: run balance-leader / balance-region / hot-region
Sched->>OC: candidate operator (score-based)
OC->>OC: check schedule-limit / priority
Note over OC: admitted operator queued
Leader->>PD: next region heartbeat
PD-->>Leader: heartbeat response carries operator
Leader->>Leader: execute step, or skip if busy
Leader->>PD: heartbeat reports new state
PD->>OC: mark operator step done / timeout
关键点:PD 每次收到 Region 心跳都会检查该 Region
是否有 pending
operator,如果有,就把它塞进心跳响应;后续心跳用来确认算子的执行进度。如果
Region Leader
在算子发出后没有按预期更新状态(比如迟迟不完成
AddLearner),PD
会在超时后判定算子失败并重新规划——这也是为什么
max-snapshot-count、max-pending-peer-count
等窗口参数直接决定了”卡住的算子”要多久才会被发现。
五、约束层:调度器不能违反的规则
调度器打分决定”往哪调”,但放置规则(Placement Rules) 决定”能不能调到那”。每个 TiKV 节点可以打标签:
[server]
labels = "zone=z1,rack=r1,host=h1"默认策略要求同一 Region 的副本在
location_labels
指定的层级上互不相同(隔离),加上
max-replicas(默认 3)与可选的
constraints(例如把某类副本限制在特定
zone)。调度器生成候选目标 Store
时必须先满足这层约束,打分只在合法候选集合内比较——这解释了为什么”最空的节点”有时反而不会被选中:它可能违反标签隔离规则。放置规则的细节与副本迁移的执行步骤留给后续排障篇;本篇只需知道调度分两层:约束(能不能去)与打分(该不该去)。
六、与 distributed/27 的分工:叙事 vs 机制
《数据再平衡》
把 PD 放进”固定分区 / 动态分区 /
按比例分区”三种策略的对照里,讲的是为什么
TiKV 选动态分区、re-balance
的一般性目标(负载均衡、最小化迁移量、可用性)。那篇文章里的
BalanceLeaderScheduler、BalanceRegionScheduler、HotRegionScheduler
是教学简化版伪代码,用来说明调度器的一般模式。
本篇的差异在于钉版本化的机制细节:心跳字段与周期、Operator/OperatorStep
的真实拼装方式(AddLearner → PromoteLearner → RemovePeer,而不是直接换副本)、*-schedule-limit
的默认值与优先级顺序、“算子只是建议”这条容易被忽略的边界。两篇不冲突:distributed/27
回答”为什么这样设计”,本篇回答”具体怎么跑起来”。
七、学术谱系与开放问题
谱系:把”元数据与调度决策集中在一个进程里,靠周期性心跳而非实时指令驱动数据搬迁”这个设计模式往前追,最直接的先例是 Spanner 的 placement driver(Corbett et al., Spanner: Google’s Globally-Distributed Database,OSDI 2012,第 2.4 节):
The placement driver periodically communicates with the spanservers to find data that needs to be moved, either to meet updated replication constraints or to balance load. […] Movedir is the background task used to move directories between Paxos groups.
Spanner 的 directory(可再细分为
fragment)对应 TiKV 的
Region;movedir 对应 PD 的
move-peer
算子;两者都强调迁移在后台异步进行、只在收尾用一次事务做原子切换,避免阻塞在线读写。TiKV
文档从未直接声明”移植了 Spanner 的 placement
driver”,但架构同构关系足够清楚:单一权威节点持有全局元数据视图,用后台任务把可迁移的数据单元在存储组之间搬动,这是这一类系统的共同范式,而不是
TiKV 独有的发明。
争论:集中式调度(PD/Spanner
路线)与去中心化调度是否哪个更优,业界没有共识,CockroachDB
的演进史本身就是一次公开的架构辩论。CockroachDB 早期用
ReplicateQueue——每个节点独立扫描自己持有的副本,靠
gossip 出的
StoreDescriptor(可能滞后到一分钟)做决策;社区在
RFC: Rebalancing V2
中明确讨论过改成中心化分配器,理由是去中心化容易因为信息滞后而震荡(thrashing),但最终止步于”中心化分配器需要迭代全部
RangeDescriptor,实现和性能都有代价,中心节点本身也可能出故障”。CockroachDB
后来引入 StoreRebalancer 只解决 QPS
维度的均衡,仍然是去中心化架构,而不是转向 PD
式的中心节点。
这构成一个有文献支撑的真实架构分歧:
- 中心化路线(Spanner placement driver、TiKV PD):全局视图精确、调度决策容易做优先级排序,代价是中心节点是单点(PD 用 Raft 做自身高可用来缓解,但调度逻辑本身仍在单个 leader 进程里跑)。
- 去中心化路线(CockroachDB
ReplicateQueue):没有单点,但天然要面对信息滞后导致的震荡——CockroachDB 自己的 RFC 承认了这一点,并靠限速和退避缓解而非根治。
开放问题:多大规模、多高的调度频率下,中心化
PD 的”单 leader 进程算分”会成为瓶颈?PD
官方文档没有公开这个上限的量化数据,社区讨论多集中在”如何调参躲开瓶颈”(分
batch、调
-schedule-limit),而不是”瓶颈在哪”的系统性测量——这仍是留给读者在自己集群规模上验证的问题,本文不编造具体阈值。
八、常见误解
| 误解 | 事实 |
|---|---|
| 「PD 一发现不均衡就立刻搬数据」 | PD 只在心跳响应里给建议;执行时机取决于心跳周期与 Region Leader 是否接受 |
| 「算子下发后一定会执行」 | Region Leader 可以根据自身状态跳过算子;手动
operator add 同样可能被跳过 |
| 「move-peer 和 transfer-leader 代价相同」 | transfer-leader 只是 Raft
内部协商,几毫秒;move-peer 要经 Snapshot
传输数据,代价高一个量级 |
| 「新副本直接加进 Raft 组投票」 | 先加 Learner 追日志,追上后才
PromoteLearner,避免未同步副本冲抵多数派 |
| 「PD 调度是全局最优解」 | 各调度器独立打分、独立生成候选,是启发式的局部决策,不是集中式全局优化 |
九、小结
三句话小结:
- PD 靠 store 心跳(10s) 和 region 心跳(60s 或状态变化时) 重建集群视图,调度是异步、心跳驱动、可被跳过的建议,不是实时指令。
- 调度的基本单元是 Operator(由
AddLearner → PromoteLearner → RemovePeer等 OperatorStep 组成),默认由balance-leader、balance-region、balance-hot-region三个调度器打分产出,且要先通过放置规则的合法性检查,再受*-schedule-limit窗口限流。 - “中心化调度节点 + 后台异步迁移”这套范式在 Spanner 的
placement driver / movedir 里已有先例;CockroachDB
坚持去中心化
ReplicateQueue并为此付出震荡代价,说明这两条路线至今没有定论。
下一篇进入 Split / Merge / 热点:Region 何时该被切开、何时该被合并,以及 Load Base Split 如何识别热点。
返回 系列目录
参考资料
规范 / 文档(A 级)
- TiKV Development Guide,
Scheduling(
server/schedulers包、balance-leader/balance-region/balance-hot-region 逻辑)。 - TiDB Docs, TiDB Scheduling(heartbeat、basic scheduling operators、“operators are only suggestions”)。
- TiDB Docs, Best Practices for PD Scheduling(balance-leader/balance-region 评分差异、热点调度自 v3.0 起的读写区分)。
- TiDB Docs, PD Control User
Guide(v6.5,
config show各*-schedule-limit默认值;operator add命令)。 - TiKV
etc/config-template.toml(pd-heartbeat-tick-interval、pd-store-heartbeat-tick-interval默认值)。 - tikv/pd Wiki, Balance Scheduling、Metadata
Management(
RegionHeartbeatRequest字段、CreateMovePeerOperator)。
论文(A 级)
- Corbett, James C., et al. Spanner: Google’s Globally-Distributed Database. OSDI 2012(第 2.4 节 placement driver / movedir,PD 架构范式的直接先例)。
源码 / 工程讨论(A/B 级)
- CockroachDB,
docs/RFCS/20160503_rebalancing_v2.md(去中心化
ReplicateQueue与中心化分配器的公开权衡讨论;B 级但含具体工程理由)。 - CockroachDB,
docs/tech-notes/rebalancing.md(
StoreRebalancer与ReplicateQueue的分工)。
站内对读
- Region 模型、Snapshot、log GC 与副本迁移。
- 《数据再平衡》(固定/动态/按比例分区的一般性叙事,与本篇机制细节互补)。
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【TiKV / HTAP 内核】HTAP / 分布式 KV 全景:TiDB–TiKV–PD–TiFlash 四件套
定位 TiKV 7.x/8.x 在 RocksDB 单机 LSM、Raft 协议与 HTAP 范式之间补的那一层:TiDB、TiKV、PD、TiFlash 四组件各自的抽象边界,以及本系列 18 篇的读法。
【TiKV / HTAP 内核】Multi-Raft:一 Region 一 Raft 组
拆解 TiKV 的 Multi-Raft 架构:为什么每个 Region 是独立的 Raft 组,与 etcd 单 Raft 组对照差在哪;跨 Region 事务代价作为开放问题收束,不重写 Raft 协议本身。
【TiKV / HTAP 内核】TSO:物理加逻辑的全局时间戳
拆解 PD 的 TSO 如何用 46 位物理毫秒 + 18 位逻辑计数器分配全局单调时间戳,PD leader 如何靠 etcd 时间窗口做故障恢复、客户端如何批量取号;用一句话对照 Spanner TrueTime 的硬件不确定区间路线与 Milvus 的 GuaranteeTs 用法。
【TiKV / HTAP 内核】CockroachDB 对照:Range + Raft 的另一种落地
对照 CockroachDB 的 Range/Leaseholder/Raft leader 分层与 Percolator 式事务,找出 TiKV/TiDB 分离部署与 CockroachDB 单进程一体化的关键工程差异;OceanBase 一段边界,回链 distributed/39 选型对照。