我们如何构建 tracio.ai 的 30 毫秒以内流水线
从信号采集到访客 ID,全程 30 毫秒以内:基于 Go、ClickHouse、Redis 与分布式处理的架构。
在着手构建 tracio.ai 的设备识别引擎时,我们有一个不容妥协的要求:整条流水线——从接收加密信号到返回访客 ID——必须在第 95 百分位下于 30 毫秒以内完成。本文将详细讲解我们为达成这一目标所构建的架构。
流水线概览
识别流水线包含五个阶段:信号解密、信号归一化、哈希计算、身份解析以及响应序列化。每个阶段都独立优化,能够并行执行的阶段则并行运行。总预算为 30ms,大致分配为:解密 2ms、归一化 3ms、哈希 2ms、身份解析 20ms、序列化 1ms。剩余 2ms 作为缓冲。
信号解密是对客户端加密传输的逆向处理。我们使用 Go 的加密库并配合硬件加速,对典型的 4KB 载荷可在 1ms 以内完成解密。归一化会解析信号 JSON、校验类型,并应用平台专属的转换——例如对 user agent 字符串进行归一化,去除与版本相关的噪声。
分布式身份解析
身份解析——判断这台设备此前是否出现过——是对延迟最敏感的阶段。我们将设备画像存储在 Redis 中,通过分布式键路由层在集群上进行分片。路由依据硬件层级指纹分配键,从而确保对同一台设备的查找始终命中同一个 Redis 节点。
我们的分片实现使用虚拟节点(每个物理节点 150 个)以保证分布均匀。当增加或移除节点时,只需重映射 1/N 的键,其中 N 为节点数量。我们用 Go 实现了该路由层,查找时间为 O(log n) 且零内存分配。
Redis 作为身份存储
我们在诸多方案(Memcached、ScyllaDB、DynamoDB)中选择了 Redis,原因在于其稳定的亚毫秒级响应时间以及对复杂数据结构的支持。每份设备画像以 Redis 哈希形式存储,字段包括各信号层级的哈希、访客 ID、最近一次出现的时间戳,以及置信度元数据。
身份解析查询是一次 HGETALL 调用,随后将传入的信号哈希与存储的哈希进行比对。若硬件层级匹配,我们便以高置信度返回既有的访客 ID。若仅软件层级匹配,我们会对信号级数据做相似度比较,以判断这是否为同一台设备更新了浏览器。若均不匹配,则生成新的访客 ID。
使用 ClickHouse 存储事件
每一次识别事件都会异步写入 ClickHouse。我们采用带缓冲的写入器对插入进行批处理——累积 100ms 或直到攒够 1000 条事件为止,以先到者为准。这种批处理至关重要,因为 ClickHouse 在大批量插入(一次数千行)时表现最佳,而非逐行插入。
我们的 ClickHouse 表结构针对两种最常见的查询模式做了优化:查找某个特定访客 ID 的全部事件,以及按时间段聚合事件。我们使用 MergeTree 引擎,主键为 (visitor_id, timestamp),从而实现快速的点查找和高效的范围扫描。物化视图则维护预聚合的日级与小时级指标。
在规模化下达成 30 毫秒以内
有三项架构决策对达成延迟目标至关重要。其一,流水线全程为流式处理——在整个 HTTP 请求体尚未完全接收之前,我们就已开始处理信号。其二,Redis 查找采用带持久连接的连接池,消除了 TCP 握手开销。其三,ClickHouse 写入完全异步,绝不阻塞响应路径。
在 5 万请求/秒的压力测试下,我们的 p50 延迟为 12ms,p95 为 24ms,p99 为 38ms。在 Redis 集群再平衡期间,p99 偶尔会超出 30ms 目标,但 p95 始终稳定低于 30ms。对于延迟要求更严格的客户,我们提供专属 Redis 集群,以消除多租户争用。