Docker肥大化の原因調査:binlogが400GB溜まった話
はじめに
「開発サーバでコマンドを実行しても何も動かない」そんな状況に突然直面したことはありますか。ファイルの書き込みも、ツールの起動も、ことごとく失敗する。ログを見ると ENOSPC: no space left on device というエラーが出ていました。ディスクの空き容量が完全になくなっていたのです。
調査を進めると、犯人はDockerボリュームの中に潜んでいました。MySQLのバイナリログ(変更履歴ファイル)が3ヶ月間で3,575ファイル・約400GB蓄積されており、ディスクを食い尽くしていたのです。
この記事では、その発見の経緯から根本原因の特定、本番環境への展開チェックまでを実録としてご紹介します。同じ状況に直面した方の参考になれば幸いです。
以前似たような事象に発生した時の話もあるのでぜひ読んでみてください。
Dockerボリュームの罠:テーマが反映されない原因と解決策
scriptlab.jp
開発サーバのディスクが100%になっていた
異変に気づいたきっかけ
使用しているツールを起動しようとしたところ、起動中のまま一向に先に進まない状態になりました。タイムアウトさせてエラーを確認すると、次のメッセージが表示されていました。
Error: ENOSPC: no space left on device, write
ENOSPC とは「No Space Left On Device」の略で、「ディスクに空き容量がない」という意味のエラーです。まずはサーバのディスク使用状況を確認しました。
df -h /
Filesystem Size Used Avail Use%
/dev/... 466G 447G 0 100%
466GBのディスクが完全に満杯でした。ホームディレクトリのサイズを調べても25GB程度しかなく、残りの420GB以上がどこにあるのかがすぐにはわかりませんでした。
Dockerが原因と判明するまで
候補としてDockerのディスク使用状況を調べました。docker system df コマンドを使うと、Dockerが使っているディスク量を種類別に一覧できます。
docker system df
TYPE TOTAL SIZE
Images 45 28.54GB
Containers 21 1.347GB
Local Volumes 20 404.3GB ← ここが異常
Build Cache 318 1.658GB
「Local Volumes」、つまりDockerボリュームが404GBを占有していました。ホームディレクトリ以外にこれほど大きな領域が存在していた原因がわかりました。
Docker肥大化の原因ボリュームを特定する手順
docker system df -v で詳細を確認する
-v(verbose、詳細表示)オプションをつけると、ボリュームごとのサイズが確認できます。
docker system df -v
出力されたボリューム一覧の中に、明らかに異常なサイズのものがありました。
VOLUME NAME LINKS SIZE
scriptlab_mysql_data 1 398.2GB ← 異常
asken_menu_db_volume 1 4.4GB
asken_domestic_db_volume 1 520MB
...
scriptlab_mysql_data という名前のMySQLデータボリュームが398GBになっていました。WordPressのローカル開発用のデータベースとしては明らかに異常なサイズです。
ボリュームの中身を直接確認する
ボリュームを一時的なコンテナにマウントして中身を確認します。alpine(軽量なLinux)イメージを使うと手軽に確認できます。
docker run --rm -v scriptlab_mysql_data:/data alpine \
sh -c "ls /data/ | head -20"
するとファイルが大量に並んでいました。
mysql-bin.000754
mysql-bin.000755
mysql-bin.000756
...(以下、何千行も続く)
ファイルの総数を確認すると3,575個、1ファイルあたり約103MBで合計約368GBを占有していました。これがMySQLの「バイナリログ」です。
以下の図は、今回の調査の流れを示しています。
flowchart TD
A[ENOSPCエラーが発生] --> B[df -h でディスク使用率を確認]
B --> C[使用率100%を確認]
C --> D[docker system df でDocker使用量を確認]
D --> E[Volumesが404GBを占有]
E --> F[docker system df -v でボリューム詳細を確認]
F --> G[scriptlab_mysql_dataが398GBと判明]
G --> H[ボリュームの中身を直接確認]
H --> I[mysql-bin.*ファイルが3,575個]
エラーメッセージからDocker、そして特定のボリュームへと段階的に絞り込んでいきました。
MySQLバイナリログが蓄積した仕組みと根本原因
バイナリログとは何か
バイナリログとは、MySQLが行ったデータの変更操作をすべて記録したファイルです。「いつ、どのデータが、どのように変更されたか」を記録しており、主に次の用途に使われます。
- データの時点復元(バックアップからの特定時点への巻き戻し)
- レプリケーション(データをリアルタイムで別のサーバに複製する仕組み)
ローカル開発環境では通常は不要な機能ですが、今回の設定ではバックアップ目的で有効化されていました。MySQLは max_binlog_size で指定したサイズ(今回は100MB)に達するたびに新しいファイルを作成し続けます。
expire_logs_days がMySQL 8.0で効かなかった理由
設定ファイル(custom.cnf)には自動削除の設定が書かれていました。
# Binary Logging (for backup)
log_bin = /var/lib/mysql/mysql-bin
expire_logs_days = 7
max_binlog_size = 100M
expire_logs_days = 7 は「7日経過したバイナリログを自動削除する」という設定です。しかし3ヶ月分のファイルが削除されずに残っていました。
原因を確認するため、MySQLが実際に認識している設定値を調べました。
docker exec コンテナ名 mysql -uroot -pパスワード \
-e "SHOW VARIABLES LIKE '%expire%';"
Variable_name Value
binlog_expire_logs_auto_purge ON
binlog_expire_logs_seconds 0 ← 0は「削除しない」を意味する
expire_logs_days 7
ここに根本原因がありました。expire_logs_days の値は7に設定されているものの、実際にバイナリログの削除を制御する binlog_expire_logs_seconds が0になっていたのです。
MySQL 8.0では、バイナリログの有効期限管理が expire_logs_days から binlog_expire_logs_seconds に変更されました。expire_logs_days はMySQL 8.0で非推奨(将来的に削除される予定の機能)となっており、起動ログにも次の警告が出ていました。
[Warning] The syntax 'expire-logs-days' is deprecated
and will be removed in a future release.
Please use binlog_expire_logs_seconds instead.
expire_logs_days = 7 と書いても binlog_expire_logs_seconds は0のままになるため、バイナリログが永遠に削除されない状態になっていたのです。
以下の図は、この問題がどのように発生したかを示しています。
flowchart TD
A["expire_logs_days = 7 を設定"] --> B{"MySQL 8.0での扱い"}
B --> C["非推奨パラメータとして警告のみ"]
C --> D["binlog_expire_logs_seconds は 0 のまま"]
D --> E["バイナリログが自動削除されない"]
E --> F["毎日数十ファイルずつ蓄積"]
F --> G["3ヶ月で3,575ファイル・約368GBに"]
正しい設定への修正方法
expire_logs_days を削除し、binlog_expire_logs_seconds に置き換えます。7日間は秒数に換算すると604,800秒(7 × 24 × 60 × 60)です。
修正前:
expire_logs_days = 7
修正後:
binlog_expire_logs_seconds = 604800
設定ファイルを修正した後、MySQLを再起動して設定が反映されたか確認します。
docker restart コンテナ名
docker exec コンテナ名 mysql -uroot -pパスワード \
-e "SHOW VARIABLES LIKE '%expire%';"
Variable_name Value
binlog_expire_logs_seconds 604800 ← 正しく反映された
expire_logs_days 0
これでバイナリログが7日経過後に自動削除されるようになりました。
本番環境への横展開チェックと修正
ローカルで見つけた問題は本番にも潜んでいることがある
ローカル開発環境での問題を修正した後、本番サーバでも同じ設定ファイルが使われていないかを確認しました。設定ファイルを共有しているプロジェクト構成では、同じ設定ミスが本番環境にもそのまま存在していることが少なくありません。
本番環境でMySQLの変数を確認すると、まったく同じ状態でした。
binlog_expire_logs_seconds 0 ← ローカルと同じ問題
expire_logs_days 7
本番サーバのバイナリログのファイル数を確認すると、こちらは4ファイルのみでディスクにはまだ余裕がありました。ローカル環境よりも先に問題を発見できたため、大事には至りませんでした。
本番環境でも同様に binlog_expire_logs_seconds = 604800 に修正し、MySQLを再起動して設定が反映されたことを確認しました。
flowchart LR
A[ローカル環境で問題発見・修正] --> B{本番環境も同じ設定か確認}
B -->|同じ設定だった| C[本番も修正]
B -->|設定が違った| D[対応不要]
C --> E[ディスク枯渇を予防できた]
設定ファイルを複数の環境で共有しているケースでは、ローカルで見つけた問題を本番にも展開して確認することを習慣にしておくと、同様のトラブルを未然に防ぐことができます。
終わりに
今回の問題は、ENOSPC エラーからディスク使用率の確認、Dockerのボリューム一覧、そして特定のボリュームの中身の確認へと順を追って絞り込むことで、根本原因にたどり着くことができました。
MySQL 8.0では expire_logs_days が非推奨となっており、設定しても binlog_expire_logs_seconds が0のままになるという仕様変更が原因でした。設定ファイルを書いただけで安心せず、SHOW VARIABLES コマンドで実際にMySQLが認識している値を確認することが重要です。
同じ設定ファイルを使っている環境があれば、ローカルで発見した問題をセットで確認するようにしておくと安心です。今回は本番のディスク枯渇を未然に防ぐことができました。MySQLのバイナリログ設定を見直す際のお役に立てれば幸いです。
コメントを残す