一、先从一个老问题说起

把业务库的数据实时同步进 Doris 做分析,传统链路长这样:
MySQL → Canal/Debezium → Kafka → Flink → Doris
这条链路上的每一个环节都很成熟,但问题也很现实:
- 组件多:Kafka 集群、Flink 集群、Connector,每一层都要部署、监控、值班;
- 延迟叠加:数据每多流经一个系统,端到端延迟就叠加一段;
- 语义难对齐:exactly-once 要靠 Flink 两阶段提交 + Sink 端配合,出问题排查链路长。
很多场景的诉求其实很简单------"把上游的变更持续搬进 Doris",并不需要复杂的流式计算。为这个诉求维护一整套流处理基础设施,性价比很低。
Doris 4.1 给出的答案是 Streaming Job:把 CDC 读取能力直接内置进 Doris 内核,省掉所有中间组件,创建同步任务只需要一条 SQL。
二、Streaming Job 是什么
Streaming Job 是 Doris 内置的持续导入作业:提交之后,Doris 会持续运行这个作业,实时读取上游数据源的增量数据并写入 Doris 表。它复用了 Doris 已有的 Job 框架,语法上只是在 CREATE JOB 里加一个 ON STREAMING:
sql
CREATE JOB my_job
ON STREAMING
DO
INSERT INTO db1.tbl1
SELECT * FROM cdc_stream( ... );
目前支持三种数据源(官方文档:持续导入概览):
| 数据源 | 支持版本 | 单表同步 | 整库同步 |
|---|---|---|---|
| MySQL | 5.6、5.7、8.0.x | ✓ | ✓ |
| PostgreSQL | 14、15、16、17 | ✓ | ✓ |
| S3 | - | ✓ | - |
版本边界需要说清楚:MySQL / PostgreSQL 的 CDC 持续同步能力自 4.1.0 起完整支持 ,S3 持续导入更早一些,在 4.0.x 已经落地(4.1.0 Release Notes)。
三、两种同步模式:先搞清楚这个,再动手

