2、实时数据源接入:消息队列(Kafka)核心概念、Topic与Partition、生产者与消费者模型
好,咱们正式开始聊实时数据源接入。说到实时计算,第一个绕不开的坎儿就是消息队列。市面上消息队列不少,但Kafka几乎成了实时数据管道的事实标准。我这些年做过的实时项目,十有八九底层都跑着Kafka。为什么大家都选它?说白了,就三个字——扛得住。
你想想看,实时数据流就像一条永不停歇的河。数据从四面八方涌进来,下游的消费方可能有好几个。如果没有一个足够健壮的中间层,系统分分钟被冲垮。Kafka就是那个能帮你「削峰填谷」的大水库。
核心认知:Kafka不是传统意义上的消息队列,它更像一个「分布式提交日志」。每条消息都被持久化到磁盘,并且可以重复消费。这个设计理念,让它天然适合做实时数据管道。
2.1 核心概念:从一条消息说起
我们先从最基础的概念讲起。Kafka里最基本的数据单元叫消息(Record)。一条消息包含Key、Value、Timestamp和可选的Headers。Key用来决定消息发到哪个分区,Value就是真正的业务数据,Timestamp记录消息产生的时间。
我记得刚接触Kafka时,有个问题困扰了我很久——为什么消息要有Key?后来在做一个用户行为追踪的项目时才真正理解。当时需要保证同一个用户的操作日志按顺序处理,如果没有Key,消息会随机分配到不同分区,顺序就乱了。加上用户ID作为Key后,同一个用户的所有操作都进了同一个分区,顺序自然就保证了。
我的习惯:只要业务上需要保证消息的顺序性,我一定会给消息设置Key。哪怕Key的取值很简单,也比没有强。
消息发出去之后,存在哪里?存在Broker上。Broker就是Kafka集群中的一台服务器。一个集群通常由多个Broker组成,这样一台挂了,其他Broker还能顶上。我见过不少新手把Kafka部署成单节点,结果Broker一重启,整个实时管道就断了。嗯,这里要注意——生产环境至少3个Broker起步。
2.2 Topic与Partition:数据怎么分片
消息到了Broker,不是胡乱堆在一起的。Kafka用Topic(主题)来归类消息。你可以把Topic想象成一个文件夹,同类消息都往里面放。比如订单数据放「order_topic」,用户点击日志放「click_topic」。
但光有Topic还不够。一个Topic里的数据量可能非常大,单台机器存不下,也处理不过来。所以Kafka又把Topic切成了多个Partition(分区)。每个Partition就是一个有序的、不可变的消息序列。
| 概念 | 类比 | 说明 |
|---|---|---|
| Topic | 文件夹 | 归类消息的逻辑容器 |
| Partition | 文件里的分页 | 物理存储单元,支持并行读写 |
| Offset | 页码 | 消息在分区内的唯一编号 |
Partition的数量决定了Kafka的并行处理能力。为什么这么说?因为一个Partition只能被同一个消费组里的一个消费者线程消费。如果你有8个Partition,但只有2个消费者,那就有6个Partition闲着。反过来,如果你有2个Partition,却有8个消费者,那6个消费者也是闲着的。
我曾经踩过的坑:有次上线前预估流量,觉得4个Partition就够了。结果上线当天流量暴涨,消费者处理不过来,消息积压了几百万条。后来紧急扩容Partition到16个,但Kafka不支持减少Partition,只能重建Topic。那次之后,我定了个规矩——Partition数量按峰值流量的2倍预留。
每个Partition里的消息都有一个唯一的Offset(偏移量)。Offset从0开始,每条新消息的Offset递增1。消费者通过记录Offset来知道自己消费到了哪条消息。这就像看书时夹的书签,下次翻开直接跳到那一页。
2.3 生产者与消费者模型
聊完了数据怎么存,再来看数据怎么生产和消费。
生产者(Producer)负责把消息发到Kafka。生产者的核心工作是决定消息发到哪个Topic的哪个Partition。如果消息有Key,Kafka会对Key做哈希,然后映射到具体的Partition。如果没有Key,就用轮询方式均匀分发。
我个人的习惯是,生产者的确认机制一定要设对。Kafka提供了三种acks级别:
- acks=0:发出去就不管了,性能最高,但可能丢消息
- acks=1:Leader分区确认收到就算成功,性能中等
- acks=all:所有副本都确认收到才算成功,最安全但最慢
实时特征工程里,数据准确性比延迟更重要。所以我一般用acks=all。虽然慢一点,但不会丢数据。你想想看,如果特征数据丢了,模型算出来的结果就是错的,那延迟再低也没意义。
// 生产者配置示例(Java)
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("acks", "all"); // 最安全的确认机制
props.put("retries", 3); // 失败重试次数
Producer<String, String> producer = new KafkaProducer<>(props);
producer.send(new ProducerRecord<>("user_click_topic", "user_001", "{\"page\":\"home\"}"));
producer.close();
消费者(Consumer)负责从Kafka拉取消息。消费者属于某个消费组(Consumer Group)。同一个消费组里的消费者,共同消费一个Topic的消息,每条消息只会被组内的一个消费者处理。不同消费组之间互不影响,各自独立消费。
这个设计非常巧妙。举个例子,订单数据发到Kafka后,实时风控系统和实时推荐系统都要用。那就建两个消费组,风控组和推荐组。两个组各自消费,互不干扰。风控组处理慢了,不会影响推荐组。
避坑指南:我曾经遇到过一个问题——消费者莫名其妙地重复消费。排查了半天,发现是消费者处理完消息后,还没来得及提交Offset就挂了。重启后,它从上一次提交的Offset开始消费,导致部分消息被重复处理。解决方案是先提交Offset,再处理业务逻辑,或者用「至少一次」语义配合幂等性处理。
// 消费者配置示例(Java)
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("group.id", "feature_engine_group");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("enable.auto.commit", "false"); // 手动提交Offset
Consumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Arrays.asList("user_click_topic"));
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
for (ConsumerRecord<String, String> record : records) {
// 处理业务逻辑
processFeature(record.value());
// 处理完成后手动提交Offset
consumer.commitSync();
}
}
2.4 小结:Kafka在实时特征工程中的定位
说了这么多,总结一下Kafka在实时特征工程里的角色。它就像一个数据总线,把各种数据源(APP日志、数据库变更、外部API)统一接入,然后分发给下游的实时计算引擎(Flink、Spark Streaming)。
我参与过的项目中,Kafka集群的规模从3台到几十台不等。但不管规模大小,核心设计思路是一样的:Topic按业务划分,Partition按并行度规划,生产者保证数据不丢,消费者保证不重复不遗漏。
下一章,我们会聊到如何用Flink从Kafka里读取数据,做真正的实时特征计算。到时候你会发现,Kafka和Flink配合起来,简直天衣无缝。