PostgreSQL「no space left on device」の原因と対処

PostgreSQL「no space left on device」の原因と対処

概要。OS が書き込み先デバイスの空きブロックまたは inode を返せなくなると、PostgreSQL はファイル作成・拡張・書き込み時に「no space left on device(ENOSPC)」で失敗する。増えがちな場所は pg_wal(WAL)、ログ出力先、テンポラリ、テーブルスペース、バックアップ置き場。発生条件の整理、原因別の切り分け、緊急確保手順と恒久対策、監視ポイントまでを一気にまとめる。

発生条件(どんな時に出るか)

・PGDATA 配下やテーブルスペースのマウントで「空きブロック」または「inode」が尽きた
・コンテナや VM の割り当てストレージ上限に到達(overlay2、LVM、クラウドのボリューム制限)
・WAL の過剰蓄積(レプリケーションスロット、長時間停止したスタンバイ、wal_keep_size 等)
・巨大な並べ替え/ハッシュでテンポラリが爆発(work_mem 不足、索引無しの重いクエリ)
・ログやバックアップ、ダンプを同一ディスクに置いて肥大
・inode 枯渇(小さなファイルを大量生成)や quota 超過でも同様に ENOSPC になる

まず最初の安全確認(書き込み圧力を下げる)

・アプリからの更新を抑える(読み取り系のみ許可)。長大トランザクションを終了させる
・自動バキュームや重いバッチが動いていれば一時停止を検討
・クラッシュ回避のため「pg_wal を絶対に手で削除しない」。復旧不能になる

OS レベルの現状把握クイックコマンド

# ディスクと inode の空き
df -h
df -i

# どのディレクトリが増えているか(PGDATA 直下)
du -xhd1 "$PGDATA" | sort -h

# WAL/ログ/テンポラリの代表格
du -sh "$PGDATA/pg_wal" 2>/dev/null
du -sch "$PGDATA"/log/* 2>/dev/null | tail -1
find "$PGDATA" -type d -name "pgsql_tmp" -print -exec du -sh {} \;

# 最近巨大化したファイルの特定(24 時間以内に 100MB 超)
find "$PGDATA" -xdev -type f -size +100M -mtime -1 -ls

PostgreSQL から見える危険サイン

-- WAL とアーカイバの状況
SELECT * FROM pg_stat_archiver;
SELECT slot_name, plugin, active, restart_lsn,
       pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained
FROM pg_replication_slots;

-- 長時間トランザクション(VACUUM/不要WAL保持の元凶)
SELECT pid, usename, state, xact_start, now()-xact_start AS xact_age, query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY xact_start;

-- データ/指数の肥大状況
SELECT relkind, schemaname||'.'||relname AS rel,
       pg_size_pretty(pg_total_relation_size(relid)) AS total,
       pg_size_pretty(pg_relation_size(relid)) AS table,
       pg_size_pretty(pg_indexes_size(relid)) AS indexes
FROM pg_catalog.pg_statio_user_tables
ORDER BY pg_total_relation_size(relid) DESC
LIMIT 20;

-- 設定の重要どころ
SELECT name, setting, unit
FROM pg_settings
WHERE name IN ('data_directory','log_directory','temp_tablespaces',
               'wal_keep_size','max_wal_size','archive_mode','archive_command',
               'work_mem','maintenance_work_mem','log_temp_files');

緊急対応:数百 MB~数 GB を素早く空ける

・アプリ/OS のログを圧縮・削除(logrotate を即時実行)。PostgreSQL のログ出力先が別マウントならそちらを優先
・PGDATA 配下に紛れ込んだバックアップや古い .dump/.gz を移動
・巨大な core dump、不要キャッシュ(/var/cache、パッケージキャッシュ)を清掃
・平常時は「非常用の予約ファイル」を用意しておき、満杯時に消して即時確保

# logrotate を手動起動(ディストリによりコマンドは異なる)
sudo logrotate -f /etc/logrotate.conf

# 非常用 1GB 予約ファイル(平常時に作成しておく)
sudo fallocate -l 1G /var/lib/postgresql/.reserve

# ディスクが詰まったら即削除
sudo rm -f /var/lib/postgresql/.reserve

WAL(pg_wal)が膨らむ原因と対処

・スタンバイ停止やネットワーク断で送れない → 復旧後に追いつくまで増える
・レプリケーションスロットが非アクティブで残存 → restart_lsn 以前の WAL を保持
・wal_keep_size が過大 → 物理レプリカ用の保持が大きすぎる
・アーカイブ先が詰まり archiver が失敗 → pg_wal が再利用できない

-- 使っていないレプリケーションスロットを削除(要確認)
SELECT slot_name, active FROM pg_replication_slots WHERE active = false;
SELECT pg_drop_replication_slot('unused_slot_name');

-- wal_keep_size を縮小(再起動/リロードが必要な場合あり)
ALTER SYSTEM SET wal_keep_size = '256MB';  -- 例
SELECT pg_reload_conf();

-- アーカイブ失敗が続くならアーカイブ先の空き/到達性を回復
SELECT * FROM pg_stat_archiver;  -- failed_count, last_failed_wal を確認

テンポラリ肥大(ソート/ハッシュ/集計)の抑制

・索引の追加、クエリ計画の見直しでテンポラリ発生自体を減らす
・work_mem を適切化(大き過ぎは逆に同時並行で危険)。ログで可視化

-- 10MB 超のテンポラリ発生をログへ
ALTER SYSTEM SET log_temp_files = '10MB';
SELECT pg_reload_conf();

-- 一時ディレクトリ/テーブルスペースの使用量確認
du -sh "$PGDATA"/base/*/pgsql_tmp 2>/dev/null
-- 可能なら temp_tablespaces を別ディスクに
ALTER SYSTEM SET temp_tablespaces = 'ts_temp';  -- 事前に作成したテーブルスペース名

