普元数据开发平台 普元数据开发平台
产品介绍
安装部署
快速入门
操作指南
应用集成
运维指南
  • 如何解决CDC同步作业表主键分布不均导致CDC卡顿、内存溢出问题
  • 问题背景
  • 问题现象
  • 关键日志
  • 根本原因分析
  • 1. 分片算法整体流程
  • 2. UUID 类型映射链
  • 3. 分布因子计算失效
  • 4. 分片路径决策
  • 5. 当前配置下的决策路径
  • 为什么读写数量一直为 0?
  • 场景一:非均匀分片路径(默认配置)
  • 场景二:采样分片路径(低 threshold 配置)
  • 为什么会内存溢出?
  • 优化方案
  • 方案一:调整分片参数 + 增加 JVM 内存(推荐,无需改表结构)
  • 方案二:设置快照切分列(如果表除了主键还有其他数值型唯一键)
  • 方案三:增加 JVM 内存 + checkpoint 超时
  • 推荐组合配置
  • 参数调优速查表
  • 源码分析参考

# 如何解决CDC同步作业表主键分布不均导致CDC卡顿、内存溢出问题

# 问题背景

场景:使用SQLServer-CDC进行数据实时抽取到StarRocks

SeaTunnel版本:DWS720

SQLServer版本:SQLServer2019

StarRocks版本:3.2.9

源表数据量:167W

# 问题现象

使用DWS数据同步CDC组件从业务库抽数到数仓(StarRocks),已经成功使用SQLServer-CDC做了大量的实时同步。 但最近在抽一个SQLServer表的时候遇到了异常情况,作业启动之后,日志显示读写数量一直为0,且长时间不停止,运行一段时间后发现内存溢出,作业异常停止。

作业模型如下:

{
   "env": {
      "shade.identifier": "base64",
      "job.mode": "STREAMING",
      "parallelism": 1,
      "job.name": "sqlserver-cdc2doris_1776852127901",
      "checkpoint.interval": 5000
   },
   "source": [
      {
         "username": "c2E=",
         "password": "******",
         "plugin_name": "SqlServer-CDC",
         "result_table_name": "CDCSource1776412849012",
         "node_code": "CDCSource1776412849012",
         "connection_code": "sqlserver2019",
         "database-names": [
            "test_li_dws"
         ],
         "table-names": [
            "test_li_dws.dbo.tbl_PlannedOrderDetail"
         ],
         "table-names-config": [
            {
               "table": "test_li_dws.dbo.tbl_PlannedOrderDetail",
               "primaryKeys": [
                  "PODID"
               ],
               "snapshotSplitColumn": "PODID"
            }
         ],
         "base-url": "jdbc:sqlserver://192.168.16.181:1433;databaseName=test_li_dws;trustServerCertificate=true",
         "startup.mode": "initial",
         "incremental.parallelism": 1,
         "exactly_once": true,
         "format": "default",
         "schema-changes.enabled": false
      }
   ],
   "transform": [],
   "sink": [
      {
         "username": "ZHdz",
         "password": "******",
         "database": "demo_sink",
         "plugin_name": "StarRocks",
         "source_table_name": "CDCSource1776412849012",
         "source_node_code": "CDCSource1776412849012",
         "node_code": "StarRocksSink1776412883181",
         "connection_code": "StarRocks146",
         "nodeUrls": [
            "192.168.0.146:8030"
         ],
         "base-url": "jdbc:mysql://192.168.0.146:9030?nullCatalogMeansCurrent=true&allowLoadLocalInfile=true&useSSL=false",
         "batch_max_rows": 1024,
         "batch_max_bytes": 5242880,
         "enable_upsert_delete": false,
         "schema_save_mode": "CREATE_SCHEMA_WHEN_NOT_EXIST",
         "data_save_mode": "DROP_DATA",
         "field_ide": "ORIGINAL",
         "tableMapping": [
            {
               "source_table_name": "tbl_PlannedOrderDetail",
               "table_name": "tbl_PlannedOrderDetail",
               "save_mode_create_template": "CREATE TABLE `${database}`.`${table}` \n ( \n `PODID`,\n ${rowtype_fields} \n) ENGINE=OLAP\n PRIMARY KEY (`PODID`)\n COMMENT '${comment}'\n DISTRIBUTED BY HASH (`PODID`)"
            }
         ]
      }
   ]
}

