0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PostgreSQLのWALを図解する

0
Posted at

この記事は riku-kobayashi.jpに掲載した記事の転載です。

PostgreSQLの運用でよくわからないまま触ってしまいがちな設定は、大抵WAL(Write-Ahead Log)がらみです。

この記事では、まずWALがそもそも何のための仕組みかを図解し、そのうえでAmazon Aurora PostgreSQLがWALをどう扱っているか、素のPostgreSQLとの違いを整理します。

前提: WALはログを先に書くための仕組み

WAL(Write-Ahead Log)を直訳すると「先に書いておくログ」です。

この名前がそのまま仕組みの本質を表していて、データそのものを書き換える前に、その変更内容をログとして先に書いておくというのがWALのやっていることです。

PostgreSQLのテーブルの実体は、ディスク上の8KBページの集まりです。

何も工夫しないなら、UPDATEやINSERTのたびにこのページをディスクへ書き直せば永続性は確保できます。

ただしこれは困った方式です。

トランザクションは普通、テーブルの中のあちこちのページに散らばって書き込みを行います。

コミットのたびに関係する全ページをその場でディスクに書き戻すとなると、ランダムI/O1が大量に発生します。

ディスクにとってランダムI/Oは重い処理です。

WALはこの問題を、データページ自体を書く代わりに、何を変更したかというログレコードだけを先に書くという発想で回避します。

コミット時に本当に待つのは、WALレコードのfsync2だけです。

WALファイルへの書き込みは末尾への追記なので、順序どおりに積むだけのシーケンシャルI/Oで済みます。

一方、実際に変更されたデータページは共有バッファ上にとどまり続け、ディスクのデータファイルへはすぐに書き戻されません。

同じページに対する複数回の更新をメモリ上でまとめてから、あとでまとめて1回だけ書き込めるので、結果的にディスクへの書き込み回数自体も減ります。

この、コミットの確定に必要なI/Oと実データの反映を分離する発想が、WALのすべての基本です。

チェックポイントとクラッシュリカバリ

共有バッファ上の変更済みページ(ダーティページ)は、いつかはデータファイルに反映しないといけません。

これを行うのがチェックポイントです。

チェックポイントはcheckpoint_timeout(デフォルト5分)またはmax_wal_size(デフォルト1GB分のWAL)のどちらかの条件に達したタイミングで走り、その時点のダーティページを一括でデータファイルに書き込みます。

同時に、そこまでのダーティページが確実にデータファイルへ反映済みであることを示す位置(REDOポイント3)をWAL上に記録します。

このおかげで、クラッシュリカバリの手順は単純になります。

PostgreSQLが再起動すると、最後のチェックポイントのREDOポイントを探し、そこから先のWALレコードを順番に再適用していきます。

クラッシュ直前の状態は、チェックポイント時点のデータファイルとそのあとのWALの再生を組み合わせれば必ず復元できます。

再生の起点は直前のチェックポイントではなく、そのチェックポイントが記録したREDOポイントです。

図の通り、REDOポイントからクラッシュまでの間に書かれたWALレコードだけが再生対象になります。

ここで再生されるWALレコードの中身は、基本的にこのページのこのバイトをこう変えた、という差分だけです。

ただし常に差分だけで済むわけではなく、full_page_writesという設定が絡む例外があります。

なぜfull_page_writesが必要なのか

8KBのページをディスクに書いている途中で(ハードウェア障害やOSのクラッシュで)電源が落ちると、ページの一部だけが書き変わった破損ページが残る可能性があります。

差分だけでは、この破損したページを正しい状態に戻せません。

そこでPostgreSQLは、チェックポイント後にそのページへ初めて変更が入ったとき、差分ではなくページ全体のイメージをWALに書き込みます(full_page_writes、デフォルトで有効)。

これがあれば、破損したページはそのまま丸ごと上書きして復元できます。

チェックポイント直後にWALの書き込み量が跳ね上がるのは、このフルページ書き込みが理由です。

WALのもう一つの役目: レプリケーションとPITR

WALはクラッシュリカバリのためだけの仕組みではありません。

このデータベースに何が起きたかを時系列で全部記録したログという性質上、他の用途にも使い回せます。

  • ストリーミングレプリケーション: スタンバイ側がプライマリのWALレコードを受け取り、自分でも同じように再生することで、プライマリと同じ状態を追いかけ続けます
  • PITR(Point-in-Time Recovery): ベースバックアップ + アーカイブしたWALを組み合わせれば、あの時点までの状態を復元できます
  • 論理レプリケーション: wal_level = logicalにすると、WALからどのテーブルの行がどう変わったかまで復元できる形の情報を残すようになり、wal2jsonのようなCDC(Change Data Capture)ツールや論理レプリケーションで使えます

これらはどれも、WALファイルが役目を終えるまで(レプリカに送るまで、アーカイブするまで)ディスク上に残り続ける必要がある、という共通点を持ちます。

レプリケーションスロットを使うと、接続が切れたレプリカのために必要なWALをPostgreSQLが自動で保持し続けますが、そのレプリカが二度と復帰しないと、WALが溜まり続けてディスクを圧迫します。

運用で一番よく踏む地雷はここです。

Auroraでは何が変わるのか

ここまでの話は、RDS for PostgreSQL(EC2で自前運用するPostgreSQLとほぼ同じ構造で、EBS上にデータファイルとWALを置く)にはそのまま当てはまります。

ところがAmazon Aurora PostgreSQLは、同じWALレコードのフォーマットを使いながら、そこから先の扱いを根本的に変えています。

普通のPostgreSQL(RDS for PostgreSQL含む)は、WALを書く役目とデータファイルを書く役目を両方とも自分(コンピュートインスタンス)で抱えています。

