先抛一个反常识的结论:Grab 的 Counter Service 把存储后端从宽列数据库(Cassandra 这类)整体迁到 Aerospike,没有停机、没有数据损坏,换来的是 P99 读延迟降低约 50%,磁盘数据从约 3 TB 降到 1 TB,单节点成本下降 45% 到 50%。这个服务支撑着 Grab 的反欺诈平台,每天处理约 10 亿次请求、每秒数万次查询。迁移本身不稀奇,稀奇的是他们怎么做到在不停机的情况下完成存储引擎替换,以及为什么换一个存储引擎能带来这么大的收益。

先说这个服务是干什么的。Counter Service 回答的是带时间窗口的计数查询,比如“最近 15 分钟有多少次叫车请求”“过去一小时有多少次支付失败”。这类查询在反欺诈场景里是高频操作,欺诈检测需要快速判断某个用户或某个设备在短时间内是否出现了异常行为模式。

原来的设计把 15 分钟、小时、天这几个粒度的计数分别存成不同的行。每个进来的事件要触发三次并行读、一次内存自增、一次批量写,加起来是四次网络往返。宽列模型用 clustering columns(聚簇列,就是决定同一分区内数据物理排列顺序的列)来高效检索时间范围内的行,这是 Cassandra 这类数据库的经典用法。但问题在于,这种模型下每次事件处理都要跨网络做多次操作,延迟和资源消耗都上去了。

Grab 没有直接粗暴地替换存储实现,而是先做了一层抽象。他们把存储访问从 Rust 服务的业务逻辑里拆出来,搞了一个 storage facade(存储门面,就是统一接口,背后可以接不同实现)。这个门面暴露了 legacy(旧存储)、Aerospike、mock(模拟实现)三种实现,配置项控制单后端、影子模式和分流模式。影子模式就是让新老存储同时接收读请求,但只把新存储的结果拿来和老的对比,不影响线上流量。他们把影子读的流量从 5% 逐步提升到 20%、50%、100%,每一步都用现有指标做数据一致性校验,确认没问题才把真实流量切过去。

真正大的架构改动在数据模型。Grab 最初考虑过保留“每个时间桶一行”的模型,但 Aerospike 每条记录大约要维护 64 字节的主索引元数据。在这个服务的记录量级下,记录数量直接决定了容量成本。所以他们换了个思路:把同一个 counter 和粒度下的所有时间桶合并成一条记录,里面放一个按时间戳排序的 map(键值对集合)。写入用 Aerospike 的原子 map 自增操作,过期的条目显式删除。这样记录数直接降了一个数量级以上,索引和磁盘占用自然就下来了。

读路径也跟着变了。原来的后端在服务器端按时间范围过滤并返回分页行,Aerospike 返回的是包含 map 的记录,Grab 在客户端做过滤。Aerospike 的 batch API(批量接口,可以把多个记录操作打包成一个请求发到对应节点)把多个键的访问合并成更少的网络请求,这对服务这种多键访问模式特别友好。

rollout 过程中也踩了坑。Aerospike 的 Rust 客户端一开始是同步 API,而且在集群替换时 DNS 处理有问题。团队后来换成了异步客户端,并且和上游维护者一起解决了 DNS 问题才继续推进。

最让我意外的是他们这种“先抽象、再影子、后切换”的迁移思路。很多团队换存储就是直接双写或者停机迁移,Grab 这套方法把风险拆成了可控的小步。对做后端架构的开发者来说,这套方法论可以直接借鉴:任何存储迁移都可以先做一层存储抽象,用影子模式验证数据一致性,再逐步切流量。而数据模型层面,他们证明了“记录数越少越好”在 Aerospike 这类带固定索引开销的数据库里是硬道理——合并记录、用原子操作、显式清理过期数据,这些思路在 KV 存储(键值存储,就是按 key 直接查 value 的数据库)场景下都能用。

要注意的坑是:Aerospike 的客户端生态不如 Cassandra 成熟,Rust 客户端尤其如此,迁移前要评估好客户端能力和维护方的响应速度。另外,把时间桶合并成单条记录后,单条记录会持续增长,需要设计好过期清理策略,否则记录会无限膨胀。Grab 的做法是显式删除过期条目,这个细节值得借鉴。

Accelerating Performance by Incrementally Integrating Rust into Existing Codebas
Accelerating Performance by Incrementally Integrating Rust into Existing Codebas
阅读原文 → 返回 AI 技术文档

内容与图片版权归原作者所有 · 原文: https://www.infoq.com/news/2026/10/grab-counter-aerospike-migration/?utm_campaign=infoq_content&utm_source=infoq&utm_medium=feed&utm_term=global