Konboi Note

ローカルの PostgreSQL Docker volume ダイエット作戦

· Konboi

はじめに

ローカル開発環境の PostgreSQL の Docker volume がいつの間にか 100GB を超えていた。

System のストレージに関する警告も出るようになってしまったので調査してみると、過去の検証用に投入したすでに使っていないデータが大量に残っていたことが原因だった。

最終的には不要データの削除、REINDEXVACUUM FULL で、合計 108GB から 15GB まで削減できた。

発端

まずレコード数を確認した。

SELECT count(*) FROM table_a;
-- 59085409

SELECT count(*) FROM table_b;
-- 46287932

合計で約 1 億件近いデータが存在していた。

今回はローカルの検証データで、すでに不要になっていたため削除することにした。

DELETEしたのに容量が減らない

不要なデータを削除した。

DELETE FROM table_a WHERE ...;
DELETE FROM table_b WHERE ...;

ただ、ディスク使用量を確認してもほとんど変化がなかった。

当初、DELETEVACUUM を実行したので容量も減ると思っていた。しかし PostgreSQL の挙動は違った。

PostgreSQL の DELETE はデータを物理的にすぐ削除するのではなく、削除済みとして扱える状態にする。通常の VACUUM を実行すると、その領域は PostgreSQL 内で再利用可能になるだけらしい。

つまり以下のようになる。

DELETE
VACUUM

だけだと以下のような挙動になる。

  • 削除済み領域は再利用可能になる
  • ただし、ディスク使用量は大きく減らないことがある

ディスク容量そのものを回収したい場合は、VACUUM FULL やテーブルの再作成を検討する必要がある。

VACUUMを実行したらエラー

また、通常の VACUUM を実行した際に以下のエラーで少し嵌った。

VACUUM table_a;

すると以下のエラーが発生した。

ERROR: could not resize shared memory segment ...
No space left on device

一見ディスク容量不足のように見えるが、実際には Docker コンテナの共有メモリ(/dev/shm)が不足していた。

今回は一時的に maintenance_work_mem を下げることで実行できた。

SET maintenance_work_mem = '8MB';

ローカルの Docker 上で大きいテーブルに対して VACUUMREINDEX を実行する場合、ディスク容量だけでなく共有メモリの制限にも注意が必要そうだった。

テーブルサイズを調査

次にテーブルとインデックスのサイズを確認した。

SELECT
    pg_size_pretty(pg_relation_size('table_a')) AS table_size,
    pg_size_pretty(pg_indexes_size('table_a')) AS index_size,
    pg_size_pretty(pg_total_relation_size('table_a')) AS total_size;

table_a は以下。

table_size : 21GB
index_size : 2.7GB
total_size : 24GB

table_b は以下。

table_size : 31GB
index_size : 53GB
total_size : 84GB

ここで驚いたのは table_b のインデックスサイズだった。

データ本体よりインデックスの方が大きい。テーブルが大きいと思っていたが、実際にはインデックスの肥大化もかなり効いていそうだった。

REINDEXを実施

まず table_b のインデックスを再構築した。

REINDEX TABLE table_b;

結果は以下。

-- REINDEX前
table_size : 31GB
index_size : 53GB
total_size : 84GB

-- REINDEX後
table_size : 31GB
index_size : 4.3GB
total_size : 35GB

インデックスだけで約 49GB 削減された。

長期間の更新や削除により、かなりの index bloat が発生していたようだ。

この時点でかなりディスク使用量は減ったが、テーブル本体はまだ大きいままだった。

VACUUM FULLを実施

続いて VACUUM FULL を実行した。

VACUUM FULL table_a;
VACUUM FULL table_b;

table_a の実行前後は以下。

-- 実行前
table_size : 21GB
index_size : 2.7GB
total_size : 24GB

-- 実行後
table_size : 5.3GB
index_size : 635MB
total_size : 6GB

table_b の実行前後は以下。

-- 実行前
table_size : 31GB
index_size : 4.3GB
total_size : 35GB

-- 実行後
table_size : 4.7GB
index_size : 4.3GB
total_size : 9GB

VACUUM FULL はテーブルを書き換えるため、実行中は対象テーブルに強いロックがかかる。また、書き換え用の追加ディスク容量も必要になる。

今回はローカル開発環境だったのでエイヤッと実行したが、本番環境で同じことをするならメンテナンス時間や代替手段をちゃんと検討する必要がある。

結果

最終的には以下のようになった。

TableBeforeAfter
table_a24GB6GB
table_b84GB9GB

合計では以下。

108GB -> 15GB

約 86% 削減できた。

さいごに

今回のケースでは不要データの削除がきっかけだったが、調査してみると本当の問題はインデックスとテーブルの肥大化だった。

特に REINDEX だけで約 50GB 削減できたのは予想外で、PostgreSQL の運用について良い学びになった。

ローカル環境は雑に使いがちだが、気づかないうちにかなり容量を食っていることがある。たまには pg_total_relation_size やインデックスサイズを見ておくとよさそう。