文章目录
-
- [故障分析案例:RK3588 NVR 设备内存耗尽与 OOM 频发](#故障分析案例:RK3588 NVR 设备内存耗尽与 OOM 频发)
- 一、故障现象
-
- [1.1 系统级现象](#1.1 系统级现象)
- [1.2 业务级现象](#1.2 业务级现象)
- [1.3 关键监控数据快照](#1.3 关键监控数据快照)
- [1.4 OOM 事件时间线](#1.4 OOM 事件时间线)
- 二、排查过程
-
- [2.1 第一层:确认瓶颈类型](#2.1 第一层:确认瓶颈类型)
- [2.2 第二层:内存状态深度分析](#2.2 第二层:内存状态深度分析)
-
- [2.2.1 内存分布拆解](#2.2.1 内存分布拆解)
- [2.2.2 kswapd 行为分析](#2.2.2 kswapd 行为分析)
- [2.2.3 bi/bo 异常解读](#2.2.3 bi/bo 异常解读)
- [2.3 第三层:OOM 历史回溯](#2.3 第三层:OOM 历史回溯)
-
- [2.3.1 OOM 触发者与受害者分析](#2.3.1 OOM 触发者与受害者分析)
- [2.3.2 OOM 后的二次崩溃](#2.3.2 OOM 后的二次崩溃)
- [2.4 第四层:进程级内存分析](#2.4 第四层:进程级内存分析)
-
- [cdvserver 异常行为](#cdvserver 异常行为)
- [2.5 第五层:代码变更关联分析](#2.5 第五层:代码变更关联分析)
- 三、根因分析
-
- [3.1 直接原因](#3.1 直接原因)
- [3.2 深层原因](#3.2 深层原因)
- [3.3 因果链](#3.3 因果链)
- [3.4 故障定性与排除项](#3.4 故障定性与排除项)
- [3.5 责任分层](#3.5 责任分层)
- 四、解决方案
-
- [4.1 修复优先级](#4.1 修复优先级)
- [4.2 方案一:zram(最推荐)](#4.2 方案一:zram(最推荐))
- [4.3 方案二:JVM 内存参数调优(立竿见影)](#4.3 方案二:JVM 内存参数调优(立竿见影))
- [4.4 操作命令](#4.4 操作命令)
- [4.5 部署 zram 前后效果比对](#4.5 部署 zram 前后效果比对)
- 五、经验总结
-
- [5.1 方法论沉淀](#5.1 方法论沉淀)
- [5.2 改进措施(Action Item)](#5.2 改进措施(Action Item))
- 附录:
-
- 排查命令速查
- [安装 zram 的代码](#安装 zram 的代码)
- [卸载 zram 的代码](#卸载 zram 的代码)
故障分析案例:RK3588 NVR 设备内存耗尽与 OOM 频发
| 项目 | 内容 |
|---|---|
| 设备型号 | Rockchip RK3588C NVR DEMO LP4 V10 Board(ARM64) |
| 操作系统 | openEuler,内核 5.10.66 |
| 总内存 | 7.6 GB(7677 MiB) |
| Swap | 0 GB(未配置) |
| 故障发生时间 | 2026-09-10 |
| 业务场景 | C 系列新产品 UMS 综合管理平台 |
一、故障现象
1.1 系统级现象
- Load Average 持续飙高 :
200.83, 181.72, 139.35 - CPU I/O 等待极高 :
wa占比 60%~81% - kswapd0 CPU 占用 39%:内核换页守护进程持续满负荷运转
- 可用内存极低 :
free仅 79 MB / 7677 MB - 阻塞队列堆积 :
b(uninterruptible sleep)高达 197~212
1.2 业务级现象
- cdvserver 进程 CPU 近 400%(4 核满载)
- java 进程被反复 OOM Kill(dmesg 中 6 次以上记录)
- 系统响应严重迟缓:ssh 登录、命令执行延迟明显
- page allocation failure:连 512KB 连续物理内存都分配失败
1.3 关键监控数据快照
plaintext
# vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
15 212 0 78480 2944 1060260 0 0 599 850 2 1 17 14 62 6 0
7 178 0 77232 2896 1056584 0 0 78780 1472 39197 65471 24 16 0 60 0
1 182 0 119528 1848 1005356 0 0 60952 1292 35261 57105 23 15 0 62 0
3 201 0 117308 3004 1007516 0 0 107180 100 39477 56354 5 17 0 79 0
12 197 0 76504 2596 1062892 0 0 105856 692 39296 58770 4 14 0 81 0
plaintext
# top 关键行
top - 16:17:08 up 10 days, 5:59, 7 users, load average: 200.83, 181.72, 139.35
Tasks: 427 total, 2 running, 424 sleeping, 0 stopped, 1 zombie
%Cpu(s): 32.4 us, 55.1 sy, 0.0 ni, 0.0 id, 10.9 wa, 0.0 hi, 1.5 si, 0.0 st
MiB Mem : 7677.2 total, 79.0 free, 6591.1 used, 1007.1 buff/cache
MiB Swap: 0.0 total, 0.0 free, 0.0 used. 185.3 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
3332062 root 20 0 4566644 173740 0 S 399.7 2.2 3059:21 cdvserver
3960797 root 20 0 6943076 186944 0 S 55.7 2.4 375:04.30 bus_all_in_one
132 root 20 0 0 0 0 S 39.1 0.0 189:47.49 kswapd0
2547560 root 20 0 9058832 233040 32 S 38.8 3.0 1165:55 java
3334338 root 20 0 4930572 107660 0 S 16.3 1.4 2450:36 msserver
457 root 20 0 1853596 14168 0 S 16.0 0.2 28:43.61 containerd
3494077 root 0 -20 0 0 0 I 15.7 0.0 1:09.40 kworker/6:1H-kblockd
3433802 root 20 0 35.6g 1.4g 80 S 10.8 18.5 39:15.42 java
3514454 root 20 0 1768264 15152 0 D 9.2 0.2 0:53.62 swGoServer
4254 root 20 0 831488 48268 0 S 8.9 0.6 665:49.13 cadvisor
3963112 root 20 0 2487704 28444 0 D 7.7 0.4 117:12.96 fw_base
3334905 root 20 0 6071704 624196 0 S 2.8 7.9 496:51.48 java
3335610 root 20 0 17.9g 501020 64 S 1.8 6.4 313:56.40 java
1.4 OOM 事件时间线
shell
[root@openEuler ~]# grep -inw oom /var/log/messages
35586:Sep 10 13:46:34 openEuler kernel: [876463.580958] mali fb000000.gpu: OOM notifier: dev mali0 0 kB
35587:Sep 10 13:46:34 openEuler kernel: [876463.586975] civetweb-worker invoked oom-killer: gfp_mask=0x3100dca(GFP_HIGHUSER_MOVABLE|__GFP_ZERO|0x2000000), order=0, oom_score_adj=0
35878:Sep 10 13:46:35 openEuler kernel: [876466.040834] oom-kill:constraint=CONSTRAINT_NONE,nodemask=(null),cpuset=/,mems_allowed=0,global_oom,task_memcg=/system.slice/sshd.service,task=java,pid=1558603,uid=0
40301:Sep 10 15:16:34 openEuler kernel: [881887.619303] mali fb000000.gpu: OOM notifier: dev mali0 0 kB
40302:Sep 10 15:16:34 openEuler kernel: [881887.859679] mali fb000000.gpu: OOM notifier: dev mali0 0 kB
40303:Sep 10 15:16:34 openEuler kernel: [881887.879838] PullMessageServ invoked oom-killer: gfp_mask=0x1100cca(GFP_HIGHUSER_MOVABLE), order=0, oom_score_adj=0
40577:Sep 10 15:16:34 openEuler kernel: [881890.166313] oom-kill:constraint=CONSTRAINT_NONE,nodemask=(null),cpuset=/,mems_allowed=0,global_oom,task_memcg=/system.slice/sshd.service,task=java,pid=3278850,uid=0
43560:Sep 10 16:37:19 openEuler kernel: [886743.941864] mali fb000000.gpu: OOM notifier: dev mali0 0 kB
43561:Sep 10 16:37:19 openEuler kernel: [886743.947793] dmesg invoked oom-killer: gfp_mask=0x40cc0(GFP_KERNEL|__GFP_COMP), order=3, oom_score_adj=0
43892:Sep 10 16:37:19 openEuler kernel: [886746.695121] oom-kill:constraint=CONSTRAINT_NONE,nodemask=(null),cpuset=/,mems_allowed=0,global_oom,task_memcg=/system.slice/sshd.service,task=java,pid=3433802,uid=0
要点 :三次 OOM 分别发生在 13:46、15:16、16:37,均在下午时段;每次 OOM 前都伴随 mali fb000000.gpu: OOM notifier ------ GPU 驱动同样感知到全局内存压力,佐证这是系统级内存枯竭而非单一进程问题。
二、排查过程
2.1 第一层:确认瓶颈类型
| 指标 | 数值 | 瓶颈类型判定 |
|---|---|---|
us 32.4% |
中等 | 用户态 CPU 消耗存在,但不是主要矛盾 |
sy 55.1% |
极高 | 内核态消耗过半,ksoftirqd/kswapd 等内核线程繁忙 |
wa 10.9% → 81% |
爆炸性增长 | I/O 等待是主要瓶颈,CPU 大部分时间在等 I/O |
id 0% |
无 | CPU 完全无空闲 |
初步判断 :不是纯 CPU 瓶颈,而是 I/O 阻塞型瓶颈,根因指向内存子系统。
2.2 第二层:内存状态深度分析
2.2.1 内存分布拆解
plaintext
总内存: 7677 MB
├─ used: 6591 MB (85.8%) ← 进程实际占用
├─ cache: ~1000 MB (13.1%) ← 文件缓存,可回收
├─ buff: ~3 MB (0.04%) ← 块设备缓冲,可忽略
└─ free: 79 MB (1.0%) ← 真正空闲
关键发现:
free仅剩 79MB,已跌破内核低水位线(low watermark)buff/cache仅 1GB,且 kswapd 已在疯狂回收used高达 6.6GB,其中绝大部分是匿名页(anon-rss),无法在无 swap 时回收
2.2.2 kswapd 行为分析
kswapd0 CPU 占 39.1%,说明内核内存回收守护进程持续满负荷运转。其回收路径:
plaintext
1. 扫描 LRU 链表,寻找可回收页面
2. 对 clean pagecache → 直接丢弃(无需 I/O)
3. 对 dirty pagecache → 先写回磁盘(writeback),再丢弃
4. 对匿名页(anon)→ 有 swap 则换出,无 swap 则无法回收
当前系统 Swap=0,所以匿名页完全无法回收。
2.2.3 bi/bo 异常解读
| 指标 | 数值 | 含义 |
|---|---|---|
bi |
60,000~107,000 blocks/s | 巨量块设备读 |
bo |
100~1472 blocks/s | 块设备写极少 |
疑问 :为什么 bo 极低,而 bi 爆炸?
答案:
bo低 = 没有脏页需要写回(clean cache 居多),且 Swap=0,匿名页无处可写bi高 = kswapd 回收了 pagecache 页后,进程立刻触发缺页中断 ,被迫从磁盘重新读入 → "回收-重读"乒乓效应
即:kswapd 在做无用功------回收一页,进程立刻读回一页,白消耗 CPU 和 I/O。
2.3 第三层:OOM 历史回溯
2.3.1 OOM 触发者与受害者分析
| 触发者(Trigger) | 受害者(Victim) | 说明 |
|---|---|---|
| civetweb-worker | java (pid=1558603) | 触发者是 Web 服务,但被杀的是内存大户 java |
| PullMessageServ | java (pid=3278850) | 消息拉取服务触发,被杀的还是 java |
| dmesg | java (pid=3433802) | 连 dmesg 这种小工具申请内存都触发 OOM |
结论:
- 触发者是随机的------任何进程在申请内存时都可能触发
- 受害者永远是 java------OOM killer 的打分算法总是选中内存占用最大的进程
- java 进程 anon-rss 1~2GB,且长期运行,oom_score 最高
2.3.2 OOM 后的二次崩溃
plaintext
[886746.710795] Out of memory: Killed process 3433802 (java) ...
[886748.872875] systemd-journal: page allocation failure: order:7
关键发现:
- java 被杀后,内核尝试生成 core dump(崩溃转储)
- core dump 需要分配 order:7 = 128 页 = 512KB 连续物理内存
- 但系统内存已碎片化到连 512KB 连续空间都分配不出
systemd-journal报page allocation failure
恶性循环:
plaintext
内存耗尽 → OOM 杀 java → 内核做 core dump → 申请 512KB 连续内存失败 → 系统更加不稳定
↑___________________________________________________________________________↓
2.4 第四层:进程级内存分析
| 进程 | VIRT | RES | %MEM | 说明 |
|---|---|---|---|---|
| java (pid=3433802) | 35.6g | 1.4g | 18.5% | UMS 主服务,RSS 远超 -Xmx,堆外内存膨胀 |
| java (pid=3334905) | 6.0g | 624MB | 7.9% | 另一个 JVM 实例 |
| java (pid=3335610) | 17.9g | 501MB | 6.4% | 第三个 JVM 实例 |
| cdvserver | 4.5g | 174MB | 2.2% | CPU 399%,独立的 CPU 异常问题(见 2.5) |
| msserver | 4.9g | 108MB | 1.4% | 存储服务 |
| 其他 400+ 进程 | - | ~3GB | - | Docker 容器、系统服务等 |
java 进程合计占用 > 2.5GB 物理内存。
cdvserver 异常行为
cdvserver CPU 占用 399.7% (近 4 核满载),但 RES 仅 174MB(2.2%),说明:
- 这是独立的 CPU 问题(如死循环、忙等),不是内存问题
- 与本次 OOM/内存耗尽故障无直接因果关系
- 需要 cdvserver 开发单独排查,与 pgdog 连接池适配版本无关联
2.5 第五层:代码变更关联分析
结合同日代码审查(SVN r129024 → r129122),当日上库的 pgdog 连接池适配变更:
server.sh、update、initme.sh引入pg_dog_functions.sh公共库- 新增 3 个 8G 环境备份/恢复脚本
- cdvserver 属于存储组件(CDV),不在本次 pgdog 修改范围内
结论 :cdvserver 的 CPU 异常与当日代码变更无直接关联,属于独立问题。
三、根因分析
3.1 直接原因
系统总内存 7.6GB 不足以承载当前业务负载,且无 Swap 缓冲。
内存收支不平衡:
plaintext
收入(可用): 7677 MB 总内存 - 内核保留 ≈ 7500 MB
支出(占用):
├─ java × 3 ≈ 2500 MB
├─ 各类服务进程(含 cdvserver 174MB)≈ 1500 MB
├─ Docker 容器 ≈ 1000 MB
├─ PG + pgdog ≈ 500 MB
├─ 系统/内核 ≈ 1000 MB
└─ 其他 400+ 进程 ≈ 1000 MB
──────────────────────────────
合计 ≈ 7500 MB (刚好吃满)
任何额外的内存分配请求(日志写入、网络缓冲、临时文件)都会触发 OOM。
3.2 深层原因
| 因素 | 说明 |
|---|---|
| 无 Swap | Swap=0 导致匿名页无法换出,内存紧张时无任何缓冲空间 |
| JVM RSS 超出堆配置 | 单个 java RES 1.4GB,而 ums-8G.sh 中 -Xmx 仅 768MB,说明堆外内存(DirectBuffer/Metaspace/线程栈)膨胀 |
| kswapd 效率瓶颈 | 无 swap 时只能回收 pagecache,但回收速度 < 分配速度,形成"回收-重读"乒乓 |
| 内存碎片化 | 长期运行后连续物理内存不足 512KB,连 core dump 都失败,OOM 后二次崩溃 |
| 业务密度过高 | 427 个进程(含 Docker 容器)挤在 8GB 内存上,无隔离与限额 |
| cdvserver CPU 异常 | 独立的 CPU 问题(RES 仅 174MB),不参与内存因果链,需单独排查 |
关于 VIRT 的说明 :原始记录中"java VIRT 35.6g → -Xmx 设置过大"这一推断不够准确。VIRT 是虚拟地址空间大小,包含 JVM 预留的堆上限、代码缓存、mmap 区域等,不代表实际物理占用,也不等于 -Xmx 配置值。真正的异常信号是 RES(1.4GB) 明显大于 -Xmx(768MB),即堆外内存膨胀,这才是 JVM 侧需要治理的方向。
3.3 因果链
plaintext
业务负载增长 / JVM 堆外内存膨胀
(cdvserver CPU 异常为独立问题,不参与内存因果链)
↓
匿名内存占用 6.5GB + pagecache 1GB = 内存耗尽
↓
free < low watermark → kswapd 被唤醒
↓
kswapd 扫描 LRU,回收 clean pagecache(无 I/O)
↓
进程触发缺页中断,重新从磁盘读入 → bi 爆炸、wa 飙至 81%
↓
回收速度 < 分配速度 → kswapd 无法入睡 → CPU 39%
↓
任意进程申请内存 → 全局 OOM → java 被杀
↓
内核尝试 core dump → 512KB 连续内存分配失败
↓
系统更加不稳定 → 业务中断 → 用户感知"卡顿/无响应"
3.4 故障定性与排除项
本次故障为典型的"无 Swap + 内存容量不足 + 内存碎片化"复合型系统级故障。
- 非单一进程故障:OOM 的根本原因是全局内存不足
- 非磁盘 I/O 故障 :
wa高是内存换页引起,磁盘本身无坏道或性能问题 - 非代码级 bug :当日上库代码存在若干问题(如
update脚本中PGUSRE拼写错误),但与本次系统级故障无直接因果关系
3.5 责任分层
| 层级 | 问题 | 责任方 |
|---|---|---|
| 系统配置 | Swap=0,内存无缓冲 | 系统运维/交付 |
| 应用配置 | JVM 堆外内存无上限,RSS 超出 -Xmx | 应用开发/部署 |
| 进程异常 | cdvserver CPU 399%,独立的 CPU 死循环/忙等问题 | cdvserver 开发 |
| 硬件规格 | 8GB 内存对这套业务负载偏紧张 | 产品规划/硬件选型 |
四、解决方案
4.1 修复优先级
| 优先级 | 措施 | 预期效果 |
|---|---|---|
| P0(立即) | 添加 Swap(4GB)或启用 zram | 立刻缓解 OOM,让 kswapd 能换出匿名页 |
| P1(当天) | 调低 JVM 堆与堆外内存上限 | 减少 java 内存占用,降低 OOM 频率 |
| P1(当天) | 重启 cdvserver,观察 CPU 是否恢复 | 临时恢复 CPU 占用,需开发排查根因 |
| P2(本周) | cdvserver 开发排查 CPU 死循环/忙等原因 | 独立问题,不影响 OOM,但影响系统响应 |
| P2(本周) | 禁用 core dump 或限制大小 | 避免 OOM 后的二次内存分配失败 |
| P3(长期) | 评估硬件升级至 16GB | 根本解决内存容量不足 |
除添加 Swap 外,另有 2 项不依赖磁盘 I/O 的优化方案,按优先级排列如下。
4.2 方案一:zram(最推荐)
原理:用内存的一部分做压缩块设备,匿名页换出时先压缩再存入该"内存盘",全程不落盘。
bash
# 启用 zram,大小设为内存的 50%(约 3.8GB)
modprobe zram
echo 3.8G > /sys/block/zram0/disksize
mkswap /dev/zram0
swapon /dev/zram0
优点:
- 零磁盘 I/O,不磨损存储(对 eMMC/NAND 类嵌入式存储尤其重要)
- 压缩比通常 2:1~3:1,3.8GB zram 可存 7~11GB 匿名页
- Android 系统大量使用,ARM 平台成熟稳定
- 速度比磁盘 swap 快 10 倍以上
缺点:占用少量 CPU 做压缩/解压(但远小于 kswapd 疯狂扫描 LRU 的开销)。
4.3 方案二:JVM 内存参数调优(立竿见影)
当前配置(ums-8G.sh):
bash
-Xms256m -Xmx768m # 但实际 RES 1.4GB,说明堆外内存(DirectBuffer/Metaspace)膨胀
优化方向:
bash
-Xms256m -Xmx512m # 限制堆
-XX:MaxMetaspaceSize=128M # 限制元空间
-XX:MaxDirectMemorySize=256M # 限制堆外内存
效果:单个 java 进程有望从 1.4GB 降到 600MB 左右,3 个 java 共释放 2GB+。
4.4 操作命令
bash
# 临时添加磁盘 swap(无 zram 时的备选)
dd if=/dev/zero of=/swapfile bs=1M count=4096
mkswap /swapfile
swapon /swapfile
# 释放缓存
echo 3 > /proc/sys/vm/drop_caches
# 禁用 core dump
echo 0 > /proc/sys/kernel/core_pattern
4.5 部署 zram 前后效果比对

可以看到部署 zram 后, 系统负载极大下降, 可用内存明显增高, majflt, pgscank,pgscand 明显降低, 效果十分显著.
五、经验总结
5.1 方法论沉淀
wa高不等于磁盘故障 。本例中wa飙到 81%,但bo极低,说明不是写盘压力,而是内存回收引发的"回收-重读"乒乓。判断 I/O 瓶颈前,先看bi/bo的方向和比例。- OOM 的受害者不等于元凶。三次 OOM 的触发者各不相同(civetweb-worker、PullMessageServ、dmesg),被杀的却永远是 java。查 OOM 要找"是谁把内存吃光的",而不是"谁被杀了"。
- 无 Swap 等于没有缓冲带。7.6GB 内存 + 0 Swap,一旦匿名页占满,内核除了杀进程别无选择。嵌入式/拷机环境应默认配置 zram。
- 警惕 OOM 后的二次故障。core dump 需要连续物理内存,碎片化严重时反而会加剧不稳定,需提前禁用或限制。
- VIRT 大不是问题,RSS 大才是。不要拿 VIRT 判断 JVM 配置是否合理;RSS 明显超出 -Xmx 时,应查堆外内存(DirectBuffer、Metaspace、线程栈)。
sy高要定位到具体内核线程 。本例 55.1% 的sy中,kswapd0 独占 39.1%,直接把方向引向内存子系统。- 先看内存构成再看进程。先区分 Anon/File/Slab,再定位到进程 RSS,避免在 427 个进程里无目标地抓"内存大户"。
5.2 改进措施(Action Item)
| 序号 | 改进项 | 责任方 | 时限 |
|---|---|---|---|
| 1 | 拷机/交付环境默认启用 zram,纳入基线镜像 | 系统运维/交付 | 立即 |
| 2 | 治理 JVM 参数:限制堆、元空间、堆外内存上限 | 应用开发/部署 | 当天 |
| 3 | cdvserver CPU 399% 问题单独开单排查(死循环/忙等) | cdvserver 开发 | 本周 |
| 4 | 禁用或限制 core dump,避免 OOM 二次崩溃 | 系统运维 | 本周 |
| 5 | 接入内存水位、PSI(/proc/pressure/memory)、load 的监控与告警 |
运维 | 本周 |
| 6 | 评估 16GB 硬件规格,重新核定该业务负载的内存基线 | 产品规划/硬件选型 | 长期 |
附录:
排查命令速查
bash
# 查看内存使用详情
free -h
cat /proc/meminfo | grep -E "MemTotal|MemFree|Buffers|Cached|SwapTotal|SwapFree"
# 查看 OOM 历史
dmesg -T | grep -i oom
grep -inw oom /var/log/messages
# 查看内存压力
cat /proc/pressure/memory
# 查看最耗内存的进程
ps aux --sort=-%mem | head -20
smem -r | head -20
# 查看某进程内存详情
cat /proc/<pid>/status | grep -E "VmRSS|VmSize|VmSwap"
# 查看 kswapd 统计
cat /proc/vmstat | grep -E "pgscan|pgsteal|pgmajfault"
# 临时添加 swap
dd if=/dev/zero of=/swapfile bs=1M count=4096
mkswap /swapfile
swapon /swapfile
# 释放缓存
echo 3 > /proc/sys/vm/drop_caches
# 禁用 core dump
echo 0 > /proc/sys/kernel/core_pattern
安装 zram 的代码
bash
#!/bin/bash
# =====================================================================
# zram swap 部署脚本(192.117.10.60 定制)
# 参数:zram0 / 3G / zstd / 优先级100
# swappiness=80 / page-cluster=0 / min_free_kbytes=262144(256M)
# 特性:幂等,可重复执行(自动清理旧 zram 配置后重建)
# 用法:bash deploy_zram.sh
# =====================================================================
set -e
# ---------------- 配置区(调整参数只改这里) ----------------
ZRAM_SIZE="3G" # zram 容量上限(≈40% 物理内存,实际占用=压缩后数据)
ZRAM_ALGO="zstd" # 压缩算法(备选 lz4:CPU 更省、压缩比更低)
ZRAM_PRIO=100 # swap 优先级(高于未来可能加的磁盘 swap)
# ---- 内核参数 ----
SWAPPINESS=80 # 换冷页、保 cache,但别 100 免得 CPU 颠簸
PAGE_CLUSTER=0 # 关掉给磁盘设计的预读,按需解压
MIN_FREE_KB=262144 # 256MB:兼顾原子分配兜底池与 kswapd 提前回收
# ---------------- Step 1: 前置检查 ----------------
if ! modinfo zram >/dev/null 2>&1; then
echo "[ERROR] Kernel does not support zram, abort."
exit 1
fi
echo "[OK] zram kernel module supported."
# ---------------- Step 2: 清理旧配置(保证幂等) ----------------
# 若 zram0 已是活动 swap,先安全下线:确认 MemAvailable 足够容纳原始数据量
if swapon -s 2>/dev/null | grep -q zram0; then
# 用 mm_stat 第2列 orig_data(未压缩原始量,swapoff 回填内存的真实体积)
ORIG_DATA_B=$(awk '{print $2}' /sys/block/zram0/mm_stat 2>/dev/null || echo 0)
MEM_AVAIL_B=$(awk '/MemAvailable/{print $2*1024}' /proc/meminfo)
SAFETY_MARGIN=$((256*1024*1024))
if [ $((MEM_AVAIL_B - SAFETY_MARGIN)) -lt "$ORIG_DATA_B" ]; then
echo "[ERROR] MemAvailable(${MEM_AVAIL_B}B) - margin < zram data(${ORIG_DATA_B}B), abort."
exit 1
fi
swapoff /dev/zram0
echo "[OK] Old zram0 swap offloaded."
fi
if [ -e /sys/block/zram0 ]; then
echo 1 > /sys/block/zram0/reset 2>/dev/null || true
fi
rmmod zram 2>/dev/null || true
# ---------------- Step 3: 创建并启用 zram swap ----------------
modprobe zram num_devices=1
# 算法必须在 disksize 之前设置(disksize 一旦设置设备即进入使用态)
echo "$ZRAM_ALGO" > /sys/block/zram0/comp_algorithm
echo "$ZRAM_SIZE" > /sys/block/zram0/disksize
mkswap /dev/zram0
swapon -p "$ZRAM_PRIO" /dev/zram0
echo "[OK] zram0 enabled: size=$ZRAM_SIZE algo=$ZRAM_ALGO prio=$ZRAM_PRIO"
# ---------------- Step 4: 内核参数(立即生效 + 持久化) ----------------
sysctl -w vm.swappiness="$SWAPPINESS"
sysctl -w vm.page-cluster="$PAGE_CLUSTER"
sysctl -w vm.min_free_kbytes="$MIN_FREE_KB"
cat > /etc/sysctl.d/99-zram-swap.conf <<EOF
# zram swap 配套内核参数(由 deploy_zram.sh 维护,卸载脚本会删除本文件)
# 换冷页、保 cache,但别 100 免得 CPU 颠簸
vm.swappiness = $SWAPPINESS
# 关掉给磁盘设计的预读,按需解压
vm.page-cluster = $PAGE_CLUSTER
# 256MB:兼顾原子分配兜底池与 kswapd 提前回收(出厂自算值仅 11MB)
vm.min_free_kbytes = $MIN_FREE_KB
EOF
echo "[OK] sysctl applied and persisted to /etc/sysctl.d/99-zram-swap.conf"
# ---------------- Step 5: systemd 开机自启(仅管理 zram 设备) ----------------
cat > /etc/systemd/system/zram-swap.service <<EOF
[Unit]
Description=Configure zram swap device ($ZRAM_SIZE, $ZRAM_ALGO)
Documentation=man:zramctl(8)
After=systemd-sysctl.service multi-user.target
[Service]
Type=oneshot
RemainAfterExit=yes
# 依次:加载模块 -> 设算法 -> 设容量 -> mkswap -> swapon(内核参数走 sysctl.d,此处不重复)
ExecStart=/bin/bash -c 'modprobe zram num_devices=1; echo $ZRAM_ALGO > /sys/block/zram0/comp_algorithm; echo $ZRAM_SIZE > /sys/block/zram0/disksize; mkswap /dev/zram0; swapon -p $ZRAM_PRIO /dev/zram0'
# 回退:swapoff 解压回内存后卸载模块(需保证内存足够)
ExecStop=/bin/bash -c 'swapoff /dev/zram0; rmmod zram'
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable zram-swap.service >/dev/null 2>&1
echo "[OK] zram-swap.service enabled at boot."
# ---------------- Step 6: 验证输出 ----------------
echo "================ VERIFY ================"
swapon -s
echo "----------------------------------------"
zramctl
echo "----------------------------------------"
sysctl vm.swappiness vm.page-cluster vm.min_free_kbytes
echo "----------------------------------------"
free -m
echo "========================================"
echo "[DONE] zram swap deployed."
卸载 zram 的代码
bash
#!/bin/bash
# =====================================================================
# zram swap 卸载脚本(与 deploy_zram.sh 配套)
# 功能:安全下线 zram -> 重置设备 -> 卸载模块 -> 移除自启 -> 还原内核参数
# 还原:swappiness=60 / page-cluster=3 / min_free_kbytes=11211(出厂自算值)
# 用法:bash uninstall_zram.sh
# =====================================================================
set -e
# 出厂默认值(该设备内核自算 min_free_kbytes ≈ 11211KB)
DEF_SWAPPINESS=60
DEF_PAGE_CLUSTER=3
DEF_MIN_FREE_KB=11211
# ---------------- Step 1: 确认 zram swap 存在 ----------------
if ! swapon -s 2>/dev/null | grep -q zram0; then
echo "[WARN] zram0 is not an active swap, skip swapoff."
else
# ---------------- Step 2: 安全校验(关键) ----------------
# swapoff 回填量 = orig_data(未压缩原始量,mm_stat 第2列),非压缩后大小
ORIG_DATA_B=$(awk '{print $2}' /sys/block/zram0/mm_stat 2>/dev/null || echo 0)
MEM_AVAIL_B=$(awk '/MemAvailable/{print $2*1024}' /proc/meminfo)
SAFETY_MARGIN=$((256*1024*1024))
if [ $((MEM_AVAIL_B - SAFETY_MARGIN)) -lt "$ORIG_DATA_B" ]; then
echo "[ERROR] MemAvailable(${MEM_AVAIL_B}B) - margin < zram data(${ORIG_DATA_B}B)."
echo " Unsafe to swapoff now. Free memory first (stop services or drop cache),"
echo " then retry. Abort."
exit 1
fi
echo "[OK] Memory check passed: avail=${MEM_AVAIL_B}B, zram-data=${ORIG_DATA_B}B."
# ---------------- Step 3: 下线 swap ----------------
swapoff /dev/zram0
echo "[OK] zram0 swap offloaded."
fi
# ---------------- Step 4: 重置并卸载 zram 模块 ----------------
if [ -e /sys/block/zram0 ]; then
echo 1 > /sys/block/zram0/reset 2>/dev/null || true
fi
rmmod zram 2>/dev/null || true
echo "[OK] zram module removed."
# ---------------- Step 5: 移除 systemd 开机自启 ----------------
systemctl disable --now zram-swap.service >/dev/null 2>&1 || true
rm -f /etc/systemd/system/zram-swap.service
systemctl daemon-reload
echo "[OK] zram-swap.service removed."
# ---------------- Step 6: 还原内核参数并删除持久化文件 ----------------
sysctl -w vm.swappiness="$DEF_SWAPPINESS"
sysctl -w vm.page-cluster="$DEF_PAGE_CLUSTER"
sysctl -w vm.min_free_kbytes="$DEF_MIN_FREE_KB"
rm -f /etc/sysctl.d/99-zram-swap.conf
echo "[OK] sysctl restored: swappiness=$DEF_SWAPPINESS page-cluster=$DEF_PAGE_CLUSTER min_free_kbytes=$DEF_MIN_FREE_KB"
# ---------------- Step 7: 验证输出 ----------------
echo "================ VERIFY ================"
swapon -s # 应无 zram0 条目
ls /sys/block/ | grep zram || echo "no zram device left"
sysctl vm.swappiness vm.page-cluster vm.min_free_kbytes
free -m
echo "========================================"
echo "[DONE] zram swap uninstalled."