第
11 篇 讲完悲观事务落锁与
ResolveLock,锁与数据都已经能正确落在 lock/write CF
上。但一条
SELECT count(*) FROM t WHERE age > 18
如果先把全表数据传回 TiDB 再算,网络就会成为瓶颈——TiDB
团队的解法是把过滤、聚合、TopN 这类计算下推到 TiKV
本地执行,只把结果送回来。这个下推入口叫
Coprocessor。
常见的误解是把 Coprocessor 当成”TiKV 能跑任意 SQL”。实际上 TiKV 从不解析 SQL 文本,它只执行 TiDB 编译好、序列化成 protobuf 的一棵执行器 DAG(Directed Acyclic Graph)。本文只钉这一层:DAG 请求长什么样、TiKV 怎么在 Region snapshot 上把它跑起来、下推到哪一步就不再下推,以及这套”行存批量下推”和 query-engine 系列里讲的分布式 MPP 向量化执行是不是一回事(不是)。
版本锚定:TiKV 7.x/8.x;DAG 协议定义见 pingcap/tipb 与 pingcap/kvproto(A 级,源码仓库)。执行器实现见 tikv/tikv 仓库
src/coprocessor/、components/tidb_query_executors、components/tidb_query_expr。
一、DAG 请求:TiDB 编译,TiKV 执行
TiDB 侧的 optimizer
决定”这段计划能不能下推”之后,会把子计划编译成一个
tipb.DAGRequest,通过 kvproto 定义的
Coprocessor::Request 发给相关 Region 的 TiKV
节点。TiKV 开发指南(A
级,官方文档)把这个入口描述为两件事的组合:一份计划(control
flow)加一份数据(data
flow)——计划描述”怎么算”,Region
的本地存储提供”算什么”。
Request 的关键字段(kvproto,简化):
message Request {
kvrpcpb.Context context = 1;
int64 tp = 2; // 请求类型:DAG / Analyze / Checksum
bytes data = 3; // 序列化后的 DAGRequest
repeated KeyRange ranges = 4; // 本次请求覆盖的 key 范围(落在该 Region 内)
uint64 start_ts = 7; // 快照读时间戳
}data
字段里是一棵执行器链,每个节点是
tipb 里定义的一种 executor:
| Executor | 作用 | 输入 |
|---|---|---|
TableScan |
按主键范围扫行 | Region 的 key range |
IndexScan |
按索引范围扫 | Region 的 key range |
Selection |
过滤(WHERE) |
TableScan / IndexScan |
Aggregation |
聚合(COUNT/SUM/AVG) |
TableScan / IndexScan / Selection |
TopN |
排序取前 N | 同上 |
Limit |
截断结果行数 | 同上 |
Projection |
列投影/表达式计算 | 同上 |
表达式(Selection
的过滤条件、Aggregation
的聚合函数参数)用统一的 Expr
结构描述,分三类:列引用(Column)、常量(Constant)、标量函数(ScalarFunction),每个函数在
tipb 里有固定的整数 ID——这保证了 TiDB 与 TiKV
对同一个下推表达式的理解完全一致,不依赖字符串解析。
flowchart TD
SQL["TiDB: SELECT count(*) FROM t WHERE age > 18"]
SQL --> PLAN["Planner builds pushdown fragment"]
PLAN --> DAG["tipb.DAGRequest<br/>Aggregation -> Selection -> TableScan"]
DAG --> ENC["Encode into Coprocessor.Request.data"]
ENC --> RPC["gRPC Coprocessor call, per Region"]
RPC --> TIKV["TiKV Endpoint on each Region"]
TIKV --> RESULT["Partial count per Region"]
RESULT --> MERGE["TiDB sums partial counts"]
这张图和 TiKV 官方”Distributed
SQL”文档给出的例子一致:一次 count(*) + filter
会在每个相关 Region 上独立跑一份
Aggregation -> Selection -> TableScan
链,TiDB 只需要把各 Region 返回的局部 count 相加。
二、TiKV 端处理:Endpoint 到批量执行器
请求到达 TiKV 后,入口是
Endpoint(src/coprocessor/endpoint.rs,A
级源码)。核心步骤(源码注释原文即如此描述):
- 解析原始
Request为 DAG / Checksum / Analyze 三种之一; - 根据请求的
start_ts从底层存储引擎取一个 snapshot; - 用 snapshot 和请求细节构建对应的
RequestHandler; - 在独立的 Coprocessor 线程池上运行一次(unary)或多次(streaming);
- 把结果打包成 Response 返回。
sequenceDiagram
participant TiDB
participant EP as TiKV Endpoint
participant SS as SnapshotStore (MVCC)
participant DAG as DagHandler
participant EXE as Batch Executors
TiDB->>EP: Coprocessor.Request(DAGRequest, ranges, start_ts)
EP->>EP: check_memory_locks(是否有未决锁挡路)
EP->>SS: 按 start_ts 取 Region snapshot
EP->>DAG: DagHandlerBuilder::build(deadline, batch_row_limit, quota_limiter)
DAG->>EXE: 构造 executor 链(TableScan -> Selection -> Aggregation)
EXE->>SS: 按 KeyRange 迭代读取
EXE-->>DAG: 批量结果 Chunk
DAG-->>EP: SelectResponse
EP-->>TiDB: Coprocessor.Response
check_memory_locks
这一步值得单独说:Coprocessor 读走的是 start_ts
时刻的快照,如果该范围内有更晚写入但仍未提交/未 resolve
的锁挡在路径上,请求要么等待要么触发 第
11 篇讲过的 ResolveLock 流程——Coprocessor 并不是绕开
MVCC 的旁路,它复用与普通读请求相同的锁语义。
TiKV
从早期”逐行遍历执行器树”演进为批量执行器(tidb_query_executors/tidb_query_expr):每个
executor
一次处理一批行(Chunk),表达式在一批数据上向量化求值,而不是逐行调用函数。这一步把
Coprocessor 的 CPU
效率往前推了一大截,但要强调它解决的问题和下一节要划清的”MPP
向量化”不是同一个尺度的问题。
三、下推边界:哪些能下推,哪些不能
Coprocessor 只认识 tipb 里已经定义好的
executor 和表达式函数。TiDB
优化器在生成物理计划时要显式判断”这个算子/函数能不能下推”,判断不通过就把这部分留在
TiDB 端执行。
| 类别 | 可下推 | 不下推(TiDB 端执行) |
|---|---|---|
| 扫描 | TableScan、IndexScan | — |
| 过滤/投影 | Selection、Projection | 复杂的用户自定义函数、部分未实现的内置函数 |
| 聚合 | Aggregation(SUM/COUNT/AVG/MIN/MAX
等),两阶段(局部聚合 + 最终聚合) |
需要跨行状态的窗口函数(早期版本) |
| 排序/截断 | TopN、Limit | 全局 ORDER BY 的最终归并 |
| 连接 | — | Hash Join / Sort Merge Join / Index Lookup Join 均在 TiDB 端做,Coprocessor 不做跨 Region 的 join |
Join 永远不下推到单个 Region 的 Coprocessor:join 的两侧数据分布在不同 Region、甚至不同表,Coprocessor 的输入只是”这一个 Region 的一段 key range”,天然做不了跨 Region 关联。TiDB 的 distsql 层负责把多个 Region 的 Coprocessor 结果拉回来,在自己的执行器里做 join、全局排序归并、最终聚合——这一层怎么把计划切成”哪些留在 TiDB、哪些发给 Region”,属于 第 13 篇 的范围,本篇不展开。
四、与 query-engine 系列向量化的边界
TiKV Coprocessor 的”批量执行器”和分布式 OLAP 查询引擎系列里讲的向量化执行、morsel-driven 调度,字面上都叫”向量化”,但解决的是不同尺度的问题,不应该混为一谈:
| 维度 | TiKV Coprocessor 批量执行 | query-engine 系列的 MPP 向量化 |
|---|---|---|
| 执行单元 | 单个 Region 的 key range,行存 RocksDB | 整个分布式查询的 Stage/Task,跨节点 |
| 数据来源 | 三 CF 上的行存 MVCC 记录,现读现转批 | 列存文件(Parquet)或列存引擎自身批结构 |
| 调度粒度 | 一个 Coprocessor 线程池内的请求 | Coordinator/Worker 间的 Split、Exchange、shuffle |
| 目标 | 减少 TiDB-TiKV 网络传输、把简单算子搬到数据所在节点 | 让整条分布式计划在多机上并行跑满 CPU |
| 类比 | 更接近”存储引擎自带的谓词/聚合下推” | 更接近一个完整的分布式执行引擎 |
一句话边界:Coprocessor 是行存 KV 层的下推优化,不是一个通用分布式查询执行器。TiFlash 才是 TiDB 生态里真正对标”列存 + MPP”的组件(见第 14 篇),本文不重复 MPP shuffle、Exchange 这些机制,需要通用 MPP 内核细节时请直接读 query-engine 系列。
五、隔离与资源控制
Coprocessor 请求和事务读写共享同一个 TiKV
进程,如果不加隔离,一个扫全表的分析请求可以把在线事务的延迟顶上去。TiKV
在这一层做了几件事(源码 src/coprocessor/,A
级):
- 独立线程池:Coprocessor 请求跑在专属的 read pool,不占用 raftstore/apply 线程;
Deadline:每个请求带最大处理时限,超时直接返回错误而不是无限占用线程;QuotaLimiter:限制 Coprocessor 消耗的 CPU 时间和读带宽,避免单个大查询压垮节点;ReqTag/RequestType打点:区分 DAG/Analyze/Checksum 等请求类型,方便监控归因到具体请求形态;batch_row_limit:控制单批 Chunk 的行数上限,平衡内存占用与吞吐。
这些控制项是”行存下推系统”必须补的作业——数据分析请求的资源特征(长尾、大扫描量)和事务请求(短、高频)天差地别,共享进程但不共享调度策略会互相拖累。
分页与流式返回是另一个容易被忽略的隔离手段:一次全表扫描如果一口气把所有匹配行编码进单个
Response,内存占用会随扫描量线性增长。paging_size
参数让 Endpoint 把大结果切成若干页,每页跑完就先返回,由
TiDB 侧决定要不要继续拉下一页;is_streaming
请求则允许 Endpoint 对同一个 DAG 反复调用
RequestHandler,每次产出一批结果就发送一次,而不是等全部算完再一次性打包。这两者都是”避免单次
Coprocessor 请求占用过多内存”的工程手段,与前面的
Deadline/QuotaLimiter
一起构成完整的资源边界。
六、DAG 之外:Analyze 与 Checksum 请求
Coprocessor::Request 的 tp
字段并不只对应 DAG 一种类型,Endpoint 实际处理三类请求(TiKV
源码注释原文已经点明):
| 请求类型 | 用途 | 触发场景 |
|---|---|---|
| DAG | 执行 TiDB 下推的执行器链,返回查询结果 | 普通 SELECT/聚合/TopN 查询 |
| Analyze | 采样并统计列的直方图、NDV 等 | ANALYZE TABLE,为优化器的代价模型提供统计信息 |
| Checksum | 计算指定范围内数据的校验和 | 数据校验、迁移/备份后的一致性核验 |
Analyze 请求同样跑在 Coprocessor
的线程池与资源限额之内,区别只是 handler
换成统计采样逻辑而不是执行器链;它同样需要
start_ts
来保证采样的是一个确定的快照,而不是在数据持续变化时采到不一致的样本。这也解释了为什么
ANALYZE TABLE 在大表上会显著消耗 Coprocessor
资源:它本质上仍是一次全表规模的 Coprocessor
扫描,只是产出统计信息而不是查询结果。
Checksum 请求与 SQL 查询无关,通常由 TiDB 生态里的数据迁移、备份恢复工具触发,用于比较两份数据(例如迁移前后)是否完全一致——它复用同一套 Endpoint 入口,是因为”按 Region 并行扫一遍数据”这件事本身与查询下推是同构的,没有必要另起一套 RPC 通道。
七、Coprocessor Cache:结果级缓存
除了资源限额,TiKV
还提供了一层结果缓存:Endpoint 在构建
SnapshotStore 时会带上
is_cache_enabled 与
cache_match_version 两个信息(源码
src/coprocessor/endpoint.rs)。工作方式是:TiDB
客户端可以对同一个 CopTask 缓存上一次的执行结果和对应的
Region 数据版本;下一次发起相同的请求时,如果 Region
的数据版本没有变化,TiKV
可以直接告诉客户端”复用你本地缓存的结果”,不需要重新跑一遍执行器链。
这层缓存对”重复扫描同一段几乎不变的数据”场景(例如维度表、慢变的配置表反复被同一批分析 SQL 扫描)收益明显——本质上是把”执行结果”缓存在离 TiDB 更近的位置,减少 Coprocessor 的重复计算与网络传输。它依赖的是 Region 数据版本号的单调性:只要数据发生变化,版本号就会前进,客户端缓存立刻失效,不存在读到脏结果的风险。
八、常见误解
误解一:Coprocessor 能执行任意 SQL。
不能。它只执行 TiDB 编译好的 tipb DAG,函数
ID、执行器种类都是提前约定好的有限集合;遇到不支持的表达式,TiDB
优化器直接不下推这部分。
误解二:下推越多、性能一定越好。 不一定。Coprocessor 的批量执行仍然是行存扫描 + 逐列表达式求值,与真正的列存向量化(TiFlash/ClickHouse 式)相比,读放大和 CPU 效率天花板更低。对于扫描比例极高的分析查询,下推到 Coprocessor 只是”少传网络”,不能把行存变成列存;这类负载更适合走 TiFlash。
九、开放问题
- Coprocessor v2 插件化:TiKV 提供了动态加载 Coprocessor 插件的机制,允许非 TiDB 场景直接在 Region 上跑自定义计算逻辑。这条路径与”DAG 下推”是两种不同的可扩展性设计(协议约定 vs 动态代码),插件化在生产环境的隔离与升级成本尚缺乏公开的大规模案例总结。
- 行存下推与列存 MPP 的选择时机:TiDB 优化器基于代价模型决定一张表走 TiKV Coprocessor 还是 TiFlash MPP(db-frontier/12 §2.3 已给出简化代价模型),但代价模型对倾斜数据、非均匀选择率的鲁棒性,官方文档未给出可验证的量化界限。
十、小结
三句话小结:
- TiKV Coprocessor 只执行 TiDB 编译好的
tipbDAG,不解析 SQL、不做跨 Region join。 - TableScan/IndexScan/Selection/Aggregation/TopN/Limit/Projection 可下推,Join 与全局排序归并留在 TiDB 端。
- Coprocessor 的”批量执行”是行存下推优化,不等价于 query-engine 系列讲的分布式列存 MPP 向量化,二者尺度不同、不能互相替代。
参考资料
- TiKV 官方文档,Distributed SQL(tikv.org/deep-dive,A 级)。
- TiKV Development Guide,Coprocessor Overview(tikv.github.io/tikv-dev-guide,A 级)。
- TiKV
源码:
src/coprocessor/endpoint.rs、src/coprocessor/mod.rs(tikv/tikv,A 级)。 - tipb / kvproto
仓库:
Coprocessor.Request、tipb.DAGRequest定义(pingcap/tipb、pingcap/kvproto,A 级)。 - 分布式 OLAP 查询引擎系列(向量化/MPP 执行模型对照,不重复其机制细节)。
- 本系列 第 11 篇 悲观事务与 ResolveLock(锁与 Coprocessor 快照读的交互)。
返回 系列目录
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【TiKV / HTAP 内核】HTAP / 分布式 KV 全景:TiDB–TiKV–PD–TiFlash 四件套
定位 TiKV 7.x/8.x 在 RocksDB 单机 LSM、Raft 协议与 HTAP 范式之间补的那一层:TiDB、TiKV、PD、TiFlash 四组件各自的抽象边界,以及本系列 18 篇的读法。
【TiKV / HTAP 内核】Region · Multi-Raft · PD · Percolator · TiFlash
补齐 RocksDB 单 Region、Raft 协议与 HTAP 范式之间的工程链路:以 TiKV 7.x/8.x 拆解 Region、Multi-Raft、raftstore、PD/TSO 与 Percolator 事务,并以 TiFlash Learner 收束新鲜度;CockroachDB 对照与选型。
【TiKV / HTAP 内核】Key 编码与 MVCC:三 CF 与时间戳
拆解 TiKV 的 user key + start_ts/commit_ts 编码规则与 default/lock/write 三 CF 分工,并说明为什么这套 MVCC 与 RocksDB Snapshot 的 sequence 号是两层完全不同的机制。
【TiKV / HTAP 内核】Region 模型:range、epoch、peer
拆解 TiKV Region 的 range、RegionEpoch(conf_ver/version)、peer/leader 三个核心字段,纠正常见误解;split 只给直觉,机制留给第 8 篇。