3. 数据源与消息队列:用户行为日志生成器设计、Apache Kafka核心概念、Kafka集群搭建(Docker)、Topic设计与分区策略

好,咱们进入第三章。这一章是整个实时推荐平台的「水源」和「血管」。数据源怎么来?消息队列怎么扛住千万级流量?我见过不少团队,算法模型再牛,数据管道一堵,全白搭。所以这一章,咱们把地基打扎实。

本章核心脉络:先造一个模拟用户行为的日志生成器,再理解 Kafka 的核心抽象,然后用 Docker 快速拉起集群,最后聊聊 Topic 和分区设计——这四步走完,你的数据管道就能跑起来了。

第三章知识体系与数据流图 第三章:数据源与消息队列核心架构 用户行为日志生成器 Python 模拟点击/购买/收藏 Apache Kafka 集群 3 Broker + Zookeeper Topic 与分区策略 user_behavior / 6分区 关键设计要点: • 日志生成器:随机时间间隔 + 用户画像模拟 + JSON 序列化 • Kafka:Leader/Follower 副本机制 + ISR 同步 + 至少一次语义 • 分区策略:按用户 ID 哈希分区,保证同一用户行为有序 数据流方向:日志生成器 → Kafka Producer → Broker → Topic/Partition → Consumer

3.1 用户行为日志生成器设计

做实时推荐,第一步就是要有数据。但项目初期哪有真实用户?所以咱们自己写一个日志生成器。说白了,就是模拟用户在你网站上的各种操作:浏览商品、点击详情、加入购物车、下单购买。

我个人习惯用 Python 来写这类模拟器,因为快。你想想看,一个 while 循环,配合 random 库,就能模拟出几万用户的访问模式。

设计要点:日志生成器不是乱造数据。要模拟出「用户画像」和「行为序列」。比如一个「数码爱好者」用户,浏览手机、电脑的频率应该远高于奶粉、尿布。这样才能测试推荐算法的效果。

来看一个简化版的生成器核心代码:

import json, time, random, uuid
from datetime import datetime

# 模拟用户画像
USER_PROFILES = {
    "user_001": {"interests": ["手机", "电脑", "耳机"]},
    "user_002": {"interests": ["图书", "文具", "课程"]},
    "user_003": {"interests": ["母婴", "玩具", "童装"]},
}

ACTIONS = ["view", "click", "add_cart", "purchase"]
ITEMS = {
    "手机": ["iphone_15", "xiaomi_14", "huawei_p70"],
    "电脑": ["macbook_pro", "thinkpad_x1", "rog_zephyrus"],
    "图书": ["python_cookbook", "kafka_definitive", "design_data_intensive"],
    "母婴": ["baby_car_seat", "diaper_pack", "stroller_x"],
}

def generate_log():
    user_id = random.choice(list(USER_PROFILES.keys()))
    profile = USER_PROFILES[user_id]
    category = random.choice(profile["interests"])
    item = random.choice(ITEMS[category])
    action = random.choice(ACTIONS)
    
    log = {
        "user_id": user_id,
        "item_id": item,
        "category": category,
        "action": action,
        "timestamp": datetime.utcnow().isoformat() + "Z",
        "session_id": str(uuid.uuid4())[:8]
    }
    return json.dumps(log)

# 模拟持续发送
if __name__ == "__main__":
    while True:
        print(generate_log())
        time.sleep(random.uniform(0.5, 2.0))  # 随机间隔

这段代码我实际项目里改过很多版。最开始是固定间隔,结果下游 Kafka 消费端压力测试完全不准。后来改成随机间隔 + 突发流量模拟,才更贴近真实场景。

避坑指南:我曾经在生产环境遇到过日志生成器时间戳用了本地时间,结果跨时区部署时数据全乱套。记住,所有时间戳统一用 UTC,消费端再根据用户时区转换。

3.2 Apache Kafka 核心概念

日志生成出来了,怎么传给下游?直接 HTTP 调用?并发一高后端就挂了。这时候就需要消息队列。Kafka 是目前实时数据管道的事实标准。

Kafka 的核心概念其实不多,但每个都很关键。我把它总结成一张表:

概念 一句话解释 我见过的坑
Producer 发送消息的客户端 没配置 acks=all,丢过消息
Consumer 消费消息的客户端 消费组 rebalance 导致重复消费
Broker Kafka 服务器节点 磁盘空间打满,整个集群 hang 住
Topic 消息的逻辑分类 Topic 数量过多,文件句柄耗尽
Partition Topic 的分片,并行度单位 分区数设置后不可减少
Offset 消息在分区内的序号 手动提交 offset 容易忘
ISR 与 Leader 保持同步的副本集合 ISR 收缩导致写入延迟飙升

这里重点说说 Partition。Kafka 的并行度就靠它。一个 Topic 有 N 个分区,最多就能被 N 个 Consumer 同时消费。分区数太少,消费能力上不去;分区数太多,rebalance 和文件管理开销又大。

经验公式:分区数 ≈ max(期望吞吐量 / 单个分区吞吐量, Consumer 线程数)。我一般先按 Broker 数量的 3-5 倍设置,后续再压测调整。

3.3 Kafka 集群搭建(Docker)

搭建 Kafka 集群,传统方式要装 JDK、解压 tar 包、改配置、启动 Zookeeper……太麻烦了。用 Docker Compose,10 分钟搞定。

这是我常用的 docker-compose.yml 配置:

version: '3.8'
services:
  zookeeper:
    image: confluentinc/cp-zookeeper:7.5.0
    environment:
      ZOOKEEPER_CLIENT_PORT: 2181
      ZOOKEEPER_TICK_TIME: 2000

  kafka1:
    image: confluentinc/cp-kafka:7.5.0
    depends_on:
      - zookeeper
    ports:
      - "9092:9092"
    environment:
      KAFKA_BROKER_ID: 1
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 3

  kafka2:
    image: confluentinc/cp-kafka:7.5.0
    depends_on:
      - zookeeper
    ports:
      - "9093:9092"
    environment:
      KAFKA_BROKER_ID: 2
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9093
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 3

  kafka3:
    image: confluentinc/cp-kafka:7.5.0
    depends_on:
      - zookeeper
    ports:
      - "9094:9092"
    environment:
      KAFKA_BROKER_ID: 3
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9094
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 3

启动命令就一行:

docker-compose up -d

然后验证集群状态:

# 查看 broker 列表
docker exec -it kafka1 kafka-broker-api-versions --bootstrap-server localhost:9092

# 创建测试 topic
docker exec -it kafka1 kafka-topics --create \
  --topic test_topic \
  --bootstrap-server localhost:9092 \
  --partitions 3 \
  --replication-factor 3

# 查看 topic 详情
docker exec -it kafka1 kafka-topics --describe \
  --topic test_topic \
  --bootstrap-server localhost:9092

小技巧:我习惯把 Kafka 的 Docker 网络设为 host 模式,这样容器内外的网络通信更简单。但生产环境还是建议用 bridge 网络 + 独立网段,安全隔离更好。

3.4 Topic 设计与分区策略

Topic 设计得好不好,直接决定了后续数据处理的复杂度。我见过最惨的案例:一个团队把所有用户行为都塞进一个 Topic,结果下游不同业务线互相干扰,一个消费组出问题,全链路瘫痪。

对于实时推荐平台,我建议这样设计:

Topic 名称 用途 分区数 保留策略
user_behavior 用户原始行为日志 6 7 天
item_feature 物品特征变更 3 紧凑保留
recommend_result 推荐结果输出 3 1 天
model_train_event 模型训练触发事件 1 1 天

分区策略上,我推荐用 用户 ID 哈希分区。为什么?因为同一个用户的行为必须有序。如果用户 A 的「浏览」和「购买」被分到不同分区,消费端就无法保证顺序。

Producer 端代码示例:

from kafka import KafkaProducer
import json

producer = KafkaProducer(
    bootstrap_servers=['localhost:9092', 'localhost:9093', 'localhost:9094'],
    value_serializer=lambda v: json.dumps(v).encode('utf-8'),
    # 自定义分区器:按用户 ID 哈希
    partitioner=lambda key_bytes, all_partitions, available: (
        hash(key_bytes) % len(all_partitions)
    ),
    acks='all',  # 等待所有副本确认
    retries=3
)

# 发送消息
log_data = {"user_id": "user_001", "action": "purchase", "item": "iphone_15"}
producer.send('user_behavior', key=b'user_001', value=log_data)
producer.flush()

重要提醒:分区数一旦设置,后续只能增加不能减少。我曾经在项目初期只设了 2 个分区,半年后流量涨了 10 倍,不得不新建 Topic 再迁移数据,折腾了一整周。所以初期宁可多设一些,比如 6-12 个分区。