在 Kafka 数据上使用 SQL 不需要流式引擎
Source: Dev.to
在 Kafka 上使用 SQL:数据并不一定需要流处理引擎
在过去的几年里,Kafka 已经从一个单纯的日志系统演变为完整的事件流平台。随着 ksqlDB、Kafka Streams、以及 Kafka Connect 的成熟,越来越多的团队开始用 SQL 来直接查询和处理 Kafka 中的数据,而不必再额外部署 Flink、Spark 或 Beam 等流处理引擎。
本文将阐述:
- 为什么在很多场景下,直接在 Kafka 上使用 SQL 已经足够;
- ksqlDB 提供的 推查询(push query) 与 拉查询(pull query) 的区别;
- 实际案例展示如何用纯 SQL 完成常见的实时分析需求;
- 何时仍然需要外部流处理引擎的补充。
1️⃣ 为什么可以直接在 Kafka 上使用 SQL?
-
持久化的、可重放的日志
Kafka 的主题(topic)本质上是一个有序、持久化的日志。只要保留足够的 retention 时间,所有历史事件都可以随时被重新读取,这为 SQL 查询提供了可靠的数据来源。 -
内置的状态管理
ksqlDB 在后台使用 RocksDB 来维护物化视图(materialized view),这相当于在 Kafka 之上自动搭建了一个 键值存储,无需自行实现状态管理。 -
统一的序列化/反序列化
通过 Schema Registry,ksqlDB 能够自动解析 Avro、Protobuf、JSON Schema,让 SQL 语句直接操作结构化字段,而不必手动处理字节流。 -
低延迟的查询模型
- 推查询(Push Query):类似于
SELECT * FROM orders EMIT CHANGES;,实时推送新产生的记录。 - 拉查询(Pull Query):对已物化的视图执行点查询,例如
SELECT * FROM orders_by_customer WHERE customer_id = 'C123';,返回最新的快照。
- 推查询(Push Query):类似于
这些特性让 SQL 成为在 Kafka 上进行 实时监控、仪表盘、告警 的理想工具。
2️⃣ ksqlDB 的核心概念
| 概念 | 说明 | 示例 |
|---|---|---|
| 流(STREAM) | 代表一个永远增长的、不可变的记录序列。 | CREATE STREAM page_views (user_id STRING, url STRING) WITH (kafka_topic='page_views', value_format='AVRO'); |
| 表(TABLE) | 代表一个随时间更新的键值映射,内部使用 压缩主题(compact topic)。 | CREATE TABLE user_profiles (user_id STRING PRIMARY KEY, age INT) WITH (kafka_topic='user_profiles', value_format='AVRO'); |
| 物化视图 | ksqlDB 自动为 TABLE 或 聚合查询 创建本地状态存储,以支持快速拉查询。 | CREATE TABLE orders_by_customer AS SELECT customer_id, COUNT(*) AS order_cnt FROM orders GROUP BY customer_id; |
| 推查询 | 持续输出匹配的记录流。 | SELECT * FROM orders EMIT CHANGES; |
| 拉查询 | 对物化视图执行一次性查询,返回最新快照。 | SELECT * FROM orders_by_customer WHERE customer_id='C123'; |
3️⃣ 实际案例:实时订单监控
假设我们有一个名为 orders 的主题,记录了每笔订单的 order_id、customer_id、amount、order_time。我们希望实现以下需求:
- 实时监控每分钟的订单总额(推查询);
- 随时查询某个客户的累计订单数(拉查询);
- 对异常订单(金额 > 10,000)触发告警(推查询 + 过滤)。
下面是对应的 ksqlDB 语句(代码块保持原样,不翻译):
-- 1. 创建原始流
CREATE STREAM orders (
order_id STRING,
customer_id STRING,
amount DOUBLE,
order_time BIGINT
) WITH (
kafka_topic='orders',
value_format='AVRO',
timestamp='order_time'
);
-- 2. 每分钟聚合订单总额(推查询)
CREATE TABLE minute_sales AS
SELECT
WINDOWSTART AS window_start,
WINDOWEND AS window_end,
SUM(amount) AS total_amount
FROM orders
WINDOW TUMBLING (SIZE 1 MINUTE)
GROUP BY WINDOWSTART, WINDOWEND;
-- 3. 查询某客户累计订单数(拉查询)
CREATE TABLE orders_by_customer AS
SELECT
customer_id,
COUNT(*) AS order_cnt,
SUM(amount) AS total_spent
FROM orders
GROUP BY customer_id;
-- 4. 过滤异常订单并推送告警
CREATE STREAM high_value_orders AS
SELECT *
FROM orders
WHERE amount > 10000;
推查询示例(实时监控)
SELECT window_start, window_end, total_amount FROM minute_sales EMIT CHANGES;
拉查询示例(随时查询)
SELECT order_cnt, total_spent FROM orders_by_customer WHERE customer_id='C123';
通过上述几行 SQL,我们已经完成了 实时聚合、点查询、异常检测,全部在 Kafka 上完成,无需额外的流处理框架。
4️⃣ 何时仍然需要外部流处理引擎?
虽然 ksqlDB 能满足大多数 实时分析 与 轻量级业务逻辑,但在以下场景下,仍然建议使用 Flink、Spark Structured Streaming 或 Beam:
| 场景 | 原因 |
|---|---|
| 复杂的窗口语义(如会话窗口、跨键聚合) | ksqlDB 的窗口功能相对有限,外部引擎提供更丰富的窗口算子。 |
| 自定义函数或机器学习模型 | 需要在流处理中调用 Java/Scala/Python 自定义代码,ksqlDB 只能使用内置或 UDF。 |
| 大规模的状态后端 | 当状态大小超过单机 RocksDB 能容纳的范围,需要分布式状态后端(如 Flink 的 RocksDB StateBackend + HA)。 |
| 多流/多表联结(跨主题、跨键的复杂 Join) | ksqlDB 支持的 Join 类型受限,外部引擎提供更灵活的多流 Join。 |
| 容错与一致性要求极高 | 某些业务需要 exactly‑once 语义和事务级别的控制,Flink 等框架在这方面更成熟。 |
在这些情况下,Kafka 仍然是底层的持久化层,而流处理引擎负责更高级的计算逻辑。两者可以组合使用:先用 ksqlDB 完成快速的实时监控和轻量查询,再在需要时将数据流向 Flink/Spark 进行深度处理。
5️⃣ 小结
- SQL 已经足够:对于大多数实时监控、仪表盘、告警等业务场景,直接在 Kafka 上使用 ksqlDB 的 推查询 与 拉查询 就能满足需求,省去了部署、运维额外流处理集群的成本。
- 状态自动管理:ksqlDB 通过内部的 RocksDB 为物化视图提供持久化状态,让查询保持低延迟。
- 适度扩展:当业务对窗口、联结、机器学习或大规模状态有更高要求时,仍然可以在 Kafka 之上引入 Flink、Spark 等流处理引擎,实现 “SQL + 计算引擎” 的混合模式。
通过合理评估业务需求与技术特性,你可以决定是 仅使用 ksqlDB,还是 结合外部流处理引擎,从而在成本、复杂度与性能之间找到最佳平衡点。
介绍
流处理引擎解决了一个真实的问题:对无限数据进行连续计算。Flink、ksqlDB 和 Kafka Streams 为团队提供了一种无需编写自定义消费者即可对事件流运行类 SQL 查询的方式。
该解决方案的运营成本已被广泛认可。Confluent 的官方文档指出,Flink “在部署和集群运维方面存在困难,例如调优性能或解决检查点失败”,并且“使用 Flink 的组织往往需要专门的专家团队来开发和维护”。
对于团队对 Kafka 数据提出的大量问题,存在一种更简单的架构:在对象存储的不可变分段上进行 SQL 查询。
常见的 Kafka 查询
在生产环境的调试会话和运维审查中,问题往往是重复的:
- 这个 topic 现在有什么数据?
- 在某个故障窗口期间发生了什么?
- 这条带有特定 key 的消息在哪里?
- 所有分区仍在产生数据吗?
这些并不是流处理问题,而是对历史数据的有界查询。它们只运行一次,随后结束,不需要窗口、watermarks、checkpoints 或状态恢复。
Kafka的存储模型
- Kafka 并不单独持久化每条记录,而是将它们追加到日志段(log segments),并根据大小或时间滚动这些段。
- 每个分区是一系列有序、不可变的记录。段一旦关闭,就不可再修改。
- Kafka 维护稀疏索引,使读取器能够高效地按偏移量和时间戳定位。每个段文件都有轻量级的偏移量索引和时间戳索引,消费者可以直接跳转到特定消息位置,而无需扫描整个文件。
- 保留策略会删除整个段;压缩则会重写段。这意味着 Kafka 数据已经像 SQL‑on‑files 数据集一样组织好,唯一的区别在于文件所在的位置。
自 Kafka 3.6.0 起,分层存储(tiered storage)允许这些段存放在对象存储(例如 S3)中。到 Kafka 3.9.0,该功能已达到生产就绪水平,实现了在不改变数据模型的前提下,将持久性与计算解耦。
流式引擎的开销
流式引擎为大多数查询从未使用的功能付费:
- 分布式状态后端
- 协调检查点
- 水印跟踪
- 长时间运行的集群操作
这些成本在持续聚合、连接和实时推理时是合理的,但在“显示最近的 10 条消息”这种场景下则是浪费。
生产经验
Riskified 从 ksqlDB 迁移到 Flink,指出 ksqlDB 对模式演进的严格限制使其在真实生产用例中不切实际,并且运维复杂度导致需要更多地“与系统对抗”而不是“与系统协作”。
来自 Confluent 和 Redpanda 的供应商调查显示,约 56 % 的所有 Kafka 集群的吞吐量在 1 MB/s 或以下。大多数 Kafka 使用场景是小数据,但团队却承担了大数据的运维成本。
不可变段上的查询规划
如果 Kafka 数据以不可变段的形式存储,并带有稀疏索引,查询它的方式与其他 SQL‑on‑files 工作负载相同。
查询规划步骤
- 将主题解析为段文件。
- 根据时间戳或偏移量元数据进行过滤。
- 只读取相关段。
- 应用谓词并返回结果。
没有消费者组,没有偏移提交,也没有流式作业生命周期。
示例查询
-- Last 10 messages
SELECT * FROM orders TAIL 10;
-- Time‑bounded scan
SELECT * FROM orders
WHERE ts BETWEEN '2026-01-08 09:00' AND '2026-01-08 09:05';
-- Key lookup with recent window
SELECT * FROM orders
WHERE key = 'order-12345'
AND ts >= now() - interval '24 hours';
这些是具有 SQL 语义的索引文件访问,而不是流处理。
性能考虑
对象存储比经纪人本地磁盘慢;远程存储的延迟通常高于本地块存储。对于大多数调试和运维工作流来说,一到两秒的延迟是可以接受的,而等待数分钟来部署或重启流式作业则不可接受。
如果需要亚秒级的持续结果,请使用流式引擎。界限很明确。
成本管理
SQL 在对象存储上的主要风险是 无限扫描。对象存储的定价基于存储的数据量和 API 调用次数。
一个负责任的系统应让每个查询报告:
- 将读取多少段
- 将扫描多少字节
- 估计的请求费用
没有时间限制的查询应要求明确的选择加入,使成本成为有意识的决定,而不是意外。
何时使用流处理引擎
流处理引擎仍然是以下场景的合适工具:
- 持续聚合
- 对实时流的连接
- 实时评分
- 精确一次输出
大多数 Kafka 的交互并不属于上述情况。它们是查询和检查,由于缺乏更好的接口,被迫迁移到流处理基础设施中。
结论
一旦 Kafka 数据以不可变段的形式持久化,SQL 就成为更简洁的工具。大多数团队并不需要流处理引擎来回答 Kafka 的问题;他们需要一种干净、受限的方式来查询不可变数据。对 Kafka 段使用 SQL 正好提供了这种能力。
阅读更深入的文章请访问 .