实时计算的萌芽:消息队列选型与流处理初探
从离线到实时,第一步往往最难。我记得2016年刚接手一个实时风控项目时,团队还在用轮询数据库的方式“假装实时”。结果呢?延迟高、压力大,还被业务方吐槽“这跟离线报表有啥区别”。
说白了,实时计算的萌芽,核心就三件事:消息队列选型、流处理概念、架构雏形。这一章咱们就把这三件事掰开揉碎聊透。
2.1 消息队列:实时计算的“主动脉”
消息队列是实时系统的第一道关卡。数据从业务系统产生,先丢进队列,再由下游消费。这个模式看似简单,但选型不对,后面全是坑。
2.1.1 Kafka:老牌劲旅,生态为王
Kafka 是我用得最多的消息队列。它的设计哲学很直接:顺序写磁盘、批量压缩、零拷贝。吞吐量极高,单机能扛几十万条/秒。
Kafka 核心优势
- 吞吐量:分区并行,线性扩展
- 持久化:消息落盘,可回溯重放
- 生态:Flink、Spark、Hadoop 原生支持
但 Kafka 也有短板。我在项目中遇到过一个问题:分区数太多时,rebalance 会引发“惊群效应”。消费者一挂,整个集群抖半天。另外,Kafka 不支持消息优先级、死信队列这些高级特性,得自己搭。
2.1.2 Pulsar:后起之秀,架构更灵活
Pulsar 是后来者,但设计上更“现代”。它把计算和存储分离:Broker 只管计算,BookKeeper 管存储。好处很明显——扩缩容不用搬数据。
| 特性 | Kafka | Pulsar |
|---|---|---|
| 存储架构 | 计算存储耦合 | 计算存储分离 |
| 消息模型 | 分区日志 | Topic + 订阅分层 |
| 延迟 | 毫秒级 | 亚毫秒级 |
| 多租户 | 需手动隔离 | 原生支持 |
| 运维复杂度 | 中等 | 较高(多组件) |
我个人习惯:如果团队 Kafka 经验丰富,业务场景偏批处理+流处理混合,选 Kafka 更稳妥。如果对延迟要求极高、需要多租户隔离,Pulsar 值得一试。
避坑指南:我曾经在一个 IoT 项目里选了 Pulsar,结果因为 BookKeeper 的 IO 配置没调好,写入延迟反而比 Kafka 还高。后来发现是磁盘类型不对——Pulsar 对 SSD 有硬性要求,HDD 根本扛不住。
2.2 流处理概念:从“批”到“流”的思维转变
很多刚接触实时计算的同学,容易把流处理理解成“小批处理”。其实不然。流处理的核心是“无界数据”——数据源源不断,没有终点。
2.2.1 事件时间 vs 处理时间
这是流处理里最容易踩的坑。事件时间是数据实际发生的时间,处理时间是数据到达系统的时间。两者往往不一致——网络延迟、系统故障都会导致偏差。
举个例子:用户下单时间是 10:00:00,但消息队列积压了 5 秒,系统在 10:00:05 才收到。如果按处理时间统计,这笔订单就算到了 10:00:05 这一分钟。结果呢?报表对不上,业务方找上门。
我的建议:永远优先使用事件时间。Flink 和 Kafka Streams 都支持 watermark 机制来处理乱序数据。但要注意,watermark 的延迟阈值别设太大,否则实时性就没了。
2.2.2 状态 vs 无状态
流处理算子分两类:无状态(如 filter、map)和有状态(如 window、join)。无状态好理解,每条数据独立处理。有状态就复杂了——你得记住“之前发生了什么”。
我在项目中遇到过一个问题:状态后端选型。Flink 默认用 RocksDB,但 RocksDB 的读写性能受磁盘 IO 影响很大。如果状态量不大(百 GB 以内),用 Heap State Backend 反而更快。
// Flink 状态后端配置示例
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setStateBackend(new HashMapStateBackend());
env.getCheckpointConfig().setCheckpointStorage("hdfs://namenode:8020/flink-checkpoints");
2.3 Lambda 架构初探:批流一体的“过渡方案”
Lambda 架构是 Nathan Marz 提出的,核心思想很简单:批处理保证准确性,流处理保证低延迟。两条路径各自计算,最后合并结果。
2.3.1 架构三件套
- 批处理层:用 Spark/Hive 跑全量数据,产出准确结果
- 流处理层:用 Flink/Kafka Streams 处理实时增量,产出近似结果
- 服务层:合并批和流的结果,对外提供查询
听起来很完美对吧?但实际落地时,问题不少。
我曾经踩过的坑:批处理和流处理的结果合并逻辑极其复杂。比如一个用户今天的累计消费金额,批处理算出来是 1000 元,流处理算出来是 980 元。合并时怎么处理?覆盖?累加?还是取最大值?每种策略都有业务含义,得跟产品经理反复确认。
2.3.2 Lambda 的“原罪”
说白了,Lambda 架构最大的问题是维护两套代码。批处理一套逻辑,流处理一套逻辑,bug 得修两次。而且结果不一致时,排查成本极高。
后来社区提出了 Kappa 架构——只用流处理,通过重放历史数据来保证准确性。但 Kappa 对消息队列的存储能力要求很高,不是所有场景都适用。
2.4 本章小结:选型决策树
说了这么多,最后给一张决策图,帮你快速判断该选什么。
嗯,这张图基本覆盖了主流选型路径。但记住,没有银弹。我见过不少团队盲目追新,结果运维成本翻倍。选型时多问问自己:我的团队能 hold 住吗?业务真的需要这么低的延迟吗?