長期保管/ Long-term Storage
2 年を超えた月を、検証してから外へ移す
記録は 2 年保持です。それを超えた分をデータベースに置き続けるとテーブルが膨らみ続けるので、月ごとに外へ移しています。この章の主題は「移す」ことよりも、移し終わったことをどう確かめてから消すかです。消したあとに「移せていなかった」と分かっても、取り返す手段はありません。
2 年で線を引くTwo Years
画面から引ける範囲は 2 年です。切り離しの設定は 25 ヶ月で、1 ヶ月ぶん余裕を持たせています。
| 値 | 意味 |
|---|---|
| 2 年(24 ヶ月) | 「この期間は必ず辿れる」という業務上の約束 |
| 25 ヶ月 | データベースから切り離す設定値。約束の 1 ヶ月先 |
差を付けているのは、切り離しが週に 1 回しか動かないためです。24 ヶ月ちょうどで設定すると、約束の期間内の月が、次の整備が回るまでの数日のあいだ「切り離し待ち」に入る余地が生まれます。1 ヶ月の余裕があれば、その隙間は構造的に生じません。
子テーブルの作成と切り離しPremake & Detach
月ごとの子テーブルを作ること、そして古い月を切り離すことは、データベースの中の拡張機能が受け持っています。退避と削除を受け持つ月次のバッチとは、担い手が分かれています。
| 担い手 | 周期 | やること |
|---|---|---|
| データベース内の拡張機能 ( pg_partman + pg_cron) | 週次 | 数ヶ月先までの子テーブルを作る/25 ヶ月を超えた月を親から切り離し、退避待ちの置き場(別のスキーマ)へ移す |
| 月次のバッチ ( audit-log-partition-rotator) | 月次 | 先の月の残数を点検する/退避待ちを圧縮して外へ置く/突き合わせて台帳に記録する/消す/取り出し台帳を掃除する/退避済みの存在を確かめる |
この分担には意味があります。切り離す側は消しません。切り離しは「親から外して別のスキーマへ移す」までで、データはそのまま残ります。消すのは、退避が終わって突合まで通った後段のバッチだけです。つまり、退避される前にデータが消える経路が構造的に存在しません。
起動の時刻は、重ならないようにずらしています。週次の整備と日次の集計が同じ時刻に走るため、月次の退避はそれと重ならない時刻に置いています。
退避の 4 手順Four Passes
月次のバッチは 4 つの手順を順に回します。それぞれ独立に試し、途中で失敗したものは記録して次へ進み、最後にまとめて失敗として投げ直します。1 つの失敗で残りを止めると、直せる分まで止まってしまうためです。
① で「テーブル名を解析しない」のは、命名の規則が拡張機能の版によって変わった実績があるためです。判定はすべて区切りの値(この子テーブルはいつからいつまでを受け持つか)で行っています。② で「そのスキーマに在ること自体が合図」なのも同じ考えで、名前を当てにしないための設計です。
検証してから消すVerify Then Drop
この章でいちばん気を配ったところです。消す判断の材料を 2 系統持っています。
| 確かめること | 方法 | これが守るもの |
|---|---|---|
| 行が全部出たか | 取り出し処理が返した件数と、テーブルを直接数えた件数を突き合わせる | 途中で切れた取り出しを「完了」と誤認しない |
| 壊れずに届いたか | 保存後のオブジェクトから検査値(crc32c)と容量を取得し、台帳に残す | 転送中に壊れたファイルを「保管済み」と誤認しない |
どちらかが合わなければ、そのテーブルは消しません。台帳に失敗として残し、次のテーブルへ進みます。台帳は同じ名前への上書き追加なので、翌月の実行で自動的に再試行されます(失敗のまま残り続けることはありません)。
記録の無い削除経路を作らない
退避待ちのテーブルが0 行だった場合、期間(いつからいつまでの分か)を導き出せません。この場合は触らず、警告だけを出して次へ進みます。期間を適当に埋めた台帳の行を作って消す、という選択はしていません。
月に 1 件も監査の記録が無いというのは現実にはほぼ起きませんが、そのために自動の掃除を作るよりも、痕跡を残して人が判断するほうを選びました。結果として、コードのどこにも「台帳に記録を残さずにテーブルを消す」経路が存在しません。
受け皿の子テーブルに誤って行が入っていないかの点検は、実際に数える方式にしています。データベースが持っている推定値(統計情報)を使う実装から切り替えました。推定値は、集計がまだ走っていない間は「不明」を表す値になっていて1 度も鳴らず、集計後も少数の流入は 0 のまま見逃し、古い値のままだと逆にありもしない誤配を報告します。切り替え前の完了ログには、この観測値がそもそも 1 つも出ていませんでした(鳴らない警報がログにも現れていなかった)。
退避分の取り出しRetrieval
外へ出た月を調べたいときは、15 分だけ有効な URL を都度発行します。復元の手続きは要りません。
置き場のクラスは Archive です。保管の料金がいちばん安い代わりに、最低 365 日は置いた扱いで課金されます(それより早く消すと 1 年分かかる)。25 ヶ月を超えたデータは、どう考えても 1 年以上は残すものなので、この選択で困りません。
重要なのは、このクラスが読み出しに待ち時間を要求しないことです。冷たい保管というと「取り出しの申請をして数時間待つ」形を想像しますが、このクラスは普通のファイルと同じようにミリ秒で読めます。そのため、復元の機構を持っていません。クラスを一時的に戻すような処理は、コードのどこにも書いていません(書ける経路を作らない、という形にしてあります)。
| 項目 | 内容 |
|---|---|
| 対象の確認 | 台帳に「退避済み」として在る月だけ。無ければ 404 |
| 理由 | 10〜256 文字を必須。記録の専用の欄に保存する |
| URL の寿命 | 15 分。ファイル名を指定した形で発行する |
| 記録 | 発行のたびに 1 行。どの月に対してかも残す |
| 回数の制限 | 掛けない(通常の取り出しと枠を共有すると片方が枯れる) |
失敗したときに起きることOn Failure
月次のバッチが失敗すると、重大なアラートが上がります。起票は全バッチ共通のヘルパ 1 本が唯一の実装で、バッチごとに独自の書き方を作りません。
| 失敗の種類 | そのとき何が起きるか |
|---|---|
| 先の月が足りない | 処理を止め、重大なアラート。自動で作り直さない(週次の整備そのものが動いていないことを意味するので、機械が繰り返すより人が原因を見るべき状態) |
| 取り出しや保存に失敗 | そのテーブルを台帳に失敗として記録し、消さずに残す。ほかのテーブルの処理は続ける。最後にまとめて失敗として投げ直す |
| 突合が合わない | 同上。翌月の実行で同じ名前に上書きして再試行される |
| 退避済みの実物が無い | 処理を止めて失敗させる。台帳が「保管済み」と言っているのに実物が欠けている状態を、気づかないまま続けさせない |
| 退避待ちが 0 行 | 触らず警告だけを出して次へ進む(期間を導けないため、台帳の行を作れない) |
| 想定外の名前のテーブルがある | 触らずエラーとして記録する。名前を判定に使わない原則の唯一の例外で、「触らない」方向にだけ使う |
置き場所と寿命Storage & Lifecycle
使うバケットは 監査エクスポートバケット の 1 つだけで、用途をプレフィクス(置き場所の接頭辞)で分けています。役割が違うので、クラスも寿命も別に設定しています。
| プレフィクス | 中身 | クラス | 寿命 |
|---|---|---|---|
exports/ | CSV の取り出しの結果(依頼ごとの一時的な出力) | Standard (個別に指定) | 7 日で自動削除 |
audit-archive/ | 2 年を超えて外へ出た月(YYYY-MM.csv.gz) | Archive (バケットの既定) | 設定なし = 無期限 |
バケットの既定は Archive にしてあり、取り出しの一時出力だけを書き込むときに個別で Standard に指定しています。あとからクラスを付け替える処理(書き直しが発生する)は使っていません。7 日で消えるものに Archive の最低保管期間を適用すると、消しても 1 年分の料金が発生してしまうためです。
取り出しの依頼の台帳は 90 日で消えます。オブジェクト(7 日)より台帳(90 日)のほうが長いので、8 日目から 90 日目までは「依頼の記録はあるが、実物は消えている」状態になります。この期間の依頼に対しては、窓口が期限切れと判定してダウンロード用の URL を発行しません。押せるのに落ちてこない URL を出さないためです。
audit-archive/ には寿命の設定を入れていません。「2 年を超えた記録を、さらに何年で消すか」は法務・税務の要件と併せて決めるべき事項で、技術側で先に決めてしまわないほうがよい、という判断です。台帳(audit_partition_archives)には期間・行数・検査値・容量が残っているので、方針が決まった時点でその台帳を元に消せます。