Linux 内存管理面试题
16 道题- 分类
- Linux
- 题目数
- 16 道
1 虚拟内存的核心作用是什么?分页机制如何实现地址翻译?
答案:
虚拟内存提供进程隔离、地址扩展、内存抽象三大能力。每个进程拥有独立的虚拟地址空间,通过 MMU(Memory Management Unit)将虚拟页(Page)映射到物理页框(Page Frame)。
分页机制:
- 页大小:默认 4KB(大页支持 2MB/1GB,
hugetlbfs),页越小内部碎片越少,页越大 TLB miss 越少。 - 多级页表:x86_64 默认 4 级(PML4 → PDPT → PD → PT),虚拟地址 48 位(256TB 用户空间)。多级页表避免为未使用区域分配连续物理页。
- TLB(Translation Lookaside Buffer):MMU 内部高速缓存,加速虚拟→物理地址翻译;TLB miss 时由硬件遍历页表(4 次内存访问),大页(hugepage)一次条目覆盖 2MB 显著降低 miss。
- 缺页中断(Page Fault):访问未映射页触发
do_page_fault(),区分次缺页(已映射但被换出)与主缺页(从未映射);次缺页成本低,主缺页需磁盘 IO。
# 查看进程虚拟内存布局
cat /proc/<pid>/maps
# 大页配置
echo 1024 > /proc/sys/vm/nr_hugepages
2 Swap 机制的工作原理是什么?生产环境如何合理配置 Swap?
答案:
Swap 是内核将冷页从内存换出到磁盘(Swap 分区或 Swap 文件)以扩展可用内存的机制。访问已换出页触发主缺页(major fault),需磁盘 IO,性能远低于次缺页(minor fault)。
核心参数:
/proc/sys/vm/swappiness:0-100,控制系统主动换出页的倾向。0 表示仅在内存耗尽时换出,100 表示积极换出;生产环境一般保持默认 60,数据库类负载建议 1-10。/proc/sys/vm/vfs_cache_pressure:默认 100,>100 加速回收 dentry/inode 缓存;<100 倾向保留。- Swap 分区 vs Swap 文件:分区无文件系统开销,连续 IO 性能略优;文件灵活(可在线伸缩),但 swapfile 须在快速设备上。
配置建议:
- 内存充裕(≥64GB):可设置 swappiness=1,避免匿名页强制换出影响性能。
- 内存紧张:设置 Swap ≥ 内存 25-50%,避免 OOM 触发后进程被杀。
- 云环境:使用 Swap 文件而非分区,便于迁移和扩缩。
# 创建 8GB Swap 文件
fallocate -l 8G /swapfile && chmod 600 /swapfile
mkswap /swapfile && swapon /swapfile
3 OOM Killer 的触发条件与打分机制是什么?如何调整 OOM 行为?
答案:
当系统内存严重不足且内核无法通过回收页缓存、释放 Slab、回收匿名页等手段腾出内存时,__alloc_pages_slowpath 失败,触发 out_of_memory() 选择进程杀死。
打分机制(oom_score / oom_adj / oom_score_adj):
- 每个进程
/proc/<pid>/oom_score:0-1000,初始值基于 RSS 比例计算。 oom_score_adj:-1000 到 +1000 调整值,叠加到内核计算的原始 oom_score 上(后者基于 RSS 比例与子进程内存)。-1000:永不被杀(如 systemd PID 1、关键基础设施进程)。0:默认值。+1000:优先被杀。
- 内核启发式:
/proc/<pid>/oom_score_adj为 0 时,内核会额外奖励长期运行进程、惩罚有特权(CAP_SYS_ADMIN)且近期使用大量内存的进程。
调整手段:
# 保护关键进程
echo -1000 > /proc/<pid>/oom_score_adj
# 容器化环境
# K8s Pod QoS:
# Guaranteed (requests==limits) → oom_score_adj=-998 最后被杀
# Burstable → oom_score_adj 在 -1000~+1000 区间
# BestEffort (无 requests/limits) → oom_score_adj=+1000 最先被杀
预防策略: 启用 cgroup 内存限制、配置 cgroup v2 memory.high 软限制、memory.max 硬限制。
4 Page Cache 是什么?它与文件 IO 的关系是什么?
答案:
Page Cache 是内核维护的磁盘页缓存,位于文件与用户缓冲区之间。所有文件 IO(除 O_DIRECT 绕过外)均经过 Page Cache:读命中时直接返回内存数据(避免磁盘 IO),未命中时触发磁盘读并填充缓存;写时先写入 Page Cache(脏页)异步回写磁盘。
关键机制:
回写策略:
/proc/sys/vm/dirty_*控制脏页比例与回写时机。dirty_background_ratio:默认 10%,内存占比超过则启动后台回写。dirty_ratio:默认 20%,同步写阻塞强制回写。dirty_expire_centisecs:默认 30s,脏页超过此时间开始回写。
写回策略:
- writeback(默认):周期性
wb_workfn、内存压力触发。 - WB_STRATEGY_BTWO(btrfs / ext4 fast commit):多块合并写入。
- writeback(默认):周期性
drop_caches:手动释放缓存(仅测试与排查时使用)。
echo 1 > /proc/sys/vm/drop_caches # 释放页缓存 echo 2 > /proc/sys/vm/drop_caches # 释放 dentry/inode echo 3 > /proc/sys/vm/drop_caches # 全部释放
**性能启示:** 数据库(MySQL InnoDB doublewrite、PostgreSQL)启用 `O_DIRECT` 绕过 Page Cache,自行管理 buffer pool;其余业务场景 Page Cache 是性能利器(顺序读 1GB 文件可全命中内存)。
5 NUMA 架构是什么?`numactl` 与 `numa_balancing` 如何调优?
答案:
NUMA(Non-Uniform Memory Access,非一致内存访问)是多路服务器(multi-socket)的内存架构。CPU 访问本地节点内存比远端节点内存快 30%-100%,因此需要感知节点亲和性。
NUMA 拓扑:
Socket 0 (CPU 0-31) Socket 1 (CPU 32-63)
├─ 内存 0-128GB ├─ 内存 128-256GB
└─ 本地访问 80ns └─ 本地访问 80ns
跨节点访问 140ns ↘ 跨节点访问 140ns
```text
**核心命令:**
查看 NUMA 拓扑
numactl –hardware
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 … 31
node 0 size: 131072 MB
node 1 cpus: 32 33 … 63
node distances:
node 0 1
0: 10 21
1: 21 10
进程绑 NUMA 节点 + 绑内存
numactl –cpunodebind=0 –membind=0 /usr/bin/mysql
或交错分配(适合内存均匀使用场景)
numactl –interleave=all /usr/bin/redis-server
**内核自动 NUMA Balancing:**
- `kernel.numa_balancing=1` 启用(**默认开启**,5.x 内核);实际生效需 `CONFIG_NUMA_BALANCING` 编译选项 + 调度器自动启发式共同作用
- 内核自动迁移数据/线程到本地节点
- **数据库场景反而建议关闭**(迁移抖动影响延迟):
```bash
sysctl -w kernel.numa_balancing=0
典型性能问题:
跨节点内存访问 →
perf stat -e node-loads,node-load-misses看 miss 率单节点内存耗尽 →
numastat -p <pid>看各节点内存分配K8s 拓扑亲和性:
spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: topology.kubernetes.io/zone operator: In values: ["us-east-1a"]
6 大页(HugePage)是什么?为什么数据库场景要用 hugetlbfs?
答案:
大页(HugePage)是 Linux 提供的大内存页机制,默认页 4KB,大页支持 2MB(x86_64 PD 一级映射)和 1GB(x86_64 PDPT 一级映射)。
为什么需要大页:
- TLB miss 减少:TLB 是 MMU 内部 cache,条目有限(典型 64-1536 条)。4KB 页下,64GB 内存 = 16M 页 → TLB 必然频繁 miss;2MB 大页下,64GB = 32K 页 → 命中率显著提升
- 页表层级减少:x86_64 4 级页表(4KB 页需要 4 次内存访问)vs 3 级页表(2MB 大页)+ 2 级(1GB)
- 缺页中断开销降低:一次大页申请代替 512 次 4KB 页申请(2MB / 4KB = 512)
配置与使用:
# 1. 查看当前大页
cat /proc/meminfo | grep -i huge
# HugePages_Total: 1024
# HugePages_Free: 512
# HugePages_Rsvd: 0
# Hugepagesize: 2048 kB
# 2. 预留大页
echo 1024 > /proc/sys/vm/nr_hugepages # 1024 × 2MB = 2GB
# 3. 挂载 hugetlbfs
mount -t hugetlbfs hugetlbfs /mnt/huge
# 4. MySQL 使用大页(my.cnf)
large-pages
# 5. JVM 使用大页
java -XX:+UseLargePages -XX:LargePageSizeInBytes=2m -jar app.jar
# 6. 透明大页(THP,Transparent HugePages)
# 优:自动合并为 2MB 页,应用无感
# 劣:合并/拆分有 CPU 开销 + 内存碎片;**生产数据库建议关闭**
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/enabled
```text
**MySQL InnoDB 调优:**
/etc/my.cnf
[mysqld] innodb_buffer_pool_size = 32G large-pages
需要先在 OS 预留足够大页
sysctl -w vm.nr_hugepages=16384 # 32GB / 2MB
**K8s 中大页:**
- `v1.HugePages` 是 K8s 资源(v1.22+ GA)
- 限制为 2MB 大页(不支持 1GB)
- Pod 必须独占大页(不能共享)
```yaml
resources:
requests:
hugepages-2Mi: 1Gi
limits:
hugepages-2Mi: 1Gi
7 KSM(Kernel Samepage Merging)是什么?应用场景与风险?
答案:
KSM 是 Linux 内核的内存去重机制,由 KVM 虚拟化场景引入(ksm 服务),扫描用户进程页,识别相同页(内容完全一致)合并为单一 COW 页,多个进程共享同一物理页。
KSM 工作机制:
- 内核守护进程
ksmd周期性扫描MADV_MERGEABLE标记的页 - 使用
memcmp+ 哈希树识别相同页 - 合并后页标记为只读,任一进程写入触发 COW 复制
- 进程可通过
MADV_UNMERGEABLE取消共享
启用与监控:
# 启用 KSM
echo 1 > /sys/kernel/mm/ksm/run
# 监控合并效果
cat /sys/kernel/mm/ksm/pages_shared # 当前共享页数
cat /sys/kernel/mm/ksm/pages_sharing # 共享引用计数
cat /sys/kernel/mm/ksm/pages_unshared # 已扫描但未合并
cat /sys/kernel/mm/ksm/full_scans # 完整扫描次数
# 进程声明可合并
madvise(addr, len, MADV_MERGEABLE)
# 进程声明不可合并
madvise(addr, len, MADV_UNMERGEABLE)
应用场景:
- KVM 虚拟机:多 VM 跑相同 OS(内核页、lib 页、静态数据可合并)
- 容器场景:OpenShift/CRI-O 默认开启 KSM,节省容器镜像共享页内存
- Java 应用:JVM 堆通常不受益(堆对象大多不同),但 metaspace/class data 可合并
风险与禁忌:
- CPU 开销:ksmd 扫描消耗 CPU,单核可达 10-30%
- NUMA 跨节点:跨节点合并降低内存局部性
- 实时性敏感:合并/COW 拆分有延迟抖动
- 安全边界:KVM hypervisor 默认不合并跨 VM 页(不同 VM 的"零页"看似相同但安全域不同)
监控建议:
# 1. 合并收益
shared=$(cat /sys/kernel/mm/ksm/pages_sharing)
saved_mb=$((shared * 4 / 1024)) # 4KB 页
echo "KSM saved: ${saved_mb}MB"
# 2. 禁用 KSM(如 K8s 节点发现高 CPU 占用)
echo 0 > /sys/kernel/mm/ksm/run
8 cgroup v2 内存控制详解:memory.max / memory.high / memory.low 三层限制如何配合?
答案:
cgroup v2 提供三层内存限制机制,模拟"硬限 / 软限 / 保底"资源分配:
| 文件 | 含义 | 触发行为 |
|---|---|---|
memory.max | 硬限制 | 超过 → OOM Killer 杀进程(不可恢复) |
memory.high | 软限制 | 超过 → 内核主动回收(异步、可恢复) |
memory.low | 保底 | 内存紧张时不被回收(保护关键进程) |
memory.current | 当前使用 | 实时统计 |
memory.events | low/high/max/oom 事件 | 触发次数计数 |
工作机制:
memory.max(硬墙)
↑
[ memory.high 软线:进入 reclaim ]
↑
[ 实际使用 memory.current 增长 ]
↓
memory.low(保底线:他人被回收也保你不被回收)
配置示例:
# 创建 cgroup
mkdir /sys/fs/cgroup/test
# 设置硬限制 1GB
echo "1073741824" > /sys/fs/cgroup/test/memory.max
# 设置软限制 768MB(触发回收)
echo "805306368" > /sys/fs/cgroup/test/memory.high
# 设置保底 256MB
echo "268435456" > /sys/fs/cgroup/test/memory.low
# 把进程加进去
echo $PID > /sys/fs/cgroup/test/cgroup.procs
# 监控
cat /sys/fs/cgroup/test/memory.current
cat /sys/fs/cgroup/test/memory.events
# low:0 high:5 max:0 oom:0
OOM 事件分析:
# 触发 OOM 时查看 kernel 日志
dmesg | grep -i "out of memory"
# Out of memory: Killed process 1234 (java) total-vm:2GB, anon-rss:1.5GB
# 看哪个 cgroup 触发 OOM
cat /sys/fs/cgroup/memory.events
# oom 计数非 0 → 触发过 OOM
K8s 资源映射:
| K8s 字段 | cgroup v2 映射 |
|---|---|
resources.limits.memory | memory.max |
| (无对应) | memory.high(K8s 不直接暴露) |
requests.memory | 与 cgroup 关系较弱,调度时用 |
PSI(Pressure Stall Information): cgroup v2 还提供资源压力指标:
cat /sys/fs/cgroup/test/memory.pressure
# some avg10=0.00 avg60=0.00 avg300=0.00 total=0
# full avg10=0.00 avg60=0.00 avg300=0.00 total=0
some:至少一个任务因内存阻塞(任意延迟)full:所有任务都因内存阻塞(完全卡顿)
生产实践:
- 数据库:设置
memory.high = memory.max × 80%触发主动回收 - Java 应用:
-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0配合 cgroup - Prometheus 监控:
container_memory_working_set_bytes / container_spec_memory_limit_bytes接近 1 是危险信号
9 OOM Killer 实战:如何解读 OOM 日志并预防?
答案:
OOM(Out of Memory)触发时内核通过 __oom_kill_process() 选目标并写日志,解读 OOM 日志是 SRE 核心能力。
典型 OOM 日志:
[12345.678] java invoked oom-killer: gfp_mask=0x100cca(GFP_HIGHUSER_MOVABLE), order=0
[12345.679] CPU: 2 PID: 1234 Comm: java Tainted: G W IO 5.15.0
[12345.680] Call Trace:
[12345.681] <TASK>
[12345.682] dump_stack_lvl+0x44/0x5c
[12345.683] dump_header+0x4f/0x250
[12345.684] oom_kill_process.cold+0xb/0x10
[12345.685] out_of_memory+0x114/0x350
[12345.686] __alloc_pages_slowpath+0xc3a/0xd80
[12345.687] __alloc_pages+0x2e2/0x320
[12345.688] alloc_pages_vma+0x88/0x220
[12345.689] do_anonymous_page+0x6b/0x3a0
[12345.690] __handle_mm_fault+0x660/0x6f0
[12345.691] handle_mm_fault+0xda/0x2d0
[12345.692] do_user_addr_fault+0x1bb/0x6a0
[12345.693] exc_page_fault+0x84/0x180
[12345.694] </TASK>
[12345.695] memory: usage 16777216kB, limit 16777216kB, oom_score_adj 0
[12345.696] swap: usage 0kB, limit 0kB
[12345.697] Memory cgroup stats for /kubepods/burstable/pod-uid:
[12345.698] anon 16106127360 # 16GB 匿名页
[12345.699] file 0
[12345.700] kernel_stack 1048576
[12345.701] slab 524288
[12345.702] sock 0
[12345.703] Tasks state (sum normalized for memory cgroup):
[12345.704] java 1234 16777216 1000 0 # 1000 = oom_score
[12345.705] Killed process 1234 (java) total-vm:18000000kB, anon-rss:16777216kB
关键信息解读:
gfp_mask分配标志(GFP_HIGHUSER_MOVABLE = 用户态高区可迁移页)order分配阶数(0 = 2^0 × 4KB = 4KB,order=2 = 16KB 大块)total-vm进程虚拟内存 18GBanon-rss实际物理内存 16GB(注意 VM » RSS = 未触发 swap)memory cgroup statscgroup v2 统计Tasks state候选进程 oom_score(最高分被杀)
预防 OOM:
# 1. 设置 cgroup 硬限制
echo "8G" > /sys/fs/cgroup/myapp/memory.max
# 2. 配置 OOM 通知(events)
while inotifywait -e modify /sys/fs/cgroup/myapp/memory.events; do
if grep -q "oom 1" /sys/fs/cgroup/myapp/memory.events; then
alert "OOM killed at $(date)"
fi
done
# 3. 关键进程保护
echo -1000 > /proc/<pid>/oom_score_adj
# 4. Java 应用调小堆
java -Xmx4g -jar app.jar # 显式限制
java -XX:MaxRAMPercentage=70.0 -jar app.jar # 自适应 cgroup
K8s OOM 排查:
# Pod OOMKilled 状态
kubectl describe pod <pod-name>
# Last State: Terminated
# Reason: OOMKilled
# Exit Code: 137
# 看 container_memory_working_set_bytes 是否超 limit
kubectl top pod <pod-name> --containers
# 调大 limits
resources:
limits:
memory: 8Gi # 之前 4Gi
避免 OOM 的系统级实践:
- 监控
node_memory_MemAvailable_bytes< 10% 触发告警 - 启用 swap(虽然慢,但比 OOM 杀进程可控)
- 配置 cgroup v2
memory.high主动回收(比 memory.max 硬限好) - JVM/Python/Runtime 都使用容器感知模式(
UseContainerSupport、--max-old-space-size)
10 Linux 内存管理的伙伴系统(Buddy)与 Slab 分配器是如何工作的?
答案:
Linux 内核物理页分配采用伙伴系统(Buddy Allocator) 管理页框(page frame,4KB 基础单位),Slab/Slub 分配器在内部分配小块对象(如 task_struct、inode、dentry)。两者配合完成"页面级粗粒度 + 字节级细粒度"分配。
Buddy 伙伴系统:
- 物理内存按 2 的幂次组织成页块(page block):1 页(4K)、2 页(8K)、4 页(16K)…最大
MAX_ORDER=11(4MB)。 - 分配时按需求向上对齐到最近的阶,找不到则拆大块(“分裂”);释放时检查伙伴是否空闲,伙伴也空闲则合并(“合并”)减少碎片。
- 数据结构:每个阶对应一个空闲链表,在
free_area[MAX_ORDER]中;空闲块用page->lru串成链表,page->private存阶数。 - 外部碎片难以完全避免,反碎片机制:
/proc/sys/vm/extfrag_threshold、内存规整(/proc/sys/vm/compact_memory)。
# 看 buddy 系统各阶空闲页数
cat /proc/buddyinfo
# Node 0, zone Normal 12 345 1023 567 234 89 45 12 3 0 0
# ^1页 ^2页 ^4页 ^8页 ... ^4MB
# 若 0 阶空闲页少 → 内核分配失败风险(K8s pod 调度慢、OOM 概率上升)
# 触发内存规整
echo 1 > /proc/sys/vm/compact_memory
# 看 zone 信息
cat /proc/zoneinfo | head -40
Slab / Slub 分配器:
- Buddy 最小分配粒度 1 页(4K),但
task_struct只有几 KB,浪费严重。Slab 在 Buddy 之上预分配对象缓存。 - Slab 拓扑:
kmem_cache(按对象类型创建,如kmalloc-256)→slab(由 1+ 物理页组成)→ 多个object(被分配的实际对象)。 - Linux 2.6 默认 SLAB,2.6.22 起默认 SLUB(更简单高效),可选 SLOB(嵌入式)。
- per-CPU 缓存:每个 CPU 有
cpu_slab,分配/释放无需加锁;空时从节点共享链表取 slab。
# 看 slab 使用情况(按对象类型汇总)
slabtop
# Active / Total Objects (% used) : 234567 / 312345 (75.1%)
# Active / Total Slabs : 8910 / 8910
# ...
# OBJS ACTIVE USE OBJ SIZE SLABS OBJ/SLAB CACHE SIZE NAME
# 125000 100000 80% 0.50K 2500 50 1000K dentry
# 看内核 slab 总占用
cat /proc/meminfo | grep -E "Slab|SReclaimable"
# Slab: 234567 kB
# SReclaimable: 123456 kB # 可回收(dentry/inode 缓存)
# SUnreclaim: 111111 kB # 不可回收(VFS 内核对象)
# 强制回收可回收 slab
echo 2 > /proc/sys/vm/drop_caches # 注意:这是 page cache,slab 回收用 drop_slab
# 或
sync && echo 3 > /proc/sys/vm/drop_caches
碎片化问题与处理:
- 内部碎片:Slab 对象大小固定,可能浪费(这是 Slab 不可避免的代价)
- 外部碎片:Buddy 长期分配/释放后,大块连续页难找
- 处理:内存规整(
compact_memory)、CMA(cma区域给 DMA 用)、hugepage - 监控:
/proc/buddyinfo高阶空闲页 < 一定阈值即告警
典型场景:
- 持续高分配/释放某类对象 → Slab 膨胀 → 内核用
slabtop找热点 → 加slab_nomerge禁用合并(调试)或优化业务 - K8s 节点内存告警但
free正常 → 查 Slab 是否异常占用(尤其dentry和inode缓存) - 数据库场景:禁用 THP(透明大页)+ 主动
echo 1 > /proc/sys/vm/compact_memory减少分配抖动
11 Linux Buffer 与 Cache 的区别是什么?`free` 命令输出如何解读?
答案:
free 命令中的 buffers 和 cached 都属于"可回收的 Page Cache"(Linux 2.4 之后已统一),但语义上有历史区分:
| 类型 | 内核归属 | 主要内容 | 释放命令 |
|---|---|---|---|
| Buffers | fs/buffer.c | 块设备原始数据缓存(磁盘 block) | drop_caches=1 |
| Cached | mm/filemap.c | 文件页缓存(普通文件 mmap 后的页) | drop_caches=2 |
| SReclaimable | mm/slab.c | 可回收的 Slab(dentry/inode 缓存) | drop_caches=3(部分) |
free 命令输出解读:
free -h
# total used free shared buff/cache available
# Mem: 62G 18G 12G 1.0G 31G 42G
# Swap: 8G 0B 8G
- total:物理内存总量
- used:被应用占用的内存(
total - free - buff/cache - shared不严格相等) - free:完全未使用的内存(这部分越少越好,浪费)
- shared:
tmpfs/shm共享内存 - buff/cache:Buffers + Cached + SReclaimable(可回收,不要当作"占用")
- available:真正可用内存估算(=
free + buff/cache 可回收部分 - 不可回收预留)
available vs free:
free不考虑 Page Cache 可回收available是内核zone_watermark_ok()估算的"在不触发 swap 的情况下能再分配给应用的内存"- 监控告警应基于
MemAvailable(Prometheusnode_memory_MemAvailable_bytes),不是MemFree
Cached 的细分(slabtop 视角):
slabtop -o
# 关键对象类型:
# - dentry:目录项缓存(每个文件一个)
# - inode:文件元数据缓存
# - kmalloc-*:通用小块分配
# - task_struct:进程描述符(数量 ≈ 进程数 + 线程数)
# - skbuff:网络包缓存
手动清理 Page Cache:
# 释放 pagecache(仅匿名页以外的文件页缓存)
echo 1 > /proc/sys/vm/drop_caches
# 释放 dentries 和 inodes
echo 2 > /proc/sys/vm/drop_caches
# 释放 pagecache + dentries + inodes
echo 3 > /proc/sys/vm/drop_caches
# 务必先 sync,避免脏页丢失
sync && echo 3 > /proc/sys/vm/drop_caches
典型场景:
- 业务进程 RSS 小但
available持续降低 → Page Cache 被大量占满(文件 IO 频繁),属正常,不要误报警 - 想要测试 IO 性能但 Page Cache 干扰 → 测前
drop_caches=3 buff/cache长时间不释放(> 80% total)→ 内核vm.vfs_cache_pressure=100(默认)下主动回收力度低,调高到 200 让内核更快回收
12 生产环境为什么通常关闭 Swap?什么时候又必须开启?
答案:
现代服务器内存普遍充裕(≥ 64GB),且多数业务对延迟敏感,默认关闭 Swap 是常见选择。但这不是教条,数据库 / 内存敏感型应用与资源受限环境(容器 / 边缘)的策略截然相反。
关闭 Swap 的核心理由:
- 延迟不可预测:Swap 触发后,匿名页(heap、stack)从匿名映射换出到磁盘,再次访问触发Major Page Fault,延迟从 ns 级暴涨到 ms 级(HDD)或 100μs 级(NVMe),且可能引发"swap 抖动"(一个进程唤回,另一个又换出)。
- 掩盖内存泄漏:本应 OOM 暴露的内存问题,被 Swap 软化,进程越来越慢但不报警,故障发现延迟。
- 影响监控与告警:
MemAvailable公式会算入 Swap 可回收量,使可用内存虚高。 - 容器化部署不友好:K8s 节点开启 Swap 后 pod 可以"借用"交换空间,QoS 边界被破坏;kubelet 默认
memory.swap.max=0(K8s 1.22+)。 - SSD 寿命与吞吐:持续 swap 会大量写 SSD,缩短寿命并占用 IO 带宽。
必须开启 Swap 的场景:
| 场景 | 原因 |
|---|---|
| 内存资源极度紧张 | 边缘设备、嵌入式、内存 < 4GB |
| 突发内存尖峰 | 偶发短时 spike(10s 内),比 OOM Kill 更可控 |
| 休眠(Hibernate) | 必须有 Swap 保存内存镜像 |
| 内存敏感型应用主动调优 | MySQL innodb_buffer_pool_size 设大但预留 Swap 防 OOM |
| 不想被 OOM Kill 突然终止 | 用 swap 慢但可救回数据 |
K8s 1.22+ 支持 Swap(Beta):
# kubelet 启动参数
--feature-gates=NodeSwap=true
--fail-swap-on=false
# Pod spec
apiVersion: v1
kind: Pod
spec:
containers:
- name: app
resources:
limits:
memory: 1Gi
requests:
memory: 512Mi
# 节点全局 swap 限制(kubelet 配置)
# memorySwap:
# swap:
# limit: 500Mi
生产建议配置:
# 关闭 Swap(推荐)
swapoff -a
sed -i '/swap/s/^/#/' /etc/fstab
# 调低 swappiness(即使开启也尽量不用)
sysctl vm.swappiness=10
# 默认 60,0 = 完全不用,100 = 积极使用
# 数据库 / 延迟敏感 → 1-10
# 普通应用 → 10-30
# 看当前 swap 使用
swapon -s
cat /proc/swaps
判断是否需要 Swap:
- 监控
si/so(swap in/out)长期 > 0 → 内存不足,应扩容或加 Swap - 监控
node_memory_SwapTotal_bytes - SwapFree_bytes> 0 → 有 swap 活动 - 数据库:长期
so > 0会导致 checkpoint 慢、buffer pool 污染
典型场景:
- 故障:业务进程 OOM Killed,有 Swap → 进程因换出卡住几分钟后被杀,体验更糟
- 故障:JVM 堆大 + 容器
memory.limit小,无 swap → 容器 OOM Killed 干净,pod 立即重启恢复
13 磁盘空间满(`No space left on device`)该如何排查?
答案:
磁盘满(INODE 满或容量满)会导致:日志写失败、应用报错、数据库宕机、容器重启。先定位是哪类资源耗尽,再分而治之。
Step 1: 区分 INODE 满 vs 容量满
# 一行看清:哪个文件系统满,剩余 INODE 与容量
df -hT # 看容量
df -iT # 看 inode
# Filesystem Type Size Used Avail Use% Mounted Inodes IUsed IFree IUse%
# /dev/sda1 ext4 50G 49G 500M 99% / 3.2M 3.2M 5 100% ← INODE 满
# /dev/sdb1 xfs 100G 100G 0 100% /data 10M 100 10M 1%
# ^^^^^ 容量满
# 总结
df -hT --total 2>/dev/null | awk 'NR>1 && $6+0 > 90 {print}'
Step 2: 定位大文件 / 大目录
# 全盘扫大文件(从大到小)
find / -type f -size +1G -exec du -h {} \; 2>/dev/null | sort -rh | head -20
# 看指定目录占用
du -h --max-depth=1 /var 2>/dev/null | sort -rh | head -10
du -sh /var/log/* 2>/dev/null | sort -rh | head -10
# 排除已 mount 的伪文件系统
du -h --max-depth=2 -x / 2>/dev/null | sort -rh | head -20
Step 3: 找"被删除但未释放"的文件
# 已删除但仍有进程持有 fd,df 不释放
lsof +L1
# COMMAND PID USER FD TYPE DEVICE SIZE/OFF NLINK NODE NAME
# nginx 1234 root 5w REG 253,1 1048576000 0 67890 /var/log/nginx/access.log (deleted)
# 处理:清空文件(不删 inode)让进程续写
> /proc/1234/fd/5
# 或滚动日志后让应用 reopen
nginx -s reopen
# 看所有 deleted 但未释放的空间
lsof +L1 2>/dev/null | awk '$5 == "REG" {sum+=$7} END {print sum/1024/1024 "MB"}'
Step 4: INODE 耗尽专项
# INODE 满的典型:大量小文件(如邮件队列、session 文件、docker 旧层)
for d in /var /tmp /home; do
echo "$d:"; find $d -xdev -type f | wc -l
done
# 清理 docker 旧镜像层
docker system prune -a
# 清理邮件队列
find /var/spool/postfix/maildrop -type f -mtime +7 -delete
Step 5: 容器内特殊处理
# 容器 overlayfs 占满宿主机空间
docker system df
# 看每个容器的 writable layer
docker inspect <cid> | jq '.[0].GraphDriver'
# K8s 容器日志
kubectl exec -it <pod> -- df -h /
# 清理
kubectl exec -it <pod> -- find /tmp -size +100M -delete
# 或配置 log rotation
# /etc/docker/daemon.json
{
"log-driver": "json-file",
"log-opts": {"max-size": "100m", "max-file": "3"}
}
紧急处理 SOP:
# 1. 找 5 个最大可立即删的东西
find /var/log -name "*.gz" -mtime +30 -delete
journalctl --vacuum-size=200M
rm -rf /var/cache/apt/archives/*.deb
rm -rf /tmp/*.tmp
docker system prune -f
# 2. 转移日志
mv /var/log/large.log /var/log/large.log.old && touch /var/log/large.log
# 配合 logrotate 配置
# 3. 扩容(终极方案)
lvextend -L +10G /dev/vg0/data
xfs_growfs /data
# ext4: resize2fs /dev/sda1
典型场景:
- 应用日志未 rotate,
/var/log撑爆 → 配置 logrotate - K8s 容器 stdout 日志未限制 → 配
kubelet --container-log-max-size=100Mi --container-log-max-files=5 - 数据库 ibdata1 撑爆 → 改
innodb_file_per_table=ON拆分 - 文件被删但仍占空间(deleted 状态)→ 用
lsof +L1找
14 `df` 与 `du` 的区别是什么?为什么结果不一致?
答案:
df 看块设备使用情况(从文件系统元数据读),du 看目录递归占用(逐文件 stat 加总)。两者结果可能不同,原因主要是"已删除但被进程持有的文件"与"metadata 开销"。
核心差异:
| 维度 | df | du |
|---|---|---|
| 数据源 | 文件系统 superblock / statvfs | 遍历目录 + stat 每个文件 |
| 速度 | 极快(直接读元数据) | 慢(递归 IO) |
| 含义 | 块设备视角的容量 | 目录视角的文件总和 |
| 包含 deleted 文件 | ✅ 算在内(块未释放) | ❌ 看不到(路径已不存在) |
| 包含预留块 | ✅ ext4 默认 5% 预留给 root | ❌ 不算 |
结果不一致的常见原因:
deleted 文件未释放:
df看到的 Used >du看到的总和 → 必有 deleted 但持有 fd 的文件lsof +L1 /mountpointext4 预留块:
df中Used包含 root 预留 5%,du不算tune2fs -l /dev/sda1 | grep "Reserved block count" # 调整(生产慎用):tune2fs -m 1 /dev/sda1du 跳过的挂载点:默认不跨 mount point 边界(
-x行为)du -sh / # /var 单独 mount 的话,不包含 /var 占用 du -xsh / # -x 不跨 mount point,与 df 接近硬链接被多次统计:
du同一 inode 只算一次(-l重复计)du 还在执行中文件被修改:数据不一致
实战对比命令:
# 用 df 看总览
df -h /data
# /dev/sdb1 100G 98G 2.0G 98% /data
# 用 du 看分布
du -h --max-depth=1 /data | sort -rh | head -10
# 4.0K /data/tmp
# 2.0M /data/config
# 88G /data/log
# 98G /data ← 接近 df 的 98G
# 差距大时的诊断
df -h /data
# Filesystem Size Used Avail Use% Mounted on
# /dev/sdb1 100G 98G 2.0G 98% /data
du -shx /data # 不跨 mount point
# 80G /data ← df 98G - du 80G = 18G 异常
lsof +L1 /data | awk '$5 == "REG" {sum+=$7} END {print sum/1024/1024 "MB"}'
# 18432MB ← 正好 18G,找到 deleted 文件
典型场景:
- “我删了文件为什么 df 没变化” →
lsof +L1找持有 fd 的进程,重启或清空文件 - “du 加起来和 df 差不多但还报 No space” → 大量小文件 INODE 满,
df -i看 - “du 结果比 df 大” → 跨 mount point 重复统计或 du 命令有 bug,加
-x
15 文件被删除但空间未释放该如何排查与处理?
答案:
Linux 中"删除文件"只是 unlink() 系统调用解除目录项到 inode 的链接,只要进程仍持有该文件的 fd,inode 引用计数 > 0,内核就不会释放数据块。df 看到的是块设备实际分配,进程持有的"幽灵文件"仍占用。
完整诊断与处理流程:
# Step 1: 找出所有"已删除但未释放"的文件
lsof +L1
# COMMAND PID USER FD TYPE DEVICE SIZE/OFF NLINK NODE NAME
# nginx 1234 root 5w REG 253,1 1048576000 0 6789 /var/log/nginx/access.log (deleted)
# 简版:只关心被删除的大文件
lsof +L1 2>/dev/null | awk '$5 == "REG" {print}' | head -20
# 累计大小
lsof +L1 2>/dev/null | awk '$5 == "REG" {sum+=$7} END {printf "%.2f GB\n", sum/1024/1024/1024}'
# Step 2: 定位具体进程
lsof +L1 | grep "/var/log/nginx/access.log (deleted)"
# → nginx PID 1234
# Step 3: 处理(3 种方案)
方案 A:截断文件(推荐,不中断服务)
# 清空已删除但仍打开的 fd(实际写入空数据,文件大小归 0)
> /proc/1234/fd/5
# 或用 truncate 命令
truncate -s 0 /proc/1234/fd/5
方案 B:滚动日志(让应用 reopen)
# nginx
mv /var/log/nginx/access.log /var/log/nginx/access.log.old
nginx -s reopen # 让 nginx 重新打开 log 文件
# rsyslog / syslog-ng
kill -HUP $(pidof rsyslogd)
# Java 应用(logback/log4j2):通过应用 API / JMX 触发 log rotation
# 或重启应用(最稳,但中断)
方案 C:重启进程(最暴力)
systemctl restart nginx
# 进程退出时 fd 全部关闭,inode 引用归零,块释放
为何不直接 rm?
rm /var/log/nginx/access.log
# → "文件已删除",但 df 看到的占用不变(inode 还在被引用)
# → 进程继续向旧 inode 写,文件名路径消失但磁盘占用不变
# → 应用重启或 reopen 才彻底释放
预防:logrotate 配 copytruncate
# /etc/logrotate.d/nginx
/var/log/nginx/*.log {
daily
rotate 7
compress
missingok
notifempty
sharedscripts
copytruncate # ← 关键:拷贝后截断原文件,应用无感知
postrotate
[ -f /var/run/nginx.pid ] && kill -USR1 $(cat /var/run/nginx.pid)
endscript
}
copytruncate 流程:cp 保留原 inode,截断原文件为 0 字节,应用继续写原 inode(inode 复用,文件名还是它)。不丢日志、不需 reload。
典型场景:
- 容器内 stdout 日志(容器引擎用 json-file driver)被 kubectl logs 读完但应用 fd 仍开 →
docker logs看不到 - 中间件 worker 进程 reload 不全 → 旧 log fd 没关 → 长期占用
- 数据库
binlog误删 → 立即truncate阻止继续写,然后重启
16 磁盘 IO 性能问题如何系统化分析与定位?
答案:
磁盘 IO 性能问题需分层诊断:块设备 → 文件系统 → 进程。常见指标是 IOPS、吞吐量、延迟;常见瓶颈是 IO 排队、慢盘、错误率、不合理 IO 模式。
Step 1: 看块设备全局状况
# iostat:核心 IO 指标
iostat -dx 1 5
# Device r/s w/s rkB/s wkB/s await r_await w_await aqu-sz %util
# sda 12.00 234.00 156.0 9876.0 45.6 2.3 48.7 12.4 98.0
# ^^^^^^ ^^^^^ ^^^^^
# 吞吐 延迟(ms) 利用率
# %util 不等于饱和度:现代多队列设备下 %util=100% 不一定瓶颈
# 关键看:aqu-sz(队列深度)> 1 + await 飙升 才是真瓶颈
关键字段解读:
| 字段 | 含义 | 异常阈值 |
|---|---|---|
| r/s, w/s | 每秒读写次数(IOPS) | 取决于设备能力(NVMe 100K+,SATA SSD 50K,HDD 200) |
| rkB/s, wkB/s | 吞吐 | SATA SSD ~500MB/s,NVMe 3-7GB/s |
| await | 平均 IO 延迟(ms) | SSD < 1ms,HDD < 20ms |
| r_await / w_await | 读 / 写延迟 | 差异大说明读写混合工作负载失衡 |
| aqu-sz | 平均队列深度 | > 1 表示有等待 |
| %util | 设备忙碌时间比例 | 100% 未必瓶颈(异步设备能并行) |
| svctm | 单个 IO 服务时间 | 已废弃,但仍可见 |
Step 2: 看进程级 IO
# 按进程看 IO
iotop -oP
# 总览:哪个进程 IO 占比
# -o:只显示有 IO 的进程
# -P:只显示进程(不显示线程)
# 按进程看具体读写量
pidstat -d 1 5
# kB_rd/s kB_wr/s kB_ccwr/s Command
# 0.00 12345.00 0.00 mysqld
# 看具体文件 IO
cat /proc/<pid>/io
# rchar: 12345678901 ← 累计读字节(含 page cache 命中)
# wchar: 98765432101
# syscr: 1234567 ← 累计 read() 系统调用
# syscw: 2345678
# read_bytes: 10485760 ← 实际物理读
# write_bytes: ...
Step 3: 看 IO 模式
# 看 IO 分布(随机/顺序,块大小)
iostat -dx 1
# 看 rrqm/s wrqm/s 高:合并多,顺序 IO 多
# 看 r/s 高 w/s 低:读密集,缓存命中率可能低
# 看 BIO 队列
cat /sys/block/sda/queue/nr_requests # 队列深度
cat /sys/block/sda/queue/scheduler # mq-deadline / kyber / none
cat /sys/block/sda/queue/rotational # 0=SSD, 1=HDD
cat /sys/block/sda/queue/read_ahead_kb # 预读
# 调整 IO 调度器(生产调优)
echo mq-deadline > /sys/block/sda/queue/scheduler # 数据库
echo kyber > /sys/block/nvme0n1/queue/scheduler # NVMe 默认
echo none > /sys/block/sda/queue/scheduler # 虚拟机透传设备
Step 4: 慢盘专项排查
# 1. SMART 健康状态
smartctl -a /dev/sda
# 看 Reallocated_Sector_Ct(重映射扇区)、Current_Pending_Sector、Offline_Uncorrectable
# 2. dmesg 错误日志
dmesg | grep -iE "i/o error|sector|ata|scsi"
# 3. 看 IO 错误率
iostat -dx 1 | awk '{print $11}' # 第 11 列可能含错误
# 4. fio 压测验证盘本身能力
fio --name=randread --ioengine=libaio --direct=1 \
--filename=/dev/sda --bs=4k --size=1G --rw=randread \
--runtime=30 --time_based
# 看 IOPS 与延迟是否符合设备规格
Step 5: 应用层 IO 优化
| 现象 | 优化方向 |
|---|---|
| 大量 4KB 随机写 | 合并成顺序写(log structured)、用 B+ 树 |
| 高并发短 IO | IO 调度器切 none、增加 nr_requests |
| 频繁 fsync | 批量写、logfsync 间隔加大、电池保护写缓存 |
| Page Cache 命中低 | 加大 vm.dirty_ratio、vfs_cache_pressure 调整 |
| 数据库随机写多 | 切到 NVMe、用 binlog group commit |
典型场景:
- 数据库
r_await突然飙到 50ms +%util=100%→ 慢盘或 RAID 卡 BBU 故障 - K8s 节点 IO 等待
wa%高 → 找具体 pod:kubectl debug进去iotop - 同步阻塞 IO 大量 D 态 → 用
cat /proc/<pid>/stack看栈,常在__blk_queue_enter或dio_await - 容器 overlayfs 慢 → 换
vfsstorage driver 或直接挂载 host 目录