欢迎光临
我们一直在努力

面向车联网实时链路的 ClickHouse 全栈教程——第 18 章 全链路整合:ClickHouse + Paimon + Flink + Doris 生产部署实战

第 18 章 全链路整合:ClickHouse + Paimon + Flink + Doris 生产部署实战

章节定位:第 1 至 17 章带你把 ClickHouse 单产品啃透了——但生产环境不会只有 ClickHouse 一个组件。一辆车的遥测数据从车载 T-Box 出来 → Kafka → Flink ODS 做清洗 → 一份写 Paimon 湖仓一份写 ClickHouse → ClickHouse 做多 shard 聚合 → Doris 读 ClickHouse 做实时看板。这一章把第 16、17 章的集群运维、复制、分布式查询和数据栈里的其他四个核心组件(Flink / Paimon / Kafka / Doris)整合起来,讲清每个组件的角色边界、数据流动方向、以及"让全链路跑起来"的工程细节。核心心智:ClickHouse 是查询和聚合层,Paimon 是原始数据层,Flink 是流处理层,Kafka 是解耦管道,Doris 是BI 看板层——五者各司其职,不互相取代,但边界模糊处(“为什么不直接 Doris 写 Kafka?”、“为什么 ClickHouse 不直接读 Iceberg 然后跳过 Paimon?”)就是你作为架构师要做的决策。


学习目标

读完本章,你应该能够:

  • 画出完整的车辆数据全链路架构图,标注每个组件的角色边界和数据流动方向
  • 说出 Flink → ClickHouse 三种写入方式(Flink Connector / JDBC Sink / Flink 写 Kafka → ClickHouse Kafka 引擎)的优缺点和适用场景
  • 用 Flink Connector 给 demo.vehicle_telemetry 写 Exactly-Once 语义的批量写入,说出关键参数(maxBatchSize / maxInFlightRequests / flushInterval)
  • 用 ClickHouse 的 PaimonS3 / IcebergS3 / HudiS3 / Hive 外表引擎读 Paimon 湖仓数据,知道它们都是只读外表
  • 给外表建本地 Materialized View 管道,把湖仓数据周期性物化到 ClickHouse 本地表做高效查询
  • 用 Doris 的 JDBC Catalog 连 ClickHouse,说出 CREATE CATALOG 语法 + ClickHouse JDBC 驱动配置
  • 说出 ClickHouse 读 Doris 的反向方式(ClickHouse 的 JDBC 引擎),以及什么时候选 Doris 写 ClickHouse、什么时候选 ClickHouse 写 Doris
  • 用 ClickHouse 的 Kafka 引擎(Kafka table + Materialized View 管道)做流式订阅,说出 Kafka 引擎的 commit 机制(kafka_commit_on_select)
  • 说清 ClickHouse Cloud 用户跳过哪些节(Kafka 引擎、冷热分层、自建 Keeper;用 ClickPipes 替代 Flink connector 直写)

18.1 全链路架构总览

18.1.1 五个组件的角色边界

我们的车辆数据全链路涉及五个核心组件,每个有明确的角色分工:

组件角色一句话定位不应该做什么
Kafka 解耦管道 原始遥测数据的临时缓冲 + 多消费组广播 不做持久化存储、不做聚合
Flink 流处理层 清洗(脏数据过滤 / 字段补全)、ETL 转换、窗口计算 不做最终存储、不做 OLAP 查询
Paimon 原始数据湖 历史原始数据的长期存储(支持 upsert、schema 演进、时间旅行) 不做高并发 OLAP 查询
ClickHouse 查询聚合层 高吞吐写入 + 分布式聚合 + 近实时 OLAP 查询 不做流处理、不做 BI 看板可视化
Doris BI 看板层 低延迟多维度 Drill-down 看板 + 数据集服务 不做流式写入、不做历史原始数据存储

18.1.2 完整数据流:从车载传感器到 BI 看板

车辆 T-Box(100 万辆,每秒 ~100 万条原始遥测)
│
│ Kafka producer(带 VIN 分区 + ack=all)
▼
┌──────────────────────────────┐
│ Kafka 集群 │
│ topic: vehicle_raw │
│ (replication-factor=3) │
└──────┬───────────┬───────────┘
│ │
│ │ consumer group "paimon-ods"
│ ▼
│ ┌────────────────────────────┐
│ │ Flink Job: ODS 清洗 │
│ │ 1. 脏数据过滤(speed<0等) │
│ │ 2. 字段补全(城市名称映射) │
│ │ 3. 格式统一(时间戳毫秒→秒) │
│ └──────┬───────────┬──────────┘
│ │ │
│ │ sink: Paimon S3 │ sink: ClickHouse Connector
│ ▼ ▼
│ ┌──────────────┐ ┌──────────────────────┐
│ │ Apache Paimon │ │ ClickHouse 多 shard │
│ │ S3 / HDFS │ │ 集群 │
│ │ │ │ ReplicatedMergeTree │
│ │ 原始历史数据 │ │ 近实时聚合层 │
│ │ (只读给下游) │ │ │
│ └──────┬───────┘ └──────┬───────────────┘
│ │ │
│ │ PaimonS3 外表 │ JDBC 引擎
│ ▼ ▼
│ ┌────────────────────────────────────────────┐
│ │ ClickHouse │
│ │ 外表 + Materialized View → 本地表 │
│ │ (湖仓数据物化 + 本地聚合) │
│ └──────────────────────┬─────────────────────┘
│ │
│ │ Doris JDBC Catalog
│ ▼
│ ┌──────────────────────────────────────────┐
│ │ Apache Doris │
│ │ CREATE CATALOG clickhouse_federated │
│ │ SELECT * FROM clickhouse_federated. │
│ │ demo.vehicle_telemetry WHERE … │
│ │ │
│ │ BI 看板(Superset / Grafana / 自研) │
│ └──────────────────────────────────────────┘
│
└─ 备选路径:Flink 直接写 ClickHouse Kafka 引擎
(Flink → Kafka topic vehicle_cleaned → ClickHouse
Kafka 表 + Materialized View 管道)

图 18-1 车辆数据全链路五组件架构

注意图里的两个核心数据流:

  • 实时路径(粗线):Kafka → Flink → ClickHouse 多 shard → Doris 看板 → 用户实时查询。延迟目标 < 30 秒。
  • 湖仓路径(虚线):Kafka → Flink → Paimon S3 → ClickHouse PaimonS3 外表 → 本地物化视图 → 长期趋势分析。这是历史数据回补 + 冷数据分析的路径,延迟目标 天级。

18.1.3 为什么不简化:每个组件都是必要的

简化想法为什么不行
不装 Kafka,Flink 直接消费 T-Box HTTP 100 万辆车每秒 100 万条 → 流量峰值 10x,Kafka 削峰填谷是必需的。没有 Kafka 做缓冲,Flink 扛不住突发流量
不装 Paimon,ClickHouse 直接存所有原始数据 ClickHouse MergeTree 虽然是列存压缩,但原始数据长期累积(10 年 × 1000 亿行 ≈ 100PB)会把本地盘压爆。Paimon 存 S3 冷数据成本是 NVMe 的 1/50,且支持 schema 演进
不装 Doris,BI 看板直接查 ClickHouse BI 看板的 query pattern 是"低延迟多维度 Drill-down"——ClickHouse 擅长 scan + aggregate,不擅长 OLAP server 的 cube 预计算 + 语义层。Superset 等 BI 工具对 Doris 有原生优化
不用 Flink Kafka Connector,Flink 直接写 ClickHouse + Paimon 完全可行(Flink 同时开两个 sink)。Kafka 作为中间管道是解耦层——Flink sink 挂了时 ClickHouse / Paimon 不受影响;Flink 重启也能从 Kafka offset 继续,Exactly-Once 更容易实现
不用 ClickHouse,Doris 同时做实时写入和历史分析 Doris 写 Kafka 的吞吐比 ClickHouse 低 3~5 倍;ClickHouse 的 projection / materialized view / 分布式 JOIN 更成熟。Doris 做它擅长的(BI 查询),ClickHouse 做它擅长的(高吞吐写入 + 分布式聚合)

18.1.4 ClickHouse 在全链路中的"枢纽"地位

把图 18-1 的 ClickHouse 单独拉出来看,它是整个链路的枢纽节点——上游有三个写入口、下游有两个读出口:

上游(三条写入口)
┌──────────────┬──────────────┐
▼ ▼ ▼
Flink Connector Kafka 引擎 PaimonS3
(实时批量写入) (流式订阅) (外表只读)
│ │ │
│ │ │
└──────────────┼──────────────┘
▼
┌───────────────────┐
│ ClickHouse 集群 │
│ (4 shard × 3 replica)│
│ │
│ 本地 Replicated │ Materialized View
│ MergeTree 表 │ 管道(外表→本地表)
└────────┬──────────┘
│
┌────────────┼────────────┐
▼ ▼ ▼
下游(两个读出口 + 内部分布式查询)
Doris JDBC 应用层直接 ClickHouse
Catalog 查单机表 Distributed
跨 shard 查

图 18-2 ClickHouse 作为枢纽的三条写入口 + 两个读出口


18.2 Flink → ClickHouse:三种写入方式

Flink 往 ClickHouse 写数据有三条路。选择依据是你的数据特征(批量 vs 流式)、一致性要求(Exactly-Once vs At-Least-Once)、和解耦程度(是否需要 Kafka 作为中间缓冲)。

18.2.1 方式 A:官方 Flink Connector(推荐)

Flink Connector 是 ClickHouse 官方维护的 AsyncSink 实现——基于 ClickHouse 原生 HTTP 协议 + Async 批量写入。这是生产首选,因为:

  • 支持 Exactly-Once 语义(23.3+ 版本)
  • 基于 ClickHouse 原生 JDBC 协议(不是 HTTP JSON 绕一圈)
  • AsyncSinkBase 实现(Flink 1.14+ 标准 Async Sink 接口),性能好
  • 自动选 Distributed 表的健康 replica + 按 sharding key 切 shard
  • 支持 checkpoint 状态持久化 → Flink 重启后从 checkpoint 恢复
Maven 依赖

<!– Flink 2.0+ –>
<dependency>
<groupId>com.clickhouse.flink</groupId>
<artifactId>flink-connector-clickhouse-2.0.0</artifactId>
<version>1.1.1</version>
<classifier>all</classifier>
</dependency>

<!– Flink 1.17 ~ 1.20 –>
<dependency>
<groupId>com.clickhouse.flink</groupId>
<artifactId>flink-connector-clickhouse-1.17</artifactId>
<version>1.1.1</version>
<classifier>all</classifier>
</dependency>

Java 代码骨架

import com.clickhouse.flink.connector.ClickHouseSink;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.streaming.api.datastream.DataStream;

// 假设 vehicleData 是从 Kafka source 清洗后的 DataStream
DataStream<VehicleTelemetryRecord> vehicleData = ...;

// 关键参数
ClickHouseSink<VehicleTelemetryRecord> sink = ClickHouseSink.<VehicleTelemetryRecord>builder()
// ClickHouse 连接
.withUrl("jdbc:clickhouse://ck-1-1:8123,ck-1-2:8123,ck-1-3:8123")
.withDatabase("demo")
.withTable("vehicle_telemetry") // 如果是 Distributed 表,直接写分布式表名
.withUsername("flink_writer")
.withPassword("xxx")

// 批量控制
.withMaxBatchSize(50000) // 每批 5 万行(生产推荐 1 万~10 万)
.withMaxInFlightRequests(3) // 同时最多 3 个 in-flight 请求(反压控制)
.withFlushInterval(Duration.ofSeconds(5)) // 5 秒强制 flush(即使没凑满 batch)

// 路由 + 一致性
.withShardingKeyExtractor(record -> record.getVin()) // 按 vin 算 shard
.withWriteMode(WriteMode.INSERT) // INSERT / UPSERT 可选
.withFailOnTimeout(true)

// 序列化(避免 Kryo 开销,用原生 POJO ↔ ClickHouse)
.withConverter(VehicleTelemetryRecord::toClickHouseRow)

.build();

vehicleData
.assignTimestampsAndWatermarks(WatermarkStrategy.noWatermarks())
.sinkTo(sink);

关键参数解释
参数默认值生产推荐含义
maxBatchSize 无默认(必需) 10000 ~ 100000 每批最多攒多少行。官方推荐至少 1000,理想 1 万~10 万——越大越少网络 RTT
maxInFlightRequests 无默认(必需) 2 ~ 5 同时最多多少个 in-flight 请求。ClickHouse TCP 连接数有限,别开太大
flushInterval 无默认 3s ~ 10s 即使没凑满 batch,超过这个时间也必须 flush。低延迟场景开 3s
WriteMode INSERT INSERT 或 UPSERT ReplicatedReplacingMergeTree 需要 upsert 语义(同 vin + seq 覆盖),但 Connector 的 UPSERT 模式在 ClickHouse 侧其实还是 INSERT + merge 阶段去重
withShardingKeyExtractor – 写 Distributed 表时必须设 告诉 Connector 怎么算 sharding_key,让它知道往哪个 shard 发
Exactly-Once 配置

