一、背景
某用户在 TiDB 4.0 集群遇到点查抖动的 CASE,需要设置 tikv-client.max-batch-size: 0,关闭 Batch Client 特性,避免触发 gRPC 饥饿问题。
二、分析
2.1 gRPC 四种交互模式
- Simple RPC(简单模式)
客户端发一个请求,服务端处理完立刻返回一个响应。
- Server‑side streaming(服务端流式) 客户端发一个请求,服务端持续发多条数据流回去,直到流结束。
- Client‑side streaming(客户端流式) 客户端持续发多条数据给服务端,最后服务端合并处理后返回一次响应。
- Bidirectional streaming(双向流式) 客户端和服务端都可以不断互相发送数据流,彼此独立收发,直到任意一方关闭流。
Batch Client 基于「双向流式」模式:TiDB client 和 TiKV 每次保持一个长期的 bidi‑stream,复用同一条连接来收发批量命令。
2.2 为什么要 Batch Client?
- 在高并发场景下,TiDB → TiKV 的 gRPC 调用请求非常频繁。
- 每发一次 RPC,TiKV 都要调度一个线程去处理网络、反序列化、调度业务逻辑,CPU 开销不小。
- Batch Client 就是在 TiDB 端把多条小命令(Command)先积累起来,一次性打包(batch)成一个 gRPC 消息发给 TiKV,减少 RPC 次数,从而降低 TiKV gRPC 线程的 CPU 使用率。
2.3 核心参数及含义

2.4 Batch Client 启用时的工作流程示例
step1: 请求入队 TiDB 收到一条新的 KV 请求,先放到本地的「待发送队列」中。
step2: 检查批量大小
- 如果队列长度 ≥
max-batch-size(默认为 128),就立即把这 128 条打包发给 TiKV; - 否则,继续下一步。
step3: 检查等待策略是否启用
- 如果
max-batch-wait-time = 0(默认值),表示不等待,直接把队列中所有请求(不足 128 条也好)打包发出; - 如果
max-batch-wait-time > 0,则进入第 4 步。
step4: 判断 TiKV gRPC CPU 负载 查询关联 TiKV 实例的 gRPC 线程池 CPU 利用率:
- 如果 CPU 使用率 ≤
overload-threshold(200%),立即发送当前所有请求; - 如果 CPU 使用率 > 200%,进入第 5 步。
step5: 等待收集更多请求 在 max-batch-wait-time(如 50 ms)内:
- 如果队列长度达到
batch-wait-size(默认 8),立即发送这批请求; - 如果时间到(超过
max-batch-wait-time)还没达 8 条,也强制把当前队列里的请求发出。
step6: 完成一次批量发送 清空已发送的请求,队列里剩余请求重新从步骤 2 开始判断。
关闭 Batch Client 将 max-batch-size 设为 0 时,上面所有“打包发送”逻辑都不生效,TiDB 收到新请求就立刻做一次简单 RPC,保证最低延迟,但也带来更多的 gRPC 调度开销。
2.5 gRPC 线程饥饿问题
-
饥饿原因: TiKV 的 gRPC 实现对每个双向流(stream)采取“贪婪处理”策略:只要某条流还有数据就一直跑这个流,不去切换到其他流上。
-
后果: 当某些流持续发包,其他流就一直拿不到 CPU,造成延迟抖动。
-
解决思路: 关闭 Batch Client(即
max‑batch‑size=0),让每条请求都走简单模式 RPC:- 每个 SQL 操作都会新建或复用一个短连接(goroutine 并行处理),不会因为一个长流占用线程池而饿死其他流。
- 请求不再打包,虽然 RPC 次数增多,但单条请求的延迟更可控、抖动更小。
- 适合对延迟敏感、并发流模式复杂的场景。
三、总结
这个问题并不算 issue,这是 gRPC 能力上的不足。gRPC 暂不支持 preemptive scheduling 和 work stealing。如果一个大任务长期占用一个 gRPC 线程,就会使得这个线程的等待队列上的其他小任务延时增加。
高吞吐更重要、对 CPU 开销敏感的集群 → 打开 Batch Client(默认128)。
低延迟更重要的集群 → 关闭 Batch Client(max‑batch‑size=0),回到 Simple RPC。