クラッシュしても変更を失わないための記録
redoログは、InnoDBがクラッシュリカバリを実現するために持つ変更履歴です。INSERT・UPDATE・DELETEでバッファプール内のページを更新すると、そのページをディスクへ書き出す前に、まず「どのページのどの位置に何を書いたか」という変更内容をredoログに記録します。もしサーバーがクラッシュしても、再起動時にチェックポイントLSN(LSNはLog Sequence Numberの略で、redoログに記録された変更の順序を表す通し番号)以降のredoログをLSNの若い順に適用すれば、ディスクへ反映しきれていなかった変更を復元できます。
ここで自然にわく疑問は、「更新したページをそのままディスクへ書けばいいのでは」というものです。実は、それだと性能が持ちません。コミットの時点では、更新したページの多くはまだバッファプール内にあります。コミットのたびに関係するダーティページをすべてディスクへ書き出すと、そのつど大量のランダムI/Oが発生します。一方、redoログは追記型で、順番に書いていくシーケンシャルな書き込みで済みます。コミット時にはredoログだけをディスクへフラッシュし、実際のページは後からバックグラウンドでゆっくり書き出せます。こうすればコミットのレスポンスを保ちつつ、クラッシュ時にも変更を失いません。この「先にログ、後からページ」という考え方をWAL(Write-Ahead Logging)と呼びます。
ページ10のnameをAliceからBobへ更新し、コミットした直後は次の配置になります。バッファプール上のページ10はすでにBobですが、データファイル上の同じページはまだAliceです。ディスクへ先に乗っているのは、redoファイルへ追記した「ページ10のnameをBobに」という記録です。
矢印はコミット時にディスクへ書き出される経路です。データファイル側のページ10は、あとからフラッシュされるまでAliceのままです。クラッシュしても、redoの記録からバッファプール上のBobを復元できます。
ログファイルの構成
redoログは、ログバッファ(Log Buffer)というメモリ領域を経て、ディスク上のredoログファイルへ書き出されます。MySQL 8.0.30以降・8.4では、redoログはデータディレクトリ配下の#innodb_redoディレクトリに複数のファイルとして置かれ、InnoDBは既定でこれを32個のファイルに保とうとします。各ファイルのサイズは、innodb_redo_log_capacityで指定するredoログ全体の合計容量の32分の1で、実際に使われている「使用中(ordinary)」ファイルと、次に使う予定でファイル名に_tmpが付く「予備(spare)」ファイルの2種類があります。
書き込みはLSNの若い順にファイルをまたいで進み、複数のファイル全体を1つの大きなリングとして扱います。リングといっても、データページどうしをポインタでつなぐ方式ではありません。各ファイルが「次のファイル」への参照を中に持っているわけではなく、次に使うファイルは番号順で決まります。#ib_redo0の次は#ib_redo1、最後のファイルの次はまた#ib_redo0です。あるファイルがend_lsnで終わると、次のファイルはそのend_lsnから始まります。あるファイルの内容がすでにチェックポイントを通過し、データページへ反映済みとみなせるようになると、そのファイルは上書きして再利用できます。ファイル単位で使用中・予備を入れ替えられる構造のおかげで、innodb_redo_log_capacityを変更してもサーバーを再起動せずに容量をオンラインで変えられます。
容量の指定方法はバージョンで異なります。MySQL 8.0.30以降はinnodb_redo_log_capacityで合計容量(32ファイル分の合計サイズ)を指定し、既定は100MBです。この変数は実行中にも変更できます。それ以前はinnodb_log_file_size(既定48MB)とinnodb_log_files_in_group(既定2)でファイルサイズと個数を指定していましたが、MySQL 8.4では非推奨です。
LSNとチェックポイント
LSN(Log Sequence Number)は、redoログの論理的な位置を表す番号です。ログはLSNの小さい順に生成され、リカバリでもLSNの若い順に適用します。途中を飛ばすことはできません。
チェックポイントLSNは、「このLSN以前のredoログが記述する変更は、すでにディスク上のデータページへ反映されている」という境界を表します。チェックポイントより前のログ領域は上書きして再利用できます。チェックポイントを進めるには、そのLSNに対応する変更をすべてデータページへフラッシュしておく必要があります。バッファプールのリストフラッシュが、まさにこの目的で行われます。redoログの再利用できる範囲と、ダーティページをどれだけ書き出したかは、こうして連動しています。
複数ファイルを1つのリングとして見たときの配置は、次のようになります。図の各箱が#innodb_redo内の1ファイルです。実際は既定32個ですが、ここでは3個に省略しています。矢印はファイル内のポインタではなく、番号順の「次のファイル」です。#ib_redo0の末尾LSN=4000から#ib_redo1が始まり、#ib_redo2の書き込み先端LSN=7200の次は、一周して#ib_redo0へ戻ります。#ib_redo0はチェックポイントLSN=4000より前なので上書きしてよく、#ib_redo1と#ib_redo2はリカバリに必要なので残します。
チェックポイントLSNを進めるほど、再利用できる区間が広がります。進めるには、そのLSN以前の変更を持つダーティページを先にフラッシュしておく必要があります。
ログバッファ
ログバッファは、redoログファイルへ書き出す前にredoログレコードを一時的に置くメモリ領域です。コミットやページ更新で生成されるredoログは、まずここに書かれ、その後バックグラウンドのログライタースレッドがディスクへフラッシュします。多くの行を更新・挿入・削除するトランザクションほどredoログも増えますが、ログバッファを大きくしておくと、コミットまでディスクへの書き込みを待たずにバッファへ溜められ、ディスクI/Oの回数を減らせます。
サイズと配置
ログバッファのサイズはinnodb_log_buffer_sizeで指定します。MySQL 8.4では最小値が1MB、既定が64MBです。既定はバージョンで異なり、8.0では16MBでした。ログバッファはバッファプールとは別に確保され、redoログ専用に使われます。中身は論理的なLSNの範囲に対応して並び、ログライタースレッドはバッファ内のデータを順にディスクへ書き出します。書き出し済みの範囲を超えてログライターが先へ進むことはありません。
並行書き込みと順序保証
トランザクションの変更がコミットされると、その変更を記述したredoログが生成されます。書き込みは大まかに次の手順です。
- ログバッファ内に書き込む領域(バイト数)を予約する。予約によりstart_lsnからend_lsnの範囲が割り当てられる。
- 予約した範囲へredoログデータを書き込む。
- 書き込み後、「start_lsnからend_lsnまで書き込み済み」という情報をrecent writtenバッファに登録する。
複数スレッドが同時にログバッファへ書くと、書き終わる順序がLSNの並びと一致しないことがあります。たとえばスレッドAがLSN100〜150、スレッドBが150〜200を予約したとき、Bが先に書き終わってAがまだ途中、という状態が起こり得ます。ところがログライターは、リカバリで先頭から順に適用できるように、ディスクへLSNの若い順に書かなければなりません。100〜150がまだ埋まっていないのに150〜200だけをディスクへ書くことはできず、Aの完了を待つ必要があります。
recent writtenバッファは、各スレッドが「自分の範囲を書き終えた」ことを登録する補助構造です。ログライターは、登録されたリンク(start_lsn → end_lsn)の連鎖をたどることで「このLSNより前はすべてバッファに書き込み済み」と判断でき、その境界までをディスクへフラッシュします。リンクが途切れている箇所では該当スレッドの完了を待ちます。これで、複数スレッドの並行書き込みと、ログライターの順序保証が両立します。
フラッシュの制御
ログバッファをいつディスクへフラッシュするかは、innodb_flush_log_at_trx_commitで制御します。
| 値 | 動作 |
|---|---|
| 0 | 1秒に1回程度フラッシュする。コミット時にはフラッシュしない。クラッシュ時に直近1秒分のコミットが失われ得る |
| 1 | コミットのたびにフラッシュし、OSのfsyncまで完了させる。耐久性が最も高い |
| 2 | コミットのたびにOSのキャッシュへは書くが、fsyncは毎回は行わない。OSがクラッシュするとログが失われ得る |
耐久性と性能のトレードオフを、この設定で選べます。既定の1が最も安全です。
クラッシュリカバリ
クラッシュ後の再起動時、InnoDBはリカバリ処理を行います。チェックポイントLSNから、ディスク上のredoログに記録された末尾まで、LSNの順にredoログを適用します。適用とは、各ログレコードが記述する「ページのどの位置にどのバイト列を書くか」に従い、該当ページを読み込んでバッファプール上で変更を反映することです。反映されたページはダーティになり、その後のフラッシュでディスクへ書き出されます。こうして、未フラッシュだったダーティページの内容がredoログを通じて復元されます。
リカバリでは、まずチェックポイント以降のredoログをすべて適用します。ここにはクラッシュ時点で未コミットだったトランザクションの変更も含まれます。redoの適用が終わると、MySQLサーバーはアプリケーションなどからのクライアント接続をできるだけ早く受け付け始めます。未コミットだったトランザクションのロールバックは、undoログを使ってバックグラウンドスレッドが行い、新しいクライアントからのクエリ処理と並行して進むことがあります。redoは物理的な変更の再現、undoは論理的な取り消しという、役割の異なる2つを組み合わせて、クラッシュ直前の一貫した状態に戻します。