💻

WALについて

に公開

PostgreSQLなどのWAL(Write Ahead Log:ログ先行書き込み)に関して、なんとなくの知識だったところをきちんとまとめます。チェックポイントとの兼ね合いやバッファの管理なども見ていきます

WALについての公式ドキュメント
https://www.postgresql.jp/docs/8.0/wal.html

WALとは

Write Ahead Log の頭文字を取ったもので、データベースのにデータの情報を書き込む前に変更履歴をログとして残しておきましょうというものです。これをすることによって、DBがクラッシュした際にログを使用してDBを復旧出来るため、ACID特性であるAtomicity(原子性)やDurability(永続性)などが保証できます。

メリット

まず、なぜこのような仕組みをしているのかについて考え、それのメリットについても見てみます。

データの挿入や更新をしてみた時のことを考えます。基本的にはDBのディスク(HDDやSSDなど)に書き込んで永続的なものにしたいです。しかし、毎回ディスクに書き込んでいるとI/Oの回数が増えすぎてしまいます。するとコストがかかり過ぎたり、書き込みを待ってから次の処理をするしかなくなってしまいます。(Postgreの場合はスナップショットを使っているので検索などは書き込みと無関係に可能ではありますが、、)
それの対策のために、メモリに書き込むことを考えてみます。メモリ上であればデータの読み書きなどが高速などで、I/Oに関する問題は解決したように思えます。
しかし、ここで別の問題が考えられます。メモリは揮発性であり、クラッシュしたりするとデータが全て消えてしまいます。
それの対策として、WALという考えが出てきます。データを共有バッファ(メモリ上)に書き込む際に、ログファイル(ディスク)にもその変更を書き込みます

正確に言うと、共有バッファに書き込んだ直後に、その変更内容を示すログレコードがWALバッファ(メモリ上)に書き込まれ、トランザクションのコミットの際にWALバッファの内容のみがWALファイルに書き込みが行われます。共有バッファの内容はチェックポイントや、共有バッファに空きが必要になった際に行われるもので、トランザクションのコミットとは無関係に行われます
これがWAL:ログ先行書き込みの核となる部分です

これにより、ログの永続性が保証されます。また、このように共有バッファでのみ更新されたデータのことをダーティーページと言います。そして、チェックポイントが定期的に来て、ダーティーページ(共有バッファ)の情報を全てディスクに書き込みます。その際に不必要になるWALファイルを削除して一連の流れが完了します(一度ディスクに書き込めばメモリの内容は消えても問題ないのでWALファイルは削除されます(archive_modeoffの時))。

これによって、DBに書き込みされていない共有バッファ内のデータが消えても、WALファイルを使用することでその当時のダーティーページの状況が再現出来るので、それをDBに書き込めば(ロールフォワード)元のDBに当時のダーティーページの状況が反映されたDBが完成します

クラッシュリカバリ

サーバークラッシュ(電源断、OSパニック、PostgreSQLプロセスの異常終了など)により、メインメモリ(RAM)の内容は基本的に全て失われます。その際に、どのようにしてDBを復旧させるかについて考えてみます。
まず、サーバークラッシュの際に失われるデータについて考えてみましょう。RAMに保存されている情報としては、共有バッファ上のダーティページとWALバッファです。残っている情報としては、ディスクにある、クラッシュする前までのチェックポイント時のデータ情報(最新のものとは違う可能性があり、他のファイル群では最新のものを記録しているファイルもあるため、一貫性などは保証されない)と、WALファイル(通常コミットの度にディスクにフラッシュされているため、ある程度のデータがあると期待できる)、そして、最後のチェックポイントに関する情報やWALファイルの位置などを保持しているpg_controlというファイルとpg_xactというトランザクションのコミット状態を保存したファイルなどが残っています。
ではこれらの情報を使用して、DBを最新で一貫性のある状態に戻したいと思います

  1. 起動と障害検知
  2. Redoポイントの決定
  3. ロールフォワード(Redo処理)
  4. 未コミットのデータのロールバック

という5つのステップで進んでいく

  1. 起動と障害検知
  • まずPostgreSQLサーバーが起動します
  • その際に、コントロールファイルを読み込んで、前回のシャットダウンが正常でなかった(クラッシュした)ということを認識します
  • 自動的にリカバリモードに入ります
  1. Redoポイントの決定
  • コントロールファイルから、最後に完了したチェックポイントの位置を取得します。このチェックポイントが終了したタイミング以降がRedoの対象になります。これより以前のデータはディスクにフラッシュされているはずで、それ以降のデータは共有バッファ上にあったが消えてしまったはずなので、ここからのトランザクションが対象になります
  1. ロールフォワード(Redo処理)
  • 目的:クラッシュ前にコミットされたが、ディスクには書き込まれていないデータを書き込み、クラッシュ直前のコミット済みの状態にしたい
  • 処理内容:WALファイルのRedoポイント以降を順に読み進め、WALレコードに記録されている個々の変更操作をデータファイル上のページに適用します
  1. 未コミットのデータのロールバック
  • 目的: クラッシュ時点でまだコミットされていなかったトランザクションによる変更を無効化し、データベースの一貫性を保証する
  • 処理内容:PostgreのRedo処理では未コミットのデータの変更も一時的にデータファイルに書き込まれることがあります。しかし、pg_xactにより、未コミットということが分かり、MVCC(MultiVersion Concurrency Control:多版型同時実行制御)により、他のトランザクションから参照されない、不可視のような状態になります

pg_xactとは
MVCCについて

  • つまり、明示的にロールバックとして変更を取り消すのではなく、その変更は元々無かったかのように振る舞う、というわけです(ただし、2PCのPREPARE済みトランザクションなど一部例外的な処理では、リカバリ中に明示的なロールバックが行われることもあります。)
  • これらの不要になったデータ(未コミットのINSERTされた行など)はAUTO VACUUM処理によって自動的に回収されて、使用可能領域として再度利用されます

おまけ

先ほどはサーバーがクラッシュしてメモリ上のデータがなくなった時について考えましたが、ディスクが故障した時にどうするかについてです。
基本的にはディスクはバックアップを取っていて、既存のものが故障した際に再度そのデータを元のディスクに戻して使用できるようにする、などの対策をとっていますが、PostgreSQLの標準的なWALアーカイブでは、WALファイルが満たされると(ページが全て書き込まれた時)アーカイブ処理が行われます。そのため、最新のコミット情報を含む書き込み途中のWALファイルがディスク障害で失われた場合、その分のデータが失われる可能性があります。これを防ぐため、より即時性の高いWALの冗長化(例:RAID1によるWALディスクのミラーリング)や、富士通の二重化WALのような専用の仕組み、あるいはストリーミングレプリケーションのスタンバイサーバーへの同期的なWAL転送などが検討されています

二重化WALについての資料
https://www.fujitsu.com/jp/products/software/resources/feature-stories/postgres/201509wal/

まとめ

PITRやレプリケーションでの利用などについても直に書きたいと思います。

参考資料
https://ja.wikipedia.org/wiki/ログ先行書き込み

https://www.sraoss.co.jp/wp-content/uploads/files/event_seminar/material/2024/OSC_SRAOSS_Backup_20240301_v2.pdf

https://www.postgresql.jp/docs/13/storage-file-layout.html#:~:text=するサブディレクトリ-,pg_xact,-トランザクションのコミット

Discussion