跳转到内容

Linux 内存管理面试题

16 道题
分类
Linux
题目数
16 道
已阅读 0 / 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):多块合并写入。
  • 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.eventslow/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.memorymemory.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 进程虚拟内存 18GB
  • anon-rss 实际物理内存 16GB(注意 VM » RSS = 未触发 swap
  • memory cgroup stats cgroup 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_structinodedentry)。两者配合完成"页面级粗粒度 + 字节级细粒度"分配。

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 是否异常占用(尤其 dentryinode 缓存)
  • 数据库场景:禁用 THP(透明大页)+ 主动 echo 1 > /proc/sys/vm/compact_memory 减少分配抖动
11 Linux Buffer 与 Cache 的区别是什么?`free` 命令输出如何解读?

答案:

free 命令中的 bufferscached 都属于"可回收的 Page Cache"(Linux 2.4 之后已统一),但语义上有历史区分:

类型内核归属主要内容释放命令
Buffersfs/buffer.c块设备原始数据缓存(磁盘 block)drop_caches=1
Cachedmm/filemap.c文件页缓存(普通文件 mmap 后的页)drop_caches=2
SReclaimablemm/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:完全未使用的内存(这部分越少越好,浪费)
  • sharedtmpfs / shm 共享内存
  • buff/cache:Buffers + Cached + SReclaimable(可回收,不要当作"占用")
  • available真正可用内存估算(= free + buff/cache 可回收部分 - 不可回收预留

available vs free

  • free 不考虑 Page Cache 可回收
  • available 是内核 zone_watermark_ok() 估算的"在不触发 swap 的情况下能再分配给应用的内存"
  • 监控告警应基于 MemAvailable(Prometheus node_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 开销"。

核心差异:

维度dfdu
数据源文件系统 superblock / statvfs遍历目录 + stat 每个文件
速度极快(直接读元数据)慢(递归 IO)
含义块设备视角的容量目录视角的文件总和
包含 deleted 文件✅ 算在内(块未释放)❌ 看不到(路径已不存在)
包含预留块✅ ext4 默认 5% 预留给 root❌ 不算

结果不一致的常见原因:

  • deleted 文件未释放df 看到的 Used > du 看到的总和 → 必有 deleted 但持有 fd 的文件

    lsof +L1 /mountpoint
    
  • ext4 预留块dfUsed 包含 root 预留 5%,du 不算

    tune2fs -l /dev/sda1 | grep "Reserved block count"
    # 调整(生产慎用):tune2fs -m 1 /dev/sda1
    
  • du 跳过的挂载点:默认不跨 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+ 树
高并发短 IOIO 调度器切 none、增加 nr_requests
频繁 fsync批量写、logfsync 间隔加大、电池保护写缓存
Page Cache 命中低加大 vm.dirty_ratiovfs_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_enterdio_await
  • 容器 overlayfs 慢 → 换 vfs storage driver 或直接挂载 host 目录