一条 SQL 顶一条 Flink 链路?Doris Streaming Job 持续导入全景解析

一、先从一个老问题说起

把业务库的数据实时同步进 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 + 两阶段任务验证

  1. offset 滞后提交 :只有当数据在 Doris 里可见且持久化 之后,offset 才会提交。FE 侧通过 StreamingTaskTxnCommitAttachment 把 offset 随事务一起落盘------offset 推进和数据可见性是同一个事务的副产品;
  2. 单调任务 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_timeoutquery_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,可动态调整

七、限制与坑

  1. 仅支持带主键的上游表,目标表必须是 Unique Key 模型------自动建表模式会自动建成主键表,SQL 映射模式需要你自己建对;
  2. DDL 同步能力有限 :MySQL 上游的 DDL 目前不同步 ,需要手工调整 Doris 表结构;PostgreSQL 自 4.1 起支持同步 ADD COLUMN / DROP COLUMN,但列类型变更、RENAME COLUMN、约束/索引/分区变更都不会同步;
  3. 语义差异要记牢:SQL 映射同步是 exactly-once,自动建表同步是 at-least-once(靠主键幂等),对外宣传时不要笼统说"精确一次";
  4. 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 里的"流"就不只是导入,而是完整的增量计算链路。从"流式导入"到"流式数仓",这条路才刚开始。

相关推荐
爱学习的小白柏1 小时前
【AI问数技术】多Agent协同架构:查询规划/SQL生成/洞察分析/报告生成
java·网络·人工智能·windows·sql·架构·llama
SXkehuirongsheng1 小时前
APP开发定制和模板开发哪个更实用?
大数据·运维·人工智能
摆烂z2 小时前
简单理解StarRocks
sql
醉颜凉2 小时前
Elasticsearch底层原理:文档更新与删除完整执行流程深度剖析
大数据·elasticsearch·jenkins
大大大大晴天3 小时前
大数据平台为什么必须走向云原生:从资源孤岛到弹性数据基础设施
大数据·云原生
小白说大模型3 小时前
AI驱动的个性化学习路径:知识图谱与知识点关联的存储与推理
大数据·人工智能·学习·mysql·机器学习·prompt·知识图谱
这个DBA有点耶3 小时前
多模数据库深度解读:从“多库拼装”到“一库多能”的架构演进
数据库·mysql·dba
howdoyoudo2026064 小时前
AI审计手记 #01(数据补全版):107小时、17,600次操作——OpenAI越狱案完整攻击链量化分析
大数据·人工智能·安全·ai·语言模型
hrrrrxeeeee4 小时前
不同基础怎么报考 CAIE 认证|Level I 与 Level II 报考指南
大数据·人工智能·产品经理