kevy
本页内容

kevy-alloc——服务端的分配器

从 7.0 起,kevy 服务端不再用系统分配器(Linux 上是 glibc malloc),改用 kevy 自己的纯 Rust span 分配器 kevy-alloc。它是 kevy crate 的 kevy-alloc feature,默认开启,所以每一种服务端构建都带着它:cargo install kevy、发布页的二进制、容器镜像、npm install -g @goliapkg/kevy-bin。

只有服务端二进制会把它装成全局分配器。把 kevy crate 当库用的程序,仍然用它自己声明的分配器;链接这个 crate 不会改变它的分配方式。

它做什么

  • 每个 shard 一个堆,快路径上没有锁。 shard 从自己线程局部的堆里分配。别的线程释放的块会送回拥有它的那个堆。
  • 块没有头部。 Rust 每次释放都会给出大小,所以分配器从地址就能算出块所在的 span 和尺寸档,块旁边什么都不存。
  • 页会还给操作系统。 占用情况记在数据页之外的位图里,所以任何一个没有活块的 4 KiB 页都能单独归还(madvise(MADV_DONTNEED)),相邻的页照常使用。每个 shard tick 归还空闲了一段时间的页。glibc 的主 arena 只能从顶端收缩:活块下面的空闲块一直驻留在内存里。
  • 内存整理。 降级或删除的值会在原处留下空洞。当某个 shard 的 span 内空闲空间相对活数据偏大(超过活数据的 1/64,且超过 4 MiB)时,shard tick 会把分配器指出的值复制到更密的 span 里:每个 tick 0.5 ms,空闲空间超过 1/16 时最多 2 ms;同一个 tick 里的回收再把腾空的页还掉。空闲空间降到 1/256 以下就停。store 挪不动的值(索引叶子、大集合)留在原处。

实测

吞吐。 firefly(aarch64 Linux,4 KiB 页),同一个 commit 构建两份,服务端绑 2 个核,redis-benchmark -c 60,两个构建轮换跑五轮。相对 glibc 的变化:

命令吞吐
LPUSH+11.3 %
ZADD+11.4 %
HSET+3.1 %
SADD+1.4 %
GET、SET持平
INCR−2.2 %

INCR 这一项是噪声:精确的指令计数(callgrind,lx64,x86-64 Linux)显示每条命令在 kevy-alloc 上是 2015 条指令,在 glibc 上是 2016 条。按同样的计数,每一个写命令在 kevy-alloc 上的指令数都不多于 glibc。

内存。 分层存储的服务端把常驻内存控制在预算 × 1.05 以内(见 tiering.md)。在 D1 负载上(lx64,一千万个约 1 KiB 的 hash,3 GiB 预算,两个编译好的索引),glibc 会留下约占预算 3.5 % 的空洞还不回去,整理过程把它们压紧并归还。

能看到什么

INFO modules 给出进程实际使用的分配器:

module:name=alloc,impl=kevy-alloc

INFO allocator 把分配器映射的每一个字节分到有名字的几项里,按各 shard 的堆求和:alloc_live(在用)、alloc_rounding(尺寸档取整)、alloc_cache、alloc_span_free(在用 span 里的空闲槽位,也就是整理要压紧的部分)、alloc_returned(已还给操作系统)、alloc_virgin(已映射、从未触碰)、alloc_hysteresis(已腾空、留着复用)和 alloc_segment_overhead。alloc_accounted 是它们的和,等于 alloc_mapped。

分层存储的内存守卫读这些数字,不去遍历系统堆,也没有堆需要 trim:INFO tiering 里的 heap_trims_total 一直是 0。

不带它构建

cargo build --release -p kevy --bin kevy --no-default-features
cargo install kevy --no-default-features

kevy-alloc 是这个 crate 唯一的默认 feature,所以这样构建只有分配器不同。它报告 module:name=alloc,impl=system,没有 # Allocator 这一节;分层存储的守卫读 glibc 的 mallinfo2(macOS 上读 malloc zone 统计),空闲内存堆积时用 malloc_trim 收缩堆。

需要这个构建的情况:

  • 挂钩 malloc 的工具。 LD_PRELOAD 进来的分配器(jemalloc、tcmalloc)、glibc 的 MALLOC_* 调优参数、heaptrack 和 valgrind 的 memcheck 看的都是 malloc 调用。默认构建里服务端的分配不经过 malloc,这些工具几乎什么都看不到。
  • 页大于 4 KiB 的系统。 分配器按 4 KiB 归还页,系统页是其他大小时它一页都不还:Apple Silicon 上的 macOS(16 KiB),以及用 16 或 64 KiB 页编译的 arm64 Linux 内核。在这些系统上它仍然复用释放掉的内存,但不会还给操作系统。
  • 做对比。 想拿自己的负载和系统分配器比较,就两份都构建,在同一台机器上跑。