数据采集层:埋点设计原则、服务端日志采集、客户端SDK设计、数据清洗与ETL

好,咱们进入第二章。数据采集层,说白了就是用户画像的「水源地」。你后面所有的高大上模型、实时计算、精准推荐,如果这第一步的水是脏的、漏的、乱的,那后面全是白搭。我见过太多团队,算法模型调得飞起,最后发现是埋点数据不准,整个画像就是错的。嗯,咱们今天就把这层地基打牢。

一、埋点设计原则:少即是多,但该有的不能少

埋点这件事,我个人的习惯是「先想清楚要什么,再决定埋什么」。很多产品经理上来就说「全埋了吧,以后分析用」,这其实是个大坑。数据不是越多越好,垃圾数据多了,信噪比会急剧下降。

我总结了几条核心原则:

  • 业务驱动原则:每个埋点必须对应一个明确的业务分析目标。比如「用户点击了购买按钮」,是为了算转化率;「用户浏览了商品详情页超过10秒」,是为了算兴趣度。没有目标的埋点,就是垃圾。
  • 唯一标识原则:所有事件必须能关联到同一个用户。我建议用设备ID + 用户ID(登录后)的双重标识。我在项目中遇到过,用户换了设备就变成「新用户」了,画像直接断掉。
  • 标准化原则:事件名、属性名、值类型,必须统一规范。比如「click_buy_button」和「buy_btn_click」这种混乱命名,会让ETL工程师骂娘的。
  • 最小化原则:能客户端算的,别传到服务端;能聚合的,别逐条上报。减少带宽和存储压力。

核心要点:埋点设计是「业务语言」到「数据语言」的翻译。翻译错了,后面全错。

二、服务端日志采集:最可靠的数据源

服务端日志,说白了就是用户每次请求,你在后端打一条日志。这是最靠谱的数据,因为客户端可能被篡改、被拦截、被关闭,但服务端日志是你自己系统产生的,可信度最高。

我建议的采集架构是这样的:

用户请求 → Nginx/API网关 → 业务服务 → 打印日志(Log4j/Logback)
    ↓
  Kafka(消息队列)
    ↓
  Flume/Logstash(日志采集)
    ↓
  HDFS/Hive(离线) + Flink(实时)

这里有个坑,我曾经踩过。服务端日志如果直接写数据库,高并发下会把数据库打挂。一定要用消息队列做缓冲。Kafka 是标配,吞吐量高,还能做数据回溯。

日志格式我建议统一用 JSON,方便解析。举个例子:

{
  "event_time": "2024-01-15 10:30:45.123",
  "user_id": "u_123456",
  "device_id": "d_abc123",
  "event_type": "page_view",
  "page_url": "/product/1001",
  "duration_ms": 12500,
  "extra": {
    "referrer": "search",
    "platform": "ios"
  }
}

你想想看,如果每个服务都自己定义一套格式,那ETL阶段光是解析就要写一堆if-else。统一格式,后面省心十倍。

三、客户端SDK设计:轻量、稳定、容错

客户端SDK,就是嵌入在App或网页里的数据采集代码。这块我最有发言权,因为我在移动端SDK上栽过跟头。

设计原则就三条:

  • 轻量:SDK体积不能大,不能影响App启动速度。我建议核心功能控制在100KB以内。
  • 稳定:不能因为SDK崩溃导致App闪退。所有代码必须try-catch包裹,异常不能抛到上层。
  • 容错:网络不好时,数据要本地缓存,等网络恢复再上报。不能丢数据。

我给你们看一个简化的SDK核心逻辑:

// 伪代码:客户端SDK核心
class TrackerSDK {
  // 事件队列,用于本地缓存
  private val eventQueue = mutableListOf<Event>()
  
  fun track(event: Event) {
    // 1. 先加入本地队列
    eventQueue.add(event)
    
    // 2. 尝试立即上报
    try {
      reportToServer(event)
      eventQueue.remove(event) // 上报成功就移除
    } catch (e: Exception) {
      // 上报失败,保留在队列中,等下次机会
      // 这里不能抛异常!
    }
    
    // 3. 如果队列太长,触发批量上报
    if (eventQueue.size > 100) {
      flush()
    }
  }
  
  fun flush() {
    // 批量上报,减少网络请求
    val batch = eventQueue.toList()
    try {
      reportBatch(batch)
      eventQueue.clear()
    } catch (e: Exception) {
      // 批量失败,下次再试
    }
  }
}

个人经验:我曾经在某个电商App的SDK里,忘记做本地缓存。结果用户在地铁里(网络差)操作了10分钟,数据全丢了。后来加了SQLite本地存储,数据丢失率从30%降到了0.1%。

四、数据清洗与ETL:把脏数据变成干净数据

数据采集上来,你以为就能直接用了?太天真了。原始数据里什么妖魔鬼怪都有:字段缺失、格式错误、重复数据、恶意攻击数据……

我一般把ETL分成三个阶段:

阶段 做什么 常见问题
数据清洗 去重、去空、格式校验、异常值过滤 时间戳格式不统一、用户ID为空、事件名拼写错误
数据转换 字段映射、类型转换、维度退化、聚合计算 字符串转数字失败、枚举值映射错误
数据加载 写入数据仓库(Hive/ClickHouse)或实时流(Kafka) 写入性能瓶颈、数据倾斜

举个例子,清洗逻辑用SQL写大概是这样:

-- 清洗原始日志表
INSERT INTO dwd_user_event_clean
SELECT
  event_id,
  -- 统一时间格式
  CAST(event_time AS TIMESTAMP) AS event_time,
  -- 过滤无效用户ID
  CASE WHEN user_id IS NULL OR user_id = '' THEN 'unknown' ELSE user_id END AS user_id,
  -- 标准化事件名(小写+下划线)
  LOWER(REPLACE(event_type, ' ', '_')) AS event_type,
  -- 过滤异常时长(超过1小时视为异常)
  CASE WHEN duration_ms > 3600000 THEN NULL ELSE duration_ms END AS duration_ms
FROM ods_raw_event_log
WHERE dt = '2024-01-15'
  -- 过滤爬虫和攻击流量
  AND is_bot = 0
  AND is_test = 0;

避坑指南:我曾经在清洗阶段忘记过滤测试数据,结果用户画像里混入了大量测试账号的虚假行为,导致「活跃用户」指标虚高30%。后来我加了一个「is_test」字段,所有测试流量必须打标,清洗时直接过滤掉。

五、实时ETL vs 离线ETL:什么时候用哪个?

这个问题我经常被问到。我的建议很简单:

  • 实时ETL(Flink/Spark Streaming):用于需要秒级响应的场景,比如实时推荐、实时风控、实时大屏。延迟控制在秒级,但成本高。
  • 离线ETL(Hive/Spark SQL):用于T+1的分析场景,比如用户画像的日更新、周报、月报。成本低,可以处理海量数据。

我个人的做法是「双轨并行」:实时流处理核心事件(比如购买、注册),离线批处理全量数据做补充。这样既保证了实时性,又保证了数据的完整性。

嗯,数据采集层就讲到这里。说白了,这层就是「把用户行为变成数据」的过程。埋点设计是蓝图,服务端日志是主干,客户端SDK是触角,ETL是净化器。每一步都马虎不得。下一章咱们聊聊「用户画像的标签体系设计」,那才是真正开始「画」用户了。