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 前端日志上报策略

前端上报不能阻塞用户操作。我常用的方案是:

  1. 批量上报——攒够 N 条或每隔 T 秒,一次性发送。
  2. 离线缓存——网络不好时存到 localStorage,恢复后补发。
  3. 采样上报——高并发场景下按比例采样,比如只上报 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 接入架构:从埋点到消息队列

下面这张图展示了完整的接入流程:

用户行为采集接入架构图 用户行为采集接入架构 Web 前端埋点 iOS/Android 埋点 服务端后端埋点 日志收集服务(Nginx + Lua / Go) Kafka 消息队列(Topic: user_behavior) Flink 实时画像计算 HBase 离线存储 实时监控/告警 说明:埋点数据经日志收集服务统一接入 Kafka,下游多个消费者按需消费,互不影响。

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,方便上下游对接。

嗯,把这些搞定了,你的数据管道才算真正「通」了。下一层,咱们就可以开始用这些数据来构建用户画像了。