Watch at Ease看見世界,讀懂生活。

科技 · 實用指南

Linux 顯示檔案監看器達上限?提高 sysctl 前先建立 inotify 消費者臺帳

同一條 ENOSPC 可能來自監看標記、inotify 實例、佇列或同一使用者下的其他程序。先唯讀盤點,再決定是否縮小範圍或調整限制。

多個程序消費者、唯讀稽核臺帳與共用 inotify 資源池的原創概念插圖。
Watch at Ease 原創概念插圖:先記錄實例、標記與可見性,再決定下一步。 Watch at Ease

Linux 開發工具顯示 `ENOSPC: System limit for number of file watchers reached`,通常代表 inotify 資源耗盡,不是專案目錄已滿。錯誤中的路徑只是下一個註冊失敗的位置,不能證明該程序用量最大。

限制依真實使用者 ID 計算,編輯器、語言伺服器、開發伺服器、測試器、同步工具與容器程序可能共用同一資源池。即使 `max_user_watches` 很高,`max_user_instances` 仍可能耗盡;佇列溢位則是第三種故障。

調高 sysctl、重新開機或大量終止程序前,先從 `/proc` 建立唯讀消費者臺帳。比較限制、實例、標記、程序身分、權限缺口與命名空間,再做一項可逆變更並重新量測。

先確認這是 inotify 的 ENOSPC 分支

記錄完整錯誤、命令、系統與路徑。若失敗來自 `fs.watch`、Chokidar、Watchpack、編輯器或其他 Linux 檔案監看介面,inotify 耗盡是強假設;若是寫檔失敗,另查 `df -h` 與 `df -i`。

用 `id -ru` 記錄真實使用者 ID,並讀取 `max_user_watches`、`max_user_instances` 與 `max_queued_events`。前兩者可造成註冊失敗;佇列溢位會遺失事件並產生 `IN_Q_OVERFLOW`。

分開計算實例與監看標記

inotify 實例是 `anon_inode:inotify` 檔案描述元,但一個描述元可包含許多標記。描述元數量只能說明實例數,不能說明距離 `max_user_watches` 多遠。

針對每個可讀的 inotify `fdinfo`,計算以 `inotify` 開頭的行。依 PID 與真實 UID 彙總標記,並把描述元數獨立保存為 `instances`。

建立能表達權限缺口的消費者臺帳

對每個可檢查 PID,記錄真實 UID、程序名稱、實例數與標記數。不要執行從 `/proc/<pid>/cmdline` 讀出的文字;程序若在掃描中退出,應記為競爭,而不是零。

同時統計拒絕存取或不可讀的 `fdinfo`。只看得到自己的程序時仍能診斷按使用者計費的池,但不能證明全系統沒有其他消費者。遇到 hidepid、容器或命名空間邊界時,應在擁有目標程序的命名空間內重跑唯讀盤點。

執行已測試的唯讀臺帳

把腳本存成 `inotify-ledger.sh`,先在本機檢閱,再執行 `bash inotify-ledger.sh`。它只讀取 procfs 與 sysctl 檔案,不會寫入 sysctl、終止程序、重新啟動服務、刪除檔案或解析私人路徑。

```bash #!/usr/bin/env bash set -u printf 'setting\tvalue\n' for name in max_user_watches max_user_instances max_queued_events; do path="/proc/sys/fs/inotify/${name}" if [[ -r "$path" ]]; then IFS= read -r value < "$path" printf '%s\t%s\n' "$name" "$value" else printf '%s\tUNREADABLE\n' "$name" fi done printf '\npid\truid\tcomm\tinstances\twatches\tunreadable_fdinfo\n' for proc in /proc/[0-9]*; do pid=${proc##*/} [[ -r "$proc/status" ]] || continue ruid=$(awk '/^Uid:/{print $2; exit}' "$proc/status" 2>/dev/null || true) [[ -n "$ruid" ]] || continue comm=$(tr '\t\n' ' ' < "$proc/comm" 2>/dev/null || true) instances=0 watches=0 unreadable=0 shopt -s nullglob for fdinfo in "$proc"/fdinfo/*; do fd=${fdinfo##*/} target=$(readlink "$proc/fd/$fd" 2>/dev/null || true) [[ "$target" == "anon_inode:inotify" ]] || continue ((instances+=1)) if [[ -r "$fdinfo" ]]; then count=$(awk '/^inotify /{n++} END{print n+0}' "$fdinfo" 2>/dev/null || printf '0') ((watches+=count)) else ((unreadable+=1)) fi done if ((instances > 0 || watches > 0 || unreadable > 0)); then printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$pid" "$ruid" "$comm" "$instances" "$watches" "$unreadable" fi done | sort -t $'\t' -k5,5nr -k4,4nr printf '\nnamespace\ttarget\n' for ns in pid user mnt; do printf 'self_%s\t%s\n' "$ns" "$(readlink "/proc/self/ns/$ns" 2>/dev/null || printf 'UNREADABLE')" printf 'init_%s\t%s\n' "$ns" "$(readlink "/proc/1/ns/$ns" 2>/dev/null || printf 'UNREADABLE')" done ```

隔離測試讓一個 Node.js 程序監看三個不同的暫存目錄;臺帳正確回報一個實例與三個標記。PID 1 命名空間不可讀時,腳本保留 `UNREADABLE`,沒有把盲區當作零。

不要把錯誤路徑當成最大消費者

例外中的路徑只是撞到上限的那次註冊。最大消費者可能是另一工作區、擴充功能宿主、語言伺服器、同步工具或長時間執行的開發程序;公開問題顯示一個擴充功能可占用數萬標記,而另一個 serve 程序先失敗。

依標記數排序臺帳,再正常關閉一個已確認且非必要的消費者並重跑。明顯下降才能建立因果關係。重新開機會清空證據,仍無法說明寬廣監看範圍是否會復發。

提高上限前先稽核監看範圍

把最大消費者對應到監看根目錄與排除規則。`.venv`、建置輸出、快取和相依目錄通常不需持續監看;VS Code 官方文件建議先用 `files.watcherExclude` 排除大型無關資料夾。

不要假設通用的 `node_modules` 排除會涵蓋所有監看器。編輯器、擴充功能、語言伺服器、測試器和框架伺服器可各自訂閱;應在擁有標記的元件中調整,再確認臺帳下降且必要事件仍正常。

只有臺帳證明需要時才提高一項限制

若合理範圍仍超過 `max_user_watches`,依記憶體餘裕選擇有根據的值,並透過發行版支援的 sysctl 位置套用。若耗盡的是實例數,只改標記上限無效;保留舊值、設定檔與版本以便回復。

不要把同一數字貼到所有主機。VS Code 將 524,288 列為可能值並提醒最壞記憶體成本,但容器、小型 VM 與多使用者系統預算不同。套用後確認實際值,重跑工作負載並比較臺帳與記憶體。

用分支矩陣取代隨機嘗試

單一 PID 占主導時,縮小或更新該消費者;多個合理消費者合計超過標記上限時,保守提高 `max_user_watches`;實例數接近 `max_user_instances` 時,尋找過多物件或洩漏;臺帳不完整時,先處理權限或命名空間。

若實例與標記都遠低於限制,應在最小工作區重現並檢查應用程式日誌。升級問題時提供前後臺帳、準確限制、核心與應用程式版本和最小重現,不要附上秘密或私人路徑清單。

從唯讀證據分向單一大消費者、多消費者和可見性盲區的原創概念插圖。
Watch at Ease 原創概念插圖:不同證據分支只指出負責者與邊界,不自動授權修改。 Watch at Ease