小红书技术REDtech

RED OHC 开源:刷新 Java 堆外缓存 SOTA,多线程性能最高达基线 3.38 倍

Image

在小红书搜推广链路中,每秒要完成千万级笔记召回。缓存访问离请求主链很近,查询效率会影响链路耗时,数据时效也会影响召回结果。RED OHC 开源后,我们用新一轮 5 轮基准测试对照 Cassandra OHC:32 线程纯读吞吐达到基线的 3.38 倍,32 线程读写 9:1 达到 2.60 倍;单线程读、写、读写 9:1 分别为 1.13 倍、1.26 倍、1.25 倍。另一组独立的 32 线程纯读 GC profiling 中,RED OHC 每次读的堆分配约 0.001 字节,3 轮、每轮 3 秒的测量窗口内 GC 次数为 0。

本文从搜推广链路的访问特点出发,介绍 RED OHC 如何处理并发读取、写入和内存回收,并给出基准测试结果。

RED OHC 已经开源(Apache 2.0)

仓库:

https://github.com/Red-EAD/red-ohc

01

每秒千万次召回,对缓存意味着什么

Image

在搜推广链路中,一次搜索或广告请求通常会从亿级笔记库中召回候选内容,再进入排序和决策。整条链路每秒完成千万级笔记召回,笔记内容和特征数据大多从缓存读取。每个业务使用独立的缓存实例;在单个实例内部,多个读写线程会并发访问同一份数据。

缓存处在请求主链路上,因此有两项直接要求:

一是时延。 缓存查询位于请求关键路径上,单次访问的预算很紧。缓存 P99 会影响链路 P99;线程排队、GC 停顿或锁竞争,都可能传导到请求耗时。

二是结果时效与一致性。 缓存返回的数据会参与笔记召回。数据过期、读到新旧混杂的内容,或写入时条目被意外丢弃,都可能影响召回和投放结果。因此,TTL 的过期语义、更新的原子性和容量记账都需要明确。

在千万级笔记召回量下,这些要求会落到几项具体的工程约束上:

  1. 读多且并发高。大量请求会持续访问缓存。每次查找的固定成本都会被高频放大,因此读路径要尽量短,也要避免共享锁造成线程排队。

  2. 降低 GC 对请求时延的影响。几十 GB 的数据放在 JVM 堆内会增加堆管理和回收压力,采用堆外存储可以降低这部分压力,但不意味着整个进程完全不发生 GC。

  3. 持续写入伴随淘汰。新笔记进入缓存、已有内容更新,而缓存容量有限,淘汰会持续发生。淘汰策略影响命中率,进而影响回源量。

  4. 内存占用要可控。亿级条目和单机几十 GB 的容量,要求控制每条数据的额外开销;空间释放后,还要能把空闲页归还给操作系统。

  5. 降低操作的固定成本。哈希、锁、内存屏障和对象分配都可能出现在高频路径上,需要结合目标负载衡量它们的开销。

因此,这条链路需要一个支持堆外存储、低开销并发读取、精确 TTL、异步淘汰和物理内存回收的缓存引擎。

02

Cassandra OHC:正确的起点,撑不住的架构

Image

过去两三年,我们在线上使用开源的 Cassandra OHC(Off-Heap Cache)。它是 Cassandra 生态中的堆外缓存实现,也曾是我们选用的 Java 堆外缓存基线,在业务中稳定运行了两年多。

在搜推广的访问规模和并发模式下,我们在压测中观察到四方面的限制:

  • 读路径存在共享锁竞争。OHC 的读路径会访问同一缓存实例内的共享锁结构。本轮基准中,单线程纯读为 292 万 ops/s,32 线程纯读为 3,987 万 ops/s,吞吐约为单线程的 13.7 倍,低于线程数增加的幅度。多个读线程进入共享同步点时会发生竞争,这限制了读吞吐的扩展。

  • 读路径会产生堆分配。在当前压测配置下,OHC 的 get 平均每次在 Java 堆上分配约 32 字节。32 线程时,仅读路径的堆分配速率接近 1.4 GB/s,平均每秒触发 2 次以上 Young GC。堆外存储本身并不能消除 GC 影响,读路径上的分配仍会带来回收压力,并扰动尾延迟。

  • 淘汰在调用线程中同步执行。容量超限时,OHC 会在写请求线程里执行 LRU 淘汰。写线程除了更新条目,还要承担淘汰工作;并发写入时,淘汰也会带来额外竞争。

  • 底层内存难以及时归还。 条目释放后,底层内存不会随缓存容量变化及时归还操作系统,进程 RSS 回落受限。需要缩容时,往往只能重启进程。

这些限制都与读写操作和物理内存维护之间的耦合有关。更新条目时,业务线程还要承担淘汰和部分内存维护工作。去年 10 月,我们开始重写引擎,并将它命名为 RED OHC。

03

RED OHC 架构:逻辑面与物理面分离

Image

