Flink1.20.3 实时消费 Kafka 数据并解析入湖 Paimon1.4.2 全流程实战

场景:车联网 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 / 麒麟等 集群节点

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.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 代码要点解读

  1. 环境与 Table API :StreamExecutionEnvironment + StreamTableEnvironment,桥接 DataStream 与 SQL。
  2. Catalog 切换 :metastore = hive 复用 HMS;warehouse 指向 HDFS 仓库根。
  3. Kafka 临时表 :CREATE TEMPORARY TABLE,不持久化,仅当前作业可见;json.ignore-parse-errors=true 保证脏行不中断作业。
  4. 字段映射 + 分区计算 :DATE_FORMAT(FROM_UNIXTIME(CAST(COLLECT_TIME AS BIGINT)), 'yyyy-MM-dd') 由秒级时间戳算出 dt。
  5. 过滤保护 :WHERE COLLECT_TIME IS NOT NULL AND ... > 0 防止脏数据产生空分区。
  6. 配置外置 :--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 注意事项

  1. Schema 兼容:升级若改了 Paimon 表主键 / 分桶键,Savepoint 可能无法恢复,需重建表并做数据迁移。
  2. Savepoint 清理:升级成功并观察一段时间后,及时清理旧 savepoint,释放 HDFS 空间。
  3. 取消语义 :日常停止用 flink stop(带 savepoint),不要用 flink cancel(默认无 savepoint,会丢状态)。
  4. 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 平滑升级的全链路。核心要点:

  1. 分层落地:Original 贴源表先入湖,解析逻辑解耦,链路稳定易维护。
  2. 配置三套环境一套代码 :--env 切换,CI/CD 友好。
  3. Checkpoint + Savepoint 双保险:运行期容错 + 计划内启停,实现不丢不重。
  4. DS 调度 + Shell 脚本:常驻流任务的提交、升级、监控标准化。

按本文落地后,可在此基础上扩展 ODS-JSON 解析宽表、DWD 行程明细等下游作业,逐步构建车联网实时数仓。

相关推荐
宸津-代码粉碎机7 小时前
OpenAI 连夜迎战 Grok Bot 和 Muse:AI 智能体从 “会聊天” 到 “能办事”,现在入场还来得及吗
java·大数据·人工智能·分布式·python
程序猿乐锅9 小时前
【黑马点评 | 第八篇】Redisson分布式锁
java·数据库·spring boot·redis·分布式·spring·缓存
灯澜忆梦10 小时前
【RabbitMQ #7】 | 消息转换器
分布式·rabbitmq·ruby
需要82611 小时前
分布式事务:Seata AT/TCC/SAGA 的取舍与踩坑
java·spring boot·分布式·spring·spring cloud
CS创新实验室13 小时前
为什么三台机器就能选出一个“带头大哥“?——Raft 一致性算法,一次讲透
分布式·操作系统
本人手速666+15 小时前
企微开发API如何设计客户冻结状态?WeComApi 在删除、投诉和异常客户场景中的自动化边界
运维·分布式·自动化·企业微信·企微外部群开发·wecomapi·企业微信二次开发
TLA技术16 小时前
LogMiner vs 裸日志解析(六):想提速,只能“堆实例”,结果把源库拖垮
数据库·oracle·flink·dba·迁移学习
灯澜忆梦16 小时前
【RabbitMQ #4】 | Work 任务模型
分布式·rabbitmq
程序猿乐锅17 小时前
【黑马点评 | 第七篇】Redis 分布式锁的两种实现
java·数据库·redis·分布式·spring·缓存