テーブル・インデックスの膨張対策(空き確保と恒久策)

・長期トランザクションを解消してから VACUUM を実行
・重い表には並列 vacuumdb、断続的に REINDEX。ダウンタイム無し縮小が必要なら pg_repack を検討

-- 全データベースを並列バキューム(要メンテ時間)
vacuumdb -j4 -d postgres -U postgres -v --all

-- 断片化がひどい索引を再構成
REINDEX TABLE CONCURRENTLY my_schema.my_table;

-- 代表的なサイズ可視化
SELECT schemaname, relname,
       pg_size_pretty(pg_total_relation_size(relid)) AS total
FROM pg_statio_user_tables
ORDER BY pg_total_relation_size(relid) DESC
LIMIT 30;

ログ/バックアップの置き場所を分離(同居は詰まりの元)

・log_directory を別マウントへ。アーカイブ先、バックアップ先も別ディスクか外部ストレージへ
・同一マウントに置く場合は容量監視とローテーションを厳格化

-- ログの出力先を変更(ディレクトリは事前作成・権限調整)
ALTER SYSTEM SET log_directory = '/var/log/postgresql-db1';
SELECT pg_reload_conf();

-- WAL アーカイブ先も別ボリューム/オブジェクトストレージへ
ALTER SYSTEM SET archive_mode = 'on';
ALTER SYSTEM SET archive_command = 'rsync -a %p backuphost:/pgarchive/%f';

pg_wal の移設(停止メンテでのみ実施)

・ディスクを増やせない場合の最終手段。必ずサーバ停止→安全に移動→シンボリックリンク
・運用中に pg_wal を触らない

# 1) PostgreSQL 停止
sudo systemctl stop postgresql

# 2) 新マウントへ移動
sudo rsync -a --delete "$PGDATA/pg_wal/" /mnt/big/pg_wal/

# 3) 元を退避しシンボリックリンク作成
sudo mv "$PGDATA/pg_wal" "$PGDATA/pg_wal.bak"
sudo ln -s /mnt/big/pg_wal "$PGDATA/pg_wal"

