2024-03-26概要MySQLを運用するうえで、データ復旧やポイントインタイムリカバリPITRには一貫性のあるバックアップとバイナリログ(以降binlog)が理解できていれば十分なので、Redoログを意識することは殆どありません。これはMySQLが現場で利用される際は、冗長化や負荷分散のために、レプリケーション構成とともに利用されることが大きな理由です。MySQLのレプリケーションは論理レプリケーションのみがサポートされており、デフォルトでは非同期レプリケーションな上に、ほとんどの場合レプリケーションのためのbinlogが、トランザクション処理中のロギングのボトルネックになるため、Redoログ関連のチューニングする必要も滅多にないためです。Logging(WAL)方式に従ってRedoログを出力しています。トランザクションのコミット時に更新結果をディスクに永続化しなくて良い方式。ディスクまで永続化していなくても、コミット済みのトランザクションによる更新は障害後に復旧できるようにRedoログが必要になる。この記事ではMySQLがどのようなRedoログを出力しているのか読み解き、Redoログから更新クエリを処理する際に、物理的にどういったデータを更新しているのかを調べた様子を紹介します。MySQLにおけるRedoログはInnoDBのRedoログMySQLはテーブルごとにストレージエンジンを選択することができ、ストレージエンジンがメモリやディスク上でのデータ管理の実装を担っています。そのため、MySQLのRedoログと言っても、統一したredoログはないんじゃないの?と思われた方もいるかも知れません。確かにMySQLではテーブル単位でストレージエンジンを選択することができ、ストレージエンジンごとにトランザクションをサポートしたり、していなかったりします。現実にMySQLを利用する方はデフォルトのInnoDBを選択していることでしょう。言うと、MySQLがサポートしているInnoDBとNDBストレージエンジン以外のストレージエンジンはトランザクショナルではありません。Clusterというクラスタ構成で利用するストレージエンジンのため、特殊な構成とします。8.0ではテーブル定義などのメタデータである辞書データがストレージエンジンごとのファイルではなく、InnoDBのシステムテーブルとして統一して管理されるようになりました。tablesという特殊なテーブルで管理され、ファイルシステム上ではmysql.ibdファイルに保存されます。Redoログであると言えます。Redoログは8.0.30以降はデフォルトの初期状態では#innodb_redoディレクトリ以下にinnodb_redo_log_capacityの設定値を32で割ったファイルサイズで以下のように32個作成されるものです。DictionaryがInnoDBで管理されるようになったことで、MySQLに対するメタデータの更新も含めた物理的な更新がInnoDBのRedoログに書かれることがわかりました。InnoDBのRedoログを読んでみましょう。OracleにおけるLogMinerやlogdumpのような仕組みはなく、MySQLには公式でRedoログをダンプするツールがありません。ないものは仕方ないので作りました。mysqlredoコマンドです。このコマンドはInnoDBのRedoログをダンプ出力するために筆者が開発しているクライアントツールです。binlogをダンプ出力するmysqlbinlogという公式クライアントツールの様に、redoログをダンプするのでmysqlredoと名付けました。起動時のクラッシュリカバリの実装を再利用して、クライアントツールとして再実装したものです。8.0.32でしか動作確認していませんが、ソースコードから他のクライアントツール同様の手順でビルドして利用することができます。詳しくはREADMEを参照してください。Number)とredoログ・ファイルを指定することでログレコードの中身をダンプします。--file-path=/path/to/#ib_redoNダンプ出力の結果とそこから分かることを後述しますが、その前に、次の章でRedoログのフォーマットと基本的な情報を説明します。RedoログレコードのフォーマットRedoログの構造は公式ドキュメント等で説明されていませんが、一定のフォーマットに従って出力されています。ここではほとんどのログレコードが共通して先頭に持っている3つのデータをRedoログヘッダーとします。ページ番号で構成されます。ログタイプはそのログレコードに格納されているデータ種別を示す値が格納されています。スペースIDはテーブルスペース(テーブル)、ページ番号はテーブル中の更新対象のページを示す値が格納されています。ページ番号をKeyにして更新情報を示しています。ヘッダーに続くペイロードにはオフセットなどのメタ情報のほか、物理的な更新データ(bit列)が格納されています。Redoログレコードのフォーマットイメージmysqlredoコマンドはこのヘッダー情報は必ず表示し、ペイロード部分は実データがなくても表示できる部分のみを出力しています。ログタイプログタイプはmtr_log_tというenumで定義されています。名前からある程度の操作が想像できますが、主要なものは以下です。byte分のデータ書き込みを示す(N=1,2,4,8)ログレコードの大半はMLOG_{N}BYTEなので、解析には骨が折れますが、これについては後述します。space_idスペースIDはテーブルに一意に割り当てられるIDです。ハードコードされているものも含めある程度割り当てられる範囲が決まっています。これはコンフィグにもよりますが、デフォルト設定だと以下です。Undoテーブルスペース(デフォルト2つ)また、MySQLからはinformation_schema.innodb_tablespacesテーブルで確認することが可能です。information_schema.innodb_tablespacesテーブルで確認するテーブルスペース情報の例UPDATE文によるredoログを見てみる基本的な情報を抑えたので、実際の更新クエリによって生成されるRedoログを見ていきましょう。初期データ初期データは以下のように用意しました。開始点と終了地点となるLSNを確認しつつ、次のようにupdate文で更新します。先ほど確認したLSNを指定しつつ、redoログをダンプします。私の実験環境では以下のように実行しました。/mysql_remote/redo/#innodb_redo/#ib_redo18出力結果の全体はこちらにあります。出力結果を元に処理内容を分析するUPDATE文により生成されたログ範囲をまとめると以下のような出力が得られました。出力の見方を説明します。recv_multi_rec()または-recv_single_rec()から始まっている行はmysqlredoが補足しているメタデータです。Redoログはミニトランザクションという、トランザクション中の更新を一括で記録できる単位でログレコードをまとめて出力しています。MySQLでは、ミニトランザクション中で1行のログレコードだけを書くか、複数行を書くかで処理が分岐しているので、mysqlredoでもこの情報を出力するようにしています。recv_single_rec()で始まっているミニトランザクションは1行のログレコードのみが含まれているもの、-recv_multi_rec()は複数行を含んでいるものです。図5に示したダンプ結果では4つのミニトランザクションが出力されていることがわかります。各ログレコードをダンプ出力した結果はスペース2つのインデントを付けて出力されています。ログヘッダのログタイプ、スペースID、ページ番号に続いて、ペイロード部分が出力されています。数値)の文字列はこの説明用に後から追加したものです。この数値は後で説明する実行時のコールスタックと対応しています。次に各ミニトランザクションで出力されているログレコードを見てみます。ほとんどのログレコードがMLOG_(N)BYTESのログタイプで、特定のページのoffsetに何かしらの値を書いているということしかわかりませんが、よく見ると3つ目のミニトランザクションはスペースIDが2、つまりinnodb_tablespacesテーブルで確認した、t1テーブルへの書き込みで、それ以外はスペースIDが‭4294967279‬のUndoテーブルスペースへの書き込みになっていることがわかります(図4も参照)。最初の3つのミニトランザクションの最初のログレコードのログタイプは以下のようになっています。MLOG_REC_UPDATE_IN_PLACEこれとスペースIDを合わせると、なんとなくUPDATEによる更新が以下の流れで行われていることが想像できるかと思います。UNDO領域のヘッダーの作成UNDO領域へのレコードのINSERTt1テーブルへのIN_PLACE更新InnoDBが追記型ではなくIN_PLACE型でレコードを更新していることを知っていると大まかな更新の流れとしては想定通りの結果になりました。この出力だけでは具体的に何を更新しているのかを把握することはできません。ここからはmysqlredoの出力を手がかりにgdbを利用して各Redoログレコードの出力タイミングでどんな処理をしているのかを探っていきます。直接ログを出力するタイミングで止めてもよいですが、高速化のためか、実装上は複数の出力方法があるため、確実に1レコード書ききったタイミングを掴みたいと思います。周辺のコードを読んでみると、mtr_t構造体のadded_rec()メソッドを呼ぶことで、内部的に1行のRedoログレコードを出力したことをカウントしている処理が見つかります。このメソッドにブレークポイントを貼って、スタックトレースを取り、出力されたログレコードとマッピングしてみます。関数の呼び出しの関係を、関数が呼び出されるごとに1スペースのインデントをつけて表現しています。AUTOCOMMIT=ON(default設定)で単文のUPDATE文を実行したので、dispatch_sql_command()から処理が始まり、Sql_cmd_dml::execute()、続くSql_cmd_update::execute_inner()でUPDATE処理が走り、その後trans_commit_stmt()から始まるコミット処理が続いていることがわかります。関数名からもおおよその察しが着きますが、このスタックトレースの結果を頼りに、さらにログレコードを出力している処理の周辺を読むことで、ログレコード出力時の内部処理を理解していきます。UPDATEによる処理内容まとめスタックトレースを元にRedoログを出力したときの処理内容をまとめた表が以下です。listにundoのヘッダをノードと見立ててキューに追加する。headerに削除マークを記述。各ミニトランザクション中の最初のログレコードのログタイプ名から推測できたとおりではありますが、より具体的に処理の内容を把握することができました。まとめると図6のような処理を行っています。3つ目で実際に更新対象のレコードをインプレースで更新しています(このログレコードのペイロードに更新後のレコードの物理的なデータ(bit列)が続いています)。listキューに追加しています。このようにして、分析したいクエリのRedoログの内容とそれに紐づいた処理のコールスタックを把握することができました。これらの処理を行っている構造体や初期化処理を読むことで、内部の各コンポーネントやファイル上のメタデータの保存位置まで把握することができます。COMMITによってcommit処理も完了したはずの処理の中で、Commit完了を示すRedoログが出てこなかったことに気付いた方もいるかも知れません。mysqlredoによるダンプ結果にそれらしいログタイプはありませんし、mtr_id_tのenumを見返してみてもMLOG_COMMITなどのそれらしいものが見当たりません。やってこのトランザクションがコミットしたことを示しているのでしょうか?ここでログレコードと紐づけたコールスタックを改めて眺めてみると、7番目のログはtrx_commit_low()の中、かつ、trx_undo_set_state_at_finish()という関数で呼ばれていることがわかります。finish."とあり、やはりトランザクションの終了を表していそうです。56に2という値を書いていることがわかります。trx_undo_set_state_at_finish()を少し読むと、この56というオフセットはTRX_UNDO_SEG_HDRの定数であり、2という値はTRX_UNDO_CACHEDという値であることがわかります。これらがコミットを表すかどうかは、この値を読み出してトランザクションを復元する時、つまり、クラッシュリカバリの処理中の実装を読む必要がありますが、筆者はたまたまmysqlredoを作る過程でリカバリ処理に目を通していたため、ある程度目星をつけて確認することができました。TRX_UNDO_PREPARED_IN_TCの4つ以外であれば、TRX_STATE_COMMITTED_IN_MEMORYに設定し、この後に実行されるtrx_recovery_rollbackスレッドによるtrx_rollback_or_clean_recovered()関数でステートがTRX_STATE_COMMITTED_IN_MEMORYのものはコミット済みとして扱っています。上記4つの場合もbinlogの状態と合わせてコミットとするケースもありますが、ここでは省略し、別の記事にしたいと思います。これで、今回実験したUPDATE文が7つ目のRedoログレコードで、undo領域中にstateを設定する書き込みをしたことが、このトランザクションのコミットを示していることがわかりました。まとめmysqlredoコマンドを使ってクエリ実行時に出力されるredoログを確認し、その内容を足がかりにMySQLがどのデータ構造にどういった値を書いているのかを調べる方法を紹介しました。DCLがどの様に記録されるのかを確認することもできます。今回はRedoログを起点にMySQLの内部に迫ってみました。サービスの開発でもよく使うMySQLの中身に興味を持ってくれる人が増えたら幸いです。この内容はMySQLアンカンファレンスでも発表した内容になっています。今回紹介した内容はかなりマニアックでしたが、MySQLに関する雑談や相談をしたい人と気軽につながるために開催しているイベントです。完全オンラインで毎月開催しているので、是非覗いてみてください。