土法炼钢兴趣小组的算法知识备份

【TiKV / HTAP 内核】PD 元数据与调度:心跳、算子与调度器

文章导航

分类入口
databasestorage
标签入口
#tikv#pd#scheduling#heartbeat#operator#transfer-leader#balance-region#placement-driver#raft

目录

运维常把 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 的 Leaderpd-heartbeat-tick-interval 60s(或状态变化时立即触发) region_id、leader、peers、start/end key、approximate_size/keys、term、down_peers、pending_peers

来源:TiKV etc/config-template.tomlpd-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

一次 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-leaderbalance-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-leaderOpLeader)的并发。

优先级同样有区分:修复副本缺失/位置违规(高)> 热点平衡(中)> 常规 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-countmax-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 的一般性目标(负载均衡、最小化迁移量、可用性)。那篇文章里的 BalanceLeaderSchedulerBalanceRegionSchedulerHotRegionScheduler教学简化版伪代码,用来说明调度器的一般模式。

本篇的差异在于钉版本化的机制细节:心跳字段与周期、Operator/OperatorStep 的真实拼装方式(AddLearner → PromoteLearner → RemovePeer,而不是直接换副本)、*-schedule-limit 的默认值与优先级顺序、“算子只是建议”这条容易被忽略的边界。两篇不冲突:distributed/27 回答”为什么这样设计”,本篇回答”具体怎么跑起来”。


七、学术谱系与开放问题

谱系:把”元数据与调度决策集中在一个进程里,靠周期性心跳而非实时指令驱动数据搬迁”这个设计模式往前追,最直接的先例是 Spannerplacement 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 的 Regionmovedir 对应 PD 的 move-peer 算子;两者都强调迁移在后台异步进行、只在收尾用一次事务做原子切换,避免阻塞在线读写。TiKV 文档从未直接声明”移植了 Spanner 的 placement driver”,但架构同构关系足够清楚:单一权威节点持有全局元数据视图,用后台任务把可迁移的数据单元在存储组之间搬动,这是这一类系统的共同范式,而不是 TiKV 独有的发明。

争论:集中式调度(PD/Spanner 路线)与去中心化调度是否哪个更优,业界没有共识,CockroachDB 的演进史本身就是一次公开的架构辩论。CockroachDB 早期用 ReplicateQueue——每个节点独立扫描自己持有的副本,靠 gossip 出的 StoreDescriptor(可能滞后到一分钟)做决策;社区在 RFC: Rebalancing V2 中明确讨论过改成中心化分配器,理由是去中心化容易因为信息滞后而震荡(thrashing),但最终止步于”中心化分配器需要迭代全部 RangeDescriptor,实现和性能都有代价,中心节点本身也可能出故障”。CockroachDB 后来引入 StoreRebalancer 只解决 QPS 维度的均衡,仍然是去中心化架构,而不是转向 PD 式的中心节点。

这构成一个有文献支撑的真实架构分歧:

开放问题:多大规模、多高的调度频率下,中心化 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 调度是全局最优解」 各调度器独立打分、独立生成候选,是启发式的局部决策,不是集中式全局优化

九、小结

三句话小结

  1. PD 靠 store 心跳(10s)region 心跳(60s 或状态变化时) 重建集群视图,调度是异步、心跳驱动、可被跳过的建议,不是实时指令。
  2. 调度的基本单元是 Operator(由 AddLearner → PromoteLearner → RemovePeerOperatorStep 组成),默认由 balance-leaderbalance-regionbalance-hot-region 三个调度器打分产出,且要先通过放置规则的合法性检查,再受 *-schedule-limit 窗口限流。
  3. “中心化调度节点 + 后台异步迁移”这套范式在 Spanner 的 placement driver / movedir 里已有先例;CockroachDB 坚持去中心化 ReplicateQueue 并为此付出震荡代价,说明这两条路线至今没有定论。

下一篇进入 Split / Merge / 热点:Region 何时该被切开、何时该被合并,以及 Load Base Split 如何识别热点。


上一篇Snapshot、log GC 与副本迁移

下一篇Split / Merge / 热点

返回 系列目录


参考资料

规范 / 文档(A 级)

  1. TiKV Development Guide, Schedulingserver/schedulers 包、balance-leader/balance-region/balance-hot-region 逻辑)。
  2. TiDB Docs, TiDB Scheduling(heartbeat、basic scheduling operators、“operators are only suggestions”)。
  3. TiDB Docs, Best Practices for PD Scheduling(balance-leader/balance-region 评分差异、热点调度自 v3.0 起的读写区分)。
  4. TiDB Docs, PD Control User Guide(v6.5,config show*-schedule-limit 默认值;operator add 命令)。
  5. TiKV etc/config-template.tomlpd-heartbeat-tick-intervalpd-store-heartbeat-tick-interval 默认值)。
  6. tikv/pd Wiki, Balance SchedulingMetadata ManagementRegionHeartbeatRequest 字段、CreateMovePeerOperator)。

论文(A 级)

  1. Corbett, James C., et al. Spanner: Google’s Globally-Distributed Database. OSDI 2012(第 2.4 节 placement driver / movedir,PD 架构范式的直接先例)。

源码 / 工程讨论(A/B 级)

  1. CockroachDB, docs/RFCS/20160503_rebalancing_v2.md(去中心化 ReplicateQueue 与中心化分配器的公开权衡讨论;B 级但含具体工程理由)。
  2. CockroachDB, docs/tech-notes/rebalancing.mdStoreRebalancerReplicateQueue 的分工)。

站内对读

  1. Region 模型Snapshot、log GC 与副本迁移
  2. 《数据再平衡》(固定/动态/按比例分区的一般性叙事,与本篇机制细节互补)。

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-07-16 · database / storage

【TiKV / HTAP 内核】TSO:物理加逻辑的全局时间戳

拆解 PD 的 TSO 如何用 46 位物理毫秒 + 18 位逻辑计数器分配全局单调时间戳,PD leader 如何靠 etcd 时间窗口做故障恢复、客户端如何批量取号;用一句话对照 Spanner TrueTime 的硬件不确定区间路线与 Milvus 的 GuaranteeTs 用法。


By .