Linux内核内存压缩子系统

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推理基础设施中未被充分重视的一环。总结核心要点:

  1. zswap是swap前端拦截器,零磁盘I/O,适合做一级缓存层
  2. zram是块设备层压缩swap,适合作为高速交换后端
  3. 串联使用形成"zswap fallback to zram"的两级压缩架构
  4. 算法选型lz4hc是通用最佳实践,zstd适合物理内存极度紧张场景
  5. 监控先行——通过debugfs和bpftrace实时追踪命中率、拒绝率、CPU开销
  6. AI推理场景需综合调整swappiness、max_pool_percent和cgroup设置

随着模型规模持续增长和推理服务成本压力加剧,内存压缩技术将成为AI基础设施工程师手中不可或缺的工具。理解zswap/zram的内部机制,是构建高性能、低成本推理集群的必经之路。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部