RED OHC 将每个缓存实例分为逻辑面和物理面。业务线程完成查询、逻辑更新和新值发布;旧值回收、淘汰维护、索引整理及空闲页归还等工作则交由该实例的后台 daemon actor 异步处理。新值的分配仍在写路径上完成,但通过写线程私有的 arena 减少共享竞争。

逻辑面记录索引、键值映射、条目的逻辑状态和值指针。get 或 put 返回时,本次操作的逻辑结果必须已经生效。物理面负责 native 内存的页与槽位管理、旧值释放和空闲页归还。对于不必在当前请求中完成的维护工作,RED OHC 会将它们延后处理。

每个缓存实例里有三个主要角色。reader(业务读线程)和 writer(业务写线程)处理各自请求需要完成的操作;后台 daemon 线程称为 actor,负责顺序执行该实例中的大部分跨条目物理维护。

读:完全无锁

Image

RED OHC 的稳态 get 不获取互斥锁,多个读线程可以并发查找,不必在读路径上争用同一把锁。32 线程纯读测试中,RED OHC 吞吐为 OHC 基线的 3.38 倍。这是整套引擎的测试结果,无锁读取是其中一个因素。

无锁读取还需要解决旧数据何时释放的问题。条目更新或删除后,如果旧内存立即释放,仍在读取它的线程可能访问到无效地址。因此,RED OHC 会延后释放旧内存,等相关读操作结束后再由后台回收。

可以借助 EBR(Epoch-Based Reclamation,基于 epoch 的延迟回收)理解这个过程:回收方要确认仍可能访问旧数据的读者已经结束,才释放对应内存。RED OHC 采用了相同的安全回收思路,但使用的是自有机制,并非直接照搬经典 EBR 实现。

读路径尽量减少额外工作和堆分配。稳态读取的堆分配接近于零;TTL 在读取时判断,过期条目不会返回。

写:发布即提交,物理回收稍后

Image

put 在写线程私有的 arena 中为新值分配空间,使用 64 KB 至 2 MB 的连续页并按大小类分池;随后写入数据、原子发布指针并投递一条变更事件。变更投递后,写线程不必等待后台维护;返回时,本次逻辑更新已经生效。

旧值的物理释放由 actor 负责。写线程先把旧地址放入自己的退役通道,等相关读者退出保护区后,actor 再按页批量释放。条目带有版本代际,替换在提交点原子生效;读者读取到的是有效的旧值或新值,不会读到一半更新的内容。

actor:物理面的 daemon

Image

淘汰决策、TTL 维护、页回收和索引整理等跨条目工作,由 actor 在单线程中顺序处理。这里串行的是后台维护路径,业务读写线程仍可并发处理各自的请求。actor 不需要在多个维护线程之间同步共享状态,这些维护也不必挤进请求线程。

writer 将变更事件投递给 actor 后即可继续处理请求。actor 通常按 200 微秒的时间窗合并处理事件;积压增加时,会切换为逐事件响应。批处理减少低负载时的调度开销,逐事件响应则帮助 actor 及时处理高负载下的积压。

Image

空闲页也由 actor 归还操作系统。内部压测中,压测结束后缓存 RSS 从 8.5 GB 在 10 秒内回落到 51 MB,无需重启进程,也不依赖特定内存分配器。

贴近硬件:按缓存行设计,和 AI 一起看汇编

引擎中的关键状态按 64 字节缓存行对齐。条目元数据打包在原生前缀中;每个读者线程的登记槽独占一条缓存行,减少线程间相互影响。页内的写者分配游标与回收者使用的空闲位图也分开放置。写者通过一次 CAS 预留一批 64 个槽位,再从线程私有掩码中逐个发放。这样的布局减少多个核心反复写入同一缓存行,有助于降低一致性流量和缓存行抖动。

我们用 AI 辅助分析 JIT 生成的机器码,并人工核对热路径的内联情况、内存序栅栏和指令数。重点是确认一次 get 实际经过哪些指令,以及源码里不明显的固定开销。

数据结构与算法:不用 JDK 集合,主路径全 O(1)

Image

先看并发数据结构。写者到 actor 的变更通道使用无锁 MPSC 队列,页池的多生产者获取使用 xadd 变体,相关实现来自 JCTools。这些结构针对生产者和消费者角色做了路径优化与缓存行隔离。对 RED OHC 来说,选择队列需要看目标并发模式下的实际开销,而不只是 API 是否熟悉。

TTL 维护采用类似 Kafka 的分层时间轮。条目按到期时间放入不同层级,时间跨度逐级扩大,覆盖范围可达数月;常规到期处理为 O(1)。位图用于跳过空闲时间片,超远期条目则由小顶堆兜底。这样,常规 TTL 维护不需要逐条扫描整个缓存。

主路径上的索引探测、已知淘汰节点的摘挂和时间轮到期处理都按 O(1) 设计;内存回收则按页批量合并。相比需要扫描整张表或遍历链表的实现,这些操作能避免随条目数量增加而增长的线性工作。

04

除了快之外,引擎里的那些设计

除了吞吐,实际使用还涉及淘汰策略、API 兼容、TTL 测试和内存可观测性。下面介绍几项相关设计。