# 关键日志


INFO  [AbstractJdbcSourceChunkSplitter] - Splitting table test_li_dws.dbo.tbl_PlannedOrderDetail into chunks, split column: PODID, min: C317D088-40EC-4173-B938-000002777154, max: 0FC8A607-9805-4978-9256-FFFFFE3BD93F, chunk size: 8096, distribution factor upper: 100.0, distribution factor lower: 0.05, sample sharding threshold: 1000
INFO  [AbstractJdbcSourceChunkSplitter] - The distribution factor of table test_li_dws.dbo.tbl_PlannedOrderDetail is 5.52297726757771E12 according to the min split key C317D088-40EC-4173-B938-000002777154, max split key 0FC8A607-9805-4978-9256-FFFFFE3BD93F and approximate row count 1670000
INFO  [AbstractJdbcSourceChunkSplitter] - Use unevenly-sized chunks for table test_li_dws.dbo.tbl_PlannedOrderDetail, the chunk size is 8096

# 根本原因分析

核心问题:UUID 主键 + 分片算法决策路径不匹配,导致分片阶段卡死或内存溢出。

通过阅读SeaTunnel源码,追踪整个分片算法的执行路径如下:

# 1. 分片算法整体流程

generateSplits()
  ├── getSplitColumn()                    // 选择分片列
  │     ├── 检查用户配置的 snapshotSplitColumn
  │     ├── 检查主键第一列
  │     └── 检查唯一键第一列
  │     (优先选择数值类型:TINYINT > SMALLINT > INT > BIGINT > DECIMAL > STRING)
  │
  ├── splitTableIntoChunks()              // 执行分片
  │     ├── queryMinMax()                 // 查询 MIN/MAX 值
  │     ├── isEvenlySplitColumn()         // 判断是否为均匀分布列
  │     │     (STRING 类型返回 true,UUID 被视为 STRING)
  │     ├── calculateDistributionFactor() // 计算分布因子
  │     │     (STRING 类型返回 Long.MAX_VALUE → 因子极大)
  │     │
  │     ├── [均匀分布] → splitEvenlySizedChunks()
  │     │     └── ObjectUtils.plus()      // 步长递增(UUID不会走此路径)
  │     │
  │     ├── [不均匀分布] → 检查 shardCount vs sampleShardingThreshold
  │     │     ├── shardCount > threshold → sampleDataFromColumn()
  │     │     │     └── skipReadAndSortSampleData()  // 全表扫描采样
  │     │     │
  │     │     └── shardCount <= threshold → splitUnevenlySizedChunks()
  │     │           └── queryNextChunkMax()           // 迭代查询分片边界
  │     │
  │     └── [非均匀列] → splitUnevenlySizedChunks()
  │
  └── 返回 List<SnapshotSplit>

# 2. UUID 类型映射链

SQLServer UNIQUEIDENTIFIER → SeaTunnel STRING 类型 → 被视为"可均匀分片"

  • 源码位置:connector-jdbc/.../sqlserver/SqlServerTypeConverter.java

    case SQLSERVER_UNIQUEIDENTIFIER:
        builder.sourceType(SQLSERVER_UNIQUEIDENTIFIER);
        builder.dataType(BasicType.STRING_TYPE);
    
  • 源码位置:connector-cdc-base/.../splitter/JdbcSourceChunkSplitter.java:166-179

    default boolean isEvenlySplitColumn(Column splitColumn) {
        switch (fromDbzColumn(splitColumn).getSqlType()) {
            case TINYINT:
            case SMALLINT:
            case INT:
            case BIGINT:
            case DECIMAL:
            case STRING:       // ← UUID 映射为 STRING,返回 true
                return true;
            default:
                return false;
        }
    }
    

# 3. 分布因子计算失效