Streaming Job 有两种实现机制完全不同的同步模式,注意这不是"同步一张表还是多张表"的区别------自动建表同步也可以通过 include_tables 只同步一张表。
| 能力维度 | SQL 映射同步 | 自动建表同步 |
|---|---|---|
| 底层机制 | Job + TVF:INSERT INTO tbl SELECT * FROM cdc_stream() |
Job + 原生 DDL:FROM MYSQL (...) TO DATABASE db |
| 目标 | 一张已存在的 Doris 表 | 一个 Doris database 容器 |
| 建表方式 | 需预建 | 首次同步自动创建主键表 |
| 数据加工 | 支持列映射、过滤、转换(SELECT 的完整表达能力) | 原样复制,不支持 ETL |
| 语义保证 | exactly-once | at-least-once(靠主键表幂等兜底) |
| 权限 | Load | Load + Create(自动建表时) |
| 适用场景 | 需要列裁剪、字段重命名、类型转换、条件过滤 | 整库/多表镜像复制,表结构自动跟随上游 |
选型原则很简单:
- 同步过程中要对数据做加工,或者对精确一次语义有硬性要求 → SQL 映射同步;
- 希望一条 SQL 把一组表(或整库)镜像过来,下游表自动建、结构自动跟随 → 自动建表同步;
- 数据源是 S3 → 只有 SQL 映射同步(S3 TVF)一条路。
四、上手实战
4.1 Demo 1:MySQL 整库同步,一条 SQL
前置条件:MySQL 开启 binlog、账号有读 binlog 权限、JDBC 驱动 jar 可引用(文件名 / 本地路径 / HTTP 地址均可)。
sql
CREATE JOB multi_table_sync
ON STREAMING
FROM MYSQL (
"jdbc_url" = "jdbc:mysql://127.0.0.1:3306",
"driver_url" = "mysql-connector-java-8.0.25.jar",
"driver_class" = "com.mysql.cj.jdbc.Driver",
"user" = "root",
"password" = "123456",
"database" = "test",
"include_tables" = "user_info,order_info",
"offset" = "initial"
)
TO DATABASE target_test_db (
"table.create.properties.replication_num" = "1" -- 单 BE 环境需要设为 1
);
几个关键点:
include_tables逗号分隔,留空即同步整库;offset = "initial"表示先全量初始化再自动切换增量;只想要增量就写latest;TO DATABASE后的table.create.properties.*控制自动建表的表属性。
创建之后用 jobs() 表函数观察运行状态:
sql
select * from jobs("type"="insert") where ExecuteType = "STREAMING"\G
输出里最值得关注的三个字段(示例来自官方文档):
Status: RUNNING
CurrentOffset: {"ts_sec":"1765284495","file":"binlog.000002","pos":"9350", ...}
LoadStatistic: {"scannedRows":24,"loadBytes":1146,"fileNumber":0,"fileSize":0}
CurrentOffset 直接展示当前消费到的 binlog 位点(文件 + position),同步进度一目了然------排障时先看它,再看 ErrorMsg。
4.2 Demo 2:S3 持续导入
S3 场景是"目录监控"模式:Doris 持续探测指定路径下新增的文件并自动导入。
sql
CREATE JOB s3_job
ON STREAMING
DO
INSERT INTO db1.tbl1
SELECT * FROM S3 (
"uri" = "s3://bucket/demo/*.csv",
"format" = "csv",
"column_separator" = ",",
"s3.endpoint" = "https://s3.ap-southeast-1.amazonaws.com",
"s3.region" = "ap-southeast-1",
"s3.access_key" = "...",
"s3.secret_key" = "..."
);
S3 模式有两个攒批参数(通过 Job 的 PROPERTIES 设置),任一条件满足即触发一次写入:
| 参数 | 默认值 | 说明 |
|---|---|---|
s3.max_batch_files |
256 | 累计文件数达到该值触发写入 |
s3.max_batch_bytes |
10GB | 累计字节数达到该值触发写入(可配范围 100MB ~ 10GB) |
max_interval |
10(秒) | 上游没有新数据时的空闲调度间隔 |
文件全部导入完成后,作业会进入 FINISHED 状态。
五、原理浅析:它在内核里是怎么跑的
5.1 整体架构

- FE 是调度中枢 :Job 的元信息、状态机、offset 都持久化在 FE。
StreamingJobSchedulerTask负责按状态机驱动作业流转,StreamingInsertTask是实际干活的最小单元; - BE 侧跑 CDC Reader :CDC 场景集成了 Flink CDC 的读取能力(全量 snapshot + 增量 binlog/WAL),读到的数据经 Stream Load 写入 Doris;
- 增量阶段 Reader 会绑定固定 BE :源码里每个 Job 维护一个
boundBackendId,增量阶段优先在同一台 BE 上复用 CDC Reader,避免重复初始化;BE 变更时会重新绑定并持久化。
5.2 调度:时间驱动 + 事件驱动双引擎
- Job 调度(时间驱动):复用 Job 框架的时间轮,定期产生调度子任务;
- 任务调度(事件驱动):由上一个 Task 完成的回调驱动下一次调度,数据来得快时不会被固定间隔卡住。
上游没数据时也不是空转干等------按 max_interval(默认 10 秒)的间隔空闲轮询。
5.3 exactly-once 是怎么实现的

