最近在做一个数据接入需求,源库是达梦,目标端是Doris,业务方给的期限很紧,希望当天先跑通一版。当时我第一反应是直接拿Flink CDC去连源库,结果一查才发现达梦并不在 Flink CDC 官方支持列表里,MySQL CDC 那套直接拿来用根本不行。后来用 Dinky + Flink SQL 把这条链路搭通了,整体流程比预想简单,适合大多数对实时性要求不那么苛刻的场景。这篇文章就记录一下这个“简单实现”的完整过程,包括方案选型、环境准备、SQL 怎么写、以及实际踩过的坑。
1. 项目背景与方案选型
1.1 需求场景与核心约束
这种需求在不少企业里很常见:Doris 已经作为统一分析平台承接报表和 BI 查询,但是业务系统的数据源是达梦数据库,两边是割裂的。业务方希望把达梦里的订单、用户、库存这类核心表实时同步到 Doris 里,方便分析师直接查,不用每次跑到源库去做联表查询。
这类需求的真实约束往往有三个:
- 不能改动业务系统的代码,也不能在源库上装一些依赖特定内核的插件。
- 开发周期短,最好一两天内能出一个可演示的版本。
- 数据量一般,单表几百万到几千万行,日增几十万左右,不会出现动辄上亿的极端场景。
如果开发周期没有任何限制,完全可以自研一套基于日志解析的同步工具,或者在源库前面加一层消息队列,让应用双写 Kafka,再由 Flink 消费写入 Doris。但这些方案的改造成本和运维成本都太高,对于“先把链路跑通”这个目标来说属于重装备。
1.2 为什么选择 Dinky + Flink SQL
我当时的备选方案有三个:自研 Java 程序定期从达梦拉数、用 DataX 做离线同步、用 Dinky + Flink SQL 搭同步链路。三者的差别可以看这个表格:
| 方案 | 开发成本 | 维护成本 | 后续扩展能力 |
|---|---|---|---|
| 自研 Java 定时任务 | 中 | 高,每次需求变化都要改代码 | 弱,复杂 ETL 要重写 |
| DataX 定时同步 | 低 | 中,配置繁琐,任务多了以后管理困难 | 一般,只适合简单迁移 |
| Dinky + Flink SQL | 低 | 低,SQL 在线管理,作业可视化 | 强,join、窗口、清洗都在 SQL 里完成 |
我最后选了 Dinky + Flink SQL,核心原因是 Flink SQL 对后续的增量需求更友好。刚开始可能只是单表单向同步,但后面业务方大概率会提“我要把两张表 join 之后再同步”“我要过滤掉某些状态的数据”之类的需求。在 Flink SQL 里写这些逻辑就是改一段 SQL 的事,在 DataX 和自研代码里就要动结构、发版本,非常被动。
Dinky 在这条链路里充当的是 Flink SQL 开发运维平台的角色。它本身不是计算引擎,只是帮你管理 SQL 作业、提交到 Flink 集群、查看运行日志和监控状态。没有 Dinky,你得用命令行 flink run 去提作业,调试体验很差;有了 Dinky,建表语句、同步 SQL、调度配置全在一个界面上完成,排查问题也直观很多。
1.3 整体链路与数据流转
这条链路的完整数据流是:
达梦数据库作为源端,通过 Flink JDBC Connector 读取表数据,在 Flink SQL 里做必要的字段映射和清洗,然后通过 Flink Doris Connector 写入 Doris 的 Unique 模型表。
需要注意:这个方案本质上是“准实时”,不是秒级 CDC。因为 Flink JDBC Connector 本身不具备增量日志解析能力,只能通过查询的方式把数据拉出来。如果业务方要求的“实时”是分钟级、能容忍一到几分钟延迟,那这个方案完全够用,而且非常稳定。如果业务方要的是秒级延迟、要捕获删除操作,那就得换思路,我后面第 5 节会专门讲 CDC 的演进方向。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与前置配置
2.1 达梦侧前置检查
达梦在数据同步任务里通常扮演“源库”角色,所以前置工作主要是确认连接性和账号权限。
达梦数据库默认端口是 5236,实例名一般是 DAMENG,超级管理员账号是 SYSDBA。连接测试有两种方式:一是用达梦自带的 DIsql 命令行工具登录,二是用 DBeaver、Navicat 这类可视化工具。Navicat 新版本里已经内置了达梦数据库的连接类型,选好之后填地址端口账号就能连上,不用手动配驱动。
JDBC 连接信息需要记好,后面在 Flink SQL 建表时要用:
| 项目 | 值 |
|---|---|
| 驱动类 | dm.jdbc.driver.DmDriver |
| JDBC URL | jdbc:dm://192.168.10.20:5236/DAMENG |
| 默认端口 | 5236 |
| 实例名 | DAMENG |
如果后续要做周期增量同步,建议在源表的更新时间字段上建索引,否则每次按时间范围扫描全表会非常慢。不要小看这一步,我遇到过几次线上任务延迟飙升,最后排查下来就是源表缺索引,全表扫描把数据库 CPU 打满了。
2.2 Doris 侧表结构设计
Doris 作为目标端,表模型的选择直接决定同步的正确性。如果源表有更新操作,目标表必须用 Unique 模型,这样同一主键的数据多次导入只会保留最后一条。
我常用的建表语句大概是这样的:
sql复制CREATE DATABASE IF NOT EXISTS doris_dm_test;
CREATE TABLE IF NOT EXISTS doris_dm_test.user_info (
id INT,
name VARCHAR(100),
email VARCHAR(100),
update_time DATETIME
)
UNIQUE KEY(id)
DISTRIBUTED BY HASH(id) BUCKETS 3
PROPERTIES (
"replication_num" = "1",
"function_column.sequence_col" = "update_time"
);
这里有两个关键点。
一个是 UNIQUE KEY(id) 指定主键列。源表里的主键在 Doris 里必须原样保留,否则同步后数据无法做到幂等,重复跑一次任务就会产生重复记录。
另一个是 function_column.sequence_col 的设置。这个参数非常关键,它指定了 Doris 在导入时用于版本比较的字段。比如同一条 id=1 的数据,第一次导入的 update_time 是 10:00,第二次同步任务又导入了 update_time 是 09:30 的旧数据,如果没有 sequence_col,后导入的旧数据会直接覆盖新数据,造成数据回退。指定了 update_time 作为 sequence 列之后,Doris 会按这个字段比较版本,旧数据来了也覆盖不了新数据。这个坑我在生产环境踩过一次,那次数据错乱排查了很久,最后就是靠这个参数解决的。
replication_num 在测试环境可以设成 1,生产环境建议按集群副本数配置,一般设置 2 或 3。
2.3 Dinky 与 Flink 运行环境准备
Dinky 本身是一个 Web 服务,部署相对简单,装好之后访问控制台,第一次启动会初始化元数据库。Dinky 支持对接多种 Flink 模式,最常见的是 Standalone 和 YARN 模式。我这边测试环境用的是 Standalone,就是把一个 Flink 集群先启动好,Dinky 里注册一下集群地址即可。
需要提前准备的 JAR 包有三个:
- 达梦 JDBC 驱动:负责 Flink 与达梦之间的底层连接。
- flink-connector-jdbc:Flink SQL 访问关系型数据库的官方连接器,读达梦和写达梦都靠它。
- flink-doris-connector:Flink SQL 写 Doris 的官方连接器,底层走 Doris 的 Stream Load 导入通道。
注意版本匹配。flink-connector-jdbc 的版本要跟 Flink 大版本一致,比如 Flink 1.16 就用 1.16 对应的 connector;flink-doris-connector 也要选择兼容当前 Flink 版本的构建。版本不匹配最常见的报错就是各种 NoSuchMethodError、ClassNotFoundException,而且报错信息经常不直接,排查起来很浪费时间。
Dinky 里有 Jar 管理功能,可以把这些依赖上传到 Dinky,提交作业时自动加到 classpath 里。也可以直接放到 Flink 的 lib 目录下,两种方式都可以,我个人更推荐用 Dinky 的 Jar 管理,因为每个作业可以单独指定依赖,不会污染 Flink 公共环境。
3. 在 Dinky 中开发同步作业
3.1 注册集群、上传依赖 JAR
Dinky 控制台左侧菜单找到“注册中心”,在里面添加 Flink 集群。Standalone 模式只需要填集群地址,比如 http://192.168.10.30:8081,保存之后 Dinky 会去探测集群状态,显示为正常在线就说明注册成功。
然后在 Jar 管理里分别上传达梦驱动、flink-connector-jdbc、flink-doris-connector 三个 JAR 包。上传后记得记录一下 Jar 的名称和版本,后面作业配置里如果要指定依赖,会用到。
有一个细节:如果你们是多个 Flink 作业共用一个集群,建议把常用的 connector 直接放到 Flink 的 lib 目录,这样所有作业都能直接用;达梦驱动这种只有特定作业用的 JAR,放到 Dinky 的 Jar 管理里更干净,避免影响其他作业。
3.2 创建 Source 表与 Sink 表
在 Dinky 的“数据开发”里新建一个 FlinkSQL 类型作业,把下面这套 SQL 贴进去。
先建 Source 表,也就是达梦这边的读取表:
sql复制CREATE TABLE dm_user_info (
id INT,
name STRING,
email STRING,
update_time TIMESTAMP(3)
) WITH (
'connector' = 'jdbc',
'url' = 'jdbc:dm://192.168.10.20:5236/DAMENG',
'table-name' = 'USER_INFO',
'username' = 'SYSDBA',
'password' = 'your_password',
'scan.fetch-size' = '1000'
);
这里 connector 必须写成 jdbc,Flink 靠这个关键字找到对应的连接器实现。table-name 对应达梦里的表名,达梦默认对大小写不敏感,所以实际查询时会自动转成大写去匹配。scan.fetch-size 是每次从数据库拉取的行数,设得太大容易占内存,设得太小查询次数多,1000 是我自己用的比较稳的值。
再建 Sink 表,也就是 Doris 这边的目标表:
sql复制CREATE TABLE doris_user_info (
id INT,
name STRING,
email STRING,
update_time TIMESTAMP(3)
) WITH (
'connector' = 'doris',
'fenodes' = '192.168.10.30:8030',
'table.identifier' = 'doris_dm_test.user_info',
'username' = 'root',
'password' = '',
'sink.label-prefix' = 'doris_dm_sync',
'sink.properties.format' = 'json',
'sink.properties.read_json_by_line' = 'true',
'sink.enable.batch-mode' = 'true'
);
fenodes 填 Doris FE 的 HTTP 端口,不是 MySQL 协议的 9030 端口,很多新手在这一步会填错。Doris 的 Stream Load 是通过 HTTP 请求触发的,FE 的 HTTP 端口默认是 8030,所以要填 ip:8030 而不是 ip:9030。
sink.label-prefix 是每次导入任务的标签前缀,Doris 通过 label 做导入幂等,重复提交同一个 label 会直接返回成功,不会重复写入。建议每次同步任务用不同的前缀,比如按日期拼接,避免冲突。
read_json_by_line 设置为 true,表示每一行是一个完整的 JSON,Stream Load 按行解析,这个配置在 Flink Doris Connector 里几乎是标配。
3.3 提交执行与结果验证
两张表建好之后,写同步逻辑就一行 SQL:
sql复制INSERT INTO doris_user_info
SELECT id, name, email, update_time
FROM dm_user_info;
这个 INSERT 语句的逻辑是:把达梦里的 USER_INFO 表全量读出来,经 Flink 处理后写入 Doris 的 user_info 表。因为是全量同步,第一次跑的时候会把源表所有数据都拉一遍。
在 Dinky 里点“执行”按钮提交作业。执行过程中可以看运行日志,如果 Source 表建表时报找不到驱动,优先检查达梦驱动 JAR 是否已经上传并被加载;如果 Sink 表建表时报 Doris 连接失败,检查 fenodes 填的 IP 和端口是否正确。
作业执行完后,到 Doris 侧验证数据。用 MySQL 客户端连上 Doris 的 9030 端口:
sql复制SELECT COUNT(*) FROM doris_dm_test.user_info;
再抽查几条源表里比较有代表性的数据,比如最近更新的记录、字段值比较特殊的记录,确认字段一一对应,没有出现错位和丢失。
3.4 从全量同步升级为准实时同步
全量同步跑通之后,再往前一步就是准实时。思路很简单:利用 Doris Unique 模型的去重能力,每次只同步最近一段时间内有变更的数据,配合调度任务频繁执行。
增量 SQL 可以这么写:
sql复制INSERT INTO doris_user_info
SELECT id, name, email, update_time
FROM dm_user_info
WHERE update_time >= CURRENT_TIMESTAMP - INTERVAL '5' MINUTE;
这条 SQL 的含义是每次执行时只拉取最近 5 分钟内更新过的数据。调度频率可以设置成每分钟一次,这样数据延迟基本控制在 1 分钟以内。
这个方案有一个需要注意的地方:如果某条数据的更新时间比较早,但数据写入源库的时间很晚,用这种“只拉最近 5 分钟”的方式会漏掉它。解决的办法是做一个回补窗口,比如每 10 分钟跑一次最近 1 小时的增量,甚至每天凌晨做一次全量修复。我个人的经验是:业务上对数据完整性要求高的表,用“短窗口增量 + 每日全量对账”的组合最稳妥。
Dinky 本身支持作业调度配置,设置 Cron 表达式即可。如果你们公司有现成的调度平台,比如 DolphinScheduler 或 Airflow,也可以通过 Dinky 的 OpenAPI 触发作业执行,这样可以把同步任务纳入统一调度体系,报警、重试、血缘都能覆盖到。
4. 常见问题与排查技巧实录
4.1 连接与驱动类问题
遇到最多的报错是 ClassNotFoundException: dm.jdbc.driver.DmDriver。这个基本就是达梦驱动 JAR 没被加载。排查步骤很简单:先确认 JAR 上传到了 Dinky 的 Jar 管理里,再确认作业引用了这个 Jar。如果放在 Flink lib 目录,要重启 Flink 集群才会生效。
还有一种是连接超时或者 Connection refused。先确认达梦端口 5236 是否通,在 Dinky 所在机器上用 telnet 测一下:
bash复制telnet 192.168.10.20 5236
如果端口通,再确认账号密码正确。达梦有些版本初始化后 SYSDBA 的密码是随机的或者策略要求强制修改,如果提示密码过期,先用 DIsql 登录后重置。
一个容易忽略的问题是 JDBC URL 里的大写实例名。DAMENG 是达梦默认的库名,如果当初初始化时改过实例名,URL 里也要跟着改,否则会报数据库不存在。
4.2 SQL 执行与类型映射问题
Flink SQL 里最常见的报错是类型不匹配。达梦里的 NUMBER、VARCHAR2、DATE 这些类型,映射到 Flink SQL 时要选对类型。我常用的映射对照表如下:
| 达梦类型 | Flink SQL 类型 |
|---|---|
| NUMBER | INT / BIGINT / DECIMAL |
| VARCHAR2 / VARCHAR | STRING |
| DATE | TIMESTAMP(3) |
| TIMESTAMP | TIMESTAMP(3) |
| CLOB | STRING(注意长度) |
如果源表字段是 NUMBER(18,2),建议映射成 DECIMAL(18,2),不要直接映射成 DOUBLE,否则精度会在中间环节丢失。同步到 Doris 后如果发现小数位不对,八成就是这个原因。
还有一个跟时间相关的坑:达梦的 DATE 类型可能带时间部分,Flink 里如果定义成 DATE 会丢掉时分秒,导致 Doris 里看到的时间全是 00:00:00。遇到这种情况,把类型改成 TIMESTAMP(3) 再同步。
4.3 Doris 导入与数据一致性问题
Doris 导入报错里,比较常见的是 stream load error 或者 load failed。这种问题的定位思路是看 Doris BE 的日志,BE 会记录 Stream Load 失败的具体原因。我遇到过的几种情况:
- JSON 格式不对,源数据里如果有特殊字符没有转义,导致 JSON 解析失败。
- 列顺序和类型对不上,Doris 导入
