|
MySQL Binlogは、構造化クエリ言語(SQL)ステートメントを用いたデータベースに対するユーザー操作を記録するために使用されます。これはMySQLデータベースのバイナリログであり、その内容は「mysqlbin」コマンドを使用して表示できます。iQiyiは、会員制注文システムで注文イベントドリブン操作を実装するためにMySQL Binlogを使用しています。Binlogを使用することで、システム設計が簡素化され、可用性とデータの一貫性が向上しました。 この記事では、MySQLの関連する技術原則を実用的な観点から解説し、技術原則と実際の業務経験を組み合わせ、読者の皆様が関連する設計における潜在的な問題を理解できるよう支援します。この記事が皆様のお役に立ち、刺激となり、共に前進できることを願っています。 著者について: Fan Shuは現在、iQiyiの会員制トランザクションシステムの技術およびアーキテクチャ開発を主に担当しており、非同期プログラミング、サービスガバナンス、コードリファクタリングなどの分野に重点を置いています。彼はテクノロジーに情熱を注ぎ、知識を共有することを楽しんでいます。 BinlogはMySQLにおいて重要なログであり、主にMySQLのマスターデータベースとスレーブデータベース間のデータ同期とレプリケーションに使用されます。この機能により、Binlogは他の種類のデータベースとのデータ同期や、ビジネスプロセスのイベント駆動設計にも使用されます。調査と分析の結果、MySQL Binlogを使用したイベント駆動設計の実装は見た目ほど簡単ではないことがわかりました。そこで、この記事ではMySQLのBinlog、Redo Log、および内部データ更新プロセスについて解説します。これらの技術の背後にある原理を説明することで、ビジネスプロセスに発生する可能性のある問題とその回避方法を分析します。この記事がMySQLの原理を理解し、この人気のデータベース技術をよりスムーズに使用できるようになることを願っています。 まず、会員注文システムの設計についてご紹介します。注文システムは、メッセージキュー(MQ)に直接メッセージを送信し、非同期メッセージを使用して後続のビジネスプロセスを駆動することで、メッセージ駆動型設計を実現しています。大まかなビジネスプロセス図を以下に示します。 図1: メッセージを直接送信する注文イベント駆動型システム
この設計では、データベース操作とメッセージ操作の間でデータの一貫性を確保する必要があります。つまり、データの保存とメッセージの送信は両方とも成功するか、両方とも失敗するかのどちらかです。明らかに、データ保存前やトランザクション内でメッセージを送信することは不適切です。データ更新操作の後、データベーストランザクションの外でメッセージを送信します。データの保存は成功したもののメッセージの送信に失敗した場合、決済システムは通知が成功するまで再通知(上図のステップ1)を行う必要があります。 この設計は基本的な機能要件と可用性要件を満たしていますが、以下の欠点があります。 1. ビジネスシステムはメッセージミドルウェアに直接依存します。 メッセージ ミドルウェアの障害は、支払い通知の処理だけでなく、ビジネス システム上の他のインターフェイスにも影響を及ぼす可能性があります。 2. ビジネスシステムは信頼性の高い再試行を実装する必要があります。 ベストエフォート通知の目標を達成するには、リクエストの開始者と受信者の両方が信頼性の高い再試行を実装する必要があります。 3. 再試行間隔が長くなると、サービスに遅延が発生します。 再試行回数が増えると、通常、各取得間隔は長くなります。これは指数バックオフと呼ばれる現象です。この設計により、リクエストレシーバーは障害をよりスムーズに処理できるようになり、頻繁な再試行によるサービス復旧の困難を回避できます。しかし、サービス復旧後、リクエストレシーバーがバックログメッセージを処理するのに長い時間がかかり、ビジネスレイテンシーが発生する可能性があります。Hystrixのような適応型設計を採用し、リクエストレシーバーサービスが復旧した後に通常のリクエストレートに戻すことも可能です。しかし、そのような設計は明らかにはるかに複雑になります。 上記の課題に対処し、技術アーキテクチャを簡素化するために、注文テーブルをイベントテーブルとして使用するイベントテーブル設計を採用しました。注文テーブルのBinlogをサブスクライブすることで、注文イベントが生成され、後続のビジネスプロセスが駆動されます。システムアーキテクチャの観点から見ると、ビジネスシステムはメッセージブローカーに直接依存する必要がなく、データベース操作のみに集中できます。Binlogを受信する別のシステムを導入することで、MySQLデータの変更がビジネスイベントに変換され、後続のプロセスが駆動されます。具体的なプロセスは以下のとおりです。 図2: Binlogベースのイベント駆動型注文 前述の通り、Binlogベースの注文イベント駆動型設計には多くの利点がありますが、後に隠れた問題が潜んでいることが判明しました。実験の結果、注文処理に遅延が発生することが時々あることが判明しました。 通常の処理では、注文の決済イベントを受信後、注文処理サービスは注文ステータスを確認します。注文ステータスが既に決済済みの場合、処理プロセスが開始されます。しかし、処理遅延が発生している注文の場合、注文処理サービスは決済イベントを受信後、データベースにクエリを実行し、注文が決済済みステータスではないことを確認します。調査の結果、データの同時実行による上書きの問題は排除され、注文ステータスのクエリはマスターデータベースで実行されるため、マスターとスレーブ間の同期遅延の問題は発生しません。 ビジネス システムが Binlog から生成された注文支払いイベントを受信し、その後メイン データベースを照会して注文データが未払い状態であることを確認する原因は何でしょうか? この問題の原因については今は脇に置いて、まずデータを更新する際の MySQL の内部動作を見てみましょう。 このセクションでは、MySQL データ更新の原則と、このプロセスで最も重要な 2 つのログである Redo Log と Binlog について説明します。 まず、Redo ログとバイナリ ログ (Binlog) について説明します。 • Redoログ: Redoログは、InnoDBストレージエンジンが提供する物理的なログ構造です。基盤となるデータページに対する操作の詳細が記述されており、主にクラッシュセーフな操作を実現し、ディスク操作の効率を向上させるために使用されます。 • Binlog : Binlogは、特定のストレージエンジンに依存しない、MySQL自体が提供する論理ログです。データベースによって実行されたSQL文やデータの変更が記録され、主にデータレプリケーションに使用されます。InnoDBは、クラッシュ安全性を実現し、データ更新の効率を向上させるために、Redoログを導入しています。InnoDBがすべてのデータ書き込み操作をディスク上のデータページに直接保存すると、ランダムディスクI/O操作の回数が大幅に増加します。Redoログにより、一部のランダムI/O操作はシーケンシャル書き込みに変換されます。シーケンシャルディスクI/OはランダムI/Oよりもはるかに効率的であるため、Redoログメカニズムはデータ更新時のパフォーマンス向上に役立ちます(クラッシュ安全性の実現方法については、次のセクションで説明します)。以下の表は、2 種類のログの目的とその違いを示しています。
| 再実行ログ | バイナリログ | ログの種類 | 物理ログ、つまりデータ ページ内の実際のバイナリ データは、回復速度が高速です。 | 論理ログ、SQL ステートメント、または論理データの変更 (行) により、回復速度が低下します。 | 保存形式 | InnoDB データページ形式に基づくストレージ | SQL文またはデータ変更内容 | 使用 | データページをやり直す | データ複製 | 階層 | InnoDB ストレージエンジン層 | MySQLサーバー層 | 記録方法 | 循環書き込み | 追加の執筆 | ここで疑問が生じます。MySQLには現在、Redo LogとBinlogという2つのログ構造があります。構造と機能は異なりますが、記録するデータは同じです。この2つのログデータの一貫性とクラッシュ安全性をどのように確保すればよいのでしょうか?これが2フェーズコミット設計の考え方につながります。 >>>> 2段階の提出2フェーズコミットは、RedoログやInnoDBの設計上の特徴ではなく、MySQLサーバの設計上の特徴です(ただし、通常はRedoログと併せて議論されます)。MySQLはプラガブルなストレージエンジン設計を採用しているため、トランザクションのコミット時にはサーバ自体とストレージエンジンの両方がデータをコミットする必要があります。そのため、MySQLサーバの観点から見ると、分散トランザクションの問題が本質的に存在します。 この問題に対処するため、 MySQLは2フェーズコミットを導入しました。2フェーズコミットでは、REDOログに対して「準備」と「コミット」という2つの処理が実行されます。バイナリログへの書き込み処理は、REDOログの準備処理とコミット処理の間に挟まれます。2フェーズコミット設計が、様々な障害シナリオにおいてどのようにデータの一貫性を確保するのか、以下に考察してみましょう。 1. Redo Log Prepare は成功するが、Binlog への書き込み前にクラッシュする:障害からの回復後、トランザクションはロールバックされます。この場合、Redo Log と Binlog の内容は整合性を保ちます。この状況は比較的単純です。より複雑な状況は次のようになります。Binlog への書き込みと Redo Log のコミットの間に発生したクラッシュを MySQL はどのように処理するのでしょうか? 2. Binlog に書き込んだ後、Redo Log がコミットされる前にクラッシュします。- Redoログにコミットフラグが付いている場合、Redoログは実際に正常にコミットされたことを意味します。この場合は、トランザクションを直接コミットしてください。
- REDOログにコミットフラグが設定されていない場合、XID(トランザクションID)を使用して対応するバイナリログが照会され、ログの整合性がチェックされます。バイナリログが完了している場合はトランザクションがコミットされ、完了していない場合はロールバックされます。
Binlog が完了したかどうかをどのように判断するのでしょうか。簡単に言うと、ステートメント形式の Binlog の最後にコミットがあるか、行形式の Binlog に XID イベントがある場合、Binlog は完了です。 >>>> MySQLデータ更新プロセス次に、単純な更新ステートメント「update t set n = n + 1 where id = 2」を実行するときの MySQL エグゼキュータと InnoDB ストレージ エンジンのプロセスを見てみましょう (この例では単一の更新ステートメントのみを実行するため、それ自体がトランザクションです)。- エグゼキュータはまず、エンジンからID=2の行を取得します。IDは主キーであり、エンジンはツリー検索を用いてこの行を直接見つけます。ID=2の行を含むデータページが既にメモリ内に存在する場合は、エグゼキュータに直接返されます。そうでない場合は、まずディスクからメモリに読み込んでから返します。
- エグゼキュータはエンジンから行データを受け取り、その値に1を加算して(例えば、元の値がNであればN+1)、新しい行データを取得します。そして、エンジンインターフェースを呼び出して、この新しい行データを書き込みます。
- エンジンは新しいデータをメモリに更新します。次に、メモリデータページへの更新内容をREDOログバッファに記録します(REDOログバッファについてはここでは詳しく説明しません。REDOログに対する操作はファイルに直接書き込まれるのではなく、まずメモリに記録され、その後特定の時間にディスクに書き込まれるという点だけを覚えておいてください)。これでデータ更新操作は完了です。
- 次に、トランザクションのコミット操作が実行されます。トランザクションのコミット中、REDOログは「Prepared」とマークされます。通常、この時点でREDOログはバッファからディスクに書き込まれます(innodb_flush_log_at_trx_commitの値が1の場合、トランザクションがコミットされるたびにREDOログがディスクに書き込まれます)。その後、InnoDBは実行が完了し、トランザクションをコミットできることをエグゼキュータに通知します。
- 実行プログラムはこの操作の Binlog を生成し、Binlog をディスクに書き込みます。
- 実行プログラムはエンジンのコミット トランザクション インターフェイスを呼び出し、エンジンは書き込んだばかりの Redo ログをコミット ステータスに変更して更新を完了します。
この図は、更新ステートメントの実行中における MySQL エグゼキュータ、InnoDB、Binlog および Redo Log 間の相互作用を示しています (濃い緑色の背景は MySQL エグゼキュータによって処理される段階を表し、薄い緑色の背景は InnoDB によって処理される段階を表します)。 MySQLの原理に関する上記の説明から、Binlogへの書き込みはトランザクションのコミットフェーズで行われることがお分かりでしょう。しかし、MySQLではサーバー層とストレージエンジン層で異なるログ構造が導入されているため、2フェーズコミットが採用されています。Binlogへの書き込みは、ストレージエンジンが実際にトランザクションをコミットする前に行われます。つまり、Binlogを介してデータを同期するシステム(MySQLスレーブ、他のデータベース、または業務システム)では、理論的には、MySQLマスターよりも早く最新のコミットデータが反映される可能性があります。
したがって、上記の注文処理サービスが Binlog に基づいて注文支払いイベントを受信した後に、対応する注文が支払われていないことを検出した理由は、注文処理サービスがデータを照会していたときに、注文支払いデータの更新操作が MySQL 内でまだ完全にコミットされていなかったためであると考えられます。 この現象は、検証プログラムを開発することで再現できました。検証プログラムは、トランザクションがコミットされた後に完全なBinlogを受け取ると、MySQLマスターデータベース上の対応するレコードを再度クエリし、その結果からトランザクションがコミットされる前のデータの概要を取得します。 さらに、一部のピアからは、スレーブ データベースがマスター データベースよりも早くデータの送信を認識するという問題に遭遇したという報告もあります。 問題の根本原因を理解した後、どのように解決するかを検討する必要があります。現在、この問題を解決するには、再試行とBinlogデータを直接使用するという2つの方法があります。 再試行はシンプルで分かりやすいアプローチです。問題はトランザクションの前にバイナリログがコミットされていることに起因しているため、後でクエリを再試行すれば自然に問題は解決します。しかし実際には、再試行の実装方法、そして過度または無限の再試行がサービス停止につながるかどうかを検討する必要があります。再試行を実装するには、スレッドスリープやメッセージ再送信などの方法を使用できます。スレッドスリープは、スレッドの使用率を低下させ、サービスが応答しなくなる可能性もあるため、一般的には推奨されません。しかし、この問題が発生する可能性が低いことを考慮すると、スレッドスリープは使用可能であると考えています。この方法はシンプルで実装が容易であるため、迅速な問題解決に適しています。 2つ目の再試行方法は、メッセージの再送信です。例えば、RocketMQでは、Consumerが `ConsumeConcurrentlyStatus.RECONSUME_LATER` を返すことで、メッセージの再送信をトリガーできます。ただし、この再試行方法は1つ目の方法よりもコストが高く、再試行間隔も比較的長くなるため、時間的制約が厳しいビジネスでは大きな影響を及ぼします。したがって、この方法を採用するかどうかは、ビジネスと技術の両方の観点から検討する必要があります。 再試行方法の検討に加えて、状態遷移がA→B→Aという順序で進むABA問題も考慮する必要があります。業務システムは状態Bを期待していますが、実際には状態Bに到達できない可能性があります。したがって、再試行を使用してこの問題を解決する前に、業務システムがABA問題を抱えている可能性を排除する必要があります。状態ABA問題はステートマシンなどを用いて解決できますが、ここではこれ以上説明しません。 リトライ以外にも、Binlogを直接利用するというアプローチがあります。Binlog(行形式)はデータの変更を直接反映し、トランザクションコミットに関わるすべてのデータを記録するため、業務処理に直接活用できます。これにより、データベースのQPSも削減できます。新規に設計するシステムには、このアプローチが理想的です。しかし、既存のシステムでは大幅な変更が必要になる可能性があり、導入の是非はコストとメリットを比較検討する必要があります。 iQiyiのメンバーシップ開発チームでは、シニアJavaエンジニア/テクニカルエキスパートを募集しています。メンバーシップサービスはiQiyiのコアビジネスの一つであり、私たちはテクノロジーを通してこのコアビジネスに貢献し、汎用性が高く可用性の高いビジネスシステムを開発することに尽力しています。また、データベース、サービスガバナンス、メッセージキュー(MQ)などの技術に精通した人材も必要としています。ご興味のある方は、履歴書を[email protected]までお送りください(メールの件名に「メンバーシップ開発」と明記してください)。 |