服务器入坑指北:zswap 的配置取舍

举报
Tulpartner 发表于 2026/10/22 07:55:02 2026/10/22
【摘要】 zswap 是换出路径上的内存压缩缓存,背后仍需真实交换空间。参数“设了没用”常源于早期启动算法未就绪导致静默回退,以及大页策略被多层继承或后续改写。本文讲清 zswap 链路位置,逐项说明算法、池上限、滞回、回写与大页参数管什么、如何现读生效值,并给出运行期补救、持久化单元、观测计数与判定表,强调改完须复核:生效值对得上、池有存页、压力下回写增长而拒绝计数不涨、重启仍生效。

内存不大的机器上,听说 zswap 能把换出的页压进内存、少走磁盘,于是抄一串内核参数写进启动配置,重启——一切照旧。去看算法,还是那个内核内置的默认值;再去看大页策略,明明在命令行里写了,读出来的生效值却不是它。参数看起来都写对了,就是不起作用。
这类"设了没用"有两个来源:一是启动顺序——早期启动阶段,你要用的那个压缩算法可能还没被加载进来,内核会静默回退到内置默认,不报错、不提示;二是大页策略的多层控制——启动参数设的是顶层默认值,之后还可能被别的单元改写。两者的共同点是:只有静态地现读生效值才能发现。
这篇先把 zswap 在链路里的位置讲清,再逐个参数说"它管什么、怎么验证",重点是两条现读生效值的动作与运行期补救路线。文中数值都是示例,按你的机器定。

先定位:zswap 在哪条路径上

zswap 是换出路径上的内存内压缩缓存。机制按顺序是这样的:内核要把一个页换出时,先尝试把它压缩进内存里的池;压缩成功,这次写盘就被推迟、甚至完全避免;当池达到上限、或者拿不到内存时,池里的页会按淘汰策略(最近最少使用优先)被写到交换设备上。
这里有两个必须分清的点:

  • 它需要背后有真实交换空间。池只是"缓存",不是"终点"——池满或淘汰时,页仍然要落盘。没有可用交换空间,这套机制就没有落脚点。
  • 它和"把压缩块设备当交换用"是两种不同做法。后者是把一个压缩过的块设备当作交换设备本身,不需要再往真实盘上落;zswap 则是在真实交换设备前面加了一层内存缓存。两者不是替代关系,混着谈会得出错误结论。
    取舍写在这里:zswap 的收益是减少交换 I/O、降低换入延迟;代价是用 CPU 换内存——压缩和解压都要花 CPU。而且要提醒一句:池满之后,冷页仍会落盘,它不是"把换出的东西永久留在内存里"。
    交换空间怎么建、建多大,不在这一篇的范围内(那属于交换空间与压缩块设备那一篇的主题)。

启动顺序陷阱:为什么命令行里写了却不生效

这是全文最值钱的一条。压缩算法在早期启动阶段可能尚未可用:如果它不在随内核内置的那一组里(常见情况是内置只有 lzo,其它算法编译成模块),内核在早期根本拿不到它——于是静默回退到内置默认,你在命令行里写的值就不生效。
"静默"是关键:它不会报错,池也照常工作,只有启动日志里留一行提示,形如:

# 启动日志里可能看到(形态示意):
# zswap: compressor <你写的算法> not available, using default lzo
# zswap: loaded using pool lzo/zsmalloc
journalctl -k -b | grep -i zswap                # 查这一行:回退提示 + 最终加载的池
cat /sys/module/zswap/parameters/compressor     # 现读生效算法(命令行写的可能不是它)
cat /sys/module/zswap/parameters/enabled        # 现读:池是否启用

判据只有一条:现读出来的算法等于你想用的那个,才算生效;写在命令行里不算。
两条补救路线,都要给验证动作:

  1. 让早期启动阶段就能拿到该算法——把对应模块纳入早期启动镜像的加载列表。验证动作:重启后先看那行启动日志里是否还有回退提示,再现读算法。
  2. 启动后写入运行期参数(不需要重启):
# 运行期写入(示例:切到 zstd;写完后必须复核)
# echo zstd > /sys/module/zswap/parameters/compressor
# cat /sys/module/zswap/parameters/compressor    # 读出来等于你写的,才算成

运行期写入的好处是立即生效;代价是重启会丢,所以要用一次性单元或启动脚本在每次启动后写入并复核(示例见后面的配置一节)。还有一个细节:切换算法后,已经在池里的旧页仍按旧算法解压,会随着淘汰逐步消失——这一点在不同内核版本上的表述略有差异,以现读与观测为准,别当成绝对结论。

池上限不是预分配:max_pool_percent 管什么

这个值表示压缩池最多能占整机内存的百分比,默认 20。要注意它不是预分配:池是按需增长到上限的,启动时并不会先占掉那么多内存。示例里我们用 30 来演示(数值是示例,按你的机器定),比默认更激进,代价是挤压可用内存。
还有一个容易被忽略的参数:滞回阈值(默认 90,意为池容量的百分比)。它的作用是:池满之后,要等池降到这个比例,才重新接收新页。把它设为 100 等于关闭滞回。这个阈值的存在,是为了避免池在"满—清一点—又满"之间反复抖动。

