3. 用户行为采集:埋点设计、日志收集、实时消息队列接入
用户行为采集,说白了就是给系统装上「眼睛」和「耳朵」。没有这一步,后面的用户画像、物品画像全是空中楼阁。我刚开始做推荐系统时,总觉得算法模型才是核心,埋点这种「脏活」随便搞搞就行。结果呢?模型上线后指标纹丝不动,查了半天才发现——用户点击数据压根没传上来。
嗯,从那以后我再也不敢轻视采集层了。这一章,咱们就把埋点设计、日志收集、消息队列接入这三个环节彻底讲透。
3.1 埋点设计:定义你要「看」什么
埋点不是越多越好。你想想看,每个埋点都是成本——前端要发请求、后端要接数据、存储要占空间。我见过一个项目,埋了三百多个事件,结果 80% 从来没被用过。
所以,设计埋点之前先问自己三个问题:
- 业务目标是什么?——比如提升点击率、降低跳出率、优化推荐多样性。
- 需要哪些行为数据?——曝光、点击、加购、收藏、购买、分享。
- 数据怎么关联?——用户 ID、物品 ID、会话 ID、时间戳,缺一不可。
核心原则:埋点设计遵循「最小完备集」——不多埋一个无用事件,也不少埋一个关键事件。
3.1.1 事件模型:谁 + 在什么时间 + 对什么 + 做了什么
我习惯用标准的事件模型来定义埋点。说白了就是四个字段:
| 字段 | 含义 | 示例 |
|---|---|---|
user_id |
用户标识 | u_10086 |
event_time |
事件发生时间 | 2025-03-15T10:30:00+08:00 |
item_id |
物品标识 | i_9527 |
event_type |
事件类型 | click / view / add_to_cart |
除此之外,我还会加一个 properties 字段,用来携带上下文信息。比如:
- 页面来源(from_page: "homepage")
- 推荐位位置(position: 3)
- 设备信息(device: "iPhone 15")
- 网络环境(network: "wifi")
我的习惯:properties 用 JSON 格式,灵活扩展。但别塞太多字段——我见过有人把用户手机电量都埋进去了,真没必要。
3.1.2 埋点方式:前端埋点 vs 后端埋点
这两种方式各有优劣。我直接给你对比一下:
| 对比维度 | 前端埋点 | 后端埋点 |
|---|---|---|
| 数据准确性 | 可能被广告拦截器屏蔽 | 服务端可控,准确率高 |
| 事件覆盖 | 能采集曝光、停留时长等 | 只能采集有请求的事件 |
| 实时性 | 依赖客户端网络 | 服务端直接上报 |
| 开发成本 | 需要多端适配(Web/iOS/Android) | 统一接口,成本较低 |
我在项目中通常采用「前后端双埋点」策略:
- 曝光事件——前端埋点,因为后端不知道用户「看到了」什么。
- 点击/购买事件——后端埋点,确保数据不丢。
- 关键转化事件——两端都埋,互相校验。
3.2 日志收集:从客户端到服务端
埋点设计好了,接下来就是怎么把数据从用户手机/浏览器送到我们的服务器。这里有两个关键点:可靠性和实时性。
3.2.1 前端日志上报策略
前端上报不能阻塞用户操作。我常用的方案是:
- 批量上报——攒够 N 条或每隔 T 秒,一次性发送。
- 离线缓存——网络不好时存到 localStorage,恢复后补发。
- 采样上报——高并发场景下按比例采样,比如只上报 10% 的曝光事件。
注意:离线缓存要控制大小,别把用户手机存爆了。我一般限制最多 500 条,超过就丢弃最旧的。
来看一段前端埋点的伪代码:
// 前端埋点上报示例
class Tracker {
constructor() {
this.queue = [];
this.maxSize = 50;
this.flushInterval = 5000; // 5秒上报一次
setInterval(() => this.flush(), this.flushInterval);
}
track(event) {
this.queue.push({
...event,
user_id: getUserId(),
event_time: new Date().toISOString(),
page_url: window.location.href
});
if (this.queue.length >= this.maxSize) {
this.flush();
}
}
flush() {
if (this.queue.length === 0) return;
const batch = this.queue.splice(0);
// 使用 sendBeacon 确保页面关闭时也能发送
navigator.sendBeacon('/api/log', JSON.stringify(batch));
}
}
3.2.2 服务端日志接收
服务端接收日志,说白了就是一个高性能的 HTTP 接口。我推荐用 Nginx + Lua 或者 Go 来写,吞吐量高,延迟低。
核心要点:
- 异步写入——收到日志后先放内存队列,再批量写磁盘,别让客户端等。
- 限流熔断——单机 QPS 超过阈值就降级,别把服务打崩。
- 格式校验——过滤掉字段缺失、时间异常的脏数据。
3.3 实时消息队列接入:让数据「流」起来
日志收集到服务端之后,如果直接写数据库,那实时性就废了。我们需要一个消息队列,把数据「流」到下游的画像计算模块。
3.3.1 为什么需要消息队列?
你想想看,用户行为数据是持续产生的。如果每来一条数据就直接写 HBase 或 MySQL,数据库根本扛不住。消息队列的作用就是:
- 削峰填谷——瞬间流量高峰被队列缓冲,下游按自己的节奏消费。
- 解耦——采集层和计算层不直接依赖,一方挂了不影响另一方。
- 回溯重放——消费失败时可以重新拉取,数据不丢。
3.3.2 技术选型:Kafka vs Pulsar vs RabbitMQ
我直接给你一个选型参考:
| 特性 | Kafka | Pulsar | RabbitMQ |
|---|---|---|---|
| 吞吐量 | 极高(百万级/秒) | 高(十万级/秒) | 中等(万级/秒) |
| 消息持久化 | 磁盘顺序读写 | 分层存储 | 内存+磁盘 |
| 实时性 | 亚秒级 | 亚秒级 | 微秒级 |
| 运维复杂度 | 中等 | 较高 | 较低 |
| 适用场景 | 日志采集、大数据管道 | 多租户、跨地域 | 任务队列、RPC |
我个人习惯:用户行为采集场景,首选 Kafka。吞吐量够大,生态成熟,Flink/Spark 直接对接。
3.3.3 接入架构:从埋点到消息队列
下面这张图展示了完整的接入流程:
3.3.4 消息格式设计
消息进了 Kafka,格式得统一。我推荐用 Protobuf 或 Avro,比 JSON 省空间、解析快。下面是一个 Protobuf 定义示例:
// user_behavior.proto
syntax = "proto3";
message UserBehaviorEvent {
string user_id = 1;
string item_id = 2;
string event_type = 3; // click / view / purchase
int64 event_time_ms = 4; // 毫秒时间戳
string session_id = 5;
string page_id = 6;
int32 position = 7; // 推荐位位置
string from_page = 8; // 来源页面
map<string, string> extra = 9; // 扩展字段
}
我的经验:extra 字段用 map 类型,方便后续扩展。但别滥用——每个 extra 键值对都会增加序列化开销。
3.4 避坑指南:我踩过的那些坑
做用户行为采集这几年,我踩过不少坑。挑几个典型的分享给你:
- 时间戳不一致——客户端和服务端时间不同步,导致事件顺序错乱。解决方案:统一使用服务端时间戳,或者用 NTP 校准。
- 重复上报——网络重试导致同一条数据被消费多次。解决方案:在消费端做幂等去重,用
event_id唯一标识。 - 消息积压——双十一流量暴增,Kafka 消费不过来。解决方案:提前扩容分区数,设置消费延迟告警。
- 数据倾斜——某个热门物品的曝光量是其他物品的 100 倍,导致分区数据不均。解决方案:按
item_id哈希分区,或者加盐打散。
曾经有一次:我忘了给 Kafka topic 设置日志保留时间,结果磁盘被写满,整个集群挂了。现在我的习惯是:保留 7 天,超过自动删除。
3.5 本章小结
用户行为采集是推荐系统的「地基」。地基不稳,上层建筑再漂亮也没用。这一章我们讲了:
- 埋点设计要遵循「最小完备集」,事件模型四要素不能少。
- 前端埋点用批量上报+离线缓存,后端埋点用异步写入+限流。
- 消息队列选 Kafka,削峰填谷、解耦、回溯重放。
- 消息格式用 Protobuf,统一 schema,方便上下游对接。
嗯,把这些搞定了,你的数据管道才算真正「通」了。下一层,咱们就可以开始用这些数据来构建用户画像了。