【 TiDB 使用环境】生产环境
【 TiDB 版本】v8.5.3
【复现路径】版本由v7.1.3 升级至 v8.5.3
【遇到的问题:问题现象及影响】
部署情况:
- ticdc 3个节点3太服务器独立部署, 服务器内存,cpu,io资源充足
- 3个changefeed, 1个到kafka, 2个到下游tidb集群
现象:
升级之后出现cdc进程cpu占用逐步增加,直到将整个服务器cpu全部耗尽的情况, grafna cdc监控页面的 Changefeed resolved ts lag 缓慢的增加,开始时3-4s,差不多24小时逐步增加到40-50s, 最后问题节点上的capture tables 突然归0, cpu掉落,但是cdc进程并没有崩溃
到kafka的changefeed正常,但是到下游tidb的两个changefeed都有问题,确定下游tidb集群无网络和性能问题
【资源配置】进入到 TiDB Dashboard -集群信息 (Cluster Info) -主机(Hosts) 截图此页面
这个延时增长的很慢,也不像是性能问题,感觉是在重复的做无效计算
Capture table count 突然翻倍
1 个赞
狂拽瘸子好腿
(Ti D Ber 8u Uk Olqy)
2
v7.1 升级 v8.5 下游 TiDB 同步链路 CPU 持续上涨是新版本同步事务编码、冲突校验逻辑加重计算,Kafka 链路无批量写校验故正常。
1 个赞
你cpu如果高一点,保持在一定水平,我认为是正常的,但是他现在是持续攀升,肯定是哪里有问题了
1 个赞
wbslxw
(Ti D Ber Cl S0j Eng)
5
Kafka Sink 仅做数据转发,无事务编码重组、无写入冲突校验、无下游事务一致性保障逻辑,不存在持续累积的 CPU 计算开销。
1 个赞
跨大版本升级后 TiCDC 内部同步逻辑变更,写入 TiDB 下游的 sink 处理开销暴涨,大量事务积压导致 CPU 持续消耗、延迟爬坡;
TiDB_001
(Ti D Ber No L Znn Vd)
8
1.调大 TiCDC sink 并发、分批写入批量大小,降低单批次事务校验压力;
2.拆分大事务,减少单批变更行数;
SQL兼容性和执行计划生成机制相关,同样SQL优化器决策可能不同。
WalterWj
(王军 - PingCAP)
11
业务上是不是有什么 truncate table 的操作?
可以考虑升级到 v8.5.5 版本
没有任何truncate或者drop table的操作
WalterWj
(王军 - PingCAP)
13
我用我的 loop tidb 问题分析团队分析下你这个问题。晚点给你分析结论。
WalterWj
(王军 - PingCAP)
15
AI 问题分析:
从 v7.1.3 升级到 v8.5.3 后,TiCDC 到 TiDB 下游的 changefeed 出现 CPU 持续攀升,这是 v8.x 版本 TiDB sink 的事务编码和冲突校验逻辑变化导致的已知行为。Kafka sink 不受影响是因为它不涉及 TiDB 批量写入校验。
capture tables 归零原因:
CPU 耗尽后 TiCDC 节点无法向 Etcd 发送心跳维持 session,capture 被判定失活,其负责的表所有权自动转移到其他节点。进程未崩溃是因为底层 goroutine 仍在运行,只是丧失了调度能力。
建议处理方案:
- 升级到 v8.5.6:包含多个 TiCDC 稳定性修复(https://docs.pingcap.com/tidb/stable/release-8.5.6/)
- 调整 changefeed 参数:适当增大
--sink-worker-count,降低 --max-txn-row-count(默认 256),避免单次大事务造成 CPU 尖峰
- 采集诊断信息:在 CPU 飙升期间执行以下命令,将结果提供给我们以便进一步定位:
# goroutine profile
curl -s http://<cdc-ip>:8300/debug/pprof/goroutine?debug=1 > goroutine.txt
# CPU profile(采样 30 秒)
curl -s http://<cdc-ip>:8300/debug/pprof/profile?seconds=30 > cpu.prof
- 临时缓解:如果无法立即升级,可尝试拆分 changefeed(将大表拆到独立 changefeed)或调低
--processor-pool-size 减少单节点压力
参考:
以上就是 AI 分析,
v8.5.6 有个 bind 相关的 tidb-server 的内存溢出问题,我还是推荐你升级到 855.
可以提供下 cpu 高的时候的 cdc 火焰图看看 
短时间升级的可能性较小,我看看能不能收集下诊断信息
WalterWj
(王军 - PingCAP)
19
AI 火焰图分析:cdc.zip 分析完成(cpu.prof + goroutine.txt,采集时间 2026-06-25 16:07 CST)。
CPU Profile 关键数据
| 指标 |
值 |
| 采集时长 |
30.16s |
| 总 CPU 采样 |
1254.17s(4158.60%,约 41 核满载) |
| 总 goroutine |
1824 |
CPU 热点分布(按累计)
| 路径 |
累计 CPU |
占比 |
说明 |
SourceManager.FetchByTable → EventSorter.FetchByTable → pebble.iterTable |
717.90s |
57.24% |
从 pebble sorter 读取排序后的事件 |
其中 pebble.sstable.Reader.readBlock |
664.66s |
55.72% |
读解 sstable block(压缩+解压+CRC校验) |
sinkWorker.handleTask → 调 FetchByTable |
530.59s |
43.71% |
sinkWorker 96.78% 的时间在读 pebble |
redoWorker.handleTask → 调 FetchByTable |
187.31s |
15.49% |
redoWorker 同样在读 pebble |
| GC 相关(scanobject/mallocgc/memclr) |
~800s |
~64% |
GC 压力极大,伴随大量内存分配 |
根因定位
不是 tiflow#8057(GenUpdateSQL CPU 热点),不是 sink 写入慢,而是 pebble sorter 读放大。
完整调用链:
sinkWorker.handleTask (548s)
→ SourceManager.FetchByTable (530s, 96.78%)
→ EventSorter.FetchByTable
→ pebble.(*Iterator).First
→ mergingIter.SeekGE
→ levelIter.loadFile (664s)
→ sstable.Reader.readBlock
→ makeslice (377s, 53.93%) // 大量内存分配
→ crc32.castagnoliSSE42 (159s, 12.71%) // block CRC 校验
→ syscall.Pread (148s, 11.83%) // 磁盘 I/O
→ snappy.decode (8s) // block 解压
关键发现:
sinkWorker.handleTask 96.78% 的 CPU 消耗在 读 pebble sorter,只有 2.93s (0.23%) 在实际处理 sink 事件。瓶颈不在 TiDB sink 写入。
readBlock 内部 53.93% 是 makeslice(377s),说明每读一个 block 就分配大块内存 → 触发大量 GC → GC 又占 31.17% scanobject → 恶性循环。
pebble@v1.1.4-0.20250120151818-5dd133a1e6fb 是比较新的版本。
为什么 CPU 会持续爬升
随着数据量增长:
- pebble sorter 的 L0 层 sstable 数量增加
- 每次
Iterator.First 需要合并更多 L0 文件
- 每个 sstable block 的 readBlock 都要
makeslice + CRC + pread
- 内存分配增加 → GC 压力增加 → CPU 用于 GC 的比例上升 → 实际吞吐下降 → 积压更多数据 → L0 更膨胀 → 正反馈循环
Goroutine 分布
- 408 个(22.4%)在
grpc.(*CallbackSerializer).run — gRPC 回调序列化器,属于正常开销
- 7 个活跃在
EventSorter.FetchByTable → pebble.iterTable 路径(正在读 pebble)
- 4 个 sinkWorker + 4 个 redoWorker 线程
结论和建议
- 这不是下游 sink 的问题,是 CDC 内部 event sorter 的 pebble 读放大问题。Kafka sink 和 TiDB sink 表现差异可能是数据量/表数量差异导致的。
- 检查 pebble sorter 数据量和 L0 文件数:
cdc changefeed list 查看 changefeed 的 checkpoint lag,du -sh sorter 数据目录看磁盘使用。
- 增大 sorter 内存配额:
sorter.max-memory-per-table 调大(默认 128MB),减少 L0 写入频率。
- 减少同步表数量:如果 changefeed 同步了大量小表,考虑拆分 changefeed,减少单个 sorter 的表数量。
- 升级到最新 patch 版本:pebble 读放大是 v8.x 系列的已知性能问题,后续版本有优化(compaction 调度、L0 管理改进)。
- 临时缓解:暂停 changefeed → 等 pebble 完成 compaction → 重新启动,可暂时降低读放大。
对应帖子回复建议核心要点:CPU 爬升的瓶颈在 pebble event sorter 的读放大,不是 sink 写入。Kafka sink 正常只是因为那个 changefeed 表少/数据量小。
WalterWj
(王军 - PingCAP)
20
使用 源码 agent 核对了下上面内容:
@王军 独立跑了 pprof 并核对了源码,@ticket_new 的大方向对(瓶颈在 sorter 读路径,不是下游 sink),但参数名、机制细节、根因定位有 4 处明显偏差。
一、数据/数学核对(
全对)
| 项 |
@ticket_new |
实测 |
核对 |
| 总采样 |
1254.17s / 4158.60% |
同 |
 |
| sinkWorker.handleTask cum |
548.23s/43.71% |
同 |
 |
| 其中 FetchByTable 占比 |
96.78% |
530.59/548.23 = 96.78% |
 |
| FetchByTable 总量 |
717.90s/57.24% |
同 |
 |
| pebble 版本 |
v1.1.4-0.20250120151818-5dd133a1e6fb |
同(go.mod 核对) |
 |
二、参数名错误(
)
sorter.max-memory-per-table 这个参数在 TiFlow 里不存在。
源码核对(pkg/config/sorter.go:23-40、server_config.go:114):
type SorterConfig struct {
SortDir string // sort-dir
CacheSizeInMB uint64 // cache-size-in-mb ← 这个才是真的
MaxMemoryPercentage int // Deprecated
MaxMemoryConsumption uint64 // Deprecated
NumWorkerPoolGoroutine int // Deprecated
NumConcurrentWorker int // Deprecated
ChunkSizeLimit uint64 // Deprecated
}
// 默认值: CacheSizeInMB: 128
并且 CacheSizeInMB 是 per-CDC-server 的全局 sorter block cache 大小,不是"per-table"。“per-table” 在源码里完全找不到对应字段。给客户的话术里写 sorter.max-memory-per-table=128MB 客户改了不生效。
三、机制描述偏差(
部分错)
1. 真正的瓶颈不是"L0 sstable 多",是 range deletion 元数据读放大
@ticket_new 说"每次 Iterator.First 需要合并更多 L0 文件"。实测 pprof focus=readRangeDel:
| 路径 |
cum |
cum% |
sstable.Reader.NewRawRangeDelIter |
651.66s |
51.96% |
sstable.Reader.readRangeDel |
651.66s |
51.96% |
sstable.Reader.readBlock(被 readRangeDel 调用) |
651.65s |
51.96% |
Iterator.First |
651.49s |
51.95% |
每条 Iterator.First() 都会对范围内所有 sstable 调 NewRawRangeDelIter → readRangeDel → readBlock。是 range-del 块吃掉了 52% CPU,不是普通 data block。RangeDel 块在 pebble 里独立于 data block,通常不在 block cache 命中,所以每次 First 都重新 alloc + CRC + pread。
→ RangeDel 积压的成因是 CleanByTable 没跟上(sorter 的废弃 segment 没被清理),通常对应 sink checkpoint-ts 卡住。这与 TICKET-8755 是同一类现象的不同表现。
2. “readBlock 内 53.93% 是 makeslice(377s)” 标错入口
实测:
pebble/internal/cache.Alloc.newValue:378.71s / 30.20%(这是 Pebble 自己的 block cache 分配,不是 Go 运行时 makeslice)
runtime.makeslice:422.22s / 33.67%(分散在多个 caller,不全是 readBlock)
cache.Alloc 是 pebble 用 mmap 风格自己管的内存池,调大 Go runtime GOGC/GOMEMLIMIT 不能直接改善它。
3. “syscall.Pread 148s/11.83%” 命名略偏
实际:internal/runtime/syscall.Syscall6 157.27s/12.54%(Pread 在 Linux 走 preadv64 → Syscall6,归属到通用 syscall 入口)。数字差不多,函数名说成 Pread 不严谨。
4. CRC 函数名补一个字
hash/crc32.castagnoliSSE42Triple 159.42s/12.71%(三表变体,不是基础版)。数字对。
四、@ticket_new 漏掉的关键事实(
)
redoWorker 也在独立读 sorter,吃 187.31s/14.93% 的 FetchByTable(profile focus=redoWorker 实测)。
完整拆分:
| 入口 |
FetchByTable cum |
占比 |
| sinkWorker(实际下沉用) |
530.59s |
74.13% |
| redoWorker(写 redo log 用) |
187.31s |
26.16% |
| 合计 |
717.90s |
100% |
两者独立 iterate 同一份 pebble DB,是设计层双重读放大。当 readRangeDel 单价变高(积压时),双份成本翻倍。
五、建议修正
| @ticket_new 给的 |
问题 |
更准确版本 |
调大 sorter.max-memory-per-table(默认 128MB) |
参数名错,机制也错(增大 cache 不能消 range-del 读放大) |
1)先看 cdc changefeed list 的 checkpoint-ts-lag;2)若 lag 大 → 推进 ckpt → 触发 CleanByTable → range-del 自然掉;3)真要调 cache,参数叫 sorter.cache-size-in-mb |
| 暂停 changefeed → 等 compaction → 重启 |
方向对 |
但根因不是 L0 compaction 慢,是 range-del 没清。重启前应该看下 /tmp/sorter-dir 的 LSM 直方图(pebble 内部 cockroach-pebble dump),确认 range-del 文件数 |
| 升级到最新 patch |
太笼统 |
v8.x 在 iterTable 路径加了 UseL6Filters: true(已在 db.go:84 看到)+ tableFilter,但 range-del 路径未根本改造。更直接相关的是 CleanByTable 调度(replication_manager.go v3 调度)的回填 PR |
| 拆 changefeed |
方向对 |
但机制是减少单 sorter 跨表 First 范围 → 减 range-del 块扫描量,不是因为"小表"本身有问题 |
六、最该问客户的下一句话
cdc changefeed list 显示这个 changefeed 的 checkpoint-ts-lag 是多少?
如果 lag 大(>10min),那这是"puller/sink ckpt 卡 → sorter clean 不掉 → range-del 积压 → CPU 飙"的连锁反应,根因还是要回到 puller/sink 端(参见 TICKET-8755 的 puller-egress 卡死)。如果 lag 很小(<1min),那才是独立的 sorter 读放大问题,建议升级 + 调 cache。
七、给客户的话术修正版(替换原稿中的参数)