源码位置:connector-cdc-base/.../utils/ObjectUtils.java:72-73

} else if (minuend instanceof String) {
    return BigDecimal.valueOf(Long.MAX_VALUE);  // 9223372036854775807
}

UUID 作为 String 类型传入 minus() 方法,直接返回 Long.MAX_VALUE,而非真实的差值。

分布因子计算公式(AbstractJdbcSourceChunkSplitter.java:359-363):

distributionFactor = (MAX - MIN + 1) / rowCount
                   = (Long.MAX_VALUE + 1) / 1670000
                   ≈ 5.52 × 10¹²

远超 distributionFactorUpper(默认 100.0),数据被判定为分布不均匀。

# 4. 分片路径决策

计算分片数量:

shardCount = approximateRowCnt / snapshot.split.size

源码中的决策逻辑(AbstractJdbcSourceChunkSplitter.java:168-197):

if (sampleShardingThreshold < shardCount) {
    → 采样分片路径 (全表扫描)    
} else {
    → 非均匀分片路径 (迭代查询)
}

# 5. 当前配置下的决策路径

默认配置参数:

参数 类型 默认值 描述
snapshot.split.size Integer 8096 表快照的分割大小(行数),读取表快照时,捕获的表会被分割为多个分割。
snapshot.fetch.size Integer 1024 读取表快照时每次轮询的最大获取大小。
connection.pool.size Integer 20 连接池大小。
chunk-key.even-distribution.factor.upper-bound Double 100 分块键分布因子的上界。此因子用于确定表数据是否均匀分布。如果计算的分布因子小于或等于此上界(即,(MAX(id) - MIN(id) + 1) / 行数),表分块将被优化以实现均匀分布。否则,如果分布因子较大,如果估计的分片数超过 sample-sharding.threshold 指定的值,表将被视为不均匀分布并使用基于采样的分片策略。默认值为 100.0。
chunk-key.even-distribution.factor.lower-bound Double 0.05 分块键分布因子的下界。此因子用于确定表数据是否均匀分布。如果计算的分布因子大于或等于此下界(即,(MAX(id) - MIN(id) + 1) / 行数),表分块将被优化以实现均匀分布。否则,如果分布因子较小,如果估计的分片数超过 sample-sharding.threshold 指定的值,表将被视为不均匀分布并使用基于采样的分片策略。默认值为 0.05。
sample-sharding.threshold int 1000 此配置指定了触发采样分片策略的估计分片数阈值。当分布因子超出 chunk-key.even-distribution.factor.upper-bound 和 chunk-key.even-distribution.factor.lower-bound 指定的范围,并且估计的分片数(计算为近似行数 / 分块大小)超过此阈值时,将使用采样分片策略。这可以帮助更有效地处理大型数据集。默认值为 1000 分片。
inverse-sampling.rate int 1000 采样分片策略中使用的采样率的倒数。例如,如果此值设置为 1000,则意味着在采样过程中应用 1/1000 的采样率。此选项提供了控制采样粒度的灵活性,从而影响最终的分片数量。对于非常大的数据集,首选较低的采样率时,此选项特别有用。默认值为 1000。

计算过程:

shardCount = 1670000 / 8096 = 206
sampleShardingThreshold = 1000
206 < 1000 → 非均匀分片路径

# 为什么读写数量一直为 0?

作业卡在 generateSplits() 的 split 计算阶段或快照数据读取阶段,导致读写计数保持为 0。

根据不同的参数配置,存在两种卡顿场景:

# 场景一:非均匀分片路径(默认配置)

当使用默认配置(snapshot.split.size=8096,sample-sharding.threshold=1000)时,shardCount=206 < 1000,走非均匀分片路径。

Split 计算阶段执行约 206 次迭代查询(SqlServerUtils.java:227-258):

SELECT MAX([PODID]) FROM (
    SELECT TOP (8096) [PODID] FROM [table] WHERE [PODID] >= ? ORDER BY [PODID] ASC
) AS T

