场景:车联网 TBox 终端按国标 GB32960 协议上报报文,经网关接入 Kafka;Flink 以 Table API / SQL 直接将原始报文(Original)落湖 Paimon,形成 ODS 贴源层,供下游 Doris / Spark / Hive 查询与数仓分层加工。
本文基于生产实战整理,涵盖架构、建模、代码、配置、Checkpoint、打包、DolphinScheduler 调度、Savepoint 平滑启停全链路,可直接落地复用。
1. 整体架构及数据流
1.1 架构总览
#mermaid-svg-0RhVdYYewleDQRJs{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-0RhVdYYewleDQRJs .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0RhVdYYewleDQRJs .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0RhVdYYewleDQRJs .error-icon{fill:#552222;}#mermaid-svg-0RhVdYYewleDQRJs .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0RhVdYYewleDQRJs .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0RhVdYYewleDQRJs .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0RhVdYYewleDQRJs .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0RhVdYYewleDQRJs .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0RhVdYYewleDQRJs .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0RhVdYYewleDQRJs .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0RhVdYYewleDQRJs .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0RhVdYYewleDQRJs .marker.cross{stroke:#333333;}#mermaid-svg-0RhVdYYewleDQRJs svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0RhVdYYewleDQRJs p{margin:0;}#mermaid-svg-0RhVdYYewleDQRJs .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-0RhVdYYewleDQRJs .cluster-label text{fill:#333;}#mermaid-svg-0RhVdYYewleDQRJs .cluster-label span{color:#333;}#mermaid-svg-0RhVdYYewleDQRJs .cluster-label span p{background-color:transparent;}#mermaid-svg-0RhVdYYewleDQRJs .label text,#mermaid-svg-0RhVdYYewleDQRJs span{fill:#333;color:#333;}#mermaid-svg-0RhVdYYewleDQRJs .node rect,#mermaid-svg-0RhVdYYewleDQRJs .node circle,#mermaid-svg-0RhVdYYewleDQRJs .node ellipse,#mermaid-svg-0RhVdYYewleDQRJs .node polygon,#mermaid-svg-0RhVdYYewleDQRJs .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-0RhVdYYewleDQRJs .rough-node .label text,#mermaid-svg-0RhVdYYewleDQRJs .node .label text,#mermaid-svg-0RhVdYYewleDQRJs .image-shape .label,#mermaid-svg-0RhVdYYewleDQRJs .icon-shape .label{text-anchor:middle;}#mermaid-svg-0RhVdYYewleDQRJs .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-0RhVdYYewleDQRJs .rough-node .label,#mermaid-svg-0RhVdYYewleDQRJs .node .label,#mermaid-svg-0RhVdYYewleDQRJs .image-shape .label,#mermaid-svg-0RhVdYYewleDQRJs .icon-shape .label{text-align:center;}#mermaid-svg-0RhVdYYewleDQRJs .node.clickable{cursor:pointer;}#mermaid-svg-0RhVdYYewleDQRJs .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-0RhVdYYewleDQRJs .arrowheadPath{fill:#333333;}#mermaid-svg-0RhVdYYewleDQRJs .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-0RhVdYYewleDQRJs .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-0RhVdYYewleDQRJs .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0RhVdYYewleDQRJs .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-0RhVdYYewleDQRJs .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0RhVdYYewleDQRJs .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-0RhVdYYewleDQRJs .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-0RhVdYYewleDQRJs .cluster text{fill:#333;}#mermaid-svg-0RhVdYYewleDQRJs .cluster span{color:#333;}#mermaid-svg-0RhVdYYewleDQRJs div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-0RhVdYYewleDQRJs .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-0RhVdYYewleDQRJs rect.text{fill:none;stroke-width:0;}#mermaid-svg-0RhVdYYewleDQRJs .icon-shape,#mermaid-svg-0RhVdYYewleDQRJs .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0RhVdYYewleDQRJs .icon-shape p,#mermaid-svg-0RhVdYYewleDQRJs .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-0RhVdYYewleDQRJs .icon-shape .label rect,#mermaid-svg-0RhVdYYewleDQRJs .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0RhVdYYewleDQRJs .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-0RhVdYYewleDQRJs .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-0RhVdYYewleDQRJs :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 下游消费
数据湖层 (Paimon 1.4.2)
实时计算层 (Flink 1.20.3)
接入层
边端 / 车端
元数据
状态快照
TBox 终端
(GB32960 协议)
TSP 网关
解码/鉴权/路由
Kafka
TOPIC_EVGB_TCU
Kafka Source Table
(TEMPORARY TABLE)
INSERT INTO Paimon
(Table API / SQL)
Checkpoint
HDFS
Hive Metastore
Catalog
HDFS
warehouse
Doris / Spark / Hive
Ad-hoc & 数仓分层
1.2 数据流说明
| 阶段 | 组件 | 说明 |
|---|---|---|
| 上报 | TBox | 按 GB32960 封装报文(含车架号、命令标识、采集时间、报文体 hex 等) |
| 接入 | TSP 网关 | 解析链路层、鉴权、按协议类型路由到 Kafka topic |
| 缓冲 | Kafka | topic = TOPIC_EVGB_TCU,JSON 格式,作为实时管道缓冲与解耦 |
| 计算 | Flink | Table API 创建 Kafka 临时源表 → INSERT INTO Paimon 目标表 |
| 落湖 | Paimon | Hive Metastore 管理 catalog,数据文件写 HDFS(Parquet + ZSTD) |
| 容错 | Checkpoint | EXACTLY_ONCE,快照存储到 HDFS,作业故障可恢复 |
| 消费 | 下游 | Doris / Spark / Hive 通过 Paimon catalog 直接查询 ODS |
1.3 关键设计取舍
- 贴源 Original 表先落湖、再解析 :原始报文
MSG_DATA不在 Flink 端展开,避免解析逻辑耦合到接入链路;解析为宽表(json 表)由独立作业/批处理完成,符合 ODS 贴源 + ODS-JSON 分层。 - Paimon Hive Catalog:复用 HMS 元数据,与现有 Hive / Spark / Doris 生态互通,无需额外维护元存储。
- Table API + SQL 写入:源到湖是纯字段映射 + 分区计算,用 SQL 比DataStream 更简洁、可维护。
- 按
dt采集日期分区 +vin_code分桶:兼顾时间维度切片与车辆维度打散,避免单分桶热点。
2. 环境及版本依赖
2.1 软件版本矩阵
| 组件 | 版本 | 备注 |
|---|---|---|
| Flink | 1.20.3 | 流计算引擎 |
| Paimon | 1.4.2 | paimon-flink-1.20,数据湖 |
| Hadoop | 3.3.6 | HDFS + HA |
| Hive | 3.1.3 | Metastore(HMS) |
| Kafka | 3.4.0 | 消息队列 |
| Java (JDK) | 17 | Flink 1.20 要求 JDK 17 |
| Maven | 3.8+ | 构建工具 |
| DolphinScheduler | 3.x | 调度 |
| OS | CentOS 7 / 麒麟等 | 集群节点 |
2.2 Flink 与 Paimon 依赖对照
Paimon 的 Flink connector 与 Flink 版本强绑定,必须使用 paimon-flink-1.20(对应 Flink 1.20.x)。版本不匹配会直接抛 NoSuchMethodError / IncompatibleClassChangeError。
2.3 关键依赖(pom 片段)
xml
<properties>
<flink.version>1.20.3</flink.version>
<paimon.version>1.4.2</paimon.version>
<hadoop.version>3.3.6</hadoop.version>
<kafka.version>3.4.0</kafka.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<!-- Flink 核心(编译期 provided 由集群提供,打包需按需调整) -->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-streaming-java</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-table-api-java-bridge</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-table-planner-loader</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-table-runtime</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-clients</artifactId>
<version>${flink.version}</version>
</dependency>
<!-- Kafka connector -->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-kafka</artifactId>
<version>3.4.0-1.20</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-json</artifactId>
<version>${flink.version}</version>
</dependency>
<!-- Paimon -->
<dependency>
<groupId>org.apache.paimon</groupId>
<artifactId>paimon-flink-1.20</artifactId>
<version>${paimon.version}</version>
</dependency>
<!-- Hive 元数据支持(Paimon metastore=hive) -->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-sql-connector-hive-3.1.3_2.12</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-hive_2.12</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>org.apache.hive</groupId>
<artifactId>hive-exec</artifactId>
<version>3.1.3</version>
</dependency>
<!-- Hadoop 客户端 -->
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
<version>${hadoop.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
提示:
flink-streaming-java、flink-clients等是否打 provided,取决于你的部署模式(Session vs Per-Job/YARN-K8s)。下文打包方案采用 shade fat-jar,运行环境为 YARN Session + lib 目录分离,故核心 Flink 依赖不打 provided。
3. 数据源
3.1 Kafka Topic 与消息格式
| 项 | 值 |
|---|---|
| Topic | TOPIC_EVGB_TCU |
| 格式 | JSON |
| 消费组 | realtime_gb_datareporting_2_paimon |
| 启动位点 | earliest-offset(首次拉全量,后续按 group 提交位点续读) |
3.2 报文样例(JSON)
json
{
"MSG_ID": "1700000000000001",
"VIN_CODE": "LGWEV4A47NF000001",
"CMD_FLAG": "2",
"RESP_FLAG": "0xFE",
"COLLECT_TIME": "1713312000",
"MSG_DATA": "2322322F5730303030303030303030303030...",
"ENCRYPT_FLAG": "0",
"MSG_DIRECT": "1",
"UPLOAD_TIME": "1713312005"
}
字段含义:
| 字段 | 类型 | 含义 |
|---|---|---|
MSG_ID |
STRING | 报文唯一 ID(网关生成) |
VIN_CODE |
STRING | 车架号,作为分桶键 |
CMD_FLAG |
STRING | 命令标识(车辆登录/实时/补报等) |
RESP_FLAG |
STRING | 应答标志 |
COLLECT_TIME |
STRING | 采集时间,秒级 10 位时间戳字符串 |
MSG_DATA |
STRING | 报文体(GB32960 数据单元 hex) |
ENCRYPT_FLAG |
STRING | 加密标识 |
MSG_DIRECT |
STRING | 报文方向(0 上行 / 1 下行) |
UPLOAD_TIME |
STRING | 上报时间,秒级时间戳字符串 |
注意:
COLLECT_TIME为秒级时间戳。若你的网关下发的是 13 位毫秒时间戳,需在 SQL 中做/1000处理,否则FROM_UNIXTIME会得到错误日期。
4. Paimon 数据模型设计
4.1 表设计要点
- 表名 :
ods_vehicle_gb32960_msg_report_original(贴源原始表,保留未展开报文) - 分区 :
dt(按采集日期yyyy-MM-dd),便于按天切片与生命周期管理 - 分桶 :32 桶,桶键
vin_code,打散车辆维度,避免单桶写入热点 - 主键 :
(msg_id, dt, vin_code),保证同一报文幂等去重 - 文件格式:Parquet + ZSTD(level=3),兼顾压缩比与查询性能
- 合并:本表为 append-only 原始落地,使用主键仅用于幂等去重
4.2 建表 DDL
sql
-- 客户端切到 hdfs 用户
-- su - hdfs
-- 进入 Flink SQL CLI 或 Hive/Spark 客户端执行
CREATE CATALOG `paimon` WITH (
'type' = 'paimon',
'metastore' = 'hive',
'uri' = 'thrift://xxx:9083,thrift://xxx:9083',
'warehouse' = 'hdfs:///warehouse/tablespace/managed/hive'
);
USE CATALOG `paimon`;
CREATE DATABASE IF NOT EXISTS `tsp_ods`;
USE `tsp_ods`;
CREATE TABLE IF NOT EXISTS ods_vehicle_gb32960_msg_report_original (
msg_id STRING,
vin_code STRING,
cmd_flag STRING,
resp_flag STRING,
collect_time STRING,
msg_data STRING,
encrypt_flag STRING,
msg_direct STRING,
upload_time STRING,
dt STRING COMMENT '分区字段,格式 yyyy-MM-dd'
) PARTITIONED BY (dt)
WITH (
'bucket' = '32',
'bucket-key' = 'vin_code',
'primary-key' = 'msg_id,dt,vin_code',
'file.format' = 'parquet',
'file.compression' = 'zstd',
'file.compression.zstd-level' = '3',
'write-buffer-size' = '256mb',
'compaction.enabled' = 'true',
'compaction.max.file-size' = '128 mb',
'write.max-buffer-size' = '256 mb'
);
4.3 Doris / Hive 查询 Paimon
Doris 侧建 catalog 查询(HMS 模式 + HDFS HA 配置):
sql
CREATE CATALOG `paimon` PROPERTIES (
"type" = "paimon",
"paimon.catalog.type" = "hms",
"hive.metastore.uris" = "thrift://xxx:9083,thrift://xxx:9083",
"warehouse" = "hdfs:///warehouse/tablespace/managed/hive",
"dfs.nameservices" = "hdfs-ha",
"dfs.ha.namenodes.hdfs-ha" = "nn1,nn2",
"dfs.namenode.rpc-address.hdfs-ha.nn1" = "xxx:8020",
"dfs.namenode.rpc-address.hdfs-ha.nn2" = "xxx:8020",
"dfs.client.failover.proxy.provider.hdfs-ha" = "org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider"
);
-- 查询
SELECT * FROM paimon.tsp_ods.ods_vehicle_gb32960_msg_report_original LIMIT 1;
5. Flink 完整代码实现
5.1 入口类 RealTimeGBDataReportingOriginal2Paimon
java
import org.apache.flink.api.java.utils.ParameterTool;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.CheckpointingMode;
import org.apache.flink.streaming.api.environment.CheckpointConfig;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.EnvironmentSettings;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import java.io.InputStream;
import java.util.Properties;
public class RealTimeGBDataReportingOriginal2Paimon {
public static void main(String[] args) throws Exception {
System.setProperty("HADOOP_USER_NAME", "hdfs");
// 1. 创建流式执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
EnvironmentSettings settings = EnvironmentSettings.newInstance().inStreamingMode().build();
StreamTableEnvironment tEnv = StreamTableEnvironment.create(env, settings);
// 加载配置
Properties config = loadConfig(args);
// 2. 开启 Checkpoint
setCheckpoint(env, config);
// 3. 创建 Paimon Hive Catalog
String catalog = config.getProperty("paimon.catalog", "paimon");
String warehouse = config.getProperty("paimon.warehouse", "hdfs:///warehouse/tablespace/managed/hive");
tEnv.executeSql(
"CREATE CATALOG " + catalog + " WITH (" +
" 'type' = 'paimon'," +
" 'metastore' = 'hive'," +
" 'warehouse' = '" + warehouse + "'" +
")");
tEnv.executeSql("USE CATALOG `" + catalog + "`");
tEnv.executeSql("USE `" + config.getProperty("gb.paimon.database", "tsp_ods") + "`");
// 4. 创建 Kafka 源表(临时表)
String kafkaSourceDDL = ""
+ "CREATE TEMPORARY TABLE kafka_vehicle_source (\n"
+ " MSG_ID STRING,\n"
+ " VIN_CODE STRING,\n"
+ " CMD_FLAG STRING,\n"
+ " RESP_FLAG STRING,\n"
+ " COLLECT_TIME STRING,\n"
+ " MSG_DATA STRING,\n"
+ " ENCRYPT_FLAG STRING,\n"
+ " MSG_DIRECT STRING,\n"
+ " UPLOAD_TIME STRING,\n"
+ " `proctime` AS PROCTIME()\n"
+ ") WITH (\n"
+ " 'connector' = 'kafka',\n"
+ " 'topic' = '" + config.getProperty("kafka.topic") + "',\n"
+ " 'properties.bootstrap.servers' = '" + config.getProperty("kafka.bootstrap.servers") + "',\n"
+ " 'properties.group.id' = '" + config.getProperty("kafka.group.id", "realtime_gb_datareporting_2_paimon") + "',\n"
+ " 'scan.startup.mode' = '" + config.getProperty("gb.scan.startup.mode", "group-offsets") + "',\n"
+ " 'format' = 'json',\n"
+ " 'json.fail-on-missing-field' = 'false',\n"
+ " 'json.ignore-parse-errors' = 'true'\n"
+ ")";
tEnv.executeSql(kafkaSourceDDL);
// 5. 写入 Paimon 表
String insertSql = ""
+ "INSERT INTO " + config.getProperty("paimon.table.gb", "ods_vehicle_gb32960_msg_report_original") + "\n"
+ "SELECT\n"
+ " MSG_ID AS msg_id,\n"
+ " VIN_CODE AS vin_code,\n"
+ " CMD_FLAG AS cmd_flag,\n"
+ " RESP_FLAG AS resp_flag,\n"
+ " COLLECT_TIME AS collect_time,\n"
+ " MSG_DATA AS msg_data,\n"
+ " ENCRYPT_FLAG AS encrypt_flag,\n"
+ " MSG_DIRECT AS msg_direct,\n"
+ " UPLOAD_TIME AS upload_time,\n"
+ " DATE_FORMAT(FROM_UNIXTIME(CAST(COLLECT_TIME AS BIGINT)), 'yyyy-MM-dd') AS dt\n"
+ "FROM kafka_vehicle_source\n"
+ "WHERE COLLECT_TIME IS NOT NULL\n"
+ " AND CAST(COLLECT_TIME AS BIGINT) > 0";
tEnv.executeSql(insertSql);
}
/** 封装 Checkpoint 配置 */
private static void setCheckpoint(StreamExecutionEnvironment env, Properties config) {
env.enableCheckpointing(Long.parseLong(config.getProperty("checkpoint.interval.ms", "60000")));
env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);
env.getCheckpointConfig().setMinPauseBetweenCheckpoints(Long.parseLong(config.getProperty("checkpoint.min.pause.ms", "30000")));
env.getCheckpointConfig().setCheckpointTimeout(Long.parseLong(config.getProperty("checkpoint.timeout.ms", "600000")));
env.getCheckpointConfig().setMaxConcurrentCheckpoints(Integer.parseInt(config.getProperty("checkpoint.max.concurrent", "1")));
env.getCheckpointConfig().setTolerableCheckpointFailureNumber(Integer.parseInt(config.getProperty("checkpoint.tolerable.failed.number", "2")));
env.getCheckpointConfig().setExternalizedCheckpointCleanup(
CheckpointConfig.ExternalizedCheckpointCleanup.DELETE_ON_CANCELLATION);
env.getCheckpointConfig().setCheckpointStorage(
config.getProperty("checkpoint.dir", "hdfs:///flink/checkpoints") + "/gb/paimon");
Configuration conf = new Configuration();
conf.setString("state.savepoints.dir", config.getProperty("savepoint.dir", "hdfs:///flink/savepoints"));
env.configure(conf);
}
/** 加载配置文件 */
private static Properties loadConfig(String[] args) throws Exception {
ParameterTool params = ParameterTool.fromArgs(args);
params = params.mergeWith(ParameterTool.fromSystemProperties());
Properties props = new Properties();
String env = params.get("env", "prod");
String fileName = "/config-" + env + ".properties";
System.out.println(fileName);
try (InputStream in = RealTimeQBDataReporting2Doris.class.getResourceAsStream(fileName)) {
if (in == null) {
throw new RuntimeException(fileName + " not found in classpath");
}
props.load(in);
}
// 可选:通过 JVM 参数 -Dconfig.file 覆盖
String externalConfig = System.getProperty("config.file");
if (externalConfig != null) {
try (java.io.FileInputStream fis = new java.io.FileInputStream(externalConfig)) {
props.load(fis);
}
}
return props;
}
}
5.2 代码要点解读
- 环境与 Table API :
StreamExecutionEnvironment+StreamTableEnvironment,桥接 DataStream 与 SQL。 - Catalog 切换 :
metastore = hive复用 HMS;warehouse指向 HDFS 仓库根。 - Kafka 临时表 :
CREATE TEMPORARY TABLE,不持久化,仅当前作业可见;json.ignore-parse-errors=true保证脏行不中断作业。 - 字段映射 + 分区计算 :
DATE_FORMAT(FROM_UNIXTIME(CAST(COLLECT_TIME AS BIGINT)), 'yyyy-MM-dd')由秒级时间戳算出dt。 - 过滤保护 :
WHERE COLLECT_TIME IS NOT NULL AND ... > 0防止脏数据产生空分区。 - 配置外置 :
--env prod切换config-prod.properties,三套环境一套代码。
5.3 优化建议
- group.id 外置 :原代码硬编码
realtimegbdatareporting2paimon1,已调整为从config读取,避免多环境共用 group 导致位点错乱。 - scan.startup.mode :生产建议
group-offsets(按消费组续读),首次冷启可用earliest-offset,但务必在数据追平后切回,避免每次重启重拉全量。 - 毫秒时间戳兼容 :若
COLLECT_TIME为 13 位毫秒值,dt计算改为CAST(COLLECT_TIME AS BIGINT)/1000。 - 作业命名 :建议增加
env.configure(conf); conf.setString("pipeline.name", config.getProperty("gb.paimon.job.name")),便于 WebUI 识别。
6. 配置文件体系(dev / pre / prod)
6.1 配置加载机制
classpath:config-{env}.properties -- 主配置(随 jar 打包)
-Dconfig.file=/path/to/xxx.properties -- 外部覆盖(生产推荐)
通过启动参数 --env prod 选定 config-prod.properties。
6.2 config-dev.properties(开发)
properties
# Kafka
kafka.bootstrap.servers=xxx:9092
kafka.topic=TOPIC_EVGB_TCU
kafka.group.id=realtime_gb_datareporting_2
gb.scan.startup.mode=earliest-offset
# Checkpoint
checkpoint.interval.ms=300000
checkpoint.min.pause.ms=30000
checkpoint.timeout.ms=600000
checkpoint.max.concurrent=1
checkpoint.tolerable.failed.number=2
checkpoint.dir=hdfs:///flink/checkpoints
savepoint.dir=hdfs:///flink/savepoints
# Paimon
paimon.warehouse=hdfs:///warehouse/tablespace/managed/hive
paimon.catalog=paimon
paimon.database=tsp_ods
gb.paimon.database=rtm_ods
paimon.table.gb=ods_vehicle_gb32960_msg_report_original
gb.paimon.job.name=paimon.rtm_ods.ods_vehicle_gb32960_msg_report_original
6.3 config-pre.properties(预发)
properties
# Kafka
kafka.bootstrap.servers=xxx:9092
kafka.topic=TOPIC_EVGB_TCU
kafka.group.id=realtime_gb_datareporting_2
gb.scan.startup.mode=group-offsets
# Checkpoint
checkpoint.interval.ms=300000
checkpoint.min.pause.ms=30000
checkpoint.timeout.ms=600000
checkpoint.max.concurrent=1
checkpoint.tolerable.failed.number=2
checkpoint.dir=hdfs:///flink/checkpoints
savepoint.dir=hdfs:///flink/savepoints
# Paimon
paimon.warehouse=hdfs:///warehouse/tablespace/managed/hive
paimon.catalog=paimon
paimon.database=pre_tsp_ods
gb.paimon.database=pre_rtm_ods
paimon.table.gb=ods_vehicle_gb32960_msg_report_original
gb.paimon.job.name=paimon.pre_rtm_ods.ods_vehicle_gb32960_msg_report_original
6.4 config-prod.properties(生产)
properties
# Kafka
kafka.bootstrap.servers=xxx:9092
kafka.topic=TOPIC_EVGB_TCU
kafka.group.id=realtime_gb_datareporting_2
gb.scan.startup.mode=group-offsets
# Checkpoint
checkpoint.interval.ms=120000
checkpoint.min.pause.ms=30000
checkpoint.timeout.ms=600000
checkpoint.max.concurrent=1
checkpoint.tolerable.failed.number=2
checkpoint.unaligned.enable=true
checkpoint.aligned.timeout.seconds=30
checkpoint.dir=hdfs:///flink/checkpoints
savepoint.dir=hdfs:///flink/savepoints
# Paimon
paimon.warehouse=hdfs:///warehouse/tablespace/managed/hive
paimon.catalog=paimon
paimon.database=tsp_ods
gb.paimon.database=rtm_ods
paimon.table.gb=ods_vehicle_gb32960_msg_report_original
gb.paimon.job.name=paimon.tsp_ods.ods_vehicle_gb32960_msg_report_original
6.5 三环境差异速览
| 配置项 | dev | pre | prod |
|---|---|---|---|
| Kafka broker | xxx:9092 | xxx:9092 | xxx:9092 |
| 数据库前缀 | rtm_ods |
pre_rtm_ods |
rtm_ods |
| Checkpoint 间隔 | 300000ms | 300000ms | 120000ms |
| 启动位点 | earliest-offset | group-offsets | group-offsets |
| Unaligned | false | false | true |
7. Checkpoint 与重启策略
7.1 Checkpoint 配置解读
java
env.enableCheckpointing(120000); // 间隔 2min
env.getCheckpointConfig().setCheckpointingMode(EXACTLY_ONCE); // 精确一次
env.getCheckpointConfig().setMinPauseBetweenCheckpoints(30000); // 最小间隔 30s
env.getCheckpointConfig().setCheckpointTimeout(600000); // 超时 10min
env.getCheckpointConfig().setMaxConcurrentCheckpoints(1); // 并发 1
env.getCheckpointConfig().setTolerableCheckpointFailureNumber(2);// 容忍 2 次失败
env.getCheckpointConfig().setExternalizedCheckpointCleanup(
DELETE_ON_CANCELLATION); // 取消时删除(保留 savepoint)
env.getCheckpointConfig().setCheckpointStorage(
"hdfs:///flink/checkpoints/gb/paimon"); // 快照存储
| 参数 | 推荐值 | 说明 |
|---|---|---|
checkpoint.interval.ms |
生产 120000 / 测试 300000 | 越短恢复越快但压力越大 |
checkpoint.timeout.ms |
600000 | 状态较大时适当放大 |
checkpoint.tolerable.failed.number |
2 | 连续 2 次失败仍不停止,避免抖动误杀 |
checkpoint.unaligned.enable |
true(prod) | 反压场景下降低对齐延迟 |
7.2 重启策略(推荐补充)
Flink 1.20 默认无重启策略(仅 Checkpoint 失败不等于作业失败)。生产建议显式配置:
java
env.setRestartStrategy(
RestartStrategies.fixedDelayRestart(
3, // 最大重启次数
Time.minutes(1), // 间隔
Time.minutes(5) // 可选:上限
));
或在 flink-conf.yaml 全局配置:
yaml
restart-strategy: fixed-delay
restart-strategy.fixed-delay.attempts: 3
restart-strategy.fixed-delay.delay: 1min
原代码未显式设置重启策略,建议补充以应对 OOM / 短暂网络抖动。
8. Maven 项目打包
8.1 模块结构
auto-vehicle-realtime-app/ (parent pom)
└── auto-vehicle-flink-ods/ (本作业所在模块)
├── pom.xml
└── src/main/
├── java/com/xxx/car/realtime/gb/app/
│ └── RealTimeGBDataReportingOriginal2Paimon.java
└── resources/
├── config-dev.properties
├── config-pre.properties
└── config-prod.properties
8.2 父 POM 版本管理(dependencyManagement)
xml
<properties>
<flink.version>1.20.3</flink.version>
<paimon.version>1.4.2</paimon.version>
<hadoop.version>3.3.6</hadoop.version>
<kafka.version>3.4.0</kafka.version>
</properties>
8.3 子模块 pom.xml(打包关键)
xml
<build>
<plugins>
<!-- 编译:JDK17 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>17</source>
<target>17</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<!-- Shade:fat-jar,合并 META-INF/services -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.0</version>
<executions>
<execution>
<phase>package</phase>
<goals><goal>shade</goal></goals>
<configuration>
<finalName>${project.artifactId}-${project.version}</finalName>
<createDependencyReducedPom>false</createDependencyReducedPom>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>xxx.RealTimeGBDataReportingOriginal2Paimon</mainClass>
</transformer>
<!-- 合并 SPI 服务文件,避免 Flink/Paimon connector 加载失败 -->
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
8.4 打包命令
bash
# 在项目根目录执行,仅打 ods 模块(含 common 依赖)
mvn -pl auto-vehicle-flink-ods -am clean package -DskipTests
# 产物
ls auto-vehicle-flink-ods/target/
# auto-vehicle-flink-ods-1.0.0.jar (fat-jar,含依赖)
8.5 打包注意事项
- 排除签名文件 :
META-INF/*.SF|DSA|RSA必须过滤,否则SecurityException。 - ServicesResourceTransformer :Flink / Paimon 依赖 SPI 机制加载 connector,必须合并
META-INF/services。 - Hadoop 客户端 provided :集群已提供 Hadoop,打入 jar 体积大且易冲突,建议
provided。 - log4j slf4j 实现:若集群已有 log4j2 配置,建议 provided;否则打进去避免日志格式错乱。
9. DolphinScheduler 部署调度
9.1 部署模式选择
本作业推荐 YARN Session 模式:Flink Session 集群常驻,作业以 flink run 提交;适合多作业共享集群、启停快。
Per-Job / Application 模式也可,但 Savepoint 生命周期管理更复杂,本文以 Session 为例。
9.2 DolphinScheduler Shell 任务脚本
在 DS 工作流中创建 Shell 节点:
bash
#!/bin/bash
set -euo pipefail
# 环境变量
FLINK_HOME=/opt/flink
JAR_PATH=/data/soft/flink-jobs/auto-vehicle-flink-ods-1.0.0.jar
MAIN_CLASS=xxx.RealTimeGBDataReportingOriginal2Paimon
ENV=prod
PARALLELISM=9
# Savepoint 目录(升级重启时使用)
SAVEPOINT_DIR=hdfs:///flink/savepoints
# 检查是否已有同名作业运行,避免重复提交
JOB_NAME="paimon.tsp_ods.ods_vehicle_gb32960_msg_report_original"
RUNNING=$(yarn application -list 2>/dev/null | grep -c "$JOB_NAME" || true)
if [ "$RUNNING" -gt 0 ]; then
echo "Job already running, skip submit."
exit 0
fi
# 提交作业(默认不指定 savepoint,首启场景)
$FLINK_HOME/bin/flink run \
-d \
-p ${PARALLELISM} \
-c ${MAIN_CLASS} \
-D pipeline.name="${JOB_NAME}" \
-D yarn.application.name="${JOB_NAME}" \
${JAR_PATH} \
--env ${ENV}
9.3 DS 调度编排建议
| 工作流 | 触发 | 说明 |
|---|---|---|
ods-gb-paimon-startup |
手动 / 故障恢复 | 冷启动作业,一次性 |
ods-gb-paimon-upgrade |
发版时手动 | 停止→savepoint→拉起新版本(见第 10 节) |
ods-gb-paimon-monitor |
定时 5min | 检查作业存活,未运行则告警(钉钉/飞书) |
作业本身是常驻流任务,DS 主要承担「提交 / 升级 / 监控」职责,而非定时 ETL 触发。
10. Savepoint 平滑启停作业
10.1 为什么用 Savepoint 而非 Checkpoint
- Checkpoint :作业内部周期性快照,
DELETE_ON_CANCELLATION模式下取消即删除,不适合版本升级。 - Savepoint :手动触发的全量一致快照,作业停止后保留,是平滑升级 / 迁移的标准手段。
10.2 平滑停止(带 Savepoint)
bash
# 1. 查询 JobID(Flink WebUI 或 REST)
JOB_ID=$($FLINK_HOME/bin/flink list -r | grep "ods_vehicle_gb32960_msg_report_original" | awk '{print $4}')
# 2. 停止并生成 savepoint(--savepointPath 指定目录)
$FLINK_HOME/bin/flink stop \
--savepointPath hdfs:///flink/savepoints \
${JOB_ID}
# 输出示例:
# Suspending job "xxx" with a savepoint.
# Savepoint stored in hdfs:///flink/savepoints/savepoint-xxxx-abc
10.3 从 Savepoint 重启(升级 / 迁移)
bash
SP=hdfs:///flink/savepoints/savepoint-xxxx-abc
$FLINK_HOME/bin/flink run \
-d \
-s ${SP} \
-p 9 \
-c xxx.RealTimeGBDataReportingOriginal2Paimon \
-D pipeline.name="paimon.tsp_ods.ods_vehicle_gb32960_msg_report_original" \
/data/soft/flink-jobs/auto-vehicle-flink-ods-1.0.0.jar \
--env prod
-s指定 savepoint 路径,作业将从该位点恢复 Kafka offset 与 Paimon 写入状态,不丢不重。
10.4 DS 升级工作流脚本(一键升级)
bash
#!/bin/bash
set -euo pipefail
FLINK_HOME=/opt/flink
JAR_PATH=/data/soft/flink-jobs/auto-vehicle-flink-ods-1.0.0.jar
MAIN_CLASS=xxx.RealTimeGBDataReportingOriginal2Paimon
JOB_NAME="paimon.tsp_ods.ods_vehicle_gb32960_msg_report_original"
SAVEPOINT_DIR=hdfs:///flink/savepoints
# 1. 获取 JobID
JOB_ID=$($FLINK_HOME/bin/flink list -r | grep "ods_vehicle_gb32960_msg_report_original" | awk '{print $4}')
[ -z "$JOB_ID" ] && { echo "Job not running, nothing to stop."; exit 0; }
# 2. 停止 + savepoint
SP=$($FLINK_HOME/bin/flink stop --savepointPath ${SAVEPOINT_DIR} ${JOB_ID} | grep -oE 'hdfs:///flink/savepoints/savepoint-[a-z0-9-]+')
echo "Savepoint: $SP"
# 3. 等待作业完全停止
sleep 10
# 4. 从 savepoint 拉起新版本
$FLINK_HOME/bin/flink run \
-d -s ${SP} -p 9 \
-c ${MAIN_CLASS} \
-D pipeline.name="${JOB_NAME}" \
${JAR_PATH} --env prod
10.5 注意事项
- Schema 兼容:升级若改了 Paimon 表主键 / 分桶键,Savepoint 可能无法恢复,需重建表并做数据迁移。
- Savepoint 清理:升级成功并观察一段时间后,及时清理旧 savepoint,释放 HDFS 空间。
- 取消语义 :日常停止用
flink stop(带 savepoint),不要用flink cancel(默认无 savepoint,会丢状态)。 - Checkpoint 与 Savepoint 协同:Checkpoint 负责运行期容错,Savepoint 负责计划内启停,两者不可互相替代。
附:常见问题排查
| 现象 | 可能原因 | 解决 |
|---|---|---|
提交报 IncompatibleClassChangeError |
Flink / Paimon 版本不匹配 | 确认 paimon-flink-1.20 + Flink 1.20.x |
dt 全为 NULL |
COLLECT_TIME 为毫秒时间戳 |
SQL 改为 /1000 |
| 作业反复重启 OOM | Kafka 并发过高 / 状态过大 | 降低并行度、调大 TM 内存 |
| Paimon 写入慢 | 分桶过多 / 小文件未合并 | 调 bucket、开启 compaction |
| Doris 查 Paimon 报 HA 错 | Doris catalog 缺 HDFS HA 配置 | 补全 dfs.nameservices 等属性 |
| 重启后数据重复 | 未从 savepoint 恢复 / 表无主键 | 用 -s 恢复 + 主键去重 |
总结
本文以车联网国标 GB32960 报文入湖为切入点,完整覆盖了从 Kafka 消费、Flink Table API / SQL 写入 Paimon、三环境配置、Checkpoint 容错、Maven 打包、DolphinScheduler 调度到 Savepoint 平滑升级的全链路。核心要点:
- 分层落地:Original 贴源表先入湖,解析逻辑解耦,链路稳定易维护。
- 配置三套环境一套代码 :
--env切换,CI/CD 友好。 - Checkpoint + Savepoint 双保险:运行期容错 + 计划内启停,实现不丢不重。
- DS 调度 + Shell 脚本:常驻流任务的提交、升级、监控标准化。
按本文落地后,可在此基础上扩展 ODS-JSON 解析宽表、DWD 行程明细等下游作业,逐步构建车联网实时数仓。