淘汰策略覆盖不同访问模式。默认策略是 S3-FIFO,属于 2023 年提出的 FIFO 系算法;它通过两个频次计数和 ghost 队列识别条目复用,摘挂操作为 O(1)。LRU 作为兼容策略保留。针对搜推广召回负载,我们在 S3-FIFO 上加入了类似 S4-FIFO 的虚拟 Skip 区。只访问一次的条目进入小队列并按 FIFO 离开,不更新频次;这部分复用条目已有的 3 位策略状态,没有增加额外字段。ghost 队列使用堆外开地址表,扩容时增量 rehash,避免一次性迁移整张表。引擎还提供 W-TinyLFU,并用爬山法自适应调整策略参数。策略计算由 actor 单线程处理,读线程以 1/16 的概率异步采样,不必在每次命中时重排共享链表。

Image

API 沿用 Java 工程师熟悉的调用方式。操作接口采用类 JDK Map 风格,提供 get、put、putIfAbsent、replace 和 remove;构建器沿袭 Cassandra OHC 的 OHCacheBuilder。这样可以减少迁移时重新适应 API 的工作。

支持直接读取堆外数据。反序列化器可以读取堆外只读视图;高级调用方也可通过 ValueView 或 DirectValueConsumer 直接消费 native 内存,省去反序列化和额外拷贝。

降低哈希碰撞风险。准入防护参考 Caffeine 的 Hash-DoS 处理方式,减少恶意碰撞键持续进入缓存的风险。

淘汰回调提供原因。EvictionListener 和 RemovalCause 区分驱逐、过期与显式删除,下游可以据此触发预热或审计。

内存使用可审计。 引擎按字节记录容量,并提供各类页池的审计计数,便于查看内存分布和回收情况。

05

Benchmark

对照组为 Cassandra OHC 和 RED OHC 当前版本。Cassandra OHC 与线上服役版本同源,并逐类比对过字节码。

基准测试在 AMD EPYC 7T83(Zen 3)上运行,使用 JDK 11、1 GB 堆和 1 GB native 内存。JMH 采用吞吐模式,键长 16 字节、值长 64 字节,工作集包含 2.4 万条数据,访问均匀随机。RED OHC 使用默认 S3-FIFO,Cassandra OHC 使用默认 LRU,共测试 5 轮。表中吞吐单位为万 ops/s;5 轮比值范围表示本次各轮观测的最小值至最大值,不是置信区间。

Image
Image

JMH gc profiler 以 32 线程纯读运行 3 轮、每轮 3 秒。在该测量窗口中,Cassandra OHC 的 get 平均堆分配约 32 B/op、GC 22 次;RED OHC 约 0.001 B/op、GC 0 次。这组数据描述该 profiling 窗口,不代表其他负载或长期运行永远不发生 GC。

Image

在这组硬件和测试口径下,6 个场景的 RED OHC 吞吐均高于 Cassandra OHC 基线:单线程读、写、读写 9:1 分别为 1.13 倍、1.26 倍、1.25 倍;32 线程读、写、读写 9:1 分别为 3.38 倍、1.30 倍、2.60 倍,最高差距出现在 32 线程纯读。就本文给出的对照基线和负载而言,RED OHC 刷新了这组 Java 堆外缓存基准结果。微基准结论限定在所列硬件、数据集和配置内,不能直接折算为线上全链路 QPS。

06

写在最后

从引入 Cassandra OHC 到启动自研,RED OHC 的设计始终围绕一个目标展开:让请求线程尽量只处理当前读写,把淘汰、TTL 维护和内存回收交给后台执行。读路径不依赖共享锁,actor 单线程处理跨条目维护,S3-FIFO、分层时间轮和按页回收则构成维护机制。新一轮基准测试中,RED OHC 在 6 个场景下都高于 Cassandra OHC 基线;32 线程纯读达到 3.38 倍,读写 9:1 达到 2.60 倍,单线程读写混合达到 1.25 倍。

RED OHC 已在小红书搜推核心召回链路完成灰度验证并上线运行。基准测试用于比较受控负载下的性能;线上灰度则用于观察它在真实请求流量和工程约束下的运行表现。

07

作者简介

  • 何亮:就读于东北大学计算机专业,现于小红书推荐工程架构组实习,专注 JVM、推荐系统性能优化等基础架构方向,同时为 Apache Dubbo Committer,具备高并发系统性能优化与开源实践经验。

  • 立言(姜星羽):毕业于哈尔滨工业大学计算学部,现就职于小红书推荐工程架构组,负责核心推荐链路的架构演进与性能优化、图框架基础能力建设,并参与生成式排序等方向的研发工作。

  • 辰熙(张磊):有多年互联网架构研发经验,现就职于小红书推荐工程架构组,负责核心推荐链路的架构演进、性能优化和稳定性治理,支撑千万级业务流量的稳定运行。

08

团队介绍与招聘

小红书引擎架构团队负责主站与海外业务在搜索、推荐、广告和电商等场景中的引擎及架构研发,并持续探索 AI Native 场景下的引擎能力。

内推简历投递:

[email protected]

Image