数据模拟层开发:让数据先跑起来

好,到了第四章,我们终于要开始写代码了。

前面三章我们把架构、技术选型、数据模型都聊完了。但有个问题——平台搭好了,数据从哪来?你总不能用生产环境的数据做测试吧?我记得我第一次搭实时平台时,直接连了线上Kafka,结果把业务topic搞乱了,被运维追着骂了三天。

所以,我们需要一个数据模拟层。说白了,就是自己造数据。

4.1 用户行为日志的JSON格式定义

先定协议。我们模拟的是用户行为日志,每条日志就是一个JSON对象。我个人习惯把字段分成三类:基础标识、行为信息、上下文环境。

字段名 类型 说明 示例值
user_id string 用户唯一标识 "u_10001"
event_type string 事件类型 "page_view"
page_id string 页面ID "p_home"
duration_ms int 停留时长(毫秒) 3200
timestamp long 事件发生时间戳 1710000000000
ip string 客户端IP "192.168.1.1"
user_agent string 浏览器标识 "Mozilla/5.0 ..."

你可能会问:为什么不用Avro或Protobuf?嗯,在模拟层用JSON有两个好处:一是肉眼可读,调试方便;二是后面接入Flink时,JSON解析的灵活性更高。等上了生产,再考虑序列化压缩的事。

下面是我常用的日志生成函数:

import json
import random
import time
from datetime import datetime

def generate_user_event():
    event = {
        "user_id": f"u_{random.randint(10000, 99999)}",
        "event_type": random.choice(["page_view", "click", "purchase", "add_cart"]),
        "page_id": random.choice(["p_home", "p_detail", "p_cart", "p_order"]),
        "duration_ms": random.randint(500, 15000),
        "timestamp": int(time.time() * 1000),
        "ip": f"192.168.{random.randint(0,255)}.{random.randint(1,254)}",
        "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
    }
    return json.dumps(event)
小技巧:timestamp用毫秒级,因为Flink的EventTime处理需要毫秒精度。我之前用秒级时间戳踩过坑,窗口计算总是对不齐。

4.2 Kafka Producer:把数据发出去

数据造好了,得发到Kafka里去。Python连Kafka,我推荐用confluent_kafka库。为什么不用kafka-python?因为confluent_kafka底层是C写的,性能好得多。我在一次压测中,同样的机器配置,confluent_kafka的吞吐量高出3倍。

先装依赖:

pip install confluent_kafka

然后写Producer:

from confluent_kafka import Producer
import json

conf = {
    'bootstrap.servers': 'localhost:9092',
    'acks': '1',
    'retries': 3,
    'batch.size': 16384,
    'linger.ms': 10
}

producer = Producer(conf)

def delivery_report(err, msg):
    if err is not None:
        print(f'发送失败: {err}')
    else:
        print(f'发送成功: {msg.topic()} [{msg.partition()}]')

def send_event(event_json):
    producer.produce(
        topic='user_behavior',
        value=event_json.encode('utf-8'),
        callback=delivery_report
    )
    producer.poll(0)
注意:produce()之后一定要调poll()或flush(),否则回调不会执行,消息也可能积压在缓冲区。我曾经因为这个原因,丢了几万条测试数据,排查了半天。

4.3 数据速率控制:别把Kafka打爆

模拟数据最怕什么?怕你一口气发太多,把Kafka搞挂了。我见过有人写了个死循环,每秒发几万条,直接把测试集群的磁盘写满了。

所以,我们要做速率控制。常用的方法有两种:

  • 固定速率:每秒发N条,用time.sleep控制间隔
  • 动态速率:模拟真实流量,比如白天快、晚上慢

先看固定速率的实现:

import time

def run_fixed_rate(rate_per_second, duration_seconds):
    """
    rate_per_second: 每秒发送条数
    duration_seconds: 持续秒数
    """
    interval = 1.0 / rate_per_second
    start = time.time()
    count = 0
    
    while time.time() - start < duration_seconds:
        event = generate_user_event()
        send_event(event)
        count += 1
        time.sleep(interval)
    
    print(f"共发送 {count} 条数据")

动态速率稍微复杂一点。我习惯用正弦波模拟日周期:

import math

def run_dynamic_rate(base_rate=100, peak_rate=500, cycle_minutes=60):
    """
    base_rate: 最低速率
    peak_rate: 最高速率
    cycle_minutes: 一个完整周期(分钟)
    """
    start = time.time()
    
    while True:
        elapsed = (time.time() - start) / 60.0  # 转为分钟
        # 用sin函数模拟流量波动
        rate = base_rate + (peak_rate - base_rate) * (math.sin(elapsed * 2 * math.pi / cycle_minutes) + 1) / 2
        
        interval = 1.0 / rate
        event = generate_user_event()
        send_event(event)
        time.sleep(interval)

核心思路:速率控制不是限制功能,而是模拟真实场景。你的实时平台最终要面对的是波动的流量,不是匀速的。所以模拟层越接近真实,后面Flink作业的稳定性就越高。

4.4 完整的数据模拟流程

把上面几块拼起来,就是完整的数据模拟层。我画了张图,帮你理清关系:

数据模拟层整体流程 JSON日志生成器 generate_user_event() 速率控制器 固定/动态速率 Kafka Producer confluent_kafka Kafka Topic user_behavior 下游消费者 Flink / 日志消费 💡 关键配置 • bootstrap.servers • acks / retries

流程其实很简单:生成JSON → 控制速率 → 发送到Kafka。但每个环节都有坑,我帮你列一下:

  • JSON生成:字段类型要对,尤其是时间戳和数值型,别发字符串
  • 速率控制:别用死循环,加个退出条件(比如按Ctrl+C优雅退出)
  • Kafka发送:一定要处理回调,否则发没发出去你都不知道

4.5 避坑指南:我踩过的三个坑

最后,分享几个真实教训:

  1. 消息体太大:有一次我把整个页面HTML塞进了日志,单条消息超过1MB,Kafka直接报错。后来限制了单条消息不超过100KB。
  2. 分区不均匀:如果不指定key,Kafka默认轮询分区。但如果你用user_id做key,热点用户会导致数据倾斜。模拟时建议不加key,让分区均匀分布。
  3. 忘记flush:程序退出前一定要调producer.flush(),否则缓冲区里的消息会丢失。我吃过这个亏,跑了半小时的数据,一关程序全没了。
我的习惯:写一个main()函数,把速率、topic、broker地址都做成参数。这样切换环境时改个参数就行,不用改代码。