此路径 Split 计算本身可以完成,但后续的快照数据读取阶段存在以下问题:

  1. UUID 聚簇索引碎片化:SQL Server 中 UUID 作为聚簇索引键(主键默认聚簇),非连续 UUID 导致索引碎片化严重。每次范围查询 [PODID] >= ? AND [PODID] <= ? 需要大量随机 I/O。
  2. 参数类型隐式转换:ps.setObject(1, includedLowerBound) 传入 Java String 类型,SQL Server 对 uniqueidentifier 列做隐式转换,可能导致索引扫描效率降低。
  3. 单线程串行读取:parallelism=1 时所有 206 个分片串行读取,每个分片最多读 8096 行,总计 167 万行的 UUID 范围扫描非常耗时。

Split 计算完成后,数据读取阶段由于上述原因速度极慢,长时间看不到读写数量增长。

# 场景二:采样分片路径(低 threshold 配置)

当 sample-sharding.threshold 设置过小(如 100),导致 shardCount > threshold 时,触发采样分片路径。

SqlServerChunkSplitter 的 sampleDataFromColumn() 方法委托给 SqlServerUtils.skipReadAndSortSampleData()(SqlServerUtils.java:170-221):

public static Object[] skipReadAndSortSampleData(
        JdbcConnection jdbc, TableId tableId, String columnName, int inverseSamplingRate) {
    final String sampleQuery = String.format("SELECT %s FROM %s", quote(columnName), quote(tableId));

    Statement stmt = jdbc.connection()
            .createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
    stmt.setFetchSize(1024);
    ResultSet rs = stmt.executeQuery(sampleQuery);

    List<Object> results = new ArrayList<>();
    while (rs.next()) {
        count++;
        if (count % inverseSamplingRate == 0) {
            results.add(rs.getObject(1)); // 累积采样 UUID
        }
    }
    Object[] resultsArray = results.toArray();
    Arrays.sort(resultsArray); // 排序额外内存开销
    return resultsArray;
}

执行流程:

  1. 执行 SELECT [PODID] FROM [test_li_dws].[dbo].[tbl_PlannedOrderDetail] —— 全表扫描 167 万行
  2. JDBC 驱动在 TYPE_FORWARD_ONLY + fetchSize=1024 模式下流式传输所有行
  3. 每 1000 行保留一个采样值(inverseSamplingRate=1000),约保留 1670 个 UUID
  4. 对采样结果排序 Arrays.sort(resultsArray)
  5. Split 计算迟迟无法完成 → 读写计数保持为 0

# 为什么会内存溢出?

内存溢出由以下因素叠加导致:

因素 1 - UUID 索引碎片化导致全表扫描: UUID 作为聚簇索引键,插入时数据页频繁分裂,索引碎片化导致查询性能急剧下降。在碎片化严重的情况下,原本应走索引的范围查询退化为近似全表扫描,大量数据页被加载到内存。

因素 2 - 采样路径全表扫描(场景二): 当触发采样路径时,skipReadAndSortSampleData() 全表扫描 167 万行:

List<Object> results = new ArrayList<>(); // 累积约 1670 个 UUID 字符串
while (rs.next()) { ... } // 迭代 167 万次
Object[] resultsArray = results.toArray();
Arrays.sort(resultsArray); // 排序额外内存开销

JDBC 驱动在 TYPE_FORWARD_ONLY 模式下内部缓冲数据,叠加应用层对象创建,GC 压力持续增大。

因素 3 - Checkpoint 超时连锁反应: 默认 checkpoint.timeout 为 30s,checkpoint.interval为5s ,如果 split 阶段或快照读取阶段超时未完成,checkpoint 失败触发作业重启。重启后重新执行 split 计算,反复全表扫描,最终导致 OOM。

因素 4 - 快照数据读取内存累积: Split 完成后进入快照数据读取阶段,每个分片的数据通过 SnapshotSplitReadTask 加载。如果 Sink(如 StarRocks)写入速度慢于 Source 读取速度,数据在内存中累积。167 万行 UUID + 业务列数据,在 parallelism=1 串行模式下内存占用持续增长。

# 优化方案

# 方案一:调整分片参数 + 增加 JVM 内存(推荐,无需改表结构)

