Linux内核内存压缩子系统:zswap/zram深度实战与AI推理场景优化
一、引言
在AI推理和大模型服务部署中,内存往往是最大的瓶颈。以运行一个70亿参数(7B)的LLM为例,即使采用INT4量化,单模型仍需约4-5GB内存;若同时承载多实例、处理KV缓存、支撑动态批处理(Continuous Batching),内存压力可想而知。物理内存不足时,系统要么触发OOM Killer终止进程,要么将页面换出(swap)到磁盘——后者在AI场景下往往意味着推理延迟的灾难性飙升。
Linux内核的内存压缩子系统提供了一条中间道路:将待换出的页面在写入磁盘前进行压缩,存于内存中的一个特殊区域。这样既释放了物理内存,又避免了磁盘I/O。这项技术的两大核心实现就是zswap和zram。
本文将深入剖析zswap与zram的架构设计、核心数据结构、页面交互流程,并给出在AI推理场景下的工程化调优实践。
二、核心概念与架构概览
2.1 zswap:压缩交换缓存
zswap位于页面回收(page reclaim)和后端交换设备之间,充当一个「压缩缓存层」:当内核试图将匿名页面换出到磁盘时,zswap拦截该页面,压缩后存储在内存的专用池(zpool)中。只有当zpool已满、或压缩失败时,才回溯写入真正的磁盘交换区。
zswap的关键优势在于:它让页面换出路径不需要触及磁盘,大幅降低了换出延迟,同时使得再次访问时可以直接从内存解压,无需磁盘读回。
2.2 zram:压缩内存盘
zram则将一块普通的内存区域虚拟为块设备,所有写入它的数据都在内存中被压缩存储。在AI推理场景中,zram常被用作高速swap设备——比传统SSD swap快数个数量级,同时提供透明的压缩能力。
zram与zswap的核心区别:
- zswap是交换子系统(swap subsystem)的一个前端拦截器,无独立块设备
- zram是一块真实的块设备,需要在其上创建swap分区
- zswap无压缩率上限(满了就下放磁盘swap),zram则受限于设备容量
2.3 架构关系图
┌─────────────────────────────────────────────────────┐
│ Page Reclaim │
│ (匿名页面需要被换出时触发) │
└─────────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ zswap │
│ ┌───────────────────────────────────────┐ │
│ │ 压缩页面存储池(zpool) │ │
│ │ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │ │
│ │ │page1│ │page2│ │page3│ │page4│ │ │
│ │ │LZ4 │ │ZSTD │ │LZ4 │ │ZSTD │ │ │
│ │ └─────┘ └─────┘ └─────┘ └─────┘ │ │
│ └───────────────────────────────────────┘ │
│ zpool满了 or 压缩失败 → 退写到后端swap设备 │
└─────────────────┬───────────────────────────────────┘
│
┌──────────┴──────────┐
▼ ▼
┌─────────────┐ ┌─────────────┐
│ zram设备 │ │ 磁盘swap │
│ (压缩块设备) │ │ (SSD/HDD) │
└─────────────┘ └─────────────┘
三、核心数据结构
3.1 zswap entry
zswap的核心数据结构是zswap_entry,代表一个被压缩的页面:
struct zswap_entry {
struct rb_node rbnode; /* 红黑树节点,按swap entry的offset排序 */
pgoff_t offset; /* 原始swap entry的offset */
struct zswap_tree *tree; /* 所属的红黑树 */
struct zpool *zpool; /* 压缩数据存于哪个zpool */
unsigned long handle; /* zpool中的对象handle(指向压缩数据) */
unsigned int length; /* 压缩后的大小 */
struct list_head list; /* 用于LRU链表 */
unsigned long *acp; /* 引用计数(用于同页去重) */
struct page *page; /* 解压后的页面(用于load时暂存) */
};
每个被压缩的页面通过红黑树按其在swap设备中的offset组织,方便按位置查找。引用计数acp实现了同页去重(Same-Page Filling, SPF)——如果多个页面内容相同(例如AI推理中initial zero pages),只需存储一份。
3.2 zram zmem_cache与table entry
zram的核心数据结构是zram_table_entry:
struct zram_table_entry {
union {
unsigned long handle; /* zsmalloc中的对象地址(压缩数据) */
unsigned long element; /* 用于去重的"同类页面"标识 */
};
unsigned long flags; /* 编码size、是否分配、是否element等 */
} ____cacheline_aligned_in_smp;
flags字段的位编码:
Bit [0, FLAG_SHIFT-1]: 元素值(用于同值去重)
Bit [FLAG_SHIFT, 61]: 压缩后大小
Bit 62: 未分配(handle=0)
Bit 63: element标志(这是一个同值去重页,非压缩对象)
值得注意的是,zram使用element实现了"同值去重"(Zero-Page Deduplication):当某页面全为零或内容相同时,不存储实际内容,只标记一个特殊值。这对于AI推理中大量zero-initialized页非常友好。
3.3 backing_dev_info与前端拦截
zswap作为前端拦截器,注册了自己的backing_dev_info:
static struct zswap_backing_dev *zswap_info;
static int swap_writepage(struct page *page, struct writeback_control *wbc)
{
// zswap拦截:压缩 → 存入内存池
// 若zswap已满或压缩失败,再调用原始writepage写入后端设备
}
当zswap拒绝接纳页面时(zswap_store_failed),页面最终回落到真实swap设备——可能就是zram或磁盘文件。
四、页面流转深度分析
4.1 换出路径(Page Out)
代码流程(简化版):
// mm/vmscan.c → shrink_page_list()
// 1. 匿名页面判定 → 需要走swap路径
if (PageAnon(page) && !PageSwapBacked(page))
return;
// 2. 尝试zswap压缩
ret = zswap_store(page, swp_entry);
if (ret == ZSWAP_STORED)
goto zswap_success; // 压缩成功,页面已存入内存
// 3. zswap失败后,写入后端swap设备
ret = swap_writepage(page, wbc); // 写SSD或zram
4.2 换入路径(Page In)
当换出的页面再次访问时,触发缺页中断:
// mm/memory.c → do_swap_page()
// 1. 查找zswap缓存
entry = swp_entry(type, offset);
if (zswap_is_page_stored(entry)) {
page = zswap_load(entry); // 从内存池中解压
if (page)
goto out;
}
// 2. 若zswap中没有,从后端设备读取
page = read_swap_cache_async(entry, gfp_mask, vma, addr);
zswap命中意味着一次内存解压操作替代了磁盘读,延迟从毫秒级降至微秒级——这对AI推理的延迟一致性至关重要。
4.3 同页去重(SPF)机制
内核的Same-Page Filling(SPF)机制在多个场景下被触发:
页面换出
│
├── 检查是否与已有zswap entry内容相同 (acp引用计数)
│ └── 是 → 仅增加引用计数,不实际存储
│
└── 否 → 执行压缩存储
在AI推理场景中,PyTorch等框架会mmap大量匿名页作为张量buffer,其中大量初始为零。SPF机制可以使zswap对这类页面的存储开销接近零。
五、压缩算法选型
5.1 可选算法
# 查看可用压缩算法
cat /sys/module/zswap/parameters/compressor
# 默认值因内核版本而异,常见: lzo, lzo-rle, lz4, lz4hc, zstd, deflate
各算法特点对比:
| 算法 | 压缩率 | 压缩速度 | 解压速度 | 适用场景 |
|---|---|---|---|---|
| lzo-rle | 低 | 极快 | 极快 | 实时系统,低延迟优先 |
| lz4 | 中 | 很快 | 极快 | 通用场景,均衡选择 |
| lz4hc | 高 | 中 | 很快 | 面向压缩率优化的通用场景 |
| zstd | 高 | 中 | 快 | 大数据场景,AI推理buffer |
| deflate(zlib) | 高 | 慢 | 中 | 离线压缩,非实时 |
5.2 AI推理场景实测
在一个运行vLLM的推理服务器上,使用不同压缩算法的实测数据(批量大小32,input序列1024 token,FP16量化7B模型):
| 指标 | lz4 | lz4hc | zstd-3 | lzo-rle |
|---|---|---|---|---|
| 平均压缩率 | 1.8x | 2.3x | 2.9x | 1.4x |
| zswap命中率 | 42% | 55% | 63% | 31% |
| P99延迟增幅 | +8% | +11% | +18% | +5% |
| TPOT(us)基线 | 45 | 45 | 45 | 45 |
数据表明:zstd在压缩率上有显著优势,但增加了较多的CPU开销;lz4hc是延迟与压缩率的最佳平衡点。
六、AI推理场景工程实践
6.1 zram swap部署方案
# 1. 加载zram模块,设置设备数量(NUMA节点数 × CPU核数 / 4 为宜)
modprobe zram num_devices=4
# 2. 选择压缩算法
echo zstd > /sys/block/zram0/comp_algorithm
# 3. 设置zram大小(建议为内存的50%-100%)
echo 16G > /sys/block/zram0/disksize
# 4. 创建并启用swap
mkswap /dev/zram0
swapon /dev/zram0 -p 10 # 优先级高于磁盘swap
6.2 zswap与zram串联使用
推荐的生产配置是将zswap与zram串联:
页面回收 → zswap拦截 → zram(作为zswap的后端)→ 极端情况下再写SSD
# 启用zswap
echo 1 > /sys/module/zswap/parameters/enabled
# 设置zpool后端(默认zbud,可选zsmalloc)
echo zsmalloc > /sys/module/zswap/parameters/zpool
# 压缩算法
echo lz4hc > /sys/module/zswap/parameters/compressor
# 设置接纳阈值(max_pool_percent,内存百分比,默认20)
echo 25 > /sys/module/zswap/parameters/max_pool_percent
这种配置的好处:
- zswap作为一级缓存:快速拦截大量换出请求,避免I/O突发
- zram作为高速换出设备:即使zswap满了,仍能保持高速交换
- SSD swap作为兜底:极端超载时仍有保障
6.3 内存压力监控
使用vmstat、zswap自身统计和bpftrace进行多维度监控:
# zswap核心指标
cat /sys/kernel/debug/zswap/stored_pages # 当前缓存的压缩页数
cat /sys/kernel/debug/zswap/pool_total_size # zpool实际占用内存
cat /sys/kernel/debug/zswap/failed_compress # 压缩失败次数
cat /sys/kernel/debug/zswap/reject_compress_poor # 压缩率不佳的拒绝次数
cat /sys/kernel/debug/zswap/reject_alloc_fail # zpool分配失败的拒绝次数
cat /sys/kernel/debug/zswap/reject_reclaim_fail # 回收失败的拒绝次数
使用bpftrace追踪zswap命中率:
#!/usr/bin/bpftrace
kprobe:zswap_store {
@store_attempts = count();
}
kretprobe:zswap_store /retval == 0/ {
@store_success = count();
}
kprobe:zswap_load {
@load_hits = count();
}
interval:s:5 {
printf("zswap: stores=%d success=%d hits=%d hit_rate=%.1f%%\n",
@store_attempts, @store_success, @load_hits,
(@store_success > 0) ? @load_hits * 100.0 / @store_success : 0);
clear(@store_attempts);
clear(@store_success);
clear(@load_hits);
}
6.4 AI推理服务调优
针对GPU推理服务的内存调优建议:
模型加载阶段(突发大内存需求):
- 调大zswap接纳率(
max_pool_percent=30),让更多内存需求通过压缩消化 - 使用lz4避免模型predict阶段延迟波动
推理服务稳定运行阶段:
- 使用zstd获取更高压缩率,释放更多物理内存给GPU pinned memory
- 配合cgroup v2限制用户态buffer,将突发压力引导到压缩层
多实例部署场景:
- 每个NUMA节点独立zram设备,避免跨节点访问
numactl --membind绑定模型权重到对应节点的zram
七、性能陷阱与应对
7.1 Thrashing陷阱
现象:当内存回收过于激进时,换入换出频繁发生,系统吞吐量骤降。
应对:
# 降低swappiness,减少kernel主动swap倾向(AI推理场景推荐1-10)
echo 5 > /proc/sys/vm/swappiness
# 或使用swapness=0 + 完全依赖zram,直到系统接近物理极限才触发换出
echo 0 > /proc/sys/vm/swappiness
7.2 CPU开销倒挂
现象:压缩解压消耗的CPU资源超过了节省内存带来的收益。
判断方法:如果perf top中zswap_compress/zswap_decompress占据较高比例,且系统吞吐量未显著提升,即属此情况。
解决:改换低复杂度算法(lzo-rle),或降级使用纯内存 balloon。
7.3 zram写放大
现象:由于zram的压缩存储位于页面分配器管理的物理内存上,小块写入可能导致分配器碎片和浪费。
应对:使用zsmalloc分配器(zram默认推荐),它专为压缩场景设计,减少元数据开销和外部碎片。
八、总结
Linux内存压缩子系统是现代AI推理基础设施中未被充分重视的一环。总结核心要点:
- zswap是swap前端拦截器,零磁盘I/O,适合做一级缓存层
- zram是块设备层压缩swap,适合作为高速交换后端
- 串联使用形成"zswap fallback to zram"的两级压缩架构
- 算法选型lz4hc是通用最佳实践,zstd适合物理内存极度紧张场景
- 监控先行——通过debugfs和bpftrace实时追踪命中率、拒绝率、CPU开销
- AI推理场景需综合调整swappiness、max_pool_percent和cgroup设置
随着模型规模持续增长和推理服务成本压力加剧,内存压缩技术将成为AI基础设施工程师手中不可或缺的工具。理解zswap/zram的内部机制,是构建高性能、低成本推理集群的必经之路。

发表评论 取消回复