3. 埋点采集SDK:架构设计与核心策略

埋点采集SDK,说白了就是用户行为数据的“快递员”。它得把App里发生的各种事件,安全、高效、不丢包地送到服务器。我做过好几个SDK,踩过不少坑,今天把核心设计思路掰开揉碎讲给你听。

核心原则:不影响主线程性能、不丢失数据、不浪费流量、不被恶意篡改。

3.1 SDK整体架构设计

先看一张我常用的分层架构图。它把SDK拆成四层,每层各司其职。

埋点采集SDK 分层架构 应用层(Application Layer) API 接口 · 自动采集 · 手动埋点 · 页面生命周期 处理层(Processing Layer) 数据清洗 · 格式转换 · 采样过滤 · 上下文拼接 存储层(Storage Layer) 内存缓存 · 数据库持久化 · 文件分片 · 缓存策略 网络层(Network Layer) 压缩加密 · 批量上报 · 重试机制 · 流量控制 数据流方向

每一层都有明确的边界。我见过不少团队把处理逻辑和网络逻辑混在一起,结果改一个上报策略,埋点采集也跟着崩。分层之后,各层可以独立演进。

3.2 数据缓存策略

缓存是SDK的命脉。网络随时可能断,App随时可能被杀。数据一旦丢了,用户行为分析就成了空中楼阁。

3.2.1 三级缓存模型

我个人习惯用三级缓存:

缓存级别 存储介质 容量上限 生命周期 用途
L1 内存队列 200条 App存活期间 快速写入,减少IO
L2 SQLite数据库 5000条 7天 持久化,防丢失
L3 文件系统 100MB 30天 兜底,极端情况

L1内存队列:事件来了先塞进内存队列。队列满了就批量刷到L2。这样主线程只需要O(1)的入队操作,几乎无开销。

L2数据库:我用SQLite,但有个坑——事务要分批。我曾经一次性插入3000条数据,直接卡了主线程200毫秒。后来改成每500条一个事务,毫秒级完成。

L3文件兜底:数据库写不进去了怎么办?比如磁盘空间不足。这时候序列化成JSON文件,存到缓存目录。等空间恢复了再回写到数据库。

注意:文件缓存一定要限制总大小。我见过一个SDK没做限制,用户手机里攒了2GB的埋点文件,直接被用户卸载了。

3.2.2 缓存淘汰策略

缓存满了怎么办?不能无限制存下去。我常用的策略:

  • 按时间淘汰:超过7天的数据直接丢弃。用户行为分析不需要考古。
  • 按优先级淘汰:关键事件(如支付、登录)优先保留,普通浏览事件可以丢弃。
  • 按大小淘汰:总缓存超过阈值时,删除最早的非关键事件。

我的经验:给每个事件加一个 priority 字段。上报时高优先级先发,缓存满时低优先级先删。这个字段成本极低,但关键时刻能救命。

3.3 网络上报策略

上报策略直接影响服务器压力和用户流量。我见过最粗暴的做法——来一条发一条。结果用户在地铁里刷App,SDK发了200次请求,200次都失败,电池和流量全浪费了。

3.3.1 批量上报

把多条事件打包成一个请求。我常用的参数:

参数 推荐值 说明
批量大小 50条/批 太少浪费连接,太多增加失败重试成本
上报间隔 10秒 兼顾实时性和批量效率
最大延迟 60秒 超过这个时间,即使不够50条也要发

3.3.2 网络状态感知

SDK应该感知网络状态:

  • WiFi下:正常上报,批量大小可以放大到100条。
  • 移动网络下:降低频率,批量大小缩到30条,避免消耗用户流量。
  • 弱网下:只上报关键事件,普通事件缓存等待。
  • 无网络:全部缓存,等网络恢复后一次性补发。

避坑指南:我曾经在弱网检测上犯过傻——每次请求前都去ping一下服务器。结果用户没崩溃,SDK自己先把自己ping崩溃了。后来改用系统提供的 NetworkCallback,监听网络变化,零额外开销。

3.3.3 重试与退避

上报失败是常态。我的重试策略:

// 指数退避 + 随机抖动
retryDelay = min(initialDelay * 2^retryCount, maxDelay)
retryDelay += random(0, 1000) // 避免惊群效应

// 参数示例
initialDelay = 1秒
maxDelay = 60秒
maxRetries = 5

重试超过5次还失败?别死磕了。把数据标记为“待重试”,等下一次批量上报周期再试。一直重试只会浪费电。

3.4 数据压缩与加密

数据在网络上裸奔,我是不放心的。压缩能省流量,加密能保安全。

3.4.1 压缩策略

JSON文本的压缩率很高。我常用的方案:

压缩算法 压缩率 CPU开销 适用场景
GZIP 70%-80% 中等 通用场景,服务端广泛支持
ZSTD 75%-85% 较低 移动端,压缩更快
LZ4 50%-60% 极低 实时性要求高的场景

我个人倾向用 GZIP。虽然压缩率不是最高的,但服务端不需要额外装库,兼容性最好。如果你们团队能统一技术栈,ZSTD是更好的选择。

小技巧:压缩前先去掉JSON里的空格和换行。别小看这一步,能多压5%-10%。

3.4.2 加密方案

用户行为数据涉及隐私,必须加密。我的方案:

  • 传输层:HTTPS是底线。别用HTTP,哪怕只是内网测试。
  • 内容层:AES-256-GCM加密payload。密钥由服务端下发,定期轮换。
  • 签名层:每个请求带HMAC-SHA256签名,防止数据被篡改。
// 加密流程示例
1. 生成随机IV(12字节)
2. 用AES-256-GCM加密payload
3. 拼接:IV + 密文 + GCM Tag
4. 对整个包做HMAC-SHA256签名
5. 发送:签名 + 加密包

重要:密钥不要硬编码在客户端代码里。我见过有人把密钥写在Java代码的字符串里,反编译一下就全暴露了。正确的做法是:启动时从服务端获取密钥,存在Keychain或KeyStore里。

3.5 整体流程串联

把上面几块串起来,一个完整的上报流程是这样的:

  1. 业务层调用 track(event),事件进入L1内存队列。
  2. 处理层清洗数据,补充设备ID、时间戳、用户ID等上下文。
  3. 每10秒或队列满50条,数据刷入L2数据库。
  4. 上报线程从数据库取出待发送数据,批量打包。
  5. 压缩(GZIP),加密(AES-256-GCM),签名(HMAC)。
  6. 检查网络状态,选择合适的上报策略。
  7. 发送请求。成功则删除已发送数据;失败则标记重试。
  8. 重试超过5次?数据保留,等下次周期再试。

这套架构我用了好几年,支撑过日活千万级的App。核心就一句话:稳、快、省、安全。你照着这个思路去设计,基本不会出大问题。