核心思路:

  1. 确保走非均匀分片路径(shardCount < sample-sharding.threshold),避免全表扫描采样
  2. 适当减小 snapshot.split.size,缩小单次查询范围,降低内存峰值
  3. 增大 snapshot.fetch.size,减少网络往返
  4. 增加 JVM 内存,应对 UUID 索引碎片化导致的内存压力

{
  "source": [
    {
      "plugin_name": "SqlServer-CDC",
      "snapshot.split.size": 5000,
      "sample-sharding.threshold": 10000,
      "inverse-sampling.rate": 2000,
      "snapshot.fetch.size": 2048,
      "exactly_once": true
    }
  ]
}

参数计算验证:

shardCount = 1670000 / 5000 = 334
sampleShardingThreshold = 10000
334 < 10000 → 走非均匀分片路径 ✓

关键参数说明:

参数 推荐值 默认值 作用
snapshot.split.size 5000 8096 每个分片的行数,UUID 场景建议适当调小,缩小单次查询范围
sample-sharding.threshold 10000 1000 调大以确保 shardCount < threshold,避免触发采样全表扫描
inverse-sampling.rate 2000 1000 采样率的倒数(仅在触发采样路径时生效),增大以减少内存占用
snapshot.fetch.size 2048 1024 JDBC 每次轮询的获取大小,增大可减少网络往返次数
exactly_once true false 确保分片时必须有主键

原理:非均匀分片使用 queryNextChunkMax() 迭代查询,每次只查一个分片边界:

SELECT MAX([PODID]) FROM (
    SELECT TOP (5000) [PODID] FROM [table] WHERE [PODID] >= ? ORDER BY [PODID] ASC
) AS T
  • UUID 有聚簇索引(主键),ORDER BY + TOP 可走索引,单次查询效率高
  • 334 次迭代,每次查询一个分片边界,不会 OOM
  • snapshot.split.size=5000 相比默认的 8096,减小了每次数据读取的内存峰值
  • 每 10 次查询 sleep 100ms(AbstractJdbcSourceChunkSplitter.java:481),总耗时可控

# 方案二:设置快照切分列(如果表除了主键还有其他数值型唯一键)

如果表中有自增列、时间列或其他数值类型的唯一索引列,可指定替代分片列:


{
    "table-names-config": [
        {
            "table": "test_li_dws.dbo.tbl_PlannedOrderDetail",
            "primaryKeys": [
                "PODID"
            ],
            "snapshotSplitColumn": "ID"
        }
    ]
}

注意事项:

  • snapshotSplitColumn 必须是唯一键(主键或唯一索引)中的列
  • 支持的类型:TINYINT / SMALLINT / INT / BIGINT / DECIMAL / STRING
  • 优先选择数值类型(优先级从高到低:TINYINT > SMALLINT > INT > BIGINT > DECIMAL > STRING)
  • 数值类型分片列可走均匀分片路径,性能最优

# 方案三:增加 JVM 内存 + checkpoint 超时

当无法避免采样路径或 split 阶段耗时较长时,增大资源上限:

修改 $SEATUNNEL_HOME/config/jvm_options,增加 JVM 内存:

# JVM Heap
-Xms16g
-Xmx16g

增加 checkpoint 超时时间,给 split 阶段留足时间:


{
    "env": {
        "checkpoint.timeout": "600000",
        "checkpoint.interval": 10000
    }
}

# 推荐组合配置

针对 167 万行 UUID 主键表的最优配置(在原始作业模型基础上增加优化参数):