# 4) パーミッション確認後、起動
sudo chown -h postgres:postgres "$PGDATA/pg_wal"
sudo systemctl start postgresql

コンテナ/Kubernetes/クラウド特有の詰まり

・コンテナの writable layer 容量不足 → データは必ず PVC/ボリュームへ。ログも stdout へ出し集中ローテート
・PVC 容量のオンライン拡張とファイルシステム拡張(resize2fs/xfs_growfs)
・スナップショットやバックアップジョブが同一ボリュームを専有していないかを確認

inode 枯渇や quota 超過の見分け方

・df -i で IUse% が 100% なら inode 不足。小さなファイル群(ログや一時ファイル)が原因
・ユーザ/グループ quota が設定されている環境では repquota、quota コマンドで上限超過を確認・調整

監視・予防(再発させない)

・ディスク使用率/残り inode/pg_wal 容量/アーカイブ失敗回数の監視
・長時間トランザクションのしきい値監視(xact_age)
・log_temp_files と auto_explain でテンポラリ多発クエリを洗い出し
・定期的に vacuumdb と REINDEX、メンテ後のサイズ差分を記録
・非常用予約ファイルを 1~5GB 用意し、満杯時に即削除できる体制を整える

原因別トラブルシュートの決め手一覧

・pg_wal が巨大 → レプリケーションスロット/スタンバイ停止/アーカイブ失敗を確認、不要スロット削除・復旧
・ログだらけ → logrotate 即時実行、保管日数短縮、出力先分離
・テンポラリだらけ → log_temp_files で発生源特定、索引/クエリ改善、temp_tablespaces 分離
・テーブル/索引肥大 → 長期トランザクション解消→VACUUM/REINDEX/pg_repack
・inode 枯渇 → 小ファイル群の整理、ローテーション設定の見直し
・コンテナ層満杯 → 永続ボリュームへ誘導、レイヤ肥大を避ける

現場でそのまま使える点検スクリプト(Bash)

#!/usr/bin/env bash
set -e
echo "== Disk blocks & inodes =="
df -h; echo; df -i
echo; echo "== Top dirs under PGDATA =="; du -xhd1 "$PGDATA" | sort -h
echo; echo "== pg_wal size =="; du -sh "$PGDATA/pg_wal" 2>/dev/null || true
echo; echo "== log dir size =="; du -sh "$PGDATA"/log 2>/dev/null || true
echo; echo "== temp dirs =="; find "$PGDATA" -type d -name pgsql_tmp -exec du -sh {} \; 2>/dev/null

WAL 保持の具体値を把握(SQL ひな形)

WITH slots AS (
  SELECT slot_name, active, restart_lsn
  FROM pg_replication_slots
),
prog AS (
  SELECT pg_current_wal_lsn() AS cur
)
SELECT s.slot_name, s.active,
       pg_size_pretty(pg_wal_lsn_diff(p.cur, s.restart_lsn)) AS retained_from_slot
FROM slots s CROSS JOIN prog p
ORDER BY pg_wal_lsn_diff(p.cur, s.restart_lsn) DESC;

テンポラリ多発クエリの可視化(設定と読み方)

-- 10MB 超のテンポラリをログする(恒久化は postgresql.conf へ)
ALTER SYSTEM SET log_temp_files = '10MB';
SELECT pg_reload_conf();

-- 以降、ログ(csvlog)から "temporary file" を grep
grep "temporary file" /var/log/postgresql/*.csv | sort | tail -n 50

最後に(運用の勘所)

・「pg_wal を手で消さない」「稼働中の PGDATA を直接いじらない」が大前提
・短期はログ/一時/バックアップの分離とローテート、長期はレプリケーションスロット管理・クエリ/索引改善・定期メンテ
・ディスク拡張は最も確実な恒久策。拡張後も監視しきい値と非常用予約ファイルでワンミスを防ぐ