ClickHouse系列(六):Kafka 到 ClickHouse 的生产级写入架构

定位:实时写入链路,解决 Kafka 消费慢、倾斜、积压问题


Kafka Engine 的真实定位

先纠正一个常见误解:ClickHouse 的 Kafka 表引擎不是存储表,而是消费管道

sql 复制代码
-- 这张表不存储任何数据,它是一个 Kafka Consumer
CREATE TABLE kafka_events (
    event_time DateTime,
    user_id    UInt64,
    action     String
) ENGINE = Kafka
SETTINGS
    kafka_broker_list = 'broker1:9092,broker2:9092',
    kafka_topic_list = 'user_events',
    kafka_group_name = 'ch_consumer_group',
    kafka_format = 'JSONEachRow';

直接 SELECT * FROM kafka_events 会消费数据并提交 offset------数据读完就没了。这不是 Bug,而是设计如此。Kafka 表的唯一职责是:从 Kafka 拉取数据,转交给下游

正确的架构是三层结构:

复制代码
Kafka Topic
    → Kafka Engine 表(消费管道)
    → Materialized View(转换层)
    → MergeTree 表(存储层)

ClickHouse 的 Kafka 消费模型

理解消费模型才能做好调优。ClickHouse 的 Kafka 消费流程:

复制代码
1. 后台调度线程定时唤醒(kafka_poll_timeout_ms)
2. Consumer 从 Kafka 拉取一批消息
3. 累积到 kafka_max_block_size 行 或 kafka_flush_interval_ms 超时
4. 形成一个 Block,通过 Materialized View 写入目标表
5. 写入成功后提交 Kafka offset
6. 回到步骤 1

关键参数及其含义:

参数 默认值 说明
kafka_max_block_size 65536 每批最多拉取的行数,直接决定 Part 大小
kafka_poll_timeout_ms 0 单次 poll 的超时时间
kafka_flush_interval_ms 7500 强制刷盘的时间间隔
kafka_num_consumers 1 消费者线程数
kafka_thread_per_consumer 0 是否每个 consumer 独立线程

一个容易踩的坑:offset 提交是在写入成功之后。如果 MV 写入失败(比如目标表 Part 过多),offset 不会提交,下次会重新消费,可能导致重复数据。

kafka_num_consumers 为什么看起来没生效

这是生产环境中最常见的困惑之一。设了 kafka_num_consumers = 8,但消费速度没有明显提升。

根因:Kafka 的分区数限制了并行度。

复制代码
Kafka Topic: 6 个分区
kafka_num_consumers = 8

实际效果:只有 6 个 consumer 能分到分区,2 个空闲

规则很简单:有效消费者数 = min(kafka_num_consumers, topic 分区数)

但还有第二个陷阱------kafka_thread_per_consumer

sql 复制代码
-- 默认值 0:所有 consumer 共享一个线程,轮询执行
-- 设为 1:每个 consumer 独立线程,真正并行
CREATE TABLE kafka_events (...)
ENGINE = Kafka
SETTINGS
    kafka_num_consumers = 8,
    kafka_thread_per_consumer = 1;  -- 必须设为 1 才能真正并行

验证消费者状态:

sql 复制代码
-- 查看 Kafka 表的消费情况
SELECT
    name, value
FROM system.metrics
WHERE metric LIKE '%Kafka%';

-- 查看后台任务
SELECT * FROM system.kafka_consumers;

同时在 Kafka 侧确认 consumer group 状态:

bash 复制代码
kafka-consumer-groups.sh --bootstrap-server broker1:9092 \
    --describe --group ch_consumer_group

如果看到某些分区的 consumer 为空,说明 consumer 数量超过了分区数。

Kafka 分区倾斜的根因分析

消费积压不一定是 ClickHouse 慢,可能是 Kafka 侧的分区倾斜。

典型症状:大部分分区 lag 为 0,但某几个分区 lag 持续增长。

根因分析

原因 说明
Producer 使用了固定 Key 热点 Key 导致数据集中在少数分区
分区数太少 无法充分利用 consumer 并行度
消息大小不均 某些分区的消息体积远大于其他分区
ClickHouse 单 consumer 处理慢 MV 中有复杂计算拖慢了特定 consumer

诊断方法:

bash 复制代码
# 查看每个分区的 offset 和 lag
kafka-consumer-groups.sh --bootstrap-server broker1:9092 \
    --describe --group ch_consumer_group

# 输出示例:
# TOPIC      PARTITION  CURRENT-OFFSET  LOG-END-OFFSET  LAG
# events     0          1000000         1000050         50
# events     1          800000          950000          150000  ← 倾斜
# events     2          1000000         1000030         30

解决方案:

sql 复制代码
-- 1. Kafka 侧:增加分区数(需要 Kafka 管理员操作)
-- 2. Producer 侧:使用 Round-Robin 或改进分区策略
-- 3. ClickHouse 侧:确保 consumer 数 = 分区数,且开启独立线程

10w+/s 消费能力的参数组合

要达到每秒 10 万条以上的消费能力,需要 Kafka 侧和 ClickHouse 侧协同调优。

前提条件

  • Kafka Topic 至少 16 个分区
  • ClickHouse 节点有足够的 CPU 核心(≥ 16)
  • 网络带宽充足

推荐参数组合

sql 复制代码
CREATE TABLE kafka_events (
    event_time DateTime,
    user_id    UInt64,
    action     LowCardinality(String),
    payload    String
) ENGINE = Kafka
SETTINGS
    kafka_broker_list = 'broker1:9092,broker2:9092,broker3:9092',
    kafka_topic_list = 'user_events',
    kafka_group_name = 'ch_prod_group',
    kafka_format = 'JSONEachRow',
    kafka_num_consumers = 16,
    kafka_thread_per_consumer = 1,
    kafka_max_block_size = 524288,
    kafka_flush_interval_ms = 15000,
    kafka_poll_timeout_ms = 1000;