ClickHouse Flink Connector 2.0+ 支持 Exactly-Once(基于 Flink 的 Sink V2 + Two-Phase-Commit)。需要:

  • ClickHouse 侧:目标表必须是 ReplicatedMergeTree 系列(有 Keeper session 支持事务性写入)
  • Flink job:开 checkpoint(enableCheckpointing(60000))+ 设 state backend(RocksDB 推荐)
  • Connector:withExactlyOnce(true) + ClickHouse JDBC driver ≥ 0.6.0
  • 18.2.2 方式 B:Flink JDBC Sink(通用但性能一般)

    Flink 自带 flink-connector-jdbc,理论上可以接任何支持 JDBC 的数据库包括 ClickHouse:

    // Flink 自带 JDBC sink
    JdbcSink.sink(
    "INSERT INTO demo.vehicle_telemetry VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
    (ps, record) -> {
    ps.setString(1, record.getVin());
    ps.setInt(2, record.getCityId());
    // … 其他字段
    },
    new JdbcExecutionOptions.Builder()
    .withBatchSize(50000)
    .withBatchIntervalMs(5000)
    .build(),
    new JdbcConnectionOptions.JdbcConnectionOptionsBuilder()
    .withUrl("jdbc:clickhouse://ck-1-1:8123")
    .withDriverName("com.clickhouse.jdbc.ClickHouseDriver")
    .withUsername("flink_writer")
    .withPassword("xxx")
    .build()
    );

    但 JDBC Sink 的问题:

    • 用的是 ClickHouse HTTP 协议 + JSON 格式绕一圈,比原生 AsyncSink 慢 2~5 倍
    • 不支持自动选 replica(只能写一个固定地址)
    • 不支持 Exactly-Once(最多 At-Least-Once,配合 ReplacingMergeTree 去重可接受)
    • 用 Flink 自带 JdbcSink,没有 sharding_key 概念——写 Distributed 表时可能全部打到同一个 shard

    什么时候用:你的 Flink job 里已经在同时写 MySQL + PostgreSQL + ClickHouse,想统一用 Flink 自带 JDBC Sink 减少维护成本、且能接受 ClickHouse 写入比官方 Connector 慢。

    18.2.3 方式 C:Flink 写 Kafka → ClickHouse Kafka 引擎(解耦流式管道)

    这是最解耦的方式——Flink 不直接碰 ClickHouse,而是把清洗后的数据写进 Kafka,然后 ClickHouse 自己作为 Kafka consumer 订阅。好处是"Flink 挂了 ClickHouse 不受影响、ClickHouse 挂了 Flink 不受影响"。

    Flink 侧:写 Kafka

    // Flink 清洗后的 vehicleData 写 Kafka topic vehicle_cleaned
    FlinkKafkaProducer<VehicleTelemetryRecord> kafkaSink = new FlinkKafkaProducer<>(
    "vehicle_cleaned", // topic
    new VehicleTelemetryRecordSchema(), // serializer(JSON 格式)
    kafkaProperties,
    FlinkKafkaProducer.Semantic.EXACTLY_ONCE
    );

    vehicleData.sinkTo(kafkaSink);

    ClickHouse 侧:Kafka 表 + Materialized View 管道

    ClickHouse 的 Kafka 表引擎 是 ClickHouse 原生实现的 Kafka consumer——它只做"订阅 + 解析",然后通过 Materialized View 管道 把数据写到目标表:

    — 第 1 步:建 Kafka 表(只负责订阅和解析,不存数据)
    — 注意:allow_experimental_kafka_storage_engine 在新版 ClickHouse 里可能已移除
    — Cloud 用户用 ClickPipes 替代
    CREATE TABLE IF NOT EXISTS demo.vehicle_kafka (
    vin String,
    city_id UInt32,
    report_date Date,
    reported_at DateTime,
    speed Float32,
    battery UInt32,
    battery_pct UInt8,
    vehicle_status LowCardinality(String),
    seq UInt64,
    is_deleted UInt8,
    ext String
    ) ENGINE = Kafka()
    SETTINGS
    kafka_broker_list = 'kafka-1:9092,kafka-2:9092,kafka-3:9092',
    kafka_topic_list = 'vehicle_cleaned',
    kafka_group_name = 'clickhouse_vehicle_consumer',
    kafka_format = 'JSONEachRow',
    kafka_num_consumers = 4, — 4 个 consumer(多线程加速)
    kafka_commit_every_batch = 1, — 每个 batch 就 commit offset
    kafka_handle_error_mode = 'stream'; — 错误也写入表(带 _error 列)

    — 第 2 步:建目标表(ReplicatedReplacingMergeTree)
    CREATE TABLE IF NOT EXISTS demo.vehicle_telemetry
    ENGINE = ReplicatedReplacingMergeTree(
    '/clickhouse/tables/{shard}/demo/vehicle_telemetry',
    '{replica}',
    seq
    )
    PARTITION BY cityHash64(vin) % 8
    ORDER BY (city_id, report_date, vin);

    — 第 3 步:建 Materialized View 管道(持续把 Kafka 表的数据 INSERT 到目标表)
    CREATE MATERIALIZED VIEW IF NOT EXISTS demo.vehicle_kafka_mv
    TO demo.vehicle_telemetry AS
    SELECT * FROM demo.vehicle_kafka;

    Kafka 引擎的关键设置
    设置默认生产推荐含义
    kafka_num_consumers 1 4 ~ 8 Kafka consumer 线程数。上限 = topic partition 数
    kafka_commit_every_batch 0(等 flush_interval_ms) 1 每个 batch 就 commit offset。配合 MATERIALIZED VIEW 的原子性保证至少一次
    kafka_flush_interval_ms 0(实时 flush) 5000(5s) 攒一段时间的 Kafka messages 再一起 INSERT 目标表
    kafka_skip_broken_messages 0(遇到坏消息就报错停掉) 1000 跳过最多 1000 条坏消息(脏数据容错)。生产一定要设
    kafka_handle_error_mode default(报错停掉) stream 错误也写入表,但加 _error 列标记。用于定位脏数据来源
    kafka_poll_timeout_ms 0 1000 Kafka poll 超时。不设的话 poll 会阻塞太久

    ⚠️ ClickHouse Cloud 用户注意:Kafka 引擎在 Cloud 上可能不可用或受限。Cloud 推荐用 ClickPipes 服务——托管的 Kafka → ClickHouse 管道,自动管理 consumer group / offset / 扩容。

    18.2.4 三种方式对比与车辆场景选型

    维度Flink ConnectorJDBC SinkKafka 引擎管道
    性能 最快(原生 AsyncSink + 批量) 慢 2~5 倍 中等(ClickHouse 自己 consumer)
    Exactly-Once ✅ 支持(Connector 2.0+) ❌ 不支持(最多 At-Least-Once) ✅ 支持(Kafka offset + MV 原子性)
    自动选 replica ✅ 支持 ❌ 不支持(固定地址) ✅ 支持(Kafka 表 + MV 到 Replicated 表自动走路由)
    需要 Kafka ❌ 不需要 ❌ 不需要 ✅ 必须(解耦代价)
    Flink 重启恢复 checkpoint → 恢复 checkpoint → 恢复(但可能重复) 从 Kafka offset 恢复(Flink 没参与,ClickHouse 自己续)
    运维复杂度 低(一个组件) 低(一个组件) 高(Kafka + Kafka 引擎 + MV 三件套)
    Cloud 支持 ✅ 推荐(或 ClickPipes) ✅ 支持 ❌ 可能受限

    车辆场景选型建议:

    • 短期 PoC / MVP:方式 A(Flink Connector)——最快、最少组件、一次跑通端到端。
    • 长期生产、高吞吐、需要解耦:方式 A + 方式 C 组合——Flink 同时开 Flink Connector(实时)和 Kafka sink(解耦备份),ClickHouse 侧开 Flink Connector 直写 + Kafka 引擎管道备份。两条路互为灾备。
    • 已在生产跑 Flink 写 Kafka:方式 C 直接接上——不用改 Flink,ClickHouse 加一套 Kafka 引擎 + MV 就行。

    18.3 ClickHouse 读湖仓:Paimon / Iceberg / Hudi / Hive 外表

    18.3.1 统一心智:全部是只读外表

    ClickHouse 集成湖仓的方式高度统一:每张外表引擎都是只读的——ClickHouse 不会往 Paimon/Iceberg/Hudi/Hive 里写数据(写数据的事交给 Flink / Spark / Trino),它只负责"能 SELECT 读到这些数据、能在一次查询里 JOIN 本地表 + 外表"。

    外表引擎命名规则按存储后端分:

    引擎后缀存储后端官方文档必须先有吗
    PaimonS3 / PaimonAzure / PaimonHDFS / PaimonLocal S3 / Azure / HDFS / 本地 Paimon docs ✅ Paimon 表必须已经存在
    IcebergS3 / IcebergAzure / IcebergHDFS / IcebergLocal S3 / Azure / HDFS / 本地 Iceberg docs ✅ Iceberg 表必须已经存在
    Hudi(url, key, secret) S3 Hudi docs ✅ Hudi 表必须已经存在
    Hive(‘thrift://host:port’, ‘db’, ‘table’) Hive Metastore Hive docs ✅ Hive Metastore 必须可达

    所有外表都需要:allow_experimental_paimon_storage_engine / allow_experimental_hudi_storage_engine 等配置开启(默认关闭)。

    18.3.2 Paimon 外表最常用的场景:车辆历史原始数据

    假设 Flink 已经把清洗后的车辆遥测写进了 S3 上的 Paimon 表 s3://vehicle-lake/vehicle_telemetry_paimon/:

    — 全局开 Paimon 外表功能(只需要开一次)
    SET allow_experimental_paimon_storage_engine = 1;

    — 生产推荐:用 Named Collection 存 S3 凭证(不要硬编码到 CREATE TABLE 里)
    — 在 config.xml 或 config.d/paimon.xml 里加:
    — <clickhouse>
    — <named_collections>
    — <paimon_vehicle>
    — <url>s3://vehicle-lake/vehicle_telemetry_paimon/</url>
    — <access_key_id>AKIA…</access_key_id>
    — <secret_access_key>xxxxxx</secret_access_key>
    — </paimon_vehicle>
    — </named_collections>
    — </clickhouse>

    CREATE TABLE demo.vehicle_telemetry_paimon_local
    (
    vin String,
    city_id UInt32,
    report_date Date,
    reported_at DateTime,
    speed Float32,
    battery UInt32,
    battery_pct UInt8,
    vehicle_status LowCardinality(String),
    seq UInt64,
    is_deleted UInt8,
    ext String
    )
    ENGINE = PaimonS3(paimon_vehicle, 'JSONEachRow');
    — 参数 1:Named Collection 名(或完整 URL + key + secret)
    — 参数 2:Paimon 底层数据格式(JSONEachRow / Parquet 等)

    外表查询:

    — 直接 SELECT 外表(ClickHouse 实时从 S3 读 Paimon snapshot)
    SELECT
    city_id,
    avg(speed) AS avg_speed,
    avg(battery_pct) AS avg_battery
    FROM demo.vehicle_telemetry_paimon_local
    WHERE report_date >= today() – 30
    GROUP BY city_id;

    — 外表 JOIN 本地表(核心威力!)
    — 同一查询里同时读 S3 冷数据 + 本地热数据
    SELECT
    vin,
    report_date,
    avg(speed) AS avg_speed,
    count() AS cnt,
    'cold_from_paimon' AS data_source
    FROM demo.vehicle_telemetry_paimon_local
    WHERE report_date < today() – 7
    GROUP BY vin, report_date

    UNION ALL

    SELECT
    vin,
    report_date,
    avg(speed) AS avg_speed,
    count() AS cnt,
    'hot_from_local' AS data_source
    FROM demo.vehicle_telemetry — 本地 ReplicatedMergeTree
    WHERE report_date >= today() – 7
    GROUP BY vin, report_date;

    外表 + 本地物化视图管道(把湖仓数据周期性拉到本地做高效查询):

    — 建本地目标表(ReplicatedMergeTree,和外表 schema 相同)
    CREATE TABLE demo.vehicle_telemetry_archive
    ENGINE = ReplicatedReplacingMergeTree(
    '/clickhouse/tables/{shard}/demo/vehicle_telemetry_archive',
    '{replica}', seq
    )
    PARTITION BY toYYYYMM(report_date) — 按月分区(大表分区稀疏一点)
    ORDER BY (vin, report_date);

    — 物化视图:外表 → 本地表
    CREATE MATERIALIZED VIEW demo.vehicle_telemetry_archive_mv
    TO demo.vehicle_telemetry_archive AS
    SELECT * FROM demo.vehicle_telemetry_paimon_local;

    — 手动触发一次全量拉取(后台自动跑)
    SYSTEM FLUSH LOGS; — 先确保外表数据加载到内存缓存
    OPTIMIZE TABLE demo.vehicle_telemetry_archive FINAL;

    18.3.3 Iceberg / Hudi / Hive:选择哪个

    三个湖仓外表的差异主要在底层元数据管理方式:

    维度PaimonIcebergHudiHive
    元数据 S3 + Paimon Manifest 文件 S3 + Iceberg Manifest + 可选 Hive Metastore S3 + Hudi Timeline Hive Metastore 必需
    时间旅行 ✅ 支持 ✅ 支持(快照 / 分支) ✅ 支持 ❌ 不支持
    Upsert 支持 ✅ Merge 表 ✅ Positional Delete ✅ MOR 表 ❌ 不支持
    schema 演进 ✅ 支持 ✅ 支持 ✅ 支持 ⚠️ 有限支持
    ClickHouse 外表引擎 PaimonS3(…) IcebergS3(…) Hudi(url, key, secret) Hive(‘thrift://’, ‘db’, ‘table’)
    partition pruning ✅ 支持 ✅ 支持 ✅ 支持 ✅ 支持
    Cloud 用户 ✅ 支持 ✅ 支持 ✅ 支持 ❌ Cloud 不支持(依赖 Hive Metastore)

    车辆场景建议:Paimon 是 Flink 原生湖仓(Flink 1.15+ 官方支持),我们整个数据栈从 Flink 起 → Paimon 是首选。如果已有 Iceberg 或 Hudi 基础设施,ClickHouse 也能无缝接入(外表引擎只读,不影响 Flink/Spark 那边的写入逻辑)。

    18.3.4 外表查询性能优化

    外表直接读 S3 远没有本地盘快——几个优化技巧:

  • 尽量带 partition filter:外表引擎会把 WHERE report_date >= today() – 30 下推到 Paimon/Iceberg 的 manifest 里跳过不需要的分区文件。不带 partition filter = 全表 scan = S3 成本爆炸。

  • 给外表建本地物化视图做热缓存:上一节的 MV 管道是最佳实践——冷数据查询直接查本地归档表(快 10~50 倍),外表只做回补查询(偶尔查)。

  • local_cache_for_remote_fs 开启:Hive 引擎文档强烈建议,对外表同样适用。在 config.xml 里加:

  • <local_cache_for_remote_fs>
    <enable>true</enable>
    <root_dir>/var/lib/clickhouse/cache</root_dir>
    <limit_size>21474836480</limit_size> <!– 20GB –>
    </local_cache_for_remote_fs>

  • 用 Named Collection 统一凭证:不要硬编码 S3 key/secret 到 CREATE TABLE 里——Named Collection 可以在 config.xml 里集中管理、支持动态更新。

  • 18.4 ClickHouse ↔ Doris:双向联邦

    Doris 和 ClickHouse 都能通过 JDBC 协议互相查询。选择哪个方向取决于"哪一侧是联邦查询的发起者和结果消费者"。

    18.4.1 方向 A:Doris 读 ClickHouse(主流方向)

    Doris 的 JDBC Catalog 功能让 Doris 能把 ClickHouse 当成一个"外部数据目录"——像查本地 Doris 表一样查 ClickHouse 的表。这是 BI 看板的主流模式(Doris 做 federated query + 本地 cube 预计算 + BI 工具对接)。

    步骤 1:准备 JDBC 驱动

    Doris 的 FE(Frontend)需要 ClickHouse JDBC 驱动。把 jar 放到 doris/fe/jdbc_drivers/clickhouse/ 目录下:

    # 驱动下载地址
    # https://github.com/ClickHouse/clickhouse-jdbc/releases
    # 推荐 clickhouse-jdbc-0.6.0.jar + clickhouse-jdbc-0.6.0-all.jar

    步骤 2:Doris 侧 CREATE CATALOG

    — 在 Doris FE 上执行
    CREATE CATALOG vehicle_clickhouse_catalog
    PROPERTIES (
    "type" = "jdbc",
    "jdbc_driver_url" = "https://github.com/ClickHouse/clickhouse-jdbc/releases/download/v0.6.0/clickhouse-jdbc-0.6.0-all.jar",
    "jdbc_driver_class" = "com.clickhouse.jdbc.ClickHouseDriver",
    "jdbc_url" = "jdbc:clickhouse://ck-1-1:8123,ck-1-2:8123,ck-1-3:8123/demo",
    "jdbc_user" = "doris_reader",
    "jdbc_password" = "xxx",
    — ClickHouse JDBC Catalog 特有的参数
    "lower_case_table_names" = "1", — Doris 大小写不敏感,ClickHouse 敏感
    "table_meta_cache_size" = "10000", — 缓存 10000 张表的 metadata
    "include_database_list" = "demo" — 只映射 demo 库(不扫全集群)
    );

    步骤 3:像查本地表一样查 ClickHouse

    — 切到 ClickHouse 联邦 catalog
    USE vehicle_clickhouse_catalog;

    — 直接查 ClickHouse 表
    SELECT
    city_id,
    avg(speed) AS avg_speed
    FROM demo.vehicle_telemetry — 这是 ClickHouse 里的表
    WHERE report_date >= today() – 7
    GROUP BY city_id;

    — Doris 本地表 JOIN ClickHouse 表(联邦 JOIN)
    SELECT
    v.city_id,
    d.city_name,
    avg(v.speed) AS avg_speed
    FROM demo.vehicle_telemetry v — ClickHouse 表
    JOIN doris_demo.city_dict d — Doris 本地字典表
    ON v.city_id = d.city_id
    WHERE v.report_date >= today() – 7
    GROUP BY v.city_id, d.city_name;

    注意 Doris JDBC Catalog 的局限性(Doris 官方自己说的):

    • JDBC Catalog 只适合小数据量查询和数据集成——不能加速 ClickHouse 的查询,也不适合一次性拉大量数据
    • 高性能场景用 Doris Routine Load 把 ClickHouse 数据持续同步到 Doris 本地表,然后 BI 看板只查 Doris 本地表
    • ClickHouse 表里的 LowCardinality / Array / Tuple 等复杂类型,Doris JDBC Catalog 可能需要 cast

    18.4.2 方向 B:ClickHouse 读 Doris(反向)

    ClickHouse 的 JDBC 引擎 可以查任何支持 JDBC 的数据库包括 Doris:

    — ClickHouse 侧建 Doris 外表(只读)
    CREATE TABLE demo.doris_city_dict
    (
    city_id UInt32,
    city_name String,
    province String
    )
    ENGINE = JDBC(
    'jdbc:mysql://doris-fe:9030/doris_demo', — Doris FE 用 MySQL 协议
    'root',
    '',
    'city_dict'
    );

    — 用 Doris MySQL 驱动(放在 ClickHouse 服务器的 lib 目录下)
    — 驱动名:mysql-connector-java-8.0.xx.jar

    — 查询 + JOIN 本地 ClickHouse 表
    SELECT
    t.city_id,
    d.city_name,
    avg(t.speed) AS avg_speed
    FROM demo.vehicle_telemetry_local t
    JOIN demo.doris_city_dict d ON t.city_id = d.city_id
    WHERE t.report_date >= today() – 7
    GROUP BY t.city_id, d.city_name;

    ClickHouse Cloud 用户注意:JDBC 引擎在 Cloud 上可能受限(外部 JDBC 驱动加载)。Cloud 用户优先用方向 A(Doris 读 ClickHouse)。

    18.4.3 双向都能做,怎么选

    决策点选 Doris → ClickHouse(方向 A)选 ClickHouse → Doris(方向 B)
    哪一侧在做 BI / dashboard ✅ Doris ❌ ClickHouse 不做 BI
    哪一侧有复杂 JOIN(大表 JOIN 大表) 两边都能做,ClickHouse 更擅长 scan + aggregate
    哪一侧数据量更大 如果 ClickHouse 数据量远大于 Doris → 方向 A 更优(只查需要的部分) 如果 Doris 是小字典表 → 方向 B 可以
    哪一侧有物化视图预计算 Doris cube 预计算在方向 A 下效果好
    维护成本 Doris JDBC Catalog 配置 + 驱动 ClickHouse JDBC 引擎 + 驱动(更复杂)

    生产推荐:方向 A 为主(Doris 读 ClickHouse 做 BI)+ 方向 B 按需使用(ClickHouse 偶尔查 Doris 的小字典表做 JOIN)。


    18.5 Kafka 引擎深入:流式订阅 + Materialized View 管道

    Kafka 引擎(方式 C)在 18.2.3 里已经给了基础骨架。这里深入几个生产高频踩坑点。

    18.5.1 Kafka 引擎的本质:只订阅解析,不存数据

    Kafka 引擎表本身不存任何数据——它是一张"实时视图",每次 SELECT 都会触发 Kafka poll。数据流向目标表完全依赖 Materialized View 管道:

    Kafka topic vehicle_cleaned
    │
    │ ClickHouse Kafka consumer(在后台持续 poll)
    ▼
    demo.vehicle_kafka(Kafka 引擎表,不存数据)
    │
    │ Materialized View 管道
    │ (在 Kafka consumer 线程里同步 INSERT)
    ▼
    demo.vehicle_telemetry(ReplicatedReplacingMergeTree,存数据)

    图 18-3 Kafka 引擎 + MV 管道的数据流向

    18.5.2 管道启停控制

    MV 管道创建后自动开始。如果需要临时停掉:

    — 停掉 MV(底层是 Detach + Kill query)
    SYSTEM STOP MERGES demo.vehicle_kafka_mv;
    — 或者直接 DROP MATERIALIZED VIEW(同时停管道)
    DROP MATERIALIZED VIEW IF EXISTS demo.vehicle_kafka_mv;

    — 恢复
    SYSTEM START MERGES demo.vehicle_kafka_mv;

    18.5.3 Kafka offset 管理

    Kafka 引擎表会在 ClickHouse 服务器的 /var/lib/clickhouse/[your_database]/[kafka_table_name] 目录下存一个 __consumer_offsets 文件。不要手动删这个文件——删了等于重置 consumer group,会从 earliest 或 latest 重新开始消费。

    关键设置 kafka_commit_on_select:

    • 默认 false:MV 管道消费时,commit offset 在后台按 kafka_commit_every_batch 或 flush_interval 触发。推荐保持 false(MV 管道是消费主体、commit 时机由 MV 控制)。
    • 设为 true:每次 SELECT 引擎表时才 commit offset。只在手动调试时用(比如想精确控制"消费到这里为止")。

    18.5.4 故障恢复与 Exactly-Once

    Kafka 引擎 + MV 管道的语义是 At-Least-Once(不是 Exactly-Once)——原因:

  • Kafka commit offset 和 MV INSERT 是两个独立操作
  • 如果 MV INSERT 成功但 commit offset 之前 ClickHouse crash → Kafka 重启后从上一次 commit 的 offset 重新读 → MV 再次 INSERT → 重复数据
  • 解决办法:目标表用 ReplacingMergeTree(我们的 demo.vehicle_telemetry 就是)——同一个 (vin, seq) 重复插入,merge 阶段自动去重。At-Least-Once + ReplacingMergeTree = 效果等同于 Exactly-Once。

    18.5.5 与 Flink Connector 的协同:两条写入路互为灾备

    Kafka topic vehicle_cleaned(Flink sink 写入)
    │
    ┌─────┴──────┐
    ▼ ▼
    Kafka 引擎管道 Flink Connector 直写
    (异步、解耦) (同步、高性能)
    │ │
    ▼ ▼
    demo.vehicle_telemetry(ReplicatedReplacingMergeTree)
    同一目标表,两条路都写,ReplacingMergeTree 自动去重

    图 18-4 两条写入路互为灾备的架构

    当 Flink Connector 直写不可用时(网络问题 / ClickHouse 抖动),Flink 的 Kafka sink 继续往 topic 写,ClickHouse 的 Kafka 引擎管道继续消费 → 写入不中断。反之当 Kafka 集群故障时,Flink Connector 直写继续 → 写入也不中断。

    两条路同时正常时 → 同一行数据可能被写两次 → ReplacingMergeTree 按主键去重 → 不影响正确性。


    18.6 端到端部署

    18.6.1 组件清单与端口

    组件端口部署模式资源配额(生产)
    Kafka(3 节点) 9092(broker)+ 2181(KRaft) Docker / K8s StatefulSet 每节点 8C 16G + 500GB SSD
    Flink JobManager + TaskManager 8081(Web UI) K8s Session / Application JM 4C 8G / TM 每槽 2C 4G
    Paimon(依赖 S3) 无(用 S3 API) S3 / MinIO –
    ClickHouse(4 shard × 3 replica) 9000(Native TCP)+ 8123(HTTP)+ 9440(SSL) Docker / K8s StatefulSet 每节点 16C 32G + 4TB NVMe
    ClickHouse Keeper(3 节点) 2181(客户端)+ 9234(Raft) Docker / K8s StatefulSet 每节点 4C 8G + 200GB SSD
    Doris(3 BE + 1 FE) 8030(FE HTTP)+ 9030(FE MySQL 协议)+ 8040/9060(BE) Docker / K8s StatefulSet FE 8C 16G / BE 16C 64G + 4TB NVMe
    Superset(BI 看板) 8088 Docker Compose / K8s Deployment 4C 8G

    18.6.2 Docker Compose 本地起全栈(1 节点 PoC)

    # docker-compose-vehicle-fullstack.yml
    # 单节点 PoC 用:简化版,生产请用 K8s / Helm
    version: "3.9"

    services:
    # — Kafka (KRaft mode, 3 节点简化为 1) —
    kafka:
    image: bitnami/kafka:latest
    ports: ["9092:9092"]
    environment:
    KAFKA_CFG_NODE_ID: 1
    KAFKA_CFG_PROCESS_ROLES: broker,controller
    KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093
    KAFKA_CFG_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093
    KAFKA_CFG_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
    KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT
    ALLOW_PLAINTEXT_LISTENER: "yes"

    # — MinIO(本地 S3,给 Paimon 用) —
    minio:
    image: minio/minio:latest
    ports: ["9000:9000", "9001:9001"]
    environment:
    MINIO_ROOT_USER: minioadmin
    MINIO_ROOT_PASSWORD: minioadmin
    command: server /data ––console–address ":9001"

    # — ClickHouse + Keeper(混部,单节点简化) —
    clickhouse:
    image: clickhouse/clickhouse–server:latest
    ports: ["9000:9000", "8123:8123"]
    volumes:
    – ./ch_local.xml:/etc/clickhouse–server/config.d/local.xml
    depends_on: [kafka, minio]
    ulimits:
    nofile: { soft: 262144, hard: 262144 }

    # — Doris (FE + BE) —
    doris-fe:
    image: apache/doris:latest–fe
    ports: ["8030:8030", "9030:9030"]
    environment:
    FE_SERVERS: "fe1:doris-fe:9010"
    FE_QUORUM_VOTERS: "fe1:doris-fe:9010"
    ADMIN_PASSWD: ""

    doris-be:
    image: apache/doris:latest–be
    depends_on: [doris–fe]
    environment:
    FE_SERVER: "fe1:doris-fe:9010"

    # — Superset (可选 BI) —
    superset:
    image: apache/superset:latest
    ports: ["8088:8088"]
    depends_on: [clickhouse, doris–fe]

    启动命令:

    # 起全栈
    docker compose -f docker-compose-vehicle-fullstack.yml up -d

    # 建 MinIO bucket(Paimon 用)
    mc alias set myminio http://localhost:9000 minioadmin minioadmin
    mc mb myminio/vehicle-lake

    # 建 Kafka topic
    docker compose exec kafka kafka-topics.sh –create \\
    –bootstrap-server kafka:9092 \\
    –topic vehicle_cleaned –partitions 8 –replication-factor 1

    # 建 Doris BE(注册到 FE)
    mysql -h localhost -P 9030 -u root -e "
    ALTER SYSTEM ADD BACKEND 'doris-be:9050';
    "

    # 建 ClickHouse 表(第 16/17 章的 DDL 直接用)
    clickhouse-client –host localhost –port 9000 < ch_ddl.sql

    18.6.3 K8s / Helm 生产部署要点

    生产环境用 Helm chart + StatefulSet。ClickHouse 官方有 ClickHouse Kubernetes Operator 和 ClickHouse Helm Chart。

    ClickHouse Helm values.yaml 关键配置:

    # values.yaml 节选
    clickhouse:
    replicas: 3 # 每 shard 3 副本
    shards: 4 # 4 shard
    keeper:
    enabled: true
    replicas: 3 # Keeper 3 节点混部
    storage:
    data:
    storageClass: ssd # NVMe SSD storageClass
    size: 4Ti
    config:
    remote_servers:
    vehicle_cluster:
    – shard:
    weight: "1"
    internal_replication: "true"
    replicas:
    – host: clickhouse–shard0–replica0
    – host: clickhouse–shard0–replica1
    – host: clickhouse–shard0–replica2
    # shard 1, 2, 3 同结构
    macros:
    shard: "{{ .Values.shard }}"
    replica: "{{ .Values.replica }}"
    named_collections:
    paimon_conf:
    url: "s3://vehicle-lake/paimon-table/"
    access_key_id: "xxx"
    secret_access_key: "xxx"
    ingress:
    host: clickhouse.internal

    Doris Helm 用 Apache Doris Official Helm Chart:

    doris-fe:
    replicas: 3
    image: apache/doris:2.1.6
    doris-be:
    replicas: 3
    storage:
    storageClass: ssd
    size: 4Ti
    doris-cn: # 可选 Compaction Job
    enabled: true

    18.6.4 全链路验证 checklist

    [ ] Kafka: topic vehicle_cleaned 创建成功,3 节点 replication-factor 生效
    [ ] Keeper: 3 节点 quorum 全绿(mntr 返回 imok)
    [ ] ClickHouse: 4 shard × 3 replica = 12 节点,clusterAllReplicas 查询全 is_active=1
    [ ] ClickHouse: Kafka 表创建成功,kafk_num_consumers=4
    [ ] ClickHouse: Materialized View 管道状态正常(system.macros 里能查到)
    [ ] Paimon: Flink JobManager 启动成功,sink 指向 S3 vehicle-lake bucket
    [ ] Doris: JDBC Catalog 创建成功,能 SELECT ClickHouse 的 vehicle_telemetry 表
    [ ] 端到端: 发送 1000 条模拟遥测 → 检查 Kafka → 检查 Flink 处理 → 检查 ClickHouse vehicle_kafka_mv 写入 → 检查 Doris JDBC Catalog 能查到
    [ ] 故障: 停掉 1 个 Keeper 节点 → ClickHouse 仍可写(quorum 剩 2)
    [ ] 故障: 停掉 1 个 ClickHouse replica → 其他副本仍可写

    18.7 车辆链路全链路实战:端到端跑通

    18.7.1 场景与前置条件

    我们把第 15~17 章的 ClickHouse 集群扩展到了 5 组件全栈:

    T-Box → Kafka → Flink(清洗) → Paimon(S3) + ClickHouse(4 shard × 3 replica)
    ↓ ↓
    PaimonS3 外表 Doris JDBC Catalog
    ↓ ↓
    ClickHouse 本地 MV Superset BI 看板

    前置:

    • Kafka 3 节点(KRaft 模式)、MinIO(本地 S3)、ClickHouse 4 shard × 3 replica + Keeper 3 节点、Doris 3 FE + 3 BE 全部部署完成
    • 第 16/17 章的 ReplicatedReplacingMergeTree 表 + Distributed 表 + 冷热分层 TTL 已配好
    • Flink 1.17+(Flink Connector 兼容)+ Flink Kafka Connector + Flink Paimon Connector 已在 classpath 上

    18.7.2 步骤 1:Flink ODS 清洗 Job

    下面是一个完整的 Flink DataStream Job(车辆遥测 ODS 清洗 + 双 sink):

    // VehicleODSJob.java 完整骨架
    public class VehicleODSJob {
    public static void main(String[] args) throws Exception {
    StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
    env.enableCheckpointing(60000, CheckpointingMode.EXACTLY_ONCE); // 60s checkpoint
    env.getCheckpointConfig().setCheckpointStorage("s3://vehicle-lake/flink-checkpoints/");

    // 1. Kafka Source(原始遥测)
    Properties kafkaProps = new Properties();
    kafkaProps.setProperty("bootstrap.servers", "kafka-1:9092,kafka-2:9092,kafka-3:9092");
    kafkaProps.setProperty("group.id", "flink-ods-group");
    kafkaProps.setProperty("auto.offset.reset", "latest");

    DataStream<String> rawStream = env.fromSource(
    KafkaSource.<String>builder()
    .setBootstrapServers("kafka-1:9092")
    .setTopics("vehicle_raw")
    .setGroupId("flink-ods-group")
    .setStartingOffsets(OffsetsInitializer.latest())
    .setValueOnlyDeserializer(new SimpleStringSchema())
    .build(),
    WatermarkStrategy.forBoundedOutOfOrderness(Duration.ofSeconds(5)),
    "vehicle-kafka-source"
    );

    // 2. 清洗 + 转换
    DataStream<VehicleTelemetryRecord> cleaned = rawStream
    .map(new VehicleTelemetryParser()) // JSON → POJO + 字段补全
    .filter(record ->
    record.getSpeed() >= 0
    && record.getBatteryPct() >= 0 && record.getBatteryPct() <= 100
    && record.getReportedAt().isBefore(Instant.now().plusSeconds(60))
    )
    .name("clean-telemetry");

    // 3. Sink 到 Paimon(历史湖仓)
    // 需要 Flink Paimon Connector 0.8+
    cleaned.sinkTo(
    PaimonSink.forRowDataStream(
    cleaned,
    PaimonConfig.fromMap(Map.of(
    "warehouse", "s3://vehicle-lake/",
    "database", "demo",
    "table", "vehicle_telemetry_paimon",
    "write-mode", "upsert",
    "primary-key", "vin,seq"
    ))
    ).build()
    );

    // 4. Sink 到 ClickHouse(实时聚合)
    ClickHouseSink<VehicleTelemetryRecord> chSink = ClickHouseSink
    .<VehicleTelemetryRecord>builder()
    .withUrl("jdbc:clickhouse://ck-1-1:8123,ck-1-2:8123,ck-1-3:8123")
    .withDatabase("demo")
    .withTable("vehicle_telemetry")
    .withUsername("flink_writer")
    .withPassword("xxx")
    .withMaxBatchSize(50000)
    .withMaxInFlightRequests(3)
    .withFlushInterval(Duration.ofSeconds(5))
    .withShardingKeyExtractor(VehicleTelemetryRecord::getVin)
    .withExactlyOnce(true)
    .withConverter(VehicleTelemetryRecord::toClickHouseRow)
    .build();
    cleaned.sinkTo(chSink);

    env.execute("Vehicle Telemetry ODS Job");
    }
    }

    验收:

    # 提交 Flink Job
    flink run -d -c VehicleODSJob vehicle-ods-job.jar

    # Flink Web UI (http://flink-jm:8081) 看 Source → Map → Filter → 两个 Sink 的 throughput

    # Paimon 验证(MinIO bucket vehicle-lake/demo/vehicle_telemetry_paimon/ 有 snapshot 文件夹)
    mc ls myminio/vehicle-lake/demo/vehicle_telemetry_paimon/snapshot/

    # ClickHouse 验证
    SELECT count() FROM demo.vehicle_telemetry;
    — 预期:持续增长(每秒 ~10000 行)

    18.7.3 步骤 2:Paimon 外表 + 本地 MV 管道

    — 开实验性功能
    SET allow_experimental_paimon_storage_engine = 1;

    — 外表(只读,指向 Paimon snapshot)
    CREATE TABLE IF NOT EXISTS demo.vehicle_telemetry_paimon
    (
    vin String, city_id UInt32, report_date Date,
    reported_at DateTime, speed Float32, battery UInt32,
    battery_pct UInt8, vehicle_status LowCardinality(String),
    seq UInt64, is_deleted UInt8, ext String
    ) ENGINE = PaimonS3(
    — 用 Named Collection
    paimon_vehicle, — url + key + secret(在 config.xml 里配好)
    'JSONEachRow'
    );

    — 本地归档表(ReplicatedMergeTree,按月分区做冷数据归档)
    CREATE TABLE IF NOT EXISTS demo.vehicle_telemetry_archive
    ENGINE = ReplicatedReplacingMergeTree(
    '/clickhouse/tables/{shard}/demo/vehicle_telemetry_archive',
    '{replica}', seq
    )
    PARTITION BY toYYYYMM(report_date)
    ORDER BY (vin, report_date);

    — MV 管道:外表 → 本地归档(每天凌晨触发全量拉取)
    CREATE MATERIALIZED VIEW demo.vehicle_telemetry_archive_mv
    TO demo.vehicle_telemetry_archive AS
    SELECT * FROM demo.vehicle_telemetry_paimon;

    验收:

    — 外表直接查
    SELECT count() FROM demo.vehicle_telemetry_paimon;

    — 归档表查
    SELECT
    toYYYYMM(report_date) AS month,
    count() AS rows,
    formatReadableSize(sum(bytes_on_disk)) AS size
    FROM system.parts
    WHERE table = 'vehicle_telemetry_archive' AND active = 1
    GROUP BY month
    ORDER BY month;

    18.7.4 步骤 3:Doris JDBC Catalog 联邦查询

    — Doris FE 上执行
    CREATE CATALOG vehicle_ck_catalog PROPERTIES (
    "type" = "jdbc",
    "jdbc_driver_url" = "https://github.com/ClickHouse/clickhouse-jdbc/releases/download/v0.6.0/clickhouse-jdbc-0.6.0-all.jar",
    "jdbc_driver_class" = "com.clickhouse.jdbc.ClickHouseDriver",
    "jdbc_url" = "jdbc:clickhouse://ck-1-1:8123,ck-1-2:8123,ck-1-3:8123/demo",
    "jdbc_user" = "doris_reader",
    "jdbc_password" = "xxx"
    );

    USE vehicle_ck_catalog;

    — Doris BI 看板核心查询:每城市平均车速 + 电量趋势
    SELECT
    city_id,
    report_date,
    avg(speed) AS avg_speed,
    avg(battery_pct) AS avg_battery,
    count() AS sample_count
    FROM demo.vehicle_telemetry
    WHERE report_date >= today() – 7
    GROUP BY city_id, report_date
    ORDER BY report_date, city_id;

    验收:Superset 面板能看到过去 7 天各城市车速趋势折线图。

    18.7.5 步骤 4:端到端故障演练

    故障预期结果验证命令
    Kafka 1 节点挂 3 节点 replication-factor=3 → 另两节点继续服务 echo mntr | nc kafka-2 2181
    Flink JobManager 重启 Checkpoint 恢复 → 从上次 checkpoint 继续 → 不丢数据 Flink UI 看恢复日志
    ClickHouse 1 副本挂 另两副本继续服务 + Distributed 引擎自动跳过坏副本 clusterAllReplicas('vehicle_cluster', system.replicas)
    Keeper 1 节点挂 quorum 剩 2 → 协调继续 echo mntr | nc keeper-2 2181
    Doris FE 1 节点挂 3 FE quorum 剩 2 → JDBC Catalog 继续 Doris UI 看 FE 状态

    18.8 工程要点

  • ClickHouse 在全链路中是"枢纽"——上游三条写入口(Flink Connector / Kafka 引擎 / Paimon 外表),下游两个读出口(Doris JDBC Catalog / 应用层直接查)。设计时优先保证枢纽的稳定性(多 shard × 3 replica + Keeper quorum)。

  • Paimon / Iceberg / Hudi / Hive 外表引擎全部是只读的——ClickHouse 不往湖仓写数据,写是 Flink / Spark 的事。外表的核心价值是"在一次查询里同时 JOIN 本地热数据 + 湖仓冷数据"。

  • 外表查询一定要带 partition filter——不带 = 全表 scan S3 = 成本爆炸。给外表建本地 Materialized View 管道做热缓存是生产最佳实践。

  • Doris JDBC Catalog 适合小数据量 federated query + BI 看板——不适合一次性拉大量数据。高频实时 BI 查询用 Doris Routine Load 持续同步 ClickHouse 数据到 Doris 本地表,看板只查 Doris 本地表。

  • ClickHouse Cloud 用户跳过 18.2.3(Kafka 引擎)和 18.5——Cloud 推荐用 ClickPipes 托管 Kafka → ClickHouse 管道。Cloud 里 Keeper 全托管、冷热分层不需要、Kafka 引擎可能受限。

  • Flink Connector(方式 A)是生产首选——AsyncSink + 原生 ClickHouse 协议 + Exactly-Once(23.3+)。JDBC Sink 只在"已有统一 JDBC 基础设施"时用。Kafka 引擎管道(方式 C)是灾备/解耦层,和方式 A 组合形成"两条写入路互为灾备"。

  • Flink Connector 的 Exactly-Once 需要 ClickHouse 侧目标表是 ReplicatedMergeTree——只有 Replicated 引擎有 Keeper session 支持事务性写入。MergeTree 不支持 Exactly-Once。

  • Kafka 引擎 + Materialized View 管道的语义是 At-Least-Once——配合 ReplacingMergeTree(我们的 demo 表就是)按主键去重 → 效果等同于 Exactly-Once。不要手动删 Kafka 引擎的 __consumer_offsets 文件。

  • 两条写入路(Flink Connector 直写 + Kafka 引擎管道)可以同时正常运行——同一行数据可能被写两次,但目标表是 ReplacingMergeTree 自动去重。这是生产推荐的高可用写法。

  • Docker Compose 本地起全栈是 PoC 模式——生产用 K8s + Helm + StatefulSet。ClickHouse 官方 Helm Chart 内置 Keeper 混部 / shard 副本配置 / remote_servers / macros / Named Collections。

  • 全链路验证 checklist 必须在部署后立刻跑——Kafka topic replication、Keeper quorum、ClickHouse 副本健康度、Doris JDBC Catalog 连通性、端到端写入 + 查询、故障演练。任何一项没绿灯就不可以进生产。

  • 组件角色边界:Kafka(解耦缓冲)≠ Flink(流处理)≠ Paimon(湖仓存储)≠ ClickHouse(查询聚合)≠ Doris(BI 看板)。不互相取代,各司其职。简化想法(“不装 Kafka 让 Flink 直连”、“不装 Paimon 让 ClickHouse 存所有数据”)在规模起来后会暴露问题。


  • 18.9 本章小结

    本章把第 1~17 章的单产品视角扩展到了全链路五组件生产部署:

    • 我们画出了完整的五组件架构图——Kafka 解耦缓冲 → Flink 流处理 → Paimon 湖仓原始数据 + ClickHouse 查询聚合 → Doris BI 看板。ClickHouse 在其中扮演枢纽角色(三条写入口 + 两个读出口)。

    • 我们对比了 Flink → ClickHouse 三种写入方式:Flink Connector(生产首选、AsyncSink、Exactly-Once、10~100k maxBatchSize)、JDBC Sink(通用但慢 2~5 倍)、Kafka 引擎管道(解耦、At-Least-Once + ReplacingMergeTree = 等价 Exactly-Once)。推荐方式 A + C 组合互为灾备。

    • 我们学会了用 ClickHouse Paimon/Iceberg/Hudi/Hive 外表读湖仓数据——全部只读、partition filter 下推、Named Collection 存凭证、外表 + 本地 MV 管道做热缓存。车辆场景选 Paimon(Flink 原生)。

    • 我们掌握了 Doris ↔ ClickHouse 双向联邦:主流方向 A(Doris JDBC Catalog 读 ClickHouse 做 BI)+ 方向 B(ClickHouse JDBC 引擎读 Doris 小字典表做 JOIN)。方向 A 是 BI 看板标配、Doris Routine Load 高频同步到本地表才是高性能看板正解。

    • 我们深入了 Kafka 引擎 + MV 管道 的本质——只订阅解析不存数据、At-Least-Once + ReplacingMergeTree = Exactly-Once 效果、kafka_skip_broken_messages 容错、两条写入路互为灾备。

    • 我们给出了 Docker Compose PoC + K8s/Helm 生产 两套部署方案的完整配置(端口表、Compose yaml、Helm values)+ 10 项全链路验证 checklist + 5 项故障演练清单。


    18.10 下章预告

    第 19 章《全系列复盘:ClickHouse 的 19 章知识地图》 将是整个 19 章系列的最后一章——把第 1 至 18 章所有知识点做成一张知识地图(ASCII 全景图),按"基础引擎 → 查询优化 → Replication → Distributed → 全链路"五大模块分类,标注每章覆盖的核心知识点 + 它们之间的依赖关系。同时给出 10 道跨章节综合思考题,让你检验自己是不是真的串起来了。


    18.11 官方参考资料

    主题官方文档地址
    Flink Connector(官方 AsyncSink + Exactly-Once) https://clickhouse.com/docs/integrations/connectors/data-ingestion/apache-flink
    Kafka Table Engine https://clickhouse.com/docs/engines/table-engines/integrations/kafka
    Paimon Table Engine(只读外表) https://clickhouse.com/docs/engines/table-engines/integrations/paimon
    Iceberg Table Engine https://clickhouse.com/docs/engines/table-engines/integrations/iceberg
    Hudi Table Engine https://clickhouse.com/docs/engines/table-engines/integrations/hudi
    Hive Table Engine https://clickhouse.com/docs/engines/table-engines/integrations/hive
    JDBC Table Engine(ClickHouse 读 Doris/MySQL/PG 等) https://clickhouse.com/docs/engines/table-engines/integrations/jdbc
    Named Collections(外表凭证集中管理) https://clickhouse.com/docs/operations/named-collections
    Doris JDBC Catalog Overviews https://doris.apache.org/docs/dev/lakehouse/catalogs/jdbc-catalog-overview/
    ClickHouse Kubernetes Operator https://clickhouse.com/docs/guides/sre/kubernetes-operator
    ClickHouse Helm Chart https://github.com/ClickHouse/clickhouse-helm
    Doris Helm Chart https://doris.apache.org/docs/dev/operator/install-cluster-helm
    ClickPipes(Cloud 托管 Kafka → ClickHouse 管道) https://clickhouse.com/docs/integrations/clickpipes
    Federation Queries FAQ(外部数据源 + JOIN) https://clickhouse.com/docs/resources/support-center/knowledge-base/general-faqs/federated

    18.12 版本取证说明

    本章全部 ClickHouse 侧内容(Flink Connector 参数、Kafka 引擎 SETTINGS、外表引擎命名规则、Named Collections、JDBC 引擎、Cloud 限制)均与官方文档逐字对照。Doris JDBC Catalog 内容基于 Doris 官方文档。下表按四档口径标注。

    #知识点来源版本取证
    1 Flink Connector maxBatchSize / maxInFlightRequests / flushInterval / Exactly-Once 条件 Flink Connector 文档 官方原文
    2 Flink Connector 版本兼容矩阵(Flink 1.17~2.0 / Java 11~17) 同上 官方原文
    3 Kafka 引擎 SETTINGS 全部参数(kafka_num_consumers / kafka_skip_broken_messages / kafka_handle_error_mode) Kafka 引擎文档 官方原文
    4 Kafka 引擎 + Materialized View 管道架构(引擎表不存数据、管道同步 INSERT) 同上 “Integrating with Materialized View” 段落 官方原文
    5 Paimon / Iceberg / Hudi / Hive 外表都是只读的、不支持 DDL 创建新表 各外表引擎文档开头"Provides a read-only integration" 官方原文
    6 Named Collections 统一凭证管理 Named Collections 文档 官方原文
    7 local_cache_for_remote_fs 对外表查询性能优化 Hive 引擎文档 “How to use local cache” 段落 官方原文
    8 Cloud 用户用 ClickPipes 替代 Kafka 引擎 Kafka 引擎文档开头 ClickHouse Cloud 提示 官方原文
    9 Doris JDBC Catalog 支持 ClickHouse + CREATE CATALOG 语法 Doris JDBC Catalog 文档 官方原文
    10 Doris JDBC Catalog 只适合小数据量查询、不适合一次性拉大量数据 同上 “Applicable Scenarios” 段落 官方原文
    11 Doris FE 需要 ClickHouse JDBC 驱动 jar 放在 jdbc_drivers/clickhouse/ 同上 ClickHouse-specific 段落 官方原文
    12 ClickHouse JDBC 引擎可以查 Doris(Doris FE 用 MySQL 协议) ClickHouse JDBC 引擎文档 + Doris FE MySQL 协议文档 官方原文 + 官方旁证
    13 Flink Connector Exactly-Once 要求目标表是 ReplicatedMergeTree(有 Keeper session) Flink Connector 文档 Exactly-Once 段落 官方原文
    14 ClickHouse Helm Chart 内置 Keeper 混部 / shard / remote_servers / macros / Named Collections ClickHouse Helm Chart repo README 官方原文
    15 双写入路(Flink Connector 直写 + Kafka 引擎管道)同时运行 + ReplacingMergeTree 去重 官方文档未显式提及;基于 Connector Exactly-Once + Kafka 引擎 At-Least-Once + ReplacingMergeTree 去重语义的工程推导 官方旁证
    16 全链路五组件角色边界(Kafka/Flink/Paimon/ClickHouse/Doris 分工) 各组件官方文档定位段落综合 通行约定
    17 Docker Compose / Helm values.yaml 配置示例 基于各组件官方镜像 + 配置模板 通行约定
    18 Doris JDBC Catalog “不适合一次性拉大量数据” 的具体阈值 官方未量化;按 JDBC 协议带宽 + 典型 OLAP 场景推算 通行约定

    18.13 实操清单

    #阶段操作前置验收标准
    1 看懂 画出完整五组件全链路架构图(Kafka → Flink → Paimon + ClickHouse → Doris) 读完 18.1 能说清每个组件的角色边界、数据流动方向
    2 看懂 对比 Flink Connector vs JDBC Sink vs Kafka 引擎管道三种写入方式 读完 18.2 能为你的场景选择推荐方案
    3 看懂 说出 Paimon / Iceberg / Hudi / Hive 外表都是只读的、怎么 JOIN 本地表 读完 18.3 能写出外表 + 本地表的跨存储 UNION ALL 查询
    4 动手 Docker Compose 起 Kafka + MinIO + ClickHouse + Doris + Superset 全栈 Docker compose up -d 后所有服务 healthy
    5 动手 Flink Job 提交:Kafka Source → ODS 清洗 → Paimon Sink + ClickHouse Connector Sink 步骤 4 + Flink 1.17+ Flink UI 两个 Sink 持续有 throughput
    6 动手 ClickHouse Kafka 引擎表 + MV 管道 → 消费同一 Kafka topic 步骤 4 MV 管道后台写入 vehicle_telemetry 表
    7 动手 PaimonS3 外表 → 本地 MV 管道 → vehicle_telemetry_archive 表 步骤 4 外表查询能读到 Flink 写的 Paimon 数据
    8 动手 Doris JDBC Catalog CREATE → SELECT ClickHouse 表 步骤 4 Doris 能查到 ClickHouse 的 vehicle_telemetry
    9 动手 Superset 建看板:Doris JDBC Catalog → 过去 7 天各城市车速趋势图 步骤 8 图表渲染正常
    10 动手 故障演练:停 1 Keeper / 1 ClickHouse replica / 1 Kafka broker → 全链路不中断 步骤 4 故障恢复后数据一致性验证通过
    11 值班 值班面板:Kafka lag / Flink checkpoint 状态 / ClickHouse replica health / Doris FE 状态 / S3 bucket size 读完 18.6.4 Grafana 面板覆盖 10 项验证 checklist + 故障告警
    12 值班 值班脚本:Paimon snapshot 大小趋势 → 冷数据归档策略检查 读完 18.3 Paimon snapshot 异常增长触发告警

    思考题

  • 你的业务方要求"BI 看板必须秒级响应"。现在你有两种选择:(A)Doris JDBC Catalog 直接 federated query ClickHouse 表(每次查询实时拉数据);(B)Doris Routine Load 持续同步 ClickHouse 数据到 Doris 本地表(预计算 cube)。各自的优缺点是什么?什么时候选哪个?(提示:数据量 + 查询模式 + 延迟要求三个维度)

  • 你的 Flink Connector 写入突然挂了(网络故障),但 Kafka 引擎管道正常——这时候 ClickHouse 写入不中断。Flink Connector 恢复后,同一条数据会不会被写两次?ReplacingMergeTree 怎么保证最终正确?如果是普通 MergeTree 而不是 ReplacingMergeTree 会怎么样?

  • 你想用 ClickHouse PaimonS3 外表 JOIN 本地 ReplicatedMergeTree 表,两边都是 1PB 级大表、JOIN key 是 vin。ClickHouse 会在哪个节点执行这个 JOIN?会不会跨 shard?性能瓶颈在哪里?(提示:外表查询 → 请求节点 → JOIN 策略 → 如果外表扫描大表会不会爆内存?)

  • ClickHouse Cloud 用户为什么"跳过 18.2.3 Kafka 引擎"?Cloud 里 Kafka → ClickHouse 怎么做?ClickPipes 和自建 Kafka 引擎比有什么优势和限制?


  • 本章完。下一章:第 19 章 全系列复盘:ClickHouse 的 19 章知识地图。

    赞(0)
    未经允许不得转载:171主机测评 » 面向车联网实时链路的 ClickHouse 全栈教程——第 18 章 全链路整合:ClickHouse + Paimon + Flink + Doris 生产部署实战
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址