数据采集层:埋点设计原则、服务端日志采集、客户端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是净化器。每一步都马虎不得。下一章咱们聊聊「用户画像的标签体系设计」,那才是真正开始「画」用户了。