说实话,我一看到“FlinkCDC + 达梦CDC + FlinkSQLAPI打jar包”这个组合就知道,这又是一个被国产数据库实时同步需求逼过的人整理出来的关键词串。刚拿到需求时,我也下意识觉得达梦既然“兼容Oracle”,那一套Flink CDC连接器应该能直接改改配置就上,结果真去官网翻支持矩阵、去底层看Debezium实现后才发现,这条路比想象中绕得多。
这篇文章就是想把这次从“没有官方连接器”到“能上生产稳定跑”的完整过程复盘出来。我会把真实的选型判断、Maven工程怎么搭、可落地的Flink SQL API代码长什么样、fat jar怎么打、以及提交到集群后那些奇奇怪怪的报错怎么查,一步到位写清楚。内容面向正在做国产数据库实时数仓的大数据开发,也适合技术负责人拿来做方案评估前的参考。
1. 为什么说“达梦CDC + Flink”是个容易翻车的组合:先识别需求真相
1.1 “兼容Oracle”不等于“能套用Oracle CDC”
达梦DM8在国内用得确实广,平时写SQL、用PL/SQL风格包、做数据迁移的时候,很多人都觉得它和Oracle非常像。但“语法兼容”和“日志解析协议兼容”完全是两码事。Flink CDC里的Oracle连接器并不是靠JDBC轮询表数据,而是通过Debezium去解析Oracle的Redo Log,还原出事务提交前后的完整镜像。达梦并没有向第三方开放一套和Oracle完全等价的日志解码协议,所以Flink CDC生态里没有达梦连接器,并不是官方“懒得做”,而是底层对接成本非常高。
我见过不少团队在项目评估阶段想当然地假设“达梦有Oracle兼容模式,用Oracle连接器试试”,真拿测试环境一跑就各种乱码、解析不到DML、事务顺序错乱。到了这一步再去查文档才发现,根本没人承诺这条路可行。所以,接到这类需求时,第一个动作不是写代码,而是确认业务要的究竟是“实时CDC”还是“增量抽取”。如果只是每天或每小时同步一次变化数据,那叫抽取任务,没必要上CDC;真正需要CDC的场景,通常伴随实时大屏、实时指标或下游系统需要秒级感知insert/update/delete。两个需求的技术路线差距巨大,必须前置确认,否则后面全是白忙。
1.2 目前最靠谱的三条路线与选型对比
先说结论:到目前为止(2025年初),我仍不建议任何人投入几周时间自己硬写一个“达梦的Flink CDC Source”。更稳妥的做法,是让达梦侧先把变更事件吐出来,Flink再去消费加工。
路线一:达梦官方的实时同步软件DMHS监听归档日志,将增量数据解析后以JSON或AVRO格式写入Kafka,再由Flink SQL API消费Kafka做后续加工。这是达梦生态比较标准的生产姿势,DMHS做整库多表、DDL/DML事件解析都很成熟,吞吐和稳定性都有保障。缺点是需要额外部署一套独立同步组件,还要向达梦原厂申请配合,不是所有现场都能立刻搞定。
路线二:基于达梦LogMiner能力自研Flink Source。达梦确实提供了一套类似日志分析的功能,理论上可以手动读取日志还原出变更记录。但实际操作中要处理事务提交顺序、主键缺失的日志场景、DDL事件对齐、不同版本行为差异等问题,工作量和踩坑量远超预期。我们做过一次POC,最后被版本兼容问题劝退了。
路线三:把“真CDC”降级为JDBC增量轮询或定时全量。如果业务能接受秒级甚至分钟级延迟,表里又有更新时间戳或自增主键,用Flink JDBC Connector轮询增量数据就好了。这条路线几乎不需要额外组件,代码量也小,对场景是“周期刷新维表”或“分钟级指标同步”的项目来说,性价比最高。
| 对比维度 | DMHS + Kafka路线 | 基于LogMiner自研Source | JDBC增量轮询/定时全量 |
|---|---|---|---|
| 实时性 | 秒级 | 秒级 | 秒级到分钟级 |
| 开发量 | 低(Flink侧只做消费) | 很高 | 低 |
| 运维成本 | 需维护DMHS与Kafka链路 | 需自研并持续维护 | 最低 |
| 能捕获Delete | 能 | 能 | 不能(轮询删除了的历史数据会丢) |
| 适用阶段 | 生产环境正式数据链路 | 研究型POC | 维度表、非核心明细表同步 |
1.3 顺带说下SeaTunnel这类中间件
很多团队现在已经有Apache SeaTunnel之类的数据集成平台在跑。如果你的环境里SeaTunnel已经接了达梦,能稳定把变更数据输出到Kafka或下游存储,那Flink侧就完全没必要再重复建设Source。统一接入层可以让业务方拿到干净的Topic,之后所有“达梦CDC”需求就收敛成“消费Kafka里的changelog,用Flink SQL加工”的常规任务。这也是我下面代码示例采用的主链路模型:达梦一侧通过DMHS或其它同步组件把事件吐到Kafka,Flink作业从Kafka接入。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 用Flink SQL API而不用SQL Client:这套交付姿势好在哪
2.1 SQL脚本在长期跑的作业面前有多脆弱
刚接触Flink SQL的时候,最舒服的姿势是把一段DML写进.sql文件,再通过sql-client执行。做探查、做验证确实快,但一旦作业需要长期跑在生产环境,SQL Client的短板就全露出来了:工作目录、配置文件、初始化脚本散落一地,遇到版本升级时无法锁定精确的依赖组合;SQL语法错误要到执行期才会暴露,缺少编译期检查;密码、库地址这些参数只能硬编码在脚本里,改一处要全局排查,运维起来十分痛苦。
换成Flink SQL API方式后,SQL本身还是字符串,但外层已经变成常规的Java应用。你可以把环境配置外置成参数、把公共Schema初始化收敛到启动逻辑里、捕获异常做统一埋点,甚至可以打日志记录每个SQL是否执行成功。项目交付给现场时,给对方的不是一套可改的散装脚本,而是一个版本明确的jar包加一条启动命令。
2.2 SQL API真正值钱的地方:DataStream和Table可以混用
很多人以为“Flink SQL API”只是把SQL字符串包在Java代码里执行,兜兜转转还是SQL那套。其实它的一个重要价值在于,你可以选择让一部分逻辑用DataStream API处理,一部分用Table/SQL处理,二者能互相转换。比如达梦CDC链路里,Kafka里的消息经常是DMHS自定义的复杂JSON,含before/after嵌套结构,直接在Flink SQL里解析这种动态字段非常痛苦。你可以先用DataStream API消费消息、反序列化成Row,标记好RowKind语义,再Registered成临时表,这样上游脏活干完,后续复杂指标加工全部回归SQL。
这类项目里我发现,纯SQL解决不了的问题,很多都和“事件如何正确标记为插入、更新、删除”有关。如果上游消息结构不规整,硬用字符串建表只会把自己绕晕。能DataStream和Table混用的任务,调试效率会高很多。
2.3 什么场景不建议上这套“重型”工程
我自己不是那种“什么都要上Flink SQL API”的狂热派。如果任务只有一张小表、同步逻辑不足五个算子,并且维护周期预计很短,直接用DataStream写一个几十行的小作业就够了。SQL API虽然灵活,但工程骨架、依赖配置、打包流程都有一定启动成本。项目只有周末帮业务抽个数,就不要自找麻烦造个平台。
不过针对达梦CDC这类改造项目,情况往往相反:一旦接进来,后续就会有第二张表、第三个下游需求跟上来。与其每次写碎片化代码,不如第一次就搭一个配置化、可扩展的Flink SQL API工程,这也是我最终选择这种技术路线的原因。
3. Maven工程配置与打jar包的完整做法:把坑提前到构建期
3.1 先看一份能跑的pom骨架
打Flink的fat jar,pom配置看似简单,实际第一次执行时大概率会碰见一两个让你懵半天的报错。下面这份是我整理过的最小可用配置。Flink版本以1.16.2为例,Java建议8或11。
xml复制<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<flink.version>1.16.2</flink.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- Flink Table/SQL API 相关依赖 -->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-table-api-java-bridge</artifactId>
<version>${flink.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-table-planner-loader</artifactId>
<version>${flink.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-table-runtime</artifactId>
<version>${flink.version}</version>
<scope>provided</scope>
</dependency>
<!-- Kafka连接器必须打进jar,因为集群lib里不一定有对应版本 -->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-kafka</artifactId>
<version>${flink.version}</version>
</dependency>
<!-- 如果目标端用JDBC写入 -->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-jdbc</artifactId>
<version>${flink.version}</version>
</dependency>
<!-- JSON解析相关的工具类 -->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-json</artifactId>
<version>${flink.version}</version>
</dependency>
</dependencies>
这里有个关键点:凡标了provided的依赖,最终不会被打进fat jar。因为Flink集群的lib目录里已经有对应类,强行打进去反而可能和平台自身的classloader发生冲突,表现出来就是各种ClassCastException或者方法不存在。而Kafka连接器、JDBC连接器这类,集群上未必有匹配版本,所以要让它们以compile方式进入最后的jar包。
3.2 shade插件怎么配才不炸
maven-shade-plugin是最常用的打包插件,但光把jar堆在一起不够,需要处理三件套:Manifest包含主类、去掉签名文件、合并SPI文件。这类坑几乎每次都会遇到一次。
xml复制<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.4.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.example.dmcdc.DmCdcSyncJob</mainClass>
</transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
</transformers>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
排除META-INF/*.SF那几行千万别省,否则执行时会报Invalid signature file digest for Manifest main attributes。原因是不少第三方jar本身带签名,shade合并后又没重新签名,JVM启动时发现manifest变了但签名缓存还在,直接拒绝启动。
SPI文件合并也重要。Flink的很多连接器会通过META-INF/services加载扩展类,如果shade时不做ServicesResourceTransformer,后面很容易冒出ServiceConfigurationError,而且这个错只有运行到某个深层调用才会出现,排查成本很高。
3.3 达梦JDBC驱动怎么打进去
达梦官方提供的JDBC驱动一般叫DmJdbcDriver18.jar,这个依赖在公共Maven中央仓库里没有,需要用install-file先装进本地仓库,再按普通依赖引用。我当时是到达梦安装目录或官网下载对应版本后执行:
bash复制mvn install:install-file \
-Dfile=DmJdbcDriver18.jar \
-DgroupId=com.dameng \
-DartifactId=DmJdbcDriver \
-Dversion=8.1.2.192 \
-Dpackaging=jar
然后pom里声明依赖时保持compile scope,确保驱动类被打进fat jar。有一点需要提醒,项目交付的时候尽量在说明文档里标注清楚达梦JDBC驱动的具体版本,因为不同达梦版本对应的驱动包差异可能导致连接参数认不全,现场升级时也要一并升级驱动,否则经常会出现java.sql.SQLException: 不支持的字符集或事务状态类异常。
打出jar后可以先检查驱动类是否真的进去了:
bash复制jar tf target/dm-cdc-sync-1.0.jar | grep -i dm
如果能看到类似dm/jdbc/driver/DmDriver.class的条目,说明驱动合并成功。看类的具体字节码可以用javap,也可以解压后用常用反编译工具细看,但一般确认类存在和版本号就够了。
4. Flink SQL API核心代码:一个能直接上生产的示例
4.1 让Flink识别上游是insert还是delete
这个环节算是整段的精髓。如果上游DMHS配置的是把变更后完整行数据放进Kafka,且格式是普通JSON,那Flink SQL会把这堆消息都当作append-only流,也就是只有新增事件。而你的同步目标很可能是“按主键做更新”,一旦上游在达梦里执行了update或delete,普通append模式完全无法表达删除语义,下游要么多出脏数据,要么需要额外写复杂的删除补偿逻辑。
处理思路是把Kafka里的每条消息先转成带RowKind标记的行。例如在反序列化时,遇到"opType":"DELETE"就构造Row.ofKind(RowKind.DELETE, ...),遇到"opType":"UPDATE"就分别输出RowKind.UPDATE_BEFORE和RowKind.UPDATE_AFTER。Flink的changelog流会在内部维护这些标记,下游执行CREATE TABLE AS SELECT或写入支持upsert/delete的sink时才能识别正确语义。
4.2 完整核心代码骨架
下面这个例子演示两种范式如何融合:用DataStream做源,解析为带RowKind的行后注册成一张表,后半程全部用SQL API处理。
java复制import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.serialization.SimpleStringSchema;
import org.apache.flink.api.java.utils.ParameterTool;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.DataTypes;
import org.apache.flink.table.api.Schema;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import org.apache.flink.types.Row;
import org.apache.flink.types.RowKind;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
public class DmCdcSyncJob {
public static void main(String[] args) throws Exception {
ParameterTool params = ParameterTool.fromArgs(args);
Configuration conf = new Configuration();
conf.setString("execution.checkpointing.interval", "60s");
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(conf);
env.enableCheckpointing(60000);
env.setParallelism(params.getInt("parallelism", 1));
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
String bootstrapServers = params.get("bootstrap.servers", "localhost:9092");
String topic = params.get("source.topic", "dm-cdc-user");
String groupId = params.get("group.id", "flink-dm-cdc-group");
String targetUrl = params.get("sink.url", "jdbc:dm://localhost:5236");
String targetTable = params.get("sink.table", "MIRROR_USER");
org.apache.flink.connector.kafka.source.KafkaSource<String> source =
org.apache.flink.connector.kafka.source.KafkaSource.<String>builder()
.setBootstrapServers(bootstrapServers)
.setTopics(topic)
.setGroupId(groupId)
.setStartingOffsets(org.apache.flink.connector.kafka.source.enumerator.initializer.OffsetsInitializer.earliest())
.setValueOnlyDeserializer(new SimpleStringSchema())
.build();
DataStream<String> raw = env.fromSource(source, WatermarkStrategy.noWatermarks(), "dm-cdc-kafka-source");
ObjectMapper mapper = new ObjectMapper();
DataStream<Row> cdcRows = raw.map(value -> {
JsonNode node = mapper.readTree(value);
String opType = node.path("opType").asText();
JsonNode after = node.path("after");
long id = after.path("ID").asLong();
String name = after.path("NAME").asText();
int deptId = after.path("DEPT_ID").asInt();
long updateTime = after.path("UPDATE_TIME").asLong();
if ("DELETE".equals(opType)) {
return Row.ofKind(RowKind.DELETE, id, name, deptId, updateTime);
} else if ("UPDATE".equals(opType)) {
// 对更新语义要求严格的场景,需要同时下发UPDATE_BEFORE和UPDATE_AFTER
// 这里简单起见只下发UPDATE_AFTER,对大多数镜像目标表够用
return Row.ofKind(RowKind.UPDATE_AFTER, id, name, deptId, updateTime);
}
return Row.ofKind(RowKind.INSERT, id, name, deptId, updateTime);
}).returns(Row.class);
Schema sourceSchema = Schema.newBuilder()
.column("ID", DataTypes.BIGINT())
.column("NAME", DataTypes.STRING())
.column("DEPT_ID", DataTypes.INT())
.column("UPDATE_TIME", DataTypes.BIGINT())
.primaryKey("ID")
.build();
Table cdcTable = tableEnv.fromChangelogStream(cdcRows, sourceSchema);
tableEnv.createTemporaryView("CDC_USER", cdcTable);
// 从这里往下,可以放心使用SQL做各种加工
tableEnv.executeSql(
"CREATE TABLE TARGET_USER (" +
" ID BIGINT PRIMARY KEY," +
" NAME STRING," +
" DEPT_ID INT," +
" UPDATE_TIME BIGINT" +
") WITH (" +
" 'connector'='jdbc'," +
" 'url'='" + targetUrl + "'," +
" 'table-name'='" + targetTable + "'," +
" 'username'='" + params.get("sink.username", "SYSDBA") + "'," +
" 'password'='" + params.get("sink.password", "") + "'" +
")");
tableEnv.executeSql(
"INSERT INTO TARGET_USER SELECT ID, NAME, DEPT_ID, UPDATE_TIME FROM CDC_USER");
}
}
注意,tableEnv.fromChangelogStream只在桥接模式下可用,这解释了为什么pom里必须引入flink-table-api-java-bridge。源表注册完成后,下游TARGET_USER需要支持基于主键的更新删除,JDBC连接器在遇到UPDATE_BEFORE/UPDATE_AFTER时会自动生成对应的update语句。你还可以继续用tableEnv.executeSql创建视图做过滤、join、聚合,这一层就回到大家熟悉的SQL领域了。
4.3 Checkpoint配置与消费语义
Flink在Kafka任务里有一个很容易被忽略的坑:开了checkpoint后,Kafka offset是交给Flink的checkpoint统一管理的。如果你在Kafka Consumer配置里又手动开了enable.auto.commit=true,就会出现在checkpoint还没完成时offset已经提交的情况,作业一旦故障重启,消费位点可能已经越过未处理完的数据,造成丢失。用Flink KafkaSource时,建议关闭一切自动提交行为,交给Flink自己处理。
env.enableCheckpointing(60000)表示每60秒做一次checkpoint。要保证数据不丢,还要给状态后端指定存储位置。生产环境如果是on YARN或K8s,通常通过配置文件或命令行参数设置:
bash复制./bin/flink run -d \
-p 2 \
-c com.example.dmcdc.DmCdcSyncJob \
-D state.checkpoint-storage=filesystem \
-D state.checkpoints.dir=hdfs:///flink/checkpoints/dm-cdc \
target/dm-cdc-sync-1.0.jar \
--bootstrap.servers kafka1:9092 \
--group.id flink-dm-cdc-group \
--sink.url jdbc:dm://dmserver:5236 \
--sink.username SYSDBA \
--sink.password 'xxxx'
单从输入端看,开启checkpoint后Kafka数据可以达到“不丢不重(至少一次)”。真正要往精确一次走,还需要Sink端配合事务或幂等,比如写StarRocks/Doris时用官方支持两阶段提交的sink;如果是写达梦这类普通JDBC目标表,现实目标通常按“至少一次+下游幂等覆盖”来做,否则要自研两阶段提交,复杂度很高。
5. 不会每次都搭DMHS:试试自写增量查询Source的简易方案
5.1 这种降级方案到底适不适合你
有些现场环境确实无法快速部署DMHS,可能是网络隔离,也可能是审批流程很长,而业务的需求又只是一张表每隔几十秒同步一次,字段里刚好有自增主键或者最后修改时间。遇到这种情况,可以走一条“缩水CDC”的路,本质是用Flink自定义Source定期执行增量查询,将查询到的记录组装成Row后继续走Flink SQL API加工。
但这套方案有一个天生缺陷:它捕获不到被物理删除的历史记录,因为删除发生后,不会再有任何字段变化可供查询。如果业务要求“源端删一条,目标端也删一条”,就不要对增量轮询方案抱期望。它更适合的是维表同步或“只追加、不改不删”的流水类数据。
5.2 一个能跑的示例逻辑
下面这段用RichSourceFunction实现的轮询逻辑,可以给大家做个雏形参考。核心思路是记录上一次查询到的时间戳,每次只取大于该时间戳且能按主键稳定排序的数据,发送完再更新位点。
java复制public class DmIncrementSource extends RichSourceFunction<Row> {
private volatile boolean running = true;
private final String jdbcUrl;
private final String sql;
private final long intervalMs;
private long lastUpdateTime;
public DmIncrementSource(String jdbcUrl, String sql, long intervalMs, long startTime) {
this.jdbcUrl = jdbcUrl;
this.sql = sql;
this.intervalMs = intervalMs;
this.lastUpdateTime = startTime;
}
@Override
public void run(SourceContext<Row> ctx) throws Exception {
Class.forName("dm.jdbc.driver.DmDriver");
while (running) {
try (Connection conn = DriverManager.getConnection(jdbcUrl);
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setLong(1, lastUpdateTime);
ps.setLong(2, 1000);
try (ResultSet rs = ps.executeQuery()) {
long maxTs = lastUpdateTime;
while (rs.next()) {
long ts = rs.getLong("UPDATE_TIME");
ctx.collect(Row.of(
rs.getLong("ID"),
rs.getString("NAME"),
rs.getInt("DEPT_ID"),
ts));
maxTs = Math.max(maxTs, ts);
}
if (maxTs > lastUpdateTime) {
lastUpdateTime = maxTs;
}
}
} catch (Exception e) {
// 保留异常日志并sleep后重试
e.printStackTrace();
}
Thread.sleep(intervalMs);
}
}
@Override
public void cancel() {
running = false;
}
}
查询SQL大概长这样,注意每次带上ROWNUM限制批大小,避免一次拉太多导致内存压力:
sql复制SELECT ID, NAME, DEPT_ID, UPDATE_TIME
FROM TEST.T_USER
WHERE UPDATE_TIME > ?
AND ROWNUM <= ?
ORDER BY UPDATE_TIME, ID
这个方案想跑得稳,建议查询字段加组合索引,否则越到后面全表扫描越拖垮源库。另外,要用好它有一个隐性要求:UPDATE_TIME的精度不能太低,如果业务表的时间戳只精确到秒且同一秒内大量更新,就有漏数风险。真实项目里我们一般要求业务改造秒级到毫秒级,否则不接这个方案。
5.3 扩展一下:自增主键和全量对账的打法
如果表里没有更新时间字段但有自增主键,也可以改成“记录最大ID”,每次只取ID大于上次的值。这种方法对新增数据有效,但更新发生不改变主键值,所以同样捕获不到update。实务中要做增量同步最稳的还是依赖“更新时间戳+自增ID”双游标:时间戳负责抓更新,自增ID负责兜底新增,两边结果做并集再去重。程序逻辑能多一个保障。
更彻底一点的做法是先跑一次全量对账,把源端和目标端数据算MD5分桶对比,差异数据回刷。每隔几分钟做一次增量轮询,每天凌晨做一次全量内容校验。这种方案运维上多花些精力,但现场能接受的场景确实很实在,让业务在尽量不动架构的前提下拿到了“准实时”能力。
6. 提交、验证、排错:从jar包到数据流起来的过程
6.1 打包并提交到Flink集群
Maven打好包后,在target下会生成一个体积比较大的fat jar。提交前先确认manifest里的主类有没有被正确写入:
bash复制unzip -p target/dm-cdc-sync-1.0.jar META-INF/MANIFEST.MF
看到Main-Class: com.example.dmcdc.DmCdcSyncJob就可以提交了。启动命令里注意Flink SQL API任务同样可以通过-D追加配置。实际生产中我习惯把一个环境的所有参数写成一个.conf文件用脚本自动载入,这样每次发版改动可控,不会出现现场同学参数字段拼写错误的情况。
6.2 线上验证时的5个高频问题
从jar包提交到数据真正流起来,中间会经历不少奇奇怪怪的运行时报错。我把上线初期最常撞见的几类整理成一张表,方便排查时对照。
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| 提交时报找不到主类 | shade插件没有配置ManifestResourceTransformer | 检查pom的shade配置是否包含mainClass |
运行时ClassNotFoundException: dm.jdbc.driver.DmDriver |
达梦驱动jar没打进fat jar | 用jar tf检查,确认为compile scope |
启动报Invalid signature file digest |
第三方jar签名文件未排除 | shade的filters里排除META-INF/*.SF等签名文件 |
