3. 实时数据源接入:Kafka与Pulsar作为实时数据管道、Schema Registry管理特征Schema、Exactly-Once语义保证
好,咱们进入第三章。前两章我们把 Lakehouse 的基座和实时特征的整体架构聊清楚了。这一章,得落地了。
实时特征工程,第一步就是数据怎么进来。说白了,就是实时数据管道。你特征算得再快,数据进不来或者进错了,全是白搭。我在好几个项目里都见过,架构图画得漂漂亮亮,结果一上线,数据源那层先崩了。
这一章,我们就聚焦三个核心问题:用什么管道(Kafka vs Pulsar)、怎么管Schema(Schema Registry)、怎么保证不丢不重(Exactly-Once)。
3.1 实时数据管道选型:Kafka 还是 Pulsar?
先聊管道。现在市面上主流的实时消息队列,基本就是 Kafka 和 Pulsar 二选一。很多人问我:“到底选哪个?” 我的回答是:看场景。
核心观点:Kafka 适合吞吐优先、生态成熟的场景;Pulsar 适合多租户、存储计算分离、需要更灵活扩展的场景。
我个人习惯,先看团队熟悉度。如果团队全是 Kafka 老手,别硬上 Pulsar。但如果是从零开始建平台,我会认真考虑 Pulsar。
下面这张图,是我对两者核心架构差异的理解:
你看,Kafka 的 Broker 是“大包大揽”的,既管计算又管存储。Pulsar 则拆成了两层:Broker 只做计算,BookKeeper 专门存数据。这个区别在实际运维中影响很大。
我举个例子。有一次我们做活动大促,Kafka 集群需要扩容。因为数据在 Broker 本地磁盘,加机器后要等数据重平衡,这个过程慢得让人抓狂。Pulsar 呢?加 Broker 就行,数据在 BookKeeper 里,几乎秒级生效。
下面这个表格,是我自己总结的选型参考:
| 对比维度 | Kafka | Pulsar |
|---|---|---|
| 存储计算 | 耦合,Broker 本地盘 | 分离,Broker + BookKeeper |
| 扩缩容 | 慢,需数据重平衡 | 快,Broker 无状态 |
| 多租户 | 较弱,靠 Topic 隔离 | 原生支持,Namespace 级别 |
| 消息保留 | 基于 Offset,过期删除 | 基于 TTL 和 Backlog,更灵活 |
| 生态成熟度 | 极高,Flink/Spark 原生集成 | 较高,但部分工具需适配 |
| 延迟 | 毫秒级 | 毫秒级(略低) |
我的建议:如果你们公司已经重度依赖 Kafka 生态(比如 Kafka Streams、KSQL),别折腾。如果是从零建实时数仓,且未来可能有多个业务线共用,Pulsar 的多租户能力会让你省心很多。
3.2 Schema Registry:别让数据变成一团乱麻
管道选好了,数据进来了。但有个问题:你怎么知道进来的数据长什么样?
我见过最惨的一次事故,上游改了字段类型,从 int 改成了 string,下游消费端直接报错,特征 pipeline 停了两个小时。为什么?因为没有 Schema 管理。
Schema Registry 就是干这个的。它统一管理数据的“契约”。生产者发布数据时,必须带上 Schema;消费者读取时,从 Registry 拉取 Schema 来校验和反序列化。
目前最主流的是 Confluent Schema Registry,支持 Avro、Protobuf、JSON Schema。我个人最推荐 Avro,原因很简单:体积小、序列化快、Schema 演化能力强。
看一个实际例子。我们用 Avro 定义一个用户点击事件的特征 Schema:
{
"type": "record",
"name": "UserClickEvent",
"namespace": "com.feature.lakehouse",
"fields": [
{"name": "user_id", "type": "string"},
{"name": "item_id", "type": "string"},
{"name": "click_time", "type": "long", "logicalType": "timestamp-millis"},
{"name": "page_type", "type": ["null", "string"], "default": null},
{"name": "duration_ms", "type": "int", "default": 0}
]
}
注意看 page_type 字段,我用了 ["null", "string"],并给了默认值 null。这就是 Schema 演化的关键——向后兼容。老版本的消费者读到新数据时,遇到不认识或者为空的字段,可以用默认值填充,不会崩。
避坑指南:我曾经遇到一个团队,所有 Schema 都设成“完全兼容”,结果字段删了又加、加了又改,Registry 里积压了几百个版本。消费端每次启动都要拉取全量 Schema,性能急剧下降。我的建议是:明确兼容性策略,该用 BACKWARD 就用 BACKWARD,别一味追求 FORWARD。
在实时特征场景下,Schema Registry 的典型工作流是这样的:
流程很简单:Producer 先向 Registry 注册 Schema,拿到 Schema ID。然后发消息时,只带这个 ID 和二进制数据,不重复传 Schema。Consumer 根据 ID 从 Registry 拉取 Schema,反序列化。这样既省带宽,又保证双方契约一致。
3.3 Exactly-Once 语义:实时特征的“圣杯”
好,管道有了,Schema 管好了。但还有一个终极问题:数据会不会丢?会不会重复?
在实时特征场景里,重复计算一个特征,可能导致推荐结果异常、风控误判。丢一条数据,可能漏掉一次关键转化。所以,Exactly-Once 语义是必须的。
说白了,Exactly-Once 就是“每条消息只被处理一次,且结果只生效一次”。但实现起来,没那么简单。
我把它拆成三个层面来看:
- Source 端:保证消息不丢。Kafka 通过 ACK 机制和 ISR 副本,Pulsar 通过 BookKeeper 的写入确认。
- 计算端:保证状态一致性。Flink 的 Checkpoint + 两阶段提交是经典方案。
- Sink 端:保证写入幂等。Lakehouse 表写入必须是幂等的,同一个批次写两次,结果一样。
这里我重点讲一下 Flink + Kafka 的 Exactly-Once 实现,因为这是实时特征工程最常用的组合。
Flink 的 Checkpoint 机制,说白了就是定期给整个作业的状态拍一张“快照”。当作业失败重启时,从最近一次成功的 Checkpoint 恢复。配合 Kafka 的 两阶段提交 Sink,就能做到端到端的 Exactly-Once。
看一段核心配置代码:
// Flink Kafka Producer 开启 Exactly-Once
Properties kafkaProps = new Properties();
kafkaProps.setProperty("bootstrap.servers", "localhost:9092");
kafkaProps.setProperty("transaction.timeout.ms", "60000");
FlinkKafkaProducer<String> kafkaSink = new FlinkKafkaProducer<>(
"feature-output-topic",
new SimpleStringSchema(),
kafkaProps,
FlinkKafkaProducer.Semantic.EXACTLY_ONCE
);
// 开启 Checkpoint
env.enableCheckpointing(5000); // 每 5 秒一次 Checkpoint
env.getCheckpointConfig().setMinPauseBetweenCheckpoints(2000);
env.getCheckpointConfig().setCheckpointTimeout(60000);
env.getCheckpointConfig().setMaxConcurrentCheckpoints(1);
关键点:Semantic.EXACTLY_ONCE 这个枚举值,告诉 Flink 使用两阶段提交协议。Flink 在 Checkpoint 完成时,预提交事务;当所有 Sink 都确认后,再正式提交。如果作业挂了,未提交的事务会被回滚,不会产生脏数据。
但这里有个坑。我曾经在一个项目里,Kafka 的 transaction.timeout.ms 设得太短,而 Flink 的 Checkpoint 因为数据量大,偶尔超时。结果事务被 Kafka 强制中止,作业频繁报错。后来我把这个值调大到 60 秒,才稳定下来。
注意:Exactly-Once 不是免费的。它会带来额外的性能开销(事务协调、状态快照)。如果你的场景允许偶尔丢几条数据(比如非关键统计指标),用 At-Least-Once + 下游去重,性价比更高。
最后,我把三种语义的对比整理成一张表,方便你选型时参考:
| 语义级别 | 保证 | 性能开销 | 适用场景 |
|---|---|---|---|
| At-Most-Once | 最多一次,可能丢 | 最低 | 日志采集、监控指标(可容忍少量丢失) |
| At-Least-Once | 至少一次,可能重复 | 中等 | 大部分实时 ETL(下游需幂等或去重) |
| Exactly-Once | 不丢不重 | 最高 | 金融风控、实时特征、交易计费 |
嗯,这一章的内容就到这里。实时数据源接入,说白了就是三件事:选对管道、管好 Schema、保证语义。这三件事做扎实了,后面的特征计算才能站得稳。