实时数据源接入:Kafka基础概念与架构、Kafka生产者与消费者API、Flink Kafka Connector实战

好,咱们直接进入正题。实时特征工程的第一步,说白了就是「怎么把数据实时拿过来」。你可能会想,用HTTP轮询行不行?行,但延迟高、资源浪费严重。生产环境里,我几乎没见过谁用轮询做实时特征。大家用的都是消息队列,而Kafka,就是消息队列里的「事实标准」。

我最早接触Kafka是在2016年,那时候公司要做用户实时行为画像。数据源是App的点击流,每秒几万条。用RabbitMQ试过,吞吐量上不去,消费端还经常堆积。后来换成Kafka,问题一下就解决了。嗯,从那以后,我对Kafka的定位就很清晰——它是为「高吞吐、低延迟、持久化」而生的。

Kafka核心概念与架构

先看一张整体架构图,帮你建立直观印象。

Kafka核心架构图 Kafka 核心架构图 Producer 1 Producer 2 Producer N 生产者 Kafka 集群 Broker 1 Broker 2 Broker 3 Topic: user_click / user_profile Consumer 1 Consumer 2 Consumer N 消费者(Flink等) 核心概念说明: • Producer:数据生产者,将消息发送到Topic • Broker:Kafka服务器节点,负责存储和转发消息 • Topic:消息的逻辑分类,类似数据库中的表 • Partition:Topic的物理分片,实现并行读写 • Consumer:数据消费者,从Topic拉取消息 • Consumer Group:消费者组,组内成员协同消费,实现负载均衡

这张图里,Producer往Kafka集群里写数据,Consumer从集群里读数据。中间的Broker就是Kafka服务器,Topic是数据的分类。你想想看,如果每秒要处理几十万条用户点击,没有这种架构根本扛不住。

我个人习惯把Kafka理解成一个「超级缓冲区」。它不负责计算,只负责把数据存下来、按顺序分发。为什么它快?因为它是顺序写磁盘,而不是随机写。机械硬盘顺序写能到200MB/s,比随机写快两个数量级。

关键点:Kafka的Topic可以分成多个Partition。Partition越多,并行度越高。我见过一个极端案例,某大厂把Topic分了128个Partition,单集群吞吐量突破百万条/秒。

Kafka生产者与消费者API

光懂架构不够,得能写代码。咱们先看生产者API。说白了,就是往Kafka里发消息。

// 生产者核心配置
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");
// 重要:acks=all 保证数据不丢
props.put("acks", "all");

KafkaProducer<String, String> producer = new KafkaProducer<>(props);

// 发送消息
ProducerRecord<String, String> record = 
    new ProducerRecord<>("user_click", "user_1001", "{\"event\":\"click\",\"item_id\":\"123\"}");
producer.send(record, (metadata, exception) -> {
    if (exception == null) {
        System.out.println("发送成功,偏移量:" + metadata.offset());
    } else {
        System.err.println("发送失败:" + exception.getMessage());
    }
});
producer.close();

这里有个坑,我踩过。刚开始用Kafka时,我把acks设成0,觉得这样快。结果线上丢数据了,用户行为少了一大截。后来改成all,虽然延迟多了几毫秒,但数据一条不丢。实时特征工程里,数据完整性比那几毫秒延迟重要得多。

再看消费者API。消费者从Kafka拉数据,然后处理。

Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("group.id", "feature-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");

KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Arrays.asList("user_click"));

while (true) {
    ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
    for (ConsumerRecord<String, String> record : records) {
        // 这里就是特征计算的入口
        System.out.printf("offset = %d, key = %s, value = %s%n", 
            record.offset(), record.key(), record.value());
    }
    // 手动提交,确保处理完再提交偏移量
    consumer.commitSync();
}

注意:我曾经在生产环境遇到过「自动提交偏移量」导致的数据重复消费问题。消费者处理到一半挂了,偏移量已经提交,重启后从新偏移量开始,中间的数据就丢了。所以,我建议你用手动提交,处理完一批再提交一批。

Flink Kafka Connector实战

好了,现在数据在Kafka里,怎么把它喂给Flink做特征计算?Flink官方提供了Kafka Connector,用起来非常顺手。

先加依赖:

<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-connector-kafka_2.12</artifactId>
    <version>1.17.0</version>
</dependency>

然后写Flink作业,从Kafka读数据:

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(4);

// 配置Kafka消费者
Properties kafkaProps = new Properties();
kafkaProps.setProperty("bootstrap.servers", "localhost:9092");
kafkaProps.setProperty("group.id", "flink-feature-group");

DataStream<String> stream = env.addSource(
    new FlinkKafkaConsumer<>(
        "user_click",
        new SimpleStringSchema(),
        kafkaProps
    )
);

// 实时特征计算:统计每个用户过去5分钟的点击次数
stream
    .map(value -> {
        JSONObject json = new JSONObject(value);
        return Tuple2.of(json.getString("user_id"), 1L);
    })
    .keyBy(value -> value.f0)
    .window(TumblingProcessingTimeWindows.of(Time.minutes(5)))
    .sum(1)
    .print();

env.execute("Real-time Feature Pipeline");

这段代码做了什么?它从Kafka的user_click主题里读数据,然后按用户ID分组,每5分钟统计一次点击次数。这就是一个最简单的实时特征。

小技巧:Flink Kafka Connector默认使用Kafka的消费者组机制,支持故障恢复。如果Flink作业挂了重启,它会从上次提交的偏移量继续消费,不会重复也不会丢失。但前提是——你得设置好检查点(Checkpoint)。我习惯每30秒做一次Checkpoint,这样最多丢30秒的数据。

你可能会问,为什么不用Spark Streaming?嗯,Flink在实时场景下延迟更低,毫秒级。Spark Streaming本质上是微批处理,延迟至少几百毫秒。做实时特征,延迟就是金钱。我做过对比,同样一个滑动窗口特征,Flink延迟比Spark Streaming低一个数量级。

最后,给你一个实战中的配置清单,照着做基本不会出问题:

配置项 推荐值 说明
bootstrap.servers 至少3个Broker地址 避免单点故障
acks all 保证数据不丢
enable.auto.commit false 手动提交,精确控制
Flink Checkpoint间隔 30秒 平衡恢复时间和性能
Topic分区数 ≥ Flink并行度 充分利用并行能力

好了,这一章的内容就到这里。Kafka作为实时数据源的核心,你只要把架构、API和Flink Connector这三块吃透,后面的特征计算就水到渠成了。