ticdc cpu一路攀升,直到耗尽

使用 源码 agent 核对了下上面内容:
@王军 独立跑了 pprof 并核对了源码,@ticket_new大方向对(瓶颈在 sorter 读路径,不是下游 sink),但参数名、机制细节、根因定位有 4 处明显偏差。

一、数据/数学核对(:white_check_mark: 全对)

@ticket_new 实测 核对
总采样 1254.17s / 4158.60% :white_check_mark:
sinkWorker.handleTask cum 548.23s/43.71% :white_check_mark:
其中 FetchByTable 占比 96.78% 530.59/548.23 = 96.78% :white_check_mark:
FetchByTable 总量 717.90s/57.24% :white_check_mark:
pebble 版本 v1.1.4-0.20250120151818-5dd133a1e6fb 同(go.mod 核对) :white_check_mark:

二、参数名错误(:x:

sorter.max-memory-per-table 这个参数在 TiFlow 里不存在。

源码核对(pkg/config/sorter.go:23-40server_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

并且 CacheSizeInMBper-CDC-server 的全局 sorter block cache 大小不是"per-table"。“per-table” 在源码里完全找不到对应字段。给客户的话术里写 sorter.max-memory-per-table=128MB 客户改了不生效。

三、机制描述偏差(:warning: 部分错)

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 调 NewRawRangeDelIterreadRangeDelreadBlock。是 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.newValue378.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 漏掉的关键事实(:x:

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 listcheckpoint-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。

七、给客户的话术修正版(替换原稿中的参数)