回写:决定"满"之后是黑洞还是真上限

这是第二条硬核判断。到达上限之后会发生什么,取决于回写机制是否开启:

  • 不开启(默认):只有池满才回写。于是冷页可以长期滞留在池里占着位置;池满之后,新页不再被接收,直接走交换设备——再加上前面那个滞回阈值,“满了以后新页绕过压缩"会持续一段时间。在这种状态下,"上限"并不真的只是上限,它同时意味着"到顶之后新页没有缓存可用”。
  • 开启(内核 6.8 起提供、默认关闭):在内存压力下主动把冷页回写到交换设备,池在满之前就开始收缩——这样"上限"才真正只是上限,不会出现"满了以后池子变成黑洞、新页绕过压缩"的状态。
    在更老的内核上,这个开关的文件本身不存在——现读时看不到它,就是最直接的证据。
    取舍必须写清楚,不能写成"开了就更快":开启回写意味着在持续内存压力下会带来更多交换 I/O;换来的是冷页不占着压缩池、压缩与解压的 CPU 不浪费在冷数据上,以及池行为的可预测性。所以它是"用更多 I/O 换更可预测的池",值不值得取决于你的盘和负载。
cat /sys/module/zswap/parameters/shrinker_enabled   # 现读:回写是否开启
# 开启/关闭(形态示意,取值形式按内核文档确认,改完必须复核)
# echo <Y 或 N> > /sys/module/zswap/parameters/shrinker_enabled
# cat /sys/module/zswap/parameters/shrinker_enabled

大页:配套取舍,而且"设上"不等于"生效"

大页与 zswap 的交互点在回收/换出路径:大页在被回收或换出时,通常要拆成小页处理,拆页本身有开销;而 zswap 压缩的对象是"页",粒度不一致会让这条路径更难预测。这也是为什么有人会把大页策略跟 zswap 放在一起考虑。
但不要假设"写在启动参数里就一定生效",原因有三层:

  1. 新内核把大页控制拆成按尺寸的多档,启动参数设的是顶层默认值(供继承用),各尺寸自己的取值才决定实际行为;
  2. 启动之后可能有别的服务或单元把该值改回去;
  3. 内核文档本身写明:即使把各处的控制都设为 never,也不等于全局禁用——某些合并路径会无条件忽略这些设置。
    所以判据同样是现读生效值,而且要注意读法:读出来的值里,方括号所在的那一项才是当前生效项。
cat /sys/kernel/mm/transparent_hugepage/enabled     # 看方括号在哪一项
cat /sys/kernel/mm/transparent_hugepage/defrag      # 碎片整理控制,同样看方括号
journalctl -k -b | grep -i -E 'hugepage|transparent'   # 启动时是否被接受

不生效时的替代路线:用一次性单元在启动后写入(写入启用状态与碎片整理控制),然后复核——同样看方括号那一项。
代价必须写出来:关掉大页会失去大页带来的 TLB 收益,某些负载反而更慢。所以这是"要不要用一点性能换可预测性"的取舍,不是"必须关"。把"关掉之后是否真的更好"落成可验证动作:在代表性的压力场景下,分别记录关与开两种状态的换出/换入趋势与业务响应,比较之后再定——而不是凭印象拍板。

只读观测:池在不在工作,拒绝计数在说什么

调参之前先看清现状。下面这些都不改变系统状态:

grep -i zswap /proc/meminfo                      # 池用量与已存页数(项名按内核版本)
grep -i -E 'swap|zswap' /proc/vmstat             # 换出/换入与相关计数
ls /sys/kernel/debug/zswap/ 2>/dev/null          # 若挂了调试文件系统,这里有更细的 zswap 统计
cat /sys/module/zswap/parameters/max_pool_percent

计数器怎么读,比读数本身更重要:

  • 池用量与已存页数:只要池里开始有页,就说明它在工作;一直为零,先回去看上面那两节(算法是否回退、池是否启用)。
  • 写回页数:开了回写之后,这个数在压力下增长是预期行为——它正是"冷页被主动移走"的证据。
  • 几类拒绝计数:池上限命中(页因为池满被直接落盘)、拒绝分配、压缩失败、压缩收益不足(压了还不如不压)、回写失败。前四个里,压缩收益不足通常说明数据本身难压缩;回写失败是严重信号——它意味着页没法从压缩池移到交换设备,这时候要先查交换空间是否可用、是否够用。
    判据:池有存页 = 在工作;拒绝计数持续增长 = 有东西在被挡住;回写失败增长要立即停下来查。
    如果 grep 不到 zswap 相关项:先确认池是否启用(看启用参数与调试文件系统是否挂上),再判断是不是项名与你的内核版本不同;不要把「没有输出」直接当成「池没在工作」。

参数化的顺序与判定表

