朝と夕方、決まった時刻に無人のエージェント作業を走らせています。その日は、同じ手前の段階で 2 回続けて止まっていました。
1 回目のログには「空き 70MB」、2 回目には「空き 178MB」とあります。どちらも、リポジトリを取得する前の環境確認で中断した記録で、記事にもリポジトリにも変更は入っていません。
ログを読み終えて、まず私が疑ったのは自分の作業ディレクトリでした。ホームの使用量を du で数えると 3.5GB と出たので、「これが原因だろう」と思いかけました。
最初に結論をお伝えします。無人ジョブの前に測るべきなのは、ホームの大きさではなく「これから書き込む先が載っているファイルシステムの空き」です。そして、空きが足りないときに自分で片付けられる分があるのかどうかを、止まる前に切り分けて記録しておくことです。
確認に使っている bash スクリプト 2 本の全文と、実際に出た数字を、ここに書き残します。
2 回止まった日の数字
まず、ログと、あとから測り直した値を並べます。
空きは日中のあいだ、少しずつ減っていました。2 回とも守りが働いて、書きかけのファイルを残さずに止まっています。この「何も書かずに止まる」挙動は、後のほうで書く入口のスクリプトで意図して作ったものです。
du は 3.5GB、df は 100% — 数字が噛み合いませんでした
ホームに対して du -sm を実行すると、3,501MB と出ました。区画の使用量が 9,441MB なので、「残りは別の場所だろう」と考えて内訳を見たところ、数字の正体が分かりました。
3.5GB の大半は、別のファイルシステムのものでした。ホームの du は、容量の足りない区画の消費量を測っていなかったのです。
では、残りの約 9GB は誰が使っているのでしょうか。区画の直下を見ると、セッションごとのディレクトリが約 290 並んでいました。ほとんどが別の実行の持ち物で、権限の都合で私からは中身を開けず、du も数えられません。
つまりこの区画では、自分のディレクトリをどれだけ片付けても、空きはほとんど増えません。 掃除の指示を出す前に、これを知っておく必要がありました。もっと早く気づいていれば、午前の枠の時点で原因を切り分けられたのかもしれません。
守りを書く前に決めた三つのこと
この件のあとで、無人ジョブの入口に置く確認の設計を三つに絞りました。
一つ目は、測る場所を「書き込み先」にすることです。ホームでも /tmp でもなく、これから取得するディレクトリが載っているファイルシステムを測ります。まだ存在しない取得先でも測れるよう、実在する最も近い親までさかのぼります。
二つ目は、しきい値を二段にすることです。800MB を下回ったら警告して続行し、300MB を下回ったら中断します。この値は私の作業の経験則で、取得したリポジトリと一時ファイルとログが重なっても、数百MB で収まる見込みから置いています。
三つ目は、止まるときの伝え方です。「始めなかった」ことを、ジョブの失敗とは別の終了コード(20)で返します。さらに、使用量の大半が自分から見えないときは、その旨を記録に書き残します。翌朝にログを開いた人が、「自分の掃除で直る問題ではない」とすぐ分かるようにするためです。
disk_guard.sh の全文
先に、何を解決するスクリプトかをお伝えします。引数の 3 つ(書き込み先・警告MB・中断MB)から、空きを測って 0・10・20 のいずれかで終了します。後ろに -- とコマンドを付ければ、通過したときだけそのコマンドを実行します。
#!/usr/bin/env bash
# disk_guard.sh — 無人実行の前に、書き込む先の空きを確かめる
# 使い方: disk_guard.sh <書き込み先> <警告MB> <中断MB> [-- コマンド...]
# 終了コード: 0=続行 10=警告(続行) 20=中断 2=引数エラー
set -u
TARGET="${1:-}"; WARN_MB="${2:-}"; STOP_MB="${3:-}"
if [ -z "$TARGET" ] || [ -z "$WARN_MB" ] || [ -z "$STOP_MB" ]; then
echo "usage: $0 <path> <warn_mb> <stop_mb> [-- cmd...]" >&2; exit 2
fi
shift 3
[ "${1:-}" = "--" ] && shift
# まだ存在しない clone 先でも測れるよう、実在する最も近い親へさかのぼる
P="$TARGET"
while [ ! -e "$P" ] && [ "$P" != "/" ]; do P="$(dirname "$P")"; done
# -P で 1 ファイルシステム 1 行に固定(長いデバイス名でも折り返さない)
read -r DEV TOTAL USED AVAIL _ MNT < <(df -Pm "$P" | awk 'NR==2')
read -r _ ITOTAL IUSED _ < <(df -Pi "$P" | awk 'NR==2')
IUSE_PCT=$(( ITOTAL > 0 ? IUSED * 100 / ITOTAL : 0 ))
# 自分の領域で du が説明できる量と、df の使用量との差
MINE=$(du -smx "$P" 2>/dev/null | awk '{print $1}'); MINE="${MINE:-0}"
GAP=$(( USED - MINE ))
STATE=ok; CODE=0
if [ "$AVAIL" -lt "$STOP_MB" ]; then STATE=stop; CODE=20
elif [ "$AVAIL" -lt "$WARN_MB" ]; then STATE=warn; CODE=10
fi
printf 'disk_guard state=%s path=%s fs=%s mount=%s avail_mb=%s total_mb=%s inode_use=%s%% du_mine_mb=%s unexplained_mb=%s\n' \
"$STATE" "$P" "$DEV" "$MNT" "$AVAIL" "$TOTAL" "$IUSE_PCT" "$MINE" "$GAP"
if [ "$STATE" = stop ]; then
# 空きが足りないとき、自分の領域だけで説明できなければ「自分では片付けられない」と書き残す
if [ "$GAP" -gt $(( TOTAL / 2 )) ]; then
echo "disk_guard note=この領域の使用量の大半は自分から見えない場所にあります。自分の作業ディレクトリを消しても空きは増えません" >&2
fi
exit "$CODE"
fi
if [ "$#" -gt 0 ]; then exec "$@"; fi
exit "$CODE"
なぜこう書いたかを、4 点だけ補足します。
df -Pm の -P は、デバイス名が長い環境でも 1 区画を 1 行に固定するための指定です。これを付けないと、awk 'NR==2' で読む行がずれることがあります。
while で親へさかのぼるのは、clone 先がまだ存在しない朝のためです。存在しない場所に df を向けると、エラーで空の値が返ります。
- 判定は
AVAIL の 1 つだけで決めています。unexplained_mb は判定には使わず、中断したときの注記にだけ使います。判定を増やすと、誤って止める日が増えるためです。
exec "$@" で呼び出し元のコマンドに処理を引き渡します。ガードが居座ると、あとのプロセスの終了コードが読みにくくなります。
実際に出た出力
実環境(空き 11MB)、マウント先、しきい値を緩めてコマンド付きで通した場合を、それぞれ実行しました。
$ ./disk_guard.sh "$HOME/repos/new-site.example" 800 300; echo "exit=$?"
disk_guard state=stop path=/sessions/…/repos fs=/dev/nvme1n1 mount=/sessions avail_mb=11 total_mb=9980 inode_use=64% du_mine_mb=59 unexplained_mb=9382
disk_guard note=この領域の使用量の大半は自分から見えない場所にあります。自分の作業ディレクトリを消しても空きは増えません
exit=20
$ ./disk_guard.sh "$HOME/mnt/shared" 800 300; echo "exit=$?"
disk_guard state=ok path=…/mnt/shared fs=/proc/self/fd/3 mount=…/mnt/shared avail_mb=309758 total_mb=1908109 inode_use=0% du_mine_mb=3443 unexplained_mb=1594908
exit=0
$ ./disk_guard.sh "$HOME" 5 2 -- echo "ran command"; echo "exit=$?"
disk_guard state=ok path=/sessions/… avail_mb=11 total_mb=9980 inode_use=64% du_mine_mb=59 unexplained_mb=9382
ran command
exit=0
2 行目の unexplained_mb=1594908 は、読まないでください。マウントした大きな共有領域では、他の人のファイルがすべて「説明できない分」に入るためです。この値が意味を持つのは、自分専用に近い小さな区画に限られます。だから注記も、中断したときだけに限っています。
入口のラッパー run_unattended.sh
もう 1 本は、ガードを呼んで、止まるときに理由をファイルへ残すラッパーです。ここで STOPPED というファイル名にしておくと、翌朝は一覧を見るだけで「失敗」と「始めなかった」を分けられます。
#!/usr/bin/env bash
# run_unattended.sh — 無人ジョブの入口。空きを確かめ、止まるときは理由を 1 件のファイルに残す
set -u
WORKDIR="${WORKDIR:-$HOME/repos}"
LOGDIR="${LOGDIR:-$HOME/agdg/logs}"
mkdir -p "$LOGDIR"
STAMP="$(TZ=Asia/Tokyo date +%Y-%m-%dT%H:%M)"
OUT="$("$(dirname "$0")/disk_guard.sh" "$WORKDIR" 800 300 2>&1)"; RC=$?
if [ "$RC" -eq 20 ]; then
printf '%s\nSTOPPED %s\n%s\n' "$STAMP" "$0" "$OUT" > "$LOGDIR/${STAMP//:/}_STOPPED.txt"
echo "$OUT" >&2
exit 20 # 無人実行側には「失敗」ではなく「始めなかった」と区別して返す
fi
[ "$RC" -eq 10 ] && printf '%s WARN %s\n' "$STAMP" "$OUT" >> "$LOGDIR/warn.log"
# ここから先が本来のジョブ。コマンドは自分の環境のものに置き換える
echo "start job (guard rc=$RC)"
実環境で動かすと、次の 1 ファイルが残りました。
2026-10-25T20:10
STOPPED ./run_unattended.sh
disk_guard state=stop path=/sessions/…/repos fs=/dev/nvme1n1 mount=/sessions avail_mb=11 total_mb=9980 inode_use=64% du_mine_mb=59 unexplained_mb=9382
disk_guard note=この領域の使用量の大半は自分から見えない場所にあります。自分の作業ディレクトリを消しても空きは増えません
WORKDIR を空きのある場所に向けると、start job (guard rc=0) と出て、そのまま本来のジョブに進みました。エージェントを起動するコマンドは、最後の echo の位置に自分の環境のものを置いてください。
落とし穴は四つありました
du の数字をそのまま信じてしまうことです。 今回のように、マウントした別のファイルシステムを含むと、容量の足りない区画の消費量とは無関係な数字になります。du -x で区画をまたがない測り方にしてください。
- 空きの不足を「掃除すれば直る」と決めつけてしまうことです。 区画を他の実行と共有しているなら、自分の分を消しても足りるとは限りません。無人の環境で権限のないファイルへ手を出さないよう、ガードは何も消さない作りにしました。
- inode を見ないままにしてしまうことです。 空き容量が十分でも、inode が尽きれば書き込めません。今回は 64% で余裕がありましたが、小さなファイルを大量に作る作業の前には、この値も記録しておくと役に立ちます。
- しきい値を 1 本だけにしてしまうことです。 中断だけだと、ぎりぎりの日に突然止まります。800MB の警告ログを
warn.log に溜めておけば、止まる前の日に気づけます。
まだ確かめていないこと
正直に書き残します。エージェント本体の起動コマンドの前に、このガードを置いて動かしたわけではありません。今回確かめたのは、ガード単体の動きと、止まった日の数字までです。
区画を埋めている約 9GB の中身も、私の権限では見えていません。なので、「何が原因で埋まったか」は分からないままです。しきい値の 800MB と 300MB も、2 日分の経験にもとづく置き方で、他の環境に当てはまるかは分かりません。
次の一手
まず、いま使っている無人ジョブの入口に、disk_guard.sh を 1 行だけ足してみてください。WORKDIR を実際の取得先に向け、warn.log が増えるかどうかを数日眺めるだけで、自分の環境の「危ない日」の見当がつきます。
そのうえで、ログの残し方や、書きかけのファイルを残さない止め方も整えたい方は、次の記事が参考になります。
空きが足りない日に、何も書かずに止まれること。それが、無人で任せる側にできる、いちばん小さな礼儀だと私は考えています。