AIGC标识 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

逐条解读:

  1. 饱和设备是 swap 分区,而非任何有业务数据的文件系统;吞吐很低但 util 已 100%+、写延迟破 100 ms——典型的"慢盘被小请求打穿"。
  2. python/uvicorn/MainThread 的 read_bytes 增量高达数十 MB 却没有任何对应的文件读取业务:read_bytes 会计入 swap-in 与缺页读回,这是页面正从 swap 换入的直接体现。
  3. 20 s 后 load 0.92→4.03、iowait 21.6%→34.9%,并出现 D 状态进程,wchan=folio_wait_bit_common(等待页就绪)——用户感知的"卡死"就是这个不可中断等待。
  4. 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. 根因结论

  1. 触发:开发栈(uvicorn + vite + opencode + postgres + docker-desktop)叠加大模型加载(ollama qwen3.5 6.6 GB)造成短时内存压力,页被换出到 swap。
  2. 驱动:.wslconfig 配置了 autoMemoryReclaim=gradual,WSL 持续把 VM 内存交还 Windows,使匿名页不断被挤压进 swap,放大并延续换页过程。
  3. 放大:WSL2 的 swap 位于 Hyper-V 模拟 SCSI 虚拟盘(swap.vhdx),IOPS 上限低、延迟高。被换出的页一旦被触碰即触发同步换入,进程进入 D 状态;/proc/pressure/io 在"正常"时段仍测得 some/avg300=4.4%,说明停滞持续存在。
  4. 解释反直觉现象:卡死时 MemAvailable≈20 GB。因为代价不取决于"当下缺多少内存",而取决于"历史换出了多少页、现在正同步换入"——换页风暴的账单延后支付,以挂起的形式结算。
  5. 附带方法论提醒: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 两项遗留事项(如实记录)

  1. 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 复核。
  2. 监控降噪:DSTATE_TH=1 会对 2–3 秒的良性 D 状态也开事件(308 事件中约 4/5 属轻微)。建议 DSTATE_TH=3,只保留真实故障取证价值。

8. 可复用的排查要点

  1. WSL2 磁盘 100% 优先核对 swap 设备:lsblk 将 sdX 与角色对号([SWAP] 分区是常见瓶颈点)。
  2. 设备级证据优先于进程级证据:/proc/diskstats 差分的 util/await/inflight 可直接定量;进程级 read_bytes 含换页读回且存在 PID 复用噪声。
  3. 间歇性故障靠"阈值触发 + 事件档案"取证,事后聚合(grep/awk 即够),不要指望 top 现场逮住。
  4. /proc/pressure/{io,memory} 是判断"系统是否处于停滞"的标准指标,且可证明修复效果。
  5. pswpin/pswpout(/proc/vmstat)与 smaps_rollup 的 Swap: 分别回答"是否在换页"与"谁在换页"。
  6. 验证监控工具本身:任何恒为零/恒为满的指标,先确认采样路径有效,再下结论。

posted on 2026-10-16 15:57  论语  阅读(7)  评论(0)    收藏  举报

导航