スマートフォン・タブレットからインターネットサーバーオペレーション

APPW.jp
 
retention / safe deletion

ログを消す設計
消してはいけないものを、先に決める

2GB の VPS でログを貯め始めました。順調に増えます。 貯まり続けるものは、いつか必ず容量を食い潰す。 ところが「消そう」と決めた瞬間に気づきました—— 何を消してよいのか、分かっていない。 数レコードしかないのに絶対に消せないものがある、という話です。

MongoDB 保持期間 cron 安全設計

[ § 0 ] 導入

貯め始めると、いつか止まる

Apacheログを追記分だけ集計する仕組みを作り、 PHPログと合わせて MongoDB に貯めています。生ログは置かず集計だけなので、1日あたりは大した量ではありません。

ただ、止まらずに増え続けることに変わりはない。 2GB のサーバーで、いずれ限界が来ます。

そこで削除の仕組みを作ろうとして、手が止まりました。 どのコレクションを、どこまで消してよいのか。 容量の大きいものから消せばいい、という話ではありませんでした。

[ § 1 ] 分類

3種類のデータ、3つの寿命

コレクション中身大きさ消せるか
ConnsPHPログの生レコード消せる
HttpAggApacheの時間帯集計消せる
ErrAggerror.log の時間帯集計消せる
HttpStateファイルの読み取り位置数レコード絶対に消せない

前の3つは、古いほど価値が下がります。 30日前の「何時に404が何件だったか」を見ることは、まずありません。

問題は4つ目です。HttpState各ログファイルをどこまで読んだかを保存しています。 レコードは数件。容量としては無視できる大きさです。

それでも、これがいちばん消してはいけないものでした。 消すと次の cron がログ全体を頭から読み直します。 数万行の再パースが走り、すでに集計済みの期間が二重に計上される。 小さいから消してもいい、ではなく、役割で判断する—— 当たり前ですが、容量を減らそうとしているときには忘れがちです。
[ § 2 ] 影響

消すと、何が起きるか

読み取り位置を消した場合

HttpState が空なら、収集器は「まだ何も読んでいない」と判断します。 offset = 0 から読み直し、すでに集計済みの行をもう一度処理します。 バケットは $inc で加算するので、その期間の数字が倍になる。

集計を消した場合

容量は減りますが、ベースラインが短くなります変化検知は直近と過去を比べる仕組みなので、 過去が無ければ比較そのものが成立しません。

そして、戻せない

いちばん怖いのはこれです。削除は取り消せません。 バックアップを取っていなければ、その期間は永久に見えなくなる。 「あとで見るかもしれない」を考え始めると何も消せなくなりますが、 消したあとで欲しくなっても遅いのは事実です。

[ § 3 ] 保持期間

何日残すかを、どう決めるか

容量から決めたくなります。「まだ余裕があるから90日」というように。 これはやめました。いつか必ず破綻するからです。 余裕がなくなった時点で、慌てて短くすることになる。

代わりに使う場面から逆算しました。

使う場面必要な期間
変化検知のベースライン7日
時系列グラフ最長30日
月次の振り返り30日
それ以上前を見る場面思いつかなかった

結果、30日に決めました。月次で傾向を振り返れる最小単位です。

使わないデータは、最初から持たない。 これは収集の設計でも同じ考え方でした。 生ログをDBに入れず集計だけを保存したのは、後で消す苦労を先に回避したことでもあります。
[ § 4 ] 安全装置

削除スクリプトに入れた4つ

① 既定は、確認だけ

log-purge.php
# 引数なし → 何件消えるかを表示するだけ
php log-purge.php

# --run を付けて初めて削除される
php log-purge.php --run

消す操作を既定にしない。これが一番効きます。 手で叩けば自然と確認になり、--run を打つのは意識的な行為になる。 cron には --run を書きますが、それは一度書いたら見直す機会が少ないので、 手動実行のほうを安全側に倒しておくのが合理的だと考えました。

② 最小日数のガード

タイプミスを弾く
const MIN_DAYS = 3;

if ($days < MIN_DAYS) {
    fwrite(STDERR, "[ERR] 保持日数が短すぎます...");
    exit(1);
}

--days=0--days=1 を弾きます。 ゼロを打ち込めば全部消える——そういう入口を塞いでおく。

③ 1回あたりの上限

DBを固まらせない
const BATCH_LIMIT = 200000;

