3、数据采集与存储:数据源接入、消息队列(Kafka)使用、数据落盘策略、数据压缩
数据采集与存储,是整个回放体系的基石。说白了,你后面所有的分析、比对、复现,都依赖于这一层能不能把数据「原汁原味」地捞上来并存好。
我刚开始做数据回放时,踩过最大的坑就是——数据丢了。不是全丢,是丢了一部分。你想想看,回放时发现某个请求对不上,查了半天,结果是采集阶段就漏了。嗯,从那以后,我对数据采集的可靠性要求就变得非常苛刻。
3.1 数据源接入:五花八门的入口
数据源接入,核心就一句话:把业务系统的流量「镜像」一份出来。常见的接入方式有三种:
- SDK 埋点:在业务代码中嵌入采集 SDK,主动上报请求和响应。优点是灵活,缺点是侵入性强。
- 流量拷贝(GoReplay / tcpcopy):在网关层或应用层旁路拷贝流量。无侵入,但需要网络环境支持。
- 日志采集(Filebeat / Fluentd):从业务日志中解析出请求数据。成本低,但解析逻辑容易出错。
我个人习惯:核心链路用 SDK 埋点 + 流量拷贝双保险。一个做主通道,一个做校验。我在项目中遇到过,单靠流量拷贝,在高并发下偶尔会丢包,SDK 埋点正好补上这个缺口。
接入时要注意一个关键点:数据完整性校验。每条数据都要带一个唯一 ID(traceId),方便后续对账。我曾经因为没加 traceId,排查数据缺失时花了整整两天——后来发现是采集端 buffer 满了,直接丢弃了部分数据。
3.2 消息队列(Kafka)使用:缓冲与解耦
数据采集上来后,直接写磁盘?别急。高并发场景下,业务流量是突发的,存储系统扛不住。这时候就需要消息队列来「削峰填谷」。
Kafka 是我用得最多的选择。为什么?吞吐量高、持久化可靠、支持回溯消费。说白了,它就是为这种场景设计的。
3.2.1 Topic 与分区设计
Topic 的设计直接影响后续消费效率。我一般按业务模块划分 Topic,比如:
| Topic 名称 | 用途 | 分区数 |
|---|---|---|
| order_trace | 订单链路请求数据 | 8 |
| payment_trace | 支付链路请求数据 | 4 |
| user_trace | 用户行为数据 | 6 |
分区数不是越多越好。我建议分区数 = 消费端并发数 × 2,这样既能充分利用并行能力,又不会因为分区过多导致 Kafka 元数据膨胀。
3.2.2 生产者配置要点
生产者配置直接影响数据可靠性。这里有几个关键参数:
# 生产者配置示例
acks=all # 等待所有副本确认,保证不丢数据
retries=3 # 重试次数,防止网络抖动
batch.size=16384 # 批量发送大小,提升吞吐
linger.ms=5 # 等待时间,凑够 batch 再发
compression.type=snappy # 压缩,减少网络带宽
避坑指南:我曾经把 acks 设成 1,以为性能更好。结果有一次 Kafka 节点挂了,数据丢了 2 分钟。从那以后,核心数据我全部用 acks=all。性能损失不大,但可靠性提升明显。
3.2.3 消费者设计
消费者要关注的是「消费位移管理」。我建议使用 Kafka 自带的 __consumer_offsets 来管理位移,而不是手动提交。手动提交容易出问题——要么重复消费,要么丢数据。
举个例子:
// 自动提交(推荐)
props.put("enable.auto.commit", "true");
props.put("auto.commit.interval.ms", "1000");
// 手动提交(慎用)
props.put("enable.auto.commit", "false");
// 需要在处理完每条消息后手动调用 consumer.commitSync()
自动提交的缺点是可能重复消费,但不会丢数据。在数据回放场景下,重复消费比丢数据好处理得多——去重就行。
3.3 数据落盘策略:存什么、怎么存、存多久
数据从 Kafka 消费下来后,最终要落到磁盘上。落盘策略决定了后续查询和分析的效率。
3.3.1 存储格式选择
我推荐使用 Parquet 格式。为什么?
- 列式存储:查询时只读取需要的列,IO 开销小
- 压缩率高:比 JSON 节省 70% 以上空间
- 支持谓词下推:过滤条件在读取时提前应用
当然,如果数据量不大(每天几百 GB 以内),用 JSON 也可以。我早期项目就用 JSON,后来数据量上来了,不得不迁移到 Parquet——迁移过程还挺痛苦的。
3.3.2 分区策略
数据按时间分区是最常见的做法。我一般按「天」分区,如果数据量特别大(每天 TB 级),可以按「小时」分区。
# 分区目录示例
/data/traces/dt=2025-01-15/hour=10/
/data/traces/dt=2025-01-15/hour=11/
/data/traces/dt=2025-01-16/hour=00/
注意:分区粒度不要太细。按分钟分区会导致大量小文件,查询性能反而下降。我见过有人按 5 分钟分区,结果一个分区只有几十 KB 数据,查询时打开几百个文件,慢得不行。
3.3.3 数据生命周期管理
数据不是存得越久越好。我建议:
- 热数据(最近 7 天):存 SSD,支持快速查询
- 温数据(7-30 天):存 HDD,查询速度可接受
- 冷数据(30 天以上):压缩归档,甚至迁移到对象存储
我曾经因为没做冷热分离,导致 SSD 被写满,整个回放系统挂了半天。嗯,从那以后我老老实实加了数据生命周期管理。
3.4 数据压缩:省空间就是省钱
数据压缩是容易被忽视的一环。你想想看,每天 TB 级的数据,压缩率差 10%,就是几百 GB 的差距。存储成本可不是小数目。
3.4.1 压缩算法对比
| 算法 | 压缩比 | 压缩速度 | 解压速度 | 适用场景 |
|---|---|---|---|---|
| Snappy | 2:1 | 快 | 快 | 实时写入、Kafka 传输 |
| Zstd | 3:1 ~ 5:1 | 中等 | 快 | 冷数据存储 |
| Gzip | 4:1 ~ 6:1 | 慢 | 慢 | 归档数据 |
| LZ4 | 2:1 | 极快 | 极快 | 实时查询场景 |
我个人习惯:热数据用 LZ4,追求查询速度;冷数据用 Zstd,追求压缩率。Kafka 传输时用 Snappy,因为它在速度和压缩率之间平衡得最好。
3.4.2 压缩实践
在 Parquet 中配置压缩很简单:
// 写入 Parquet 时指定压缩
ParquetWriter<GenericRecord> writer = AvroParquetWriter
.<GenericRecord>builder(path)
.withSchema(schema)
.withCompressionCodec(CompressionCodecName.ZSTD)
.build();
避坑指南:我曾经在 Kafka 生产者和 Parquet 写入时都用了压缩,结果 CPU 飙升到 90% 以上。后来发现是「双重压缩」——Kafka 压缩一次,Parquet 又压缩一次。其实只需要在最终落盘时压缩一次就够了。
3.5 整体架构图
下面这张图展示了数据采集与存储的完整链路:
3.6 小结
数据采集与存储,说白了就是三个字:稳、快、省。稳——数据不能丢;快——写入和查询要快;省——存储成本要控制住。
我做了这么多年数据回放,最大的体会是:采集阶段多花一分心思,后面分析阶段就能省十分力气。数据质量不好,后面所有工作都是白搭。
核心要点回顾:
- 数据源接入:SDK 埋点 + 流量拷贝双保险
- Kafka 使用:acks=all,分区数 = 消费并发 × 2
- 落盘策略:Parquet 格式,按天分区,冷热分离
- 数据压缩:热数据 LZ4,冷数据 Zstd,避免双重压缩