{
    "env": {
        "shade.identifier": "base64",
        "job.mode": "STREAMING",
        "parallelism": 2,
        "checkpoint.interval": 10000,
        "checkpoint.timeout": "600000",
        "job.name": "sqlserver-cdc-uuid-optimized"
    },
    "source": [
        {
            "plugin_name": "SqlServer-CDC",
            "username": "***",
            "password": "***",
            "base-url": "jdbc:sqlserver://host:1433;databaseName=db;trustServerCertificate=true",
            "database-names": [
                "your_db"
            ],
            "table-names": [
                "your_db.dbo.your_table"
            ],
            "table-names-config": [
                {
                    "table": "your_db.dbo.your_table",
                    "primaryKeys": [
                        "PODID"
                    ]
                }
            ],
            "startup.mode": "initial",
            "incremental.parallelism": 1,
            "snapshot.split.size": 5000,
            "sample-sharding.threshold": 10000,
            "inverse-sampling.rate": 2000,
            "snapshot.fetch.size": 2048,
            "exactly_once": true,
            "format": "default",
            "schema-changes.enabled": false
        }
    ],
    "sink": [
        {
            "plugin_name": "StarRocks",
            "username": "***",
            "password": "***",
            "database": "demo_sink",
            "nodeUrls": [
                "192.168.0.146:8030"
            ],
            "base-url": "jdbc:mysql://192.168.0.146:9030?nullCatalogMeansCurrent=true&allowLoadLocalInfile=true&useSSL=false",
            "batch_max_rows": 2048,
            "batch_max_bytes": 10485760,
            "enable_upsert_delete": false,
            "schema_save_mode": "CREATE_SCHEMA_WHEN_NOT_EXIST",
            "data_save_mode": "DROP_DATA",
            "field_ide": "ORIGINAL"
        }
    ]
}

CDC可选参数配置: cdcsource checkpoint参数配置 cdcsource

配置要点总结:

配置项 原始值 优化值 说明
parallelism 1 2 适当增大并行度,加快分片读取速度
checkpoint.timeout 未设置 600000 给 split + 快照读取阶段留足时间,避免超时重启
snapshot.split.size 8096(默认) 5000 减小分片大小,降低单次查询内存峰值
sample-sharding.threshold 1000(默认) 10000 确保 shardCount < threshold,避免触发采样全表扫描
inverse-sampling.rate 1000(默认) 2000 增大采样间隔(仅在触发采样时生效)
snapshot.fetch.size 1024(默认) 2048 增大 JDBC fetch size,减少网络往返
batch_max_rows 1024 2048 Sink 批量提交行数,适当增大提升写入吞吐
batch_max_bytes 5242880 10485760 Sink 批量提交大小,从 5MB 增大到 10MB

# 参数调优速查表

以下为不同数据量级下 UUID 主键表的推荐参数配置:

数据量 snapshot.split.size sample-sharding.threshold 说明
< 50 万 8096 1000 默认配置即可,shardCount < threshold
50 万 ~ 200 万 5000 10000 shardCount < threshold,走非均匀分片
200 万 ~ 500 万 4000 20000 增大 threshold 避免采样
500 万 ~ 1000 万 3000 50000 增大 threshold + 减小 split size
> 1000 万 2000 100000 配合增大 JVM 内存 (16G+)

计算公式:

shardCount = 总行数 / snapshot.split.size
要求:shardCount < sample-sharding.threshold
即:sample-sharding.threshold > 总行数 / snapshot.split.size

# 源码分析参考

以下为本次分析涉及的关键源码文件路径:

文件 路径 关键作用
AbstractJdbcSourceChunkSplitter connector-cdc-base/.../splitter/AbstractJdbcSourceChunkSplitter.java 分片算法核心:均匀/非均匀/采样三种策略
JdbcSourceChunkSplitter connector-cdc-base/.../splitter/JdbcSourceChunkSplitter.java 接口定义,包含 isEvenlySplitColumn() 判断逻辑
ObjectUtils connector-cdc-base/.../utils/ObjectUtils.java 数值运算工具,minus() 对 String 返回 Long.MAX_VALUE
SqlServerChunkSplitter connector-cdc-sqlserver/.../enumerator/SqlServerChunkSplitter.java SQLServer 分片实现,委托 SqlServerUtils
SqlServerUtils connector-cdc-sqlserver/.../utils/SqlServerUtils.java SQLServer SQL 构建与执行,包含采样和分片查询方法
SqlServerTypeConverter connector-jdbc/.../sqlserver/SqlServerTypeConverter.java 类型转换:UNIQUEIDENTIFIER → STRING
JdbcSourceOptions connector-cdc-base/.../option/JdbcSourceOptions.java 分片相关配置项定义