参数解读:

参数 理由
kafka_num_consumers 16 匹配 Kafka 分区数
kafka_thread_per_consumer 1 真正并行消费
kafka_max_block_size 524288 约 50 万行一批,减少 Part 数量
kafka_flush_interval_ms 15000 15 秒刷一次,给足累积时间
kafka_poll_timeout_ms 1000 每次 poll 等待 1 秒

注意权衡kafka_max_block_sizekafka_flush_interval_ms 越大,吞吐越高,但数据可见延迟也越大。如果业务要求秒级可见,需要适当调小。

Kafka → Raw → MV 的标准写入链路

生产环境推荐的标准三层架构:

sql 复制代码
-- 第一层:Kafka 消费管道
CREATE TABLE kafka_raw (
    event_time DateTime,
    user_id    UInt64,
    action     LowCardinality(String),
    properties String  -- 原始 JSON
) ENGINE = Kafka
SETTINGS
    kafka_broker_list = 'broker1:9092,broker2:9092',
    kafka_topic_list = 'user_events',
    kafka_group_name = 'ch_prod_group',
    kafka_format = 'JSONEachRow',
    kafka_num_consumers = 8,
    kafka_thread_per_consumer = 1,
    kafka_max_block_size = 131072;

-- 第二层:存储表(落盘)
CREATE TABLE events_raw (
    event_time DateTime,
    event_date Date DEFAULT toDate(event_time),
    user_id    UInt64,
    action     LowCardinality(String),
    properties String
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(event_date)
ORDER BY (action, user_id, event_time)
TTL event_date + INTERVAL 90 DAY
SETTINGS index_granularity = 8192;

-- 第三层:Materialized View(桥接)
CREATE MATERIALIZED VIEW mv_kafka_to_raw TO events_raw AS
SELECT
    event_time,
    toDate(event_time) AS event_date,
    user_id,
    action,
    properties
FROM kafka_raw;

为什么需要 MV 而不是直接写?

MV 在这里的角色是"自动触发器"------每当 Kafka 表拉取到一批数据,MV 自动将数据 INSERT 到目标表。你还可以在 MV 中做轻量转换:

sql 复制代码
-- 带转换的 MV:提取 JSON 字段、过滤脏数据
CREATE MATERIALIZED VIEW mv_kafka_to_raw TO events_raw AS
SELECT
    event_time,
    toDate(event_time) AS event_date,
    user_id,
    action,
    JSONExtractString(properties, 'source') AS source
FROM kafka_raw
WHERE user_id > 0 AND event_time > '2020-01-01';

多目标写入:一个 Kafka 表可以挂多个 MV,实现一份数据写入多张表:

sql 复制代码
-- MV1:写入明细表
CREATE MATERIALIZED VIEW mv_to_detail TO events_detail AS
SELECT * FROM kafka_raw;

-- MV2:写入聚合表
CREATE MATERIALIZED VIEW mv_to_agg TO events_hourly_agg AS
SELECT
    toStartOfHour(event_time) AS hour,
    action,
    count() AS cnt,
    uniqExact(user_id) AS uv
FROM kafka_raw
GROUP BY hour, action;

生产环境排障清单

当 Kafka 消费出现积压时,按以下顺序排查:

步骤 检查项 命令/SQL
1 Kafka 侧 lag kafka-consumer-groups.sh --describe
2 ClickHouse Part 数量 SELECT partition, count() FROM system.parts WHERE active GROUP BY partition
3 是否触发 Too many parts grep 'Too many parts' /var/log/clickhouse-server/clickhouse-server.err.log
4 Merge 是否积压 SELECT * FROM system.merges
5 MV 是否有报错 SELECT * FROM system.query_log WHERE type = 'ExceptionWhileProcessing' AND query LIKE '%kafka%'
6 网络/磁盘瓶颈 iostat -x 1 / iftop

紧急恢复步骤

sql 复制代码
-- 1. 暂停消费(Detach Kafka 表)
DETACH TABLE kafka_raw;

-- 2. 手动合并积压的 Part
OPTIMIZE TABLE events_raw FINAL;

-- 3. 确认 Part 数量恢复正常后重新挂载
ATTACH TABLE kafka_raw;

掌握了 Kafka 到 ClickHouse 的完整链路,你就能构建一个稳定的实时写入管道。核心原则只有一个:控制每批写入的数据量,让 Part 数量始终在安全范围内


下一篇:第 7 篇 - 分布式表与集群架构设计

相关推荐
Bohemian—Rhapsody2 小时前
麒麟v10-arm架构部署rabbitmq
arm开发·架构·rabbitmq
猫仍在2 小时前
Playwright 架构UI 自动化质量保障平台
ui·架构·自动化
2603_954708312 小时前
微电网主从控制架构:集中式调度与分布式执行的协同机制
人工智能·分布式·物联网·架构·系统架构·能源
小猿姐5 小时前
# KubeBlocks for MSSQL 高可用实现
数据库·架构·sql server
古译汉书10 小时前
【IoT死磕系列】Day 9:架构一台“自动驾驶物流车”,看8种协议如何协同作战
网络·arm开发·单片机·物联网·tcp/ip·架构·自动驾驶
KaneLogger11 小时前
从传统笔记到 LLM 驱动的结构化 Wiki
人工智能·程序员·架构
斯外戈的小白11 小时前
【Agent】LangChain 1.0架构
架构·langchain
小橘子83111 小时前
(学习)Claude Code 源码架构深度解析
学习·程序人生·架构
C'ᴇsᴛ.小琳 ℡13 小时前
架构技术演进的方向
架构