Auroraはこれを分離し、データファイルを書くという概念自体をコンピュート側からなくしました。

Writerインスタンスは、変更されたデータページそのものをネットワーク越しに送ることをしません。

送るのはredoログレコードだけで、それを6つのストレージノード(3つのアベイラビリティゾーンに2つずつ)へ送ります。

書き込みは6つ中4つのノードが永続化を確認した時点で成功とみなされ(4/6のクォーラム4)、読み取りは6つ中3つのノードの合意で成立します。

実際にデータページへログを反映してディスク上に実体化するのは、コンピュート側ではなくストレージノード側の役目です。

つまりAuroraでは、コンピュート層がダーティページをまとめてディスクに書き戻すという意味でのチェックポイントが、実質的に発生しません5。

checkpoint_timeoutやmax_wal_sizeといったパラメータ自体は残っていて設定もできますが、素のPostgreSQLのようにディスクI/Oの発生量を左右するものではなくなっています。

この構造は、クラッシュリカバリとフェイルオーバーの体感速度にそのまま効いてきます。

  • クラッシュリカバリ: 素のPostgreSQLは再起動のたびに最後のチェックポイントからWALを順番に再生し直しますが、Auroraはストレージ側が並列かつ非同期でログ適用を進めているため、再起動直後からほぼ即座に使える状態になります
  • フェイルオーバー: 素のPostgreSQLのスタンバイ昇格は、プライマリのWALにどこまで追いついているか次第で時間が変わります。Auroraのリードレプリカはそもそも独立したWALストリームを再生しているのではなく、Writerと同じストレージボリュームを見ているだけなので、フェイルオーバーはそのリードレプリカをWriterとして紐づけ直すだけで完了し、典型的には数十秒で終わります

バックアップも同じ発想です。

素のPostgreSQL(RDS for PostgreSQL含む)は、ベースバックアップとアーカイブ済みWALを組み合わせてPITRを実現しますが、Auroraは継続的にストレージ層へ届くログストリームそのものが実質的にバックアップの元データになっていて、archive_commandのような仕組みを自分で組む必要がありません。

Aurora特有の制約

データページを送らずログだけ送るという構造は、WALファイルを直接前提にした機能に制約をもたらします。

物理的なpg_basebackupによるバックアップ、Auroraの外への物理ストリーミングレプリケーションは使えません。

一方で論理レプリケーション(wal_level = logical相当)は使えます。

Auroraではクラスターパラメータグループのrds.logical_replicationを有効にして再起動する形で設定します6。

これは、論理デコードが物理的なページ配送ではなくログレコードの中身(どの行がどう変わったか)を読む仕組みだからです。

Auroraに一切WALがないわけではなく、物理的な永続化とページ反映の経路がストレージ層に付け替わっている、と捉えるのが正確です。

まとめというより所感

WALがログを先に書けば、実データの反映はあとからまとめてやればいいという発想だとすれば、Auroraはその発想を一段深く押し進めて、ログさえ確実に残れば、ページの実体はストレージ層がいつでも作り直せるというところまで持っていった仕組みだと思います。

Aurora論文には「The log is the database」というそのものずばりの節があって、まさにこの発想を言い当てた表現だと思います。

だからこそAuroraはクラッシュリカバリやフェイルオーバーが速いわけですが、逆に言うと素のPostgreSQL向けのWALチューニングの勘所(チェックポイント間隔の調整、full_page_writes、wal_compressionあたり)は、Auroraにはそのまま持ち込めません。

RDS for PostgreSQLとAuroraを同じマネージドPostgreSQLとして一括りにチューニングしようとすると、この前提の違いでハマるんじゃないかと思います。

参照

  1. ディスク上の離れた場所へ次々アクセスする書き込み・読み込みのことです。対になる概念がシーケンシャルI/O(ディスク上で連続した位置に順番にアクセスする方式)で、こちらは大幅に高速です。HDDなら物理的にヘッドを動かすシーク時間がボトルネックになりますが、SSDでもランダムアクセスはシーケンシャルアクセスより遅い傾向があります。 ↩

  2. バッファ済みの書き込みを物理ストレージまで確実に反映させることをOSに要求するシステムコールです。通常のファイル書き込みは、呼び出しが返ってきた時点でもまだOSのページキャッシュ上にあるだけで、物理ディスクには届いていないことがあります。この状態で電源が落ちるとデータは消えます。fsyncの完了を待って初めて、そのデータは電源断があっても残ることが保証されます。 ↩

  3. PostgreSQLはWAL上の位置をLSN(Log Sequence Number)というバイトオフセットで管理していて、REDOポイントはチェックポイント開始時点のLSNを指します。クラッシュリカバリはここから先のWALレコードだけを再生します。 ↩

  4. Auroraは6つのストレージノードのうち4つの書き込み確認で成功とする4/6クォーラム、3つの読み取り合意で成立する3/6クォーラムを採用しています。3AZに2ノードずつ配置されているため、1つのAZが丸ごと落ちても書き込み・読み取りとも成立する組み合わせが残る設計です。 ↩

  5. 正確には、コンピュート側の共有バッファにもダーティページの概念自体は残っています。ただしバッファを追い出す際にディスクへ書き戻す処理が発生せず、ストレージ層側がログを適用してページを再現する形になっているため、素のPostgreSQLのようにチェックポイントで大量のI/Oが発生するという挙動にはなりません。 ↩

  6. rds.logical_replicationはAuroraのクラスターパラメータグループで設定するパラメータで、素のPostgreSQLのwal_level = logicalに相当します。反映には再起動が必要です。 ↩

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?