if ($old > BATCH_LIMIT) {
    echo "削除対象 {$old} 件は上限を超えています。\n"
       . "複数回に分けて実行してください。\n";
    continue;
}

初回実行では、数百万件が対象になることがあります。 それを一度に消すとDBが応答しなくなる。 2GB のサーバーでは現実的な危険です。上限を超えたら止めて、分割を促します。

④ 消すものを、列挙する

対象の明示
const TARGETS = [
    ['Conns',   'created_at'],
    ['HttpAgg', 'bucket'],
    ['ErrAgg',  'bucket'],
];
# HttpState は含めない
「これ以外を消す」ではなく「これを消す」。 除外リスト方式だと、新しいコレクションを追加したときに自動で削除対象になります。 列挙方式なら、追加を忘れても消えないだけで済む。 間違えたときにどちらへ倒れるか——それが設計の分かれ目でした。
[ § 5 ] 実装

文字列の日時で、範囲を切る

日時は文字列で持っています。"2026/08/09 14:23:01"、 バケットは "2026/08/09 14"収集の回で決めた形式です。

文字列で範囲比較ができるのは、ゼロ埋めの固定長だからです。

辞書順が、そのまま時系列順になる
"2026/07/09 23"  <  "2026/07/10 00"  <  "2026/08/09 14"
   ↑ 文字列として比較しても、時系列として正しい

# だから $lt で範囲削除できる
{ bucket: { $lt: "2026/07/10 00" } }
桁が揃わない形式では破綻します。 "2026/8/9" のようにゼロ埋めしないと、 "2026/8/9" > "2026/12/1" という比較になってしまう。 文字列で日時を持つなら、ゼロ埋めは必須条件です。

なお集計側は bucket、生ログ側は created_at と フィールド名も形式も違うので、対象ごとに指定できるようにしています(§4の④の表)。

[ § 6 ] 画面

ダッシュボードにも載せる

cron に任せきりだと、いつ何が消えたのか分からなくなります。 保守タブを作り、コレクションごとの状況を見えるようにしました。

表示項目意味
総数いま何件あるか
削除対象基準日時より古いもの
最古 / 最新実際に持っている期間
基準日時この時刻より前が消える

二段階の確認

押した瞬間には消えません。件数を見せてから、もう一度確認を求めます。 「3,442件を削除します。よろしいですか」と数字が出れば、 想定と違うときに気づけます。

GETでは消せない

メソッドで分ける
# 件数の表示 → GET
GET  /log-api.php?tab=retention&days=30

# 実際の削除 → POST のみ
POST /log-api.php   action=purge&days=30

GETで削除できると、URLを踏んだだけで消えます。 ブラウザの先読み、リンクのプリフェッチ、クローラ—— 意図しない実行の経路はいくらでもあります。

[ § 7 ] 体感デモ

保持日数を、動かしてみる

スライダーを動かすと、何がどれだけ消えるか何が失われるかが同時に見えます。

Interactive · retention simulator
削除シミュレーター
件数は例です(1日あたりの発生量から算出、90日ぶん蓄積した想定)。実際の値はサイト規模で変わります。
保持日数 30日

短くするほど容量は減りますが、失われる機能も増えます。 7日を切るとベースラインが取れなくなり、3日未満では安全装置が作動して実行できません。 容量と機能のどちらを取るかではなく、使う場面から決める—— §3で書いたとおりです。

[ § 8 ] まとめ

間違えたとき、どちらへ倒れるか

削除の仕組みを作って分かったのは、設計の良し悪しは失敗したときに出るということでした。

既定を確認にすれば、うっかり実行しても何も起きません。 対象を列挙すれば、追加を忘れても消えないだけで済みます。 間違えたときに、安全な側へ倒れるか—— 正しく動くことより、そちらを先に考えるべき仕組みでした。

この記事のまとめ

  • 消すデータより、消してはいけないデータを先に決める
  • 大きさではなく役割で判断する。読み取り位置は数レコードだが最重要。
  • 保持期間は容量ではなく使う場面から逆算。うちは30日になった。
  • 安全装置は4つ:既定は確認・最小日数・件数上限・対象の列挙
  • 「これ以外を消す」ではなく「これを消す」。追加を忘れても消えないほうへ倒す。
  • 文字列の日時で範囲を切れるのはゼロ埋め固定長だから。桁が揃わないと破綻する。
  • GETで削除できるようにしない。URLを踏んだだけで消える経路を作らない。

関連記事

『ログを消す設計 — 消してはいけないものを、先に決める』を公開しました。