核心是持久化 offset + 两阶段任务验证:
- offset 滞后提交 :只有当数据在 Doris 里可见且持久化 之后,offset 才会提交。FE 侧通过
StreamingTaskTxnCommitAttachment把 offset 随事务一起落盘------offset 推进和数据可见性是同一个事务的副产品; - 单调任务 ID 防重放:每个 Task 携带单调递增 ID,调度器拒绝任何重复或乱序的 Task,从机制上消除重放风险。
反过来说,这也解释了为什么自动建表同步只能做到 at-least-once:整库镜像模式下 CdcClient 直接消费上游变更流,中断恢复后可能重放一小段数据------但目标表是主键表,重放的数据会被幂等覆盖,最终效果等价于 exactly-once。
5.4 几个源码里才能看到的细节
- autoResume 的退避策略 :作业因故障进入
PAUSED后,调度器按指数退避自动恢复:第 n 次重试等待2^n × 10秒,封顶 300 秒;重试超过 5 次后固定 300 秒一轮。临时网络抖动基本都能自愈; - 自动恢复有预算上限 :
streaming_job_max_auto_resume_count(默认 10,可动态修改)。重试耗尽后失败原因会被改写为CANNOT_RESUME_ERR,必须人工RESUME JOB介入------这是防止无限重试打爆上游的保护阀。另外手动暂停 和失败行数超限 (max_filter_ratio)不会触发自动恢复; - Task 超时放宽 :Streaming Task 的
insert_timeout和query_timeout默认被放宽到 30 分钟,避免长快照阶段的任务被常规超时误杀; - FE 侧 split 缓冲有软上限 :已生成未消费的 split 最多缓冲 512 个(
MAX_PENDING_SPLITS),防止 FE 内存被积压数据打满。
六、运维与可观测性
6.1 状态机

PENDING(等待调度)→ RUNNING(执行中)→ FINISHED(源消费完毕,如 S3 文件导完)
↘ PAUSED(子任务失败自动暂停,或人工暂停)
PAUSED 之后两条路:autoResume 按退避策略自动拉起(回到 PENDING),或者人工排查后 RESUME JOB。
6.2 日常运维命令
sql
-- 查看所有 Streaming 作业(重点看 Status / CurrentOffset / LoadStatistic / ErrorMsg)
select * from jobs("type"="insert") where ExecuteType = "STREAMING";
-- 查看某个作业的所有子任务(看 RunningOffset 和单 Task 的失败信息)
select * from tasks("type"="insert") where jobId = '<job_id>';
-- 暂停(手动暂停不会被 autoResume 唤醒)
PAUSE JOB WHERE jobname = '<job_name>';
-- 恢复
RESUME JOB WHERE jobname = '<job_name>';
-- 修改(如上游账号密码轮换后更新连接信息)
ALTER JOB FOR <job_name> PROPERTIES (...);
-- 删除
DROP JOB WHERE jobname = '<job_name>';
6.3 FE 配置参数
| 参数 | 说明 |
|---|---|
max_streaming_job_num |
最大 Streaming 作业数,默认 1024 |
job_streaming_task_exec_thread_num |
执行 StreamingTask 的线程数 |
max_streaming_task_show_count |
内存中每个 Job 最多保留的 Task 记录数,默认 100 |
streaming_job_max_auto_resume_count |
自动恢复重试预算,默认 10,可动态调整 |
七、限制与坑
- 仅支持带主键的上游表,目标表必须是 Unique Key 模型------自动建表模式会自动建成主键表,SQL 映射模式需要你自己建对;
- DDL 同步能力有限 :MySQL 上游的 DDL 目前不同步 ,需要手工调整 Doris 表结构;PostgreSQL 自 4.1 起支持同步
ADD COLUMN/DROP COLUMN,但列类型变更、RENAME COLUMN、约束/索引/分区变更都不会同步; - 语义差异要记牢:SQL 映射同步是 exactly-once,自动建表同步是 at-least-once(靠主键幂等),对外宣传时不要笼统说"精确一次";
- MySQL 连接常见报错 :
Public Key Retrieval is not allowed------JDBC URL 加allowPublicKeyRetrieval=true,或把用户认证方式改为mysql_native_password;
八、结尾
Streaming Job 的意义不在于"又多了导数方式",而在于 Doris 把数据接入这件事的默认形态从"搭一条外部管道"变成了"发一条 SQL"。对于大量只需要镜像同步 + 轻量加工的实时数仓场景,架构里可以直接少掉 Kafka 和 Flink 两层。
这个方向还在快速演进,社区正在讨论的 Table Stream + MTMV 增量计算(#65418)------如果落地,Doris 里的"流"就不只是导入,而是完整的增量计算链路。从"流式导入"到"流式数仓",这条路才刚开始。