顺序本身就是判断,四步:

  1. 先确认池真的在工作(有没有存进页、有没有命中)——不然后面全是白调;
  2. 再看压缩比与内存占用是否可接受;
  3. 再选算法:速度优先还是密度优先(前者省 CPU,后者省内存);
  4. 最后才调池上限与回写。
    还有一条能力边界值得记住:这套机制不支持指定压缩级别——不能像"压缩块设备当交换"那样调级别,只能在算法之间选。所以别在级别上耗时间。
    | 你看到的现象 | 该查哪里 | 结论 | 动作 |
    | — | — | — | — |
    | 命令行写了算法,行为没变 | 运行期算法现读 + 启动日志的回退提示 | 早期启动拿不到该算法,静默回退 | 让算法早期可用,或启动后写入并复核 |
    | 大页策略写了但生效值不同 | enabled/defrag 现读(方括号项) | 顶层默认值被继承,或被后续单元改写 | 启动后由一次性单元写入并复核 |
    | 池到某个点就不长了 | 上限现值 + 滞回阈值 | 池已到上限,且滞回未满足 | 评估提高上限,或开启回写 |
    | 新页像是不走压缩了 | 池上限命中计数 | 池满后新页被挡在外面 | 同上 |
    | 回写失败在增长 | 回写失败计数 | 页无法移到交换设备 | 先查交换空间可用性与容量 |
    | 压缩收益很差 | 压缩收益不足计数 | 数据本身难压缩 | 换算法,或接受现状 |

配置示例与运行期补救

下面这份是示例,参数顺序与取值都按我自己的习惯写,不要照抄——按你的机器与内核能力定:

# 内核命令行示例(顺序与取值皆为示例)
# zswap.compressor=zstd zswap.max_pool_percent=30 zswap.enabled=1 zswap.accept_threshold_percent=95

要点:示例里的算法用了 zstd(对照默认常见的 lzo:前者密度更好、更耗 CPU),上限用了 30,滞回阈值用了 95。参数名与是否支持要以你内核的现读为准,不同版本可能不同。
运行期补救用一个一次性单元(启动后写入并复核):

# /etc/systemd/system/zswap-runtime-tune.service(示例)
[Unit]
Description=Apply zswap runtime parameters after boot
After=local-fs.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/bin/sh -c 'echo zstd > /sys/module/zswap/parameters/compressor'
ExecStart=/bin/sh -c 'cat /sys/module/zswap/parameters/compressor'
[Install]
WantedBy=multi-user.target

判据:启用这个单元并重启后,cat 出来的算法等于你写的那个。回滚:停用并删除这个单元(或去掉对应的写入行),然后重启复核回到原来的值。
需要注意的是:这不保证早于所有消费者——local-fs.target 只意味着本地文件系统就绪;如果还有更早的进程要读这些参数,就得走前面那条「让算法在早期启动阶段可用」的路线。

改完怎么验证:每项一条判据

  • 生效值=期望值:算法、池上限、回写开关、大页策略各现读一次;大页要看方括号那一项。
  • 池真的在工作:池用量与已存页数开始增长。
  • 压力下写回页数增长,而拒绝计数不增长:这是"回写生效、池没有变成黑洞"的直接证据。
  • 业务侧趋势改善:在同一个压力场景下,换出/换入与响应时间的趋势比改动前好(只比较趋势,不承诺具体数值)。
  • 重启后仍然生效:运行期写入会丢,所以必须重启一次再复核——这一条专门验持久化。
  • 验证方式固定成一句话:在同一压力场景下复现一次,生效值对得上、拒绝计数不涨,才算通过。

这篇不解决什么

  • 不建交换空间:交换设备与压缩块设备那一篇讲的是"把压缩块设备当交换"与容量配置;本篇讲换出路径上的内存压缩缓存,两者是不同机制,建立方法不在这里重复。
  • 不解决内存读数口径:多个读数各说各话的问题属于另一篇;本篇只讲换出路径的压缩缓存怎么配、怎么验。
  • 不涉及 cgroup 限制层:内存上限与 OOM 取证在限制层;本篇在换出层,两者要用不同入口判断。

踩坑清单

  • 命令行里设了算法,但该算法在早期启动阶段不可用:内核静默回退到内置默认,你写的值不生效。
  • 只看命令行、不看现读生效值:写进去不等于生效。
  • 大页策略写了却不现读复核:可能被按尺寸的默认值继承,或被启动后的单元改写。
  • 池满之后以为"还会继续压缩":不开启回写时,池满后新页会被挡在外面。
  • 把开启回写当成纯收益:持续压力下它会带来更多交换 I/O。
  • 为了省内存把池上限开得过大:池是按需增长的,上限越高,可用内存被挤占得越多。
  • 关掉大页却不说代价:会失去 TLB 收益,某些负载反而更慢。
  • 把 zswap 当成"压缩块设备当交换":它需要有背后的真实交换空间,否则无落脚点。
  • 只看压缩比、不看拒绝计数:拒绝计数才是"有东西被挡住"的证据。
  • 忽略回写失败计数:它是严重信号,先查交换空间。
  • 运行期写入后不准备持久化:重启就回到旧值。
  • 改完不复核就宣布优化完成:没有现读与压测,等于没验证。
  • 在算法级别上耗时间:这套机制不支持指定压缩级别,只能在算法之间选。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。