probe 分析系统开发随笔
缘起
Google Analytics 为各大站点提供免费的分析服务的同时,与其 Google Admobs 及其他盈利内容提供了大量的数据支撑与用户兴趣定位,使得 Google 得以获得大众受众趋势与潜在的内容投放机遇。正是由于这种原因,导致越来越多的用户选择屏蔽 Google Analytics 的相关分析信息域名 https://www.google-analytics.com
。
对于企鹅物流数据统计(下简称「企鹅物流」)来说,作为一个非盈利性的兴趣向玩家社区站,一开始我们认为毋需获得这些数据,任凭各个用户访问即可。但在迭代了数个版本、增加了多个周边项目、甚至需要着手开发 App 时,我们越来越重视用户分析数据的价值。这种价值并不是作为商业性公司所思考的商业性价值,而是为了可以发现一些潜在的、未经优化且难以发现的用户痛点、了解由于何种原因突增的访问量、以及探索一些已经开发完成的功能所可以继续迭代和优化的地方。要想获得这些问题的答案,不从 UI 交互日志上能寻找到合适与可信的线索是不切实际的。
现存方案
那么接下来就是如何选择合理地收集用户的使用数据的方法了。第零点,要考虑的绝对是用户隐私。作为一个深恶痛绝国内各大厂商的绝大部分产品线均置用户隐私于垃圾桶里的大环境的开发者,我觉得这一点是一定要死守的底线。退一万步来讲,作为一个网站(而非 App)技术栈、以及所运营的内容,也决定了我们也碰不到任何用户的个人隐私信息:API 和用户隐私内容都没有何谈去读。因此,在过滤掉百度统计、CNZZ 等界面实在一言难尽、统计脚本烂到无法接受、以及海外支持不够好的服务后,我们转向了海外的服务。虽说有些质量和性价比看起来还是确实很诱人的,但是最低的也都要大概 $9/mo/1k User,这个价格对于我们是几乎无法承受的:我们一个月的用户多的时候会有 200k-400k+,意味着分析服务就要最低 $2,000 左右,这对于我们来说是个天文数字。
鉴于用户隐私、成本与需求契合性考量,最终我们把目光转向了自建分析系统上。在对需求进行了初步的归整与思考后,我们列出了以下几个维度的系统需求:
MUST
必须要达到的标准
- 正常访问情况下,峰值资源占用平均不大于300MB;
- 进程平均CPU占用率不大于10%;
- 最小可能的系统负载;
- 需要可以看到实时的、同时在线的用户数量;
- 需要可以统计一天/七天/一月的Unique Views(即拥有去重功能);
- 需要可以统计一天/七天/一月的Page Views
2. SHOULD
以最大努力需要实现的功能,Good to have
- 有良好的可视化面板;
- 可以汇报其他事件数据;
- 对其他事件数据可进行简单程度的分析。
以上几个维度的系统需求的同时实现,还是带来了不少技术上的挑战的。特别是需要同时从 potentially 大于 5,000 个用户接收分析信息,所引入的网络带宽开销与数据量持久对于预算日益吃紧的我们提出了前所未有的挑战。虽说这么多用户算不上较大量,但是 2核4G 的突发性能实例所能承受的负载还是极其有限的。为了能最好地达到上述需求,需要进行一些技术选型。
技术选型
现存的统计上报所使用的上报方式不多,种类大致可分为以下几种:
基于 Image
与 1x1 GIF
典型使用者
- Google Analytics
优点
- 浏览器支持优良
- 天然支持跨域且无需 Preflight Request
- 单像素 GIF 为所有合法图片编码格式内可能的最小图片格式
- 支持使用 Beacon API,优化出站跳转时的上报分析
缺点
- 短时间内(10s)内多次发送请求(20+),HTTP 协议所占用的 overhead 过大
- 在不使用 Session 的情况下,客户端信息(User-Agent、屏幕大小、客户端版本、语言信息等)需要在每个 Request 中都进行传递、消耗过大;如果使用 Session,则 Session 信息的保存时间过短会导致需要重传 Session 信息、过长则会大幅度增加服务端存储开销,使用 Google Analytics 发现用户的访问时长存在非常大的波动,如果使用定值、效果不一定会好,使用动态值又可能会造成更多的问题
基于 XMLHttpRequest
的跨域请求
典型使用者
- Plausible (https://plausible.io)
优点
- 浏览器支持优良
- 与「基于
Image
与 1x1 GIF」基本一致 - 可返回 204 No Content
- 跨域请求由于不传递 Credentials、没有特殊 Headers,不需要 Preflight 请求
缺点
- 与「基于
Image
与 1x1 GIF」一致
可见这两种通信方式都不是最佳选择,由于 Session 信息需要 "out-of-context" 传递、协议 overhead、与短时间多次发送请求所带来的请求拥塞都带来了奇差的数据回传延迟和带宽开销。除此之外,这些统计方法多是面向非 SPA、亦或跳转次数较少的站点进行设计的,而企鹅物流数据统计一方面为了能更好地支持语义 URL,为所有统计页面都将「物品 ID」等嵌入了 URL,另一方面为了方便用户多角度地对比数据,我们还在几乎所有提到一个特定数据项的地方增加了页面间的跳转链接。这两种优化都会比传统站点带来更高频次的 URL 变更,带来更大的报告开销。
于是,我们反转了思考路径:如何可以重用一个连接,同时在每次报告时最小化带宽开销?
第一个问题的答案相对很简单:HTTP/2 自带连接重用。但是不要忘了 HTTP 的另一个问题,请求 overhead 过大,那有没有其他的呢?WebSocket 呀!虽然使用了 HTTP/1.1,但是只是 TCP 层的相对轻量封装,连接建立后非常轻量。
第二个问题由此迎刃而解:使用 protobuf 编码并序列化消息、而后直接使用 WebSocket 提供的原生二进制消息进行通信。
但是是不是还有其他的问题要思考?
WebSocket?
WebSocket 不仅解决了连接重用和报告消息的压缩,还自带心跳机制(RFC6455 Standard - Section 5.5.2 - Ping),但是对于 Session 信息如何处理?另外,WebSocket 的浏览器支持还不算非常完善,对于旧浏览器我们如何优雅地 fallback?
对于 Session 信息与浏览器兼容性 fallback,最终我们利用 WebSocket 的握手机制实现了一套较为巧妙的逻辑。不在此赘述 WebSocket 的握手机制了,下述我们所实现的逻辑:
- Web 端检测是否支持 WebSocket,如果不支持则直接通过同一端点发送一次基于
Image
的 Probe 握手请求(特征是请求头内不包含Connection: Upgrade; Upgrade: WebSocket
),并直接结束。 - 反之,如果支持 WebSocket,则:
- 如有
probeUid
且时间小于 180 天则直接重用,反之则于客户端本地生成新的 32 位英文大小写字母与数字结合的probeUid
并连带生成时间存储; - 将
probeUid
、客户端版本、客户端平台(web
app:ios
app:android
等)与当前访问路径编码为对应的 Query String、拼接于 WebSocket 端点后 - 使用拼接过的 URL 尝试通过
new WebSocket
发起 WebSocket 连接请求
这样就一同解决了多个问题:
- WebSocket Request 部分即包含了 Session 的所有信息,因此可直接在新事件发生时直接读取、省去了 Session 这一步;
- 由于使用了 WebSocket,用户浏览时长直接等于 WebSocket 连接时长
- 对不支持 WebSocket 的客户端,使用同样的记录逻辑,但只记录初始页面与基本 UV/PV 后,返回 1x1 GIF
未完. 后续 Procrastinating.