WSL2 磁盘占用 100% 间歇性卡死:监控、定位与修复全流程
环境:Windows 11 + WSL2(Ubuntu-22.04,kernel 6.6.87.2-microsoft-standard,32 vCPU / 30 GB RAM VM)。
结论先行:卡死源于 swap 分区(/dev/sdc,8 GB VHDX)打满,驱动因素是autoMemoryReclaim=gradual
与 WSL 虚拟盘的固有高延迟。本文给出完整的监控方法、定位过程、修复配置与验证数据,可直接复用于同类问题。
1. 问题现象
WSL2 周期性出现磁盘占用 100%、系统挂起(输入延迟、进程无响应),单次持续数十秒至约 4 分钟,随后自行恢复。常规观察下无明显高负载进程,且 /proc/meminfo 始终显示约 20 GB 可用内存。间歇性故障需要事件级取证,瞬时 top/iostat 无法覆盖,因此第一步是搭建被动触发的采样监控。
2. 监控设计与 io-watch 实现
2.1 采集指标的选择
| 指标 | 数据源 | 用途 |
|---|---|---|
| 设备 util% / r-s / w-s / r_await / w_await / inflight | /proc/diskstats 两次采样差分 |
判断哪块设备饱和、延迟多高、队列积压程度 |
| CPU iowait% | /proc/stat 差分 |
判断挂起的系统级代价 |
| 1 分钟 load | /proc/loadavg |
全局受阻程度 |
| D 状态进程(含 WCHAN) | ps -eo pid,ppid,stat,wchan,comm |
直接暴露"卡在哪个内核等待点"的进程 |
| 每进程块级读写增量 | /proc/<pid>/io 两次快照差分 |
归因:谁在产生 I/O |
| swap 进出速率 | /proc/vmstat 的 pswpin/pswpout |
判断是否存在页面换入换出风暴 |
| 内存驻留/交换占用 | /proc/meminfo、/proc/<pid>/smaps_rollup |
谁把页压进了 swap |
| 内核日志 / journal 告警增量 | dmesg、journalctl -k |
排除驱动层错误 |
任何指标越线即进入事件模式:将上述全部维度落盘为一个 .event 快照文件,采样间隔由 5 s 收紧到 2 s;指标回落后记录事件结束。事件目录即取证档案,事后聚合分析(见第 3 节)。
2.2 触发阈值
| 变量 | 默认 | 含义 |
|---|---|---|
UTIL_TH |
70 | 任一块设备 util% 越线 |
IOWAIT_TH |
20 | CPU iowait% 越线 |
LOAD_TH |
8 | 1 分钟 load 越线(按核数调整) |
DSTATE_TH |
1 | D 状态进程数越线(过敏感,见 7.3) |
SWAP_TH |
1 | 单采样周期 swap 进出 KB 越线 |
2.3 WSL2 设备映射(定位前提)
分析前必须先把 sdX 对号入座,lsblk -o NAME,SIZE,TYPE,MOUNTPOINTS 输出解读:
| 设备 | 容量 | 角色 |
|---|---|---|
sdc |
8 G | SWAP 分区(WSL 自动创建的 swap VHDX) |
sdd |
1 T | 发行版根文件系统 VHDX(/、docker-desktop bind-mounts) |
sda/sdb/sde |
数百 M | WSL 系统/计划任务用虚拟盘 |
sdf |
1 T | 数据盘 VHDX |
2.4 io-watch 完整脚本(最终修正版)
运行需 sudo(否则无法读取其他用户的 /proc/<pid>/io)。数据目录 ~/.io-watch/,--report 汇总、--tail 跟踪。
关键缺陷已在代码内注释标注(sample_swap,历史版本使用带冒号的模式匹配导致 swap 指标恒为 0)。
#!/usr/bin/env bash
#
# io-watch - record and diagnose intermittent disk-hang / high-IO events.
# Tuned for WSL2 but works on any Linux.
#
# Usage:
# io-watch # start monitoring (Ctrl-C to stop)
# io-watch --report # summarize everything recorded so far
# io-watch --tail # follow the live summary log
#
# Run with sudo for full per-process I/O coverage (other users' /proc/<pid>/io
# is unreadable otherwise); the disk/block metrics work without root.
#
# Tunables (env vars):
# INTERVAL=5 seconds between samples when idle
# EVENT_INTERVAL=2 seconds between samples while an event is active
# UTIL_TH=70 flag when any block device %util >= this
# IOWAIT_TH=20 flag when CPU iowait% >= this
# LOAD_TH=8 flag when 1-min load >= this (tune to your CPU count)
# DSTATE_TH=1 flag when this many processes are in D (uninterruptible) state
# SWAP_TH=1 flag when >= this many KB were swapped in/out in a tick
# IO_WATCH_DIR=~/.io-watch
set -uo pipefail
# ---- 阈值配置(见 2.2 节表格)----
INTERVAL="${INTERVAL:-5}"
EVENT_INTERVAL="${EVENT_INTERVAL:-2}"
UTIL_TH="${UTIL_TH:-70}"
IOWAIT_TH="${IOWAIT_TH:-20}"
LOAD_TH="${LOAD_TH:-8}"
DSTATE_TH="${DSTATE_TH:-1}"
SWAP_TH="${SWAP_TH:-1}"
# sudo 场景下把数据目录落回真实用户家目录
if [[ -n "${IO_WATCH_DIR:-}" ]]; then
BASE="$IO_WATCH_DIR"
elif [[ -n "${SUDO_USER:-}" && "${SUDO_USER}" != root ]]; then
BASE="$(getent passwd "$SUDO_USER" | cut -d: -f6)/.io-watch"
else
BASE="$HOME/.io-watch"
fi
EV="$BASE/events" # 事件档案目录(*.event)
ST="$BASE/.state" # 两次采样的中间状态
LOG="$BASE/monitor.log" # 滚动摘要日志
mkdir -p "$EV" "$ST"
now() { date '+%F %T'; }
gt() { awk -v a="$1" -v b="$2" 'BEGIN{exit !(a+0 >= b+0)}'; }
# ---------------------------------------------------------------- report mode
# --report:把全部事件文件顺序输出(每文件截 160 行)
if [[ "${1:-}" == "--report" ]]; then
shopt -s nullglob
files=("$EV"/*.event)
if (( ${#files[@]} == 0 )); then
echo "No events recorded yet in $EV"
echo "(events are written when util/iowait/load/D-state/swap cross the thresholds)"
exit 0
fi
echo "Recorded events: ${#files[@]} (dir: $EV)"
for f in "${files[@]}"; do
echo "=================================================================="
echo "FILE: $f"
head -160 "$f"
done
exit 0
fi
if [[ "${1:-}" == "--tail" ]]; then
exec tail -n 40 -f "$LOG"
fi
# -------------------------------------------------------------- sampling
# CPU iowait%:/proc/stat 首行 (total, iowait) 差分
sample_cpu() {
local cur t i
cur=$(awk '/^cpu /{printf "%d %d", $2+$3+$4+$5+$6+$7+$8+$9, $6}' /proc/stat)
t=${cur% *}; i=${cur#* }
IOWAIT_PCT=0
if [[ -f "$ST/cpu" ]]; then
local pt pi dt di
read -r pt pi < "$ST/cpu"
dt=$((t - pt)); di=$((i - pi))
(( dt > 0 )) && IOWAIT_PCT=$(awk -v a="$di" -v b="$dt" 'BEGIN{printf "%.1f", 100*a/b}')
fi
echo "$t $i" > "$ST/cpu"
}
sample_swap() {
local cur pin pout
# 【历史缺陷修复点】原实现为:
# cur=$(awk '/^pswpin:/{p=$2} /^pswpout:/{o=$2} END{...}' /proc/vmstat)
# /proc/vmstat 的键是"空格分隔"(如 `pswpin 11886031`),不带冒号,
# 上述带冒号的模式永远不匹配 → SWAP_IN_KB/SWAP_OUT_KB 恒为 0:
# 1) swap 触发阈值从未生效;
# 2) 所有事件文件记录 `swap_in=0kB` 假象,直接误导根因分析方向。
# 教训:任何"恰好为零"的指标,先验证采样器本身。
cur=$(awk '$1=="pswpin"{p=$2} $1=="pswpout"{o=$2} END{printf "%d %d", p+0, o+0}' /proc/vmstat)
pin=${cur% *}; pout=${cur#* }
SWAP_IN_KB=0; SWAP_OUT_KB=0
if [[ -f "$ST/vm" ]]; then
local ppin ppout
read -r ppin ppout < "$ST/vm"
SWAP_IN_KB=$(( (pin - ppin) * 4 )) # 页 → KB
SWAP_OUT_KB=$(( (pout - ppout) * 4 ))
fi
echo "$pin $pout" > "$ST/vm"
}
# 块设备两次 /proc/diskstats 差分,输出: dev util% r/s w/s r_await w_await in-flight
# util = Δio_ms / (Δt*1000) * 100;字段 $4读完成 $7读毫秒 $8写完成 $11写毫秒 $12队列 $13io毫秒
sample_disk() {
[[ -n "${ELAPSED:-}" ]] || { cp -f /proc/diskstats "$ST/disk"; return 0; }
cp -f /proc/diskstats "$ST/disk.cur"
if [[ -f "$ST/disk" ]]; then
awk -v iv="$ELAPSED" '
FNR==NR { if ($3 ~ /^(loop|ram)/) next;
pr[$3]=$4; prd[$3]=$7; pw[$3]=$8; pwr[$3]=$11; pit[$3]=$13; next }
{ d=$3; if (d ~ /^(loop|ram)/) next; if (!(d in pr)) next;
util=(($13-pit[d])/(iv*1000))*100; if (util<0) util=0;
dr=$4-pr[d]; dw=$8-pw[d];
if (dr>0 || dw>0 || $12>0) {
ra=(dr>0)?($7-prd[d])/dr:0; wa=(dw>0)?($11-pwr[d])/dw:0;
printf "%s %.1f %.1f %.1f %.2f %.2f %d\n", d, util, dr/iv, dw/iv, ra, wa, $12;
}
}' "$ST/disk" "$ST/disk.cur"
fi
mv -f "$ST/disk.cur" "$ST/disk"
}
# 每进程累计块读写快照(read_bytes/write_bytes 为进程生命周期累计值)
snapshot_procio() {
local d pid vals rb wb comm
: > "$1.tmp"
for d in /proc/[0-9]*; do
[[ -r "$d/io" ]] || continue
pid="${d#/proc/}"
vals=$(awk '/^read_bytes:/{r=$2} /^write_bytes:/{w=$2} END{printf "%d %d", r+0, w+0}' "$d/io" 2>/dev/null) || continue
[[ -n "$vals" ]] || continue
rb=${vals% *}; wb=${vals#* }
comm=$(cat "$d/comm" 2>/dev/null)
printf '%s\t%s\t%s\t%s\n' "$pid" "$rb" "$wb" "$comm" >> "$1.tmp"
done
mv "$1.tmp" "$1"
}
# 相邻两次快照按 PID 关联求差,取读写总量 Top10
# 注意:read_bytes 计入换页读回(swap-in 归因到触碰页的进程),
# 且按 PID 关联存在复用噪声,个别异常大值须结合设备级数据判断(见 6.4)
procio_delta() {
[[ -f "$ST/procio.prev" && -f "$ST/procio.cur" ]] || return 0
awk -F'\t' '
FNR==NR { r[$1]=$2; w[$1]=$3; c[$1]=$4; next }
($1 in r) {
pr=$2-r[$1]; pw=$3-w[$1]; tot=pr+pw;
if (tot>0) printf "%d\t%s\t%d\t%d\n", tot, $4, pr, pw;
}' "$ST/procio.prev" "$ST/procio.cur" \
| sort -rn | head -10 \
| awk -F'\t' '{printf " %-22s +%d kB read +%d kB write\n", $2, $3/1024, $4/1024}'
}
# D 状态进程及其内核等待点(wchan)——"卡死"的直接证据
dstate() {
ps -eo pid,ppid,stat,wchan:34,%cpu,%mem,comm 2>/dev/null | awk 'NR==1 || $3 ~ /D/'
}
klog_delta() { # 仅输出两次采样之间新增的内核日志
local f="$ST/dmesg"
dmesg -T 2>/dev/null > "$ST/dmesg.cur" || : > "$ST/dmesg.cur"
if [[ -f "$f" ]]; then
tail -n +"$(( $(wc -l < "$f") + 1 ))" "$ST/dmesg.cur"
else
tail -n 25 "$ST/dmesg.cur"
fi
mv "$ST/dmesg.cur" "$f"
}
jrnl_delta() { # 仅输出新增的 kernel warning 级 journal 记录
local f="$ST/jrnl_ts" since
if [[ -f "$f" ]]; then since="@$(cat "$f")"; else since="-1min"; fi
journalctl -k -p warning --since "$since" --no-pager 2>/dev/null
date +%s > "$f"
}
# --------------------------------------------------------------- main loop
echo "io-watch started $(now)."
echo " data dir : $BASE (live log: $LOG)"
echo " interval : ${INTERVAL}s idle / ${EVENT_INTERVAL}s during events"
echo " triggers : util>=${UTIL_TH}% iowait>=${IOWAIT_TH}% load1>=${LOAD_TH} D-state>=${DSTATE_TH} swap>=${SWAP_TH}kB"
echo " (run under sudo for full per-process I/O; Ctrl-C to stop)"
echo
event_active=0
event_file=""
trap 'printf "\n\nStopping io-watch at %s. Run \047io-watch --report\047 to summarize.\n" "$(now)"; exit 0' INT TERM HUP
while true; do
TICK_START=$(date +%s.%N)
if [[ -f "$ST/last_time" ]]; then
ELAPSED=$(awk -v n="$TICK_START" -v p="$(cat "$ST/last_time")" 'BEGIN{printf "%.3f", n-p}')
else
ELAPSED=""
fi
sample_cpu
sample_swap
read -r MEM_AVAIL SWAP_FREE < <(awk '/^MemAvailable:/{a=$2} /^SwapFree:/{s=$2} END{printf "%d %d", a+0, s+0}' /proc/meminfo)
LOAD1=$(awk '{print $1}' /proc/loadavg)
DISK_OUT=$(sample_disk)
snapshot_procio "$ST/procio.cur"
DSTATE_OUT=$(dstate)
DSTATE_N=$(printf '%s\n' "$DSTATE_OUT" | awk '$0!=""{n++} END{print n+0}')
(( DSTATE_N > 0 )) && DSTATE_N=$(( DSTATE_N - 1 )) # 扣除表头
MAXUTIL=$(printf '%s\n' "$DISK_OUT" | awk '{if($2+0>m){m=$2+0;d=$1}} END{printf "%.1f %s", m+0, (d==""?"-":d)}')
MAXUTIL_V=${MAXUTIL% *}; MAXUTIL_D=${MAXUTIL#* }
# ---- 判定:任一维度越线即进入/保持事件模式 ----
event=0
gt "$MAXUTIL_V" "$UTIL_TH" && event=1
gt "$IOWAIT_PCT" "$IOWAIT_TH" && event=1
gt "$LOAD1" "$LOAD_TH" && event=1
(( DSTATE_N >= DSTATE_TH )) && event=1
(( SWAP_IN_KB + SWAP_OUT_KB >= SWAP_TH )) && event=1
METRICS="load=${LOAD1} iowait=${IOWAIT_PCT}% maxutil=${MAXUTIL_V}%(${MAXUTIL_D}) dstate=${DSTATE_N} swap_in=${SWAP_IN_KB}kB swap_out=${SWAP_OUT_KB}kB memavail=$((MEM_AVAIL/1024))MB"
SUM="$METRICS"
if (( event )); then
if (( ! event_active )); then # 事件开始:新建 .event 档案
event_active=1
event_file="$EV/$(date '+%Y%m%d-%H%M%S').event"
rm -f "$ST/jrnl_ts"
{
echo "# EVENT START $(now)"
echo "# METRICS $METRICS"
echo "# PIPELINE disk=| ${DISK_OUT//$'\n'/ | }"
} > "$event_file"
echo "$(now) EVENT-START $SUM -> $event_file"
fi
# 事件期间:全维度快照追加进档案
{
echo "# METRICS $(now) $METRICS"
echo "SWAP in=${SWAP_IN_KB}kB out=${SWAP_OUT_KB}kB free=$((SWAP_FREE/1024))MB"
echo "BLOCKED (D-state):"
printf '%s\n' "$DSTATE_OUT" | sed 's/^/ /'
echo "TOP-IO since last sample:"
procio_delta
echo "DISK (dev util% r/s w/s r_await w_await inflight):"
printf '%s\n' "$DISK_OUT" | sed 's/^/ /'
local_klog=$(klog_delta)
[[ -n "$local_klog" ]] && { echo "KLOG:"; printf '%s\n' "$local_klog" | sed 's/^/ /'; }
jrnl_out=$(jrnl_delta)
[[ -n "$jrnl_out" ]] && { echo "JRNL:"; printf '%s\n' "$jrnl_out" | sed 's/^/ /'; }
echo
} >> "$event_file"
printf '%s EVENT %s\n' "$(now)" "$SUM"
else
if (( event_active )); then # 事件结束:阈值回落后收档
echo "# EVENT END $(now) $SUM" >> "$event_file"
echo "$(now) EVENT-END $SUM"
event_active=0
event_file=""
else
printf '%s ok %s\n' "$(now)" "$SUM"
fi
fi
echo "$(now) $( (( event )) && echo EVENT || echo ok ) $SUM" >> "$LOG"
mv "$ST/procio.cur" "$ST/procio.prev"
echo "$TICK_START" > "$ST/last_time"
if (( event )); then sleep "$EVENT_INTERVAL"; else sleep "$INTERVAL"; fi
done
3. 数据采集与聚合定位
监控运行 5 天,得到 308 个事件(~/.io-watch/events/*.event)。聚合分析用以下命令即可完成,无需额外工具:
cd ~/.io-watch/events
# 1) 哪个设备最常登顶 util(事件开始行的 maxutil 字段计数)
grep -h '^# METRICS load' *.event | grep -oP 'maxutil=[0-9.]+%\(\K[^)]+' \
| sort | uniq -c | sort -rn
# 235 sdc ← 8G SWAP 分区(见 2.3 映射表)
# 71 sdd ← 根文件系统 VHDX
# 1 sdf / 1 sde
# 2) 各设备的延迟与队列深度分布
grep -h '^ s' *.event | awk '{d=$1; if($2+0>maxu[d])maxu[d]=$2; wa[d]+=$6; n[d]++;
if($7+0>mi[d])mi[d]=$7} END{for(d in n) printf "%s 样本%d 平均w_await=%.1fms 峰值util=%.0f%% 峰值inflight=%d\n", d,n[d],wa[d]/n[d],maxu[d],mi[d]}'
# sdc 样本481 平均w_await=60.3ms 峰值util=131.8% 峰值inflight=2048
# sdd 样本489 平均w_await=51.1ms 峰值util=175.5% 峰值inflight=42
# 3) D 状态进程中谁被卡得最频繁
awk '/BLOCKED/{f=1;next} f&&/^ PID/{next} f&&/^ /&&NF>3{print $NF} f&&!/^ /{f=0}' \
*.event | sort | uniq -c | sort -rn | head
# 97 opencode 62 MainThread(vite) 26 python(uvicorn) 21 git 20 postgres ...
核心事实(308 事件汇总):
| 指标 | 数值 |
|---|---|
| util 登顶设备 | sdc=swap 分区:235/308(76%) |
卡死期间 sdc |
util 99–110%,w_await 均值 51–103 ms,单次峰值 3,873 ms,队列积压峰值 2048 |
| 吞吐 | 仅 ~10 MB/s(2,800 r-s + 1,800 w-s 即达饱和)——虚拟 SCSI 盘 IOPS 上限极低 |
| 单次事件最长 | 4 min 32 s(与用户感知挂起一致) |
| 事件时段 | 集中于工作时间 09:00–19:00 |
4. 典型事件样本解读
摘自 20261015-172018.event(真实记录,一次挂起的头两个采样周期):
# EVENT START 2026-10-15 17:20:18
# METRICS load=0.92 iowait=21.6% maxutil=110.4%(sdc) dstate=0 swap_in=0kB swap_out=0kB memavail=20097MB
TOP-IO since last sample:
python +65424 kB read +0 kB write
MainThread +9192 kB read +0 kB write
uvicorn +4472 kB read +0 kB write
DISK (dev util% r/s w/s r_await w_await inflight):
sdc 110.4 2802.4 1873.6 1.92 102.66 11 ← swap 分区打满,写延迟 102ms
sdd 63.8 89.1 8.9 15.46 2.28 1
# METRICS 2026-10-15 17:20:32 load=4.03 iowait=34.9% maxutil=93.8%(sdc) dstate=1 swap_in=0kB ...
BLOCKED (D-state):
3716 3715 Dl+ folio_wait_bit_common 0.1 0.0 MainThread ← vite 阻塞在页等待
TOP-IO since last sample:
bash +103708 kB read +184 kB write
逐条解读:
- 饱和设备是 swap 分区,而非任何有业务数据的文件系统;吞吐很低但 util 已 100%+、写延迟破 100 ms——典型的"慢盘被小请求打穿"。
python/uvicorn/MainThread的read_bytes增量高达数十 MB 却没有任何对应的文件读取业务:read_bytes会计入 swap-in 与缺页读回,这是页面正从 swap 换入的直接体现。- 20 s 后 load 0.92→4.03、iowait 21.6%→34.9%,并出现 D 状态进程,
wchan=folio_wait_bit_common(等待页就绪)——用户感知的"卡死"就是这个不可中断等待。 memavail=20097MB全程约 20 GB:挂起时内存"充足",与"缺内存"的直觉相反(原因见 6.3)。
5. 关键矛盾与监控缺陷确认
事件档案中存在一个无法自洽的组合:sdc(swap 设备)高负载,而记录的 swap 进出速率恒为 0。两种解释互斥——要么 sdc 流量不来自换页,要么 swap 采样器失效。直接验证:
grep pswp /proc/vmstat
# pswpin 11886031 ← 键与值之间是空格,没有冒号
# pswpout 8227487
awk '/^pswpin:/{...}' /proc/vmstat # 历史实现的匹配模式
# (永不命中 → 恒输出 0 0)
确认监控脚本缺陷(修复见 2.4 注释):原 sample_swap() 使用了带冒号的模式,而 /proc/vmstat 条目为空格分隔,导致 swap 速率恒为 0、swap 触发器从未生效、全部事件记录了假象数据。修复后首次采样即实测 3 秒内 7.9 MB swap-in,与 sdc 的负载完全对上;并进一步量化出历史规模:累计换入 46.2 GB、换出 32.1 GB,当前驻留 3.1 GB。
按进程核对交换驻留(/proc/<pid>/smaps_rollup 的 Swap: 字段):python 工作进程 391 MB、vite(MainThread) 110 MB、opencode.exe 108 MB、opencode 78 MB——与 D 状态高频进程完全重合。至此证据链闭合。
6. 根因结论
- 触发:开发栈(uvicorn + vite + opencode + postgres + docker-desktop)叠加大模型加载(ollama
qwen3.56.6 GB)造成短时内存压力,页被换出到 swap。 - 驱动:
.wslconfig配置了autoMemoryReclaim=gradual,WSL 持续把 VM 内存交还 Windows,使匿名页不断被挤压进 swap,放大并延续换页过程。 - 放大:WSL2 的 swap 位于 Hyper-V 模拟 SCSI 虚拟盘(swap.vhdx),IOPS 上限低、延迟高。被换出的页一旦被触碰即触发同步换入,进程进入 D 状态;
/proc/pressure/io在"正常"时段仍测得 some/avg300=4.4%,说明停滞持续存在。 - 解释反直觉现象:卡死时
MemAvailable≈20 GB。因为代价不取决于"当下缺多少内存",而取决于"历史换出了多少页、现在正同步换入"——换页风暴的账单延后支付,以挂起的形式结算。 - 附带方法论提醒:TOP-IO 中个别 GB 级异常值(systemd/bash/Relay)为按 PID 关联统计的复用噪声,进程级计数用于排序参考,设备级
/proc/diskstats才是可定量采信的证据。
7. 修复方案与验证
7.1 配置变更(均为持久化)
C:\Users\<user>\.wslconfig —— 移除换页驱动因素(影响最大):
[experimental]
autoMemoryReclaim=disabled # 原: gradual
若需给 Windows 侧保留内存,正确做法是
[wsl2] memory=20GB固定 VM 上限,而不是保留动态回收。
/etc/sysctl.d/99-wsl-swap.conf —— 降低换页倾向与单次缺页代价:
vm.swappiness=10 # 原默认 60:显著减少对慢速 swap 的换出
vm.page-cluster=0 # 原默认 2:每次缺页仅读 1 页而非 4 页,缩短同步等待
应用后执行 wsl --shutdown 并重启。
7.2 修复后验证(重启后 20 h 45 m 实测)
| 指标 | 修复前(10-15) | 修复后(10-16) |
|---|---|---|
pswpin / pswpout |
11.90M / 8.25M 页(46.2/32.1 GB) | 0 / 0 |
| swap 驻留 | 3.1 GB / 8 GB | 0 B |
| 挂起 | 间歇性 20 s – 4.5 min | 无 |
7.3 两项遗留事项(如实记录)
vm.page-cluster运行时被重置:开机由 systemd-sysctl 正确置 0,但随后被恢复为默认 2(内核 6.6.87.2-microsoft,未定位到重置方;swappiness=10未受影响)。当前 swap 流量为零,该项无实际影响;如需保证,可每次启动后执行sudo sysctl -w vm.page-cluster=0并用sysctl vm.page-cluster复核。- 监控降噪:
DSTATE_TH=1会对 2–3 秒的良性 D 状态也开事件(308 事件中约 4/5 属轻微)。建议DSTATE_TH=3,只保留真实故障取证价值。
8. 可复用的排查要点
- WSL2 磁盘 100% 优先核对 swap 设备:
lsblk将sdX与角色对号([SWAP]分区是常见瓶颈点)。 - 设备级证据优先于进程级证据:
/proc/diskstats差分的 util/await/inflight 可直接定量;进程级read_bytes含换页读回且存在 PID 复用噪声。 - 间歇性故障靠"阈值触发 + 事件档案"取证,事后聚合(
grep/awk即够),不要指望top现场逮住。 /proc/pressure/{io,memory}是判断"系统是否处于停滞"的标准指标,且可证明修复效果。pswpin/pswpout(/proc/vmstat)与smaps_rollup的Swap:分别回答"是否在换页"与"谁在换页"。- 验证监控工具本身:任何恒为零/恒为满的指标,先确认采样路径有效,再下结论。
出处:https://www.cnblogs.com/lunyu/p/23329175
版权:本文版权归作者和博客园共有
转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任
浙公网安备 33010602011771号