Meta での MySQL Raft の構築とデプロイ
Meta では、世界最大規模の MySQL のデプロイメントを実行しています。 この展開により、メッセージング、広告、フィードなどの他の多くのサービスとともにソーシャル グラフが強化されます。 ここ数年、私たちは MySQL Raft を実装してきました。これは、レプリケートされたステート マシンを構築するために MySQL と統合された Raft コンセンサス エンジンです。 私たちは展開の大部分を MySQL Raft に移行しており、現在の MySQL 半同期データベースを MySQL Raft に完全に置き換える予定です。 このプロジェクトは、Meta での MySQL 導入に、より高い信頼性、証明可能な安全性、フェイルオーバー時間の大幅な改善、運用の簡素化など、同等または同等の書き込みパフォーマンスを備えた大きなメリットをもたらしました。
高可用性、フォールト トレランス、読み取りのスケーリングを可能にするために、Meta の MySQL データストアは、ペタバイト単位のデータを保持する数百万のシャードを備えた大規模なシャード化が行われ、地理的にレプリケートされたデプロイメントとなっています。 この展開には、複数の大陸にまたがる複数の地域とデータセンターで実行される数千台のマシンが含まれます。
以前は、レプリケーション ソリューションは MySQL 半同期 (semisync) レプリケーション プロトコルを使用していました。 これはデータ パスのみのプロトコルでした。 MySQL プライマリは、プライマリ リージョン内でプライマリの障害ドメインの外にある 2 つのログ専用レプリカ (ログテイラー) への半同期レプリケーションを使用します。 これら 2 つのログテイラーは、半同期 ACKer として機能します (ACK は、トランザクションがローカルに書き込まれたことをプライマリに通知するものです)。 これにより、データ パスの待機時間が非常に短く (ミリ秒未満) コミットできるようになり、書き込みの可用性と耐久性が高くなります。 通常の MySQL プライマリからレプリカへの非同期レプリケーションは、他のリージョンへの広範な分散のために使用されました。
コントロール プレーンの操作 (プロモーション、フェイルオーバー、メンバーシップの変更など) は、一連の Python デーモン (以下、自動化と呼びます) の責任となります。 自動化は、フェイルオーバーの場所にある新しい MySQL サーバーをプライマリとして昇格するために必要なオーケストレーションを実行します。 また、自動化により、以前のプライマリと残りのレプリカが新しいプライマリから複製されるよう指示されます。 メンバーシップの変更操作は、MySQL プール スキャナー (MPS) と呼ばれる別の自動化部分によって調整されます。 新しいメンバーを追加するには、MPS は新しいレプリカをプライマリにポイントし、それをサービス検出ストアに追加します。 フェイルオーバーはより複雑な操作であり、ログテイラー (半同期 ACKer) の末尾スレッドがシャットダウンされて、以前に停止したプライマリをフェンスします。
以前は、安全性を確保し、複雑なプロモーションおよびフェイルオーバー操作中のデータ損失を回避するために、いくつかの自動化デーモンとスクリプトは、ロック、オーケストレーション ステップ、フェンシング メカニズム、およびサービス検出システムである SMC を使用していました。 これは分散セットアップであり、これをアトミックに実行するのは困難でした。 パッチを適用する必要がある特殊なケースが増えるにつれて、自動化は時間の経過とともにより複雑になり、維持するのが難しくなりました。
私たちは全く異なるアプローチをとることにしました。 私たちは MySQL を強化し、それを真の分散システムにしました。 プロモーションやメンバーシップの変更などのコントロール プレーンの操作がほとんどの問題の引き金であることを認識し、コントロール プレーンとデータ プレーンの操作を同じレプリケートされたログの一部にしたいと考えました。 このために、私たちはよく理解されているコンセンサス プロトコル Raft を使用しました。 これは、メンバーシップとリーダーシップの信頼できる情報源がサーバー (mysqld) 内に移動したことも意味します。 これは、MySQL サーバーへのプロモーションやメンバーシップの変更全体で証明可能な正しさ (安全性) を可能にしたため、Raft を導入することの最大の貢献でした。
MySQL 用 Raft の実装は、Apache Kudu に基づいています。 MySQL とデプロイメントのニーズに合わせて大幅に強化しました。 私たちはこのフォークをオープンソース プロジェクト kuduraft として公開しました。
kuduraft に追加された主な機能の一部は次のとおりです。
また、Raft と連携するために、MySQL レプリケーションに比較的大きな変更を加える必要がありました。 このために、MyRaft と呼ばれる新しいクローズド ソース MySQL プラグインを作成しました。 MySQL はプラグイン API を介して MyRaft と接続します (同様の API が準同期にも使用されていました)。一方、MyRaft が MySQL サーバーと接続するための別の API (コールバック) を作成しました。
Raft リングは、異なるリージョンにある複数の MySQL インスタンス (図では 4 つ) で構成されます。 これらの領域間の通信ラウンドトリップ時間 (RTT) は 10 ~ 100 ミリ秒の範囲になります。 これらの MySQL のうちのいくつか (通常は 3 つ) はプライマリになることが許可されていましたが、残りは純粋なリードレプリカ (プライマリ非対応) になることのみが許可されていました。 Meta での MySQL のデプロイメントには、非常に低いレイテンシーのコミットに対する長年の要件もあります。 MySQL をストアとして使用するサービス (ソーシャル グラフなど) は、このような非常に高速な書き込みを必要とするか、そのように設計されています。
この要件を満たすために、FlexiRaft の構成ではリージョン内コミットのみを使用します (単一リージョン動的モード)。 これを有効にするには、各プライマリ対応リージョンに 2 つの追加のログテーラー (監視またはログ専用エンティティ) が必要になります。 書き込みのデータ クォーラムは 2/3 (1 つの MySQL + 2 つのログテーラーのうち 2 つの ACK) になります。 Raft は引き続き、すべてのエンティティ (プライマリ対応 MySQL 1 個 + ログテイラー 2 個) * 3 リージョン + (プライマリ非対応 MySQL) * 3 リージョン = 12 エンティティにわたってレプリケートされたログを管理および実行します。
Raft の役割: リーダーは、その名前が示すように、複製されたログの期間におけるリーダーです。 Raft のリーダーは MySQL のプライマリでもあり、クライアントの書き込みを受け入れるリーダーでもあります。 フォロワーはリングの投票メンバーであり、リーダーからメッセージ (AppendEntries) を受動的に受け取ります。 フォロワーは MySQL の観点からはレプリカであり、トランザクションをそのエンジンに適用します。 ユーザー接続からの直接書き込みは許可されません (read_only=1 が設定されています)。 学習者は、リングの非投票メンバーになります (例: 非プライマリ対応リージョンの 3 つの MySQL) (上記)。 これは、MySQL の観点からはレプリカになります。
MySQL はこれまでレプリケーションにバイナリ ログ形式を使用してきました。 この形式は MySQL のレプリケーションの中心となるものであり、私たちはこれを維持することにしました。 Raft の観点から見ると、バイナリ ログはレプリケートされたログになりました。 これは、kuduraft に対するログ抽象化の改善によって行われました。 MySQL トランザクションは、各トランザクションの開始と終了を持つ一連のイベント (Update Rows イベントなど) としてエンコードされます。 バイナリ ログには適切なヘッダーもあり、通常は終了イベント (Rotate イベント) で終わります。
MySQL が内部でログを管理する方法を調整する必要がありました。 プライマリでは、Raft はバイナリログに書き込みます。 これは標準の MySQL で発生することと何ら変わりません。 レプリカでは、Raft は標準 MySQL の別のリレー ログではなく、binlog に書き込みます。 これにより、Raft が考慮するログ ファイルの名前空間が 1 つだけになったため、Raft が簡素化されました。 フォロワーがリーダーに昇格した場合、ログの履歴にシームレスに戻って、遅れているメンバーにトランザクションを送信できます。 レプリカのアプライヤ スレッドはバイナリログからトランザクションを取得し、それらをエンジンに適用します。 このプロセス中に、新しいログ ファイルである適用ログが作成されます。 この適用ログはレプリカのクラッシュ回復において重要な役割を果たしますが、それ以外の場合はレプリケートされないログ ファイルです。
つまり、要約すると次のようになります。
標準の MySQL では次のようになります。
MySQL Raft の場合:
トランザクションはまずエンジンで準備されます。 これはユーザー接続のスレッドで発生します。 トランザクションを準備する行為には、ストレージ エンジン (InnoDB や MyRocks など) との対話が含まれ、トランザクションのメモリ内バイログ ペイロードが生成されます。 コミット時に、書き込みはグループ commit/owned_commit フローを通過します。 GTID が割り当てられ、Raft が OpId (term:index) をトランザクションに割り当てます。 この時点で、Raft はトランザクションを圧縮し、LogCache に保存し、トランザクション全体を binlog ファイルに書き込みます。 ACK を取得してコンセンサスに達するために、他のフォロワーへのトランザクションの送信を非同期的に開始します。
トランザクションの「コミット」状態にあるユーザー スレッドはブロックされ、Raft からのコンセンサスを待ちます。 ラフトが地域内投票 3 票のうち 2 票を獲得すると、コンセンサスコミットメントが達成されます。 Raft は、地域外のすべてのメンバーにもトランザクションを送信しますが、FlexiRaft と呼ばれるアルゴリズム (後述) のため、メンバーの投票は無視されます。 コンセンサスコミットでは、ユーザースレッドのブロックが解除され、トランザクションが続行されてエンジンにコミットされます。 エンジンのコミット後、書き込みクエリは終了し、クライアントに戻ります。 そのすぐ後に、Raft は下流のフォロワーにコミット マーカー (現在のコミットの OpId) を非同期で送信し、下流のフォロワーもデータベースにトランザクションを適用できるようになります。
Raft とシームレスに動作するようにクラッシュ回復に変更を加える必要がありました。 クラッシュはトランザクションの存続期間中いつでも発生する可能性があるため、プロトコルはメンバーの一貫性を確保する必要があります。 これをどのように機能させたかに関する重要な洞察をいくつか紹介します。
フェイルオーバーと定期的なメンテナンス操作により、Raft のリーダーシップの変更が引き起こされる可能性があります。 リーダーが選出されると、MyRaft プラグインは付随する MySQL をプライマリ モードに移行しようとします。 このために、プラグインは一連の手順を調整します。 Raft → MySQL からのこれらのコールバックは、実行中のトランザクションを中止し、使用中の GTID をロールバックし、エンジン側のログを apply-log から binlog に移行し、最終的には適切な read_only 設定を設定します。 このメカニズムは複雑であり、現在オープンソース化されていません。
Raft 論文と Apache Kudu は単一のグローバル クォーラムのみをサポートしていたため、リングは大きくてもデータ パス クォーラムを小さくする必要があるメタではうまく機能しませんでした。
この問題を回避するために、私たちは Flexible Paxos からアイデアを借りて FlexiRaft を開発しました。
高いレベルでは、FlexiRaft を使用すると、Raft は異なるデータ コミット クォーラム (小さい) を持つことができますが、リーダー選挙クォーラム (大きい) には対応するヒットが発生します。 クォーラム交差の証明可能な保証に従うことにより、FlexiRaft は、Raft の最長ログ ルールと適切なクォーラム交差によって証明可能な安全性が保証されることを保証します。
FlexiRaft は単一領域動的モードをサポートします。 このモードでは、メンバーは地域ごとにグループ化されます。 Raft の現在の定足数は、現在のリーダーが誰であるかによって決まります (そのため、「単一リージョン動的」という名前が付けられています)。 データ定足数は、リーダーの地域の有権者の過半数です。 昇進中、期間が連続している場合、候補者は既知の最後のリーダーの地域と交差します。 FlexiRaft は、候補者のリージョンのクォーラムも確実に達成されるようにします。そうでないと、後続の No-Op メッセージがスタックする可能性があります。 まれに条件が連続していない場合、Flexi Raft は安全のために交差する必要があるリージョンの増加セットを見つけようとするか、最悪のケースでは Flexible Paxos の N リージョン交差ケースにフォールバックします。 。 事前選挙と模擬選挙のおかげで、任期の空白が生じることはほとんどありません。
プロモーションおよびメンバーシップ変更イベントをバイナリログにシリアル化するために、MySQL バイナリログ形式の Rotate Event および Metadata イベントをハイジャックしました。 これらのイベントは、Raft の No-Op メッセージおよびメンバー追加/メンバー削除操作と同等の処理を実行します。 Apache Kudu は共同コンセンサスをサポートしていないため、一度に 1 つずつのメンバーシップ変更のみが許可されています (暗黙的なクォーラム交差のルールに従うため、1 ラウンドで 1 つのエンティティのみによってメンバーシップを変更できます)。
MySQL Raft の実装により、MySQL デプロイメントに関する懸念を非常に明確に分離することができました。 MySQL サーバーは、Raft の複製されたステート マシンを介して安全性を確保します。 データ損失がないという保証は、おそらくサーバー自体に組み込まれるでしょう。 自動化 (Python スクリプト、デーモン) によってコントロール プレーンの操作が開始され、フリートの状態が監視されます。 また、メンテナンス中やホスト障害が検出された場合には、Raft を介してメンバーを置き換えたり、プロモーションを行ったりすることもあります。 場合によっては、自動化によって MySQL トポロジの地域的な配置が変更される場合もあります。 Raft に適応するために自動化を変更することは、数年にわたる開発と展開の取り組みに及ぶ大規模な作業でした。
長期にわたるメンテナンス イベント中に、自動化により Raft にリーダーシップ禁止情報が設定されます。 ラフトは、禁止されている団体がリーダーになることを認めないか、不注意で選出された場合には直ちに避難させることになる。 自動化により、これらの地域から他の地域への移動も促進されるでしょう。
Raft をフリートに展開することは、チームにとって大きな学びでした。 私たちは当初、MySQL 5.6 で Raft を開発していましたが、MySQL 8.0 に移行する必要がありました。
重要な学びの 1 つは、Raft を使用すると正確性を推論するのが簡単である一方で、Raft プロトコル自体は可用性の懸念にはあまり役に立たないということでした。 MySQL データ クォーラムが非常に小さかったため (リージョン内のメンバー 3 人に 2 人)、リージョン内の 2 つの不正なエンティティによってクォーラムがほぼ崩壊し、可用性が低下する可能性がありました。 MySQL フリートは毎日かなりの量のチャーン (メンテナンス、ホストの障害、操作の再調整などにより) が発生するため、メンバーシップの変更を迅速かつ正確に開始して実行することが、継続的な可用性の重要な要件でした。 ロールアウト作業の大部分は、Raft クォーラムが健全であるように、ログテイラーと MySQL の置き換えを迅速に実行することに焦点を当てていました。
可用性をより堅牢にするために、kuduraft を強化する必要がありました。 これらの改善はコア プロトコルの一部ではありませんでしたが、そのコア プロトコルへのエンジニアリング アドオンと考えることができます。 Kuduraft は事前選挙をサポートしていますが、事前選挙はフェイルオーバー中にのみ行われます。 リーダーシップの適切な移行中、指定された候補者は直接実際の選挙に移行し、任期が延期されます。 これはリーダーのスタックにつながります (kuduraft は自動ステップダウンを行いません)。 この問題に対処するために、模擬選挙機能を追加しました。これは事前選挙に似ていますが、指導者が適切に交代した場合にのみ行われます。 これは非同期操作であるため、プロモーションのダウンタイムは増加しませんでした。 模擬選挙であれば、実際の選挙では部分的に成功して行き詰まってしまうケースを排除できるだろう。
ビザンチン障害の処理: Raft のメンバーリストは、Raft 自体によって祝福されていると考えられます。 ただし、新しいメンバーのプロビジョニング中、または自動化の競合により、2 つの異なる Raft リングが交差するという奇妙なケースが発生する可能性があります。 これらのゾンビ メンバーシップ ノードは排除する必要があり、相互に通信できないようにする必要がありました。 このようなゾンビメンバーからのリングへのRPCをブロックする機能を実装しました。 これはある意味、ビザンチン俳優の扱いだった。 私たちのデプロイメントで発生したこれらのまれなインシデントに気づいた後、Raft の実装を強化しました。
MySQL Raft の立ち上げ時の目標の 1 つは、エンジニアが問題の根本原因を突き止めて軽減できるように、オンコールの運用の複雑さを軽減することでした。 Raft を監視するために、いくつかのダッシュボード、CLI ツール、およびスキューバ テーブルを構築しました。 特にプロモーションやメンバーシップ変更の領域に関して、大量のログを MySQL に追加しました。 リング上のクォーラムおよび投票レポート用の CLI を作成しました。これは、リングが利用できない (クォーラムが壊れた) ときとその理由を迅速に特定するのに役立ちます。 ツールと自動化インフラストラクチャへの投資は密接に関係しており、サーバーの変更よりも大きな投資となった可能性があります。 この投資は大きな成果を上げ、運用とオンボーディングの苦痛を軽減しました。
これは望ましくありませんが、クォーラムが時々崩壊し、可用性の損失につながります。 典型的なケースは、自動化がリング内の異常なインスタンス/ログテーラーを検出せず、それらをすぐに置き換えない場合です。 これは、検出不良、ワーカー キューの過負荷、または予備のホスト容量の不足が原因で発生する可能性があります。 クォーラム内の複数のエンティティが同時にダウンする相関障害は、それほど一般的ではありません。 デプロイメントでは、適切な配置決定を通じてクォーラムの重要なエンティティ全体で障害ドメインを分離しようとするため、このようなことは頻繁に発生しません。 簡単に言うと、大規模になると、既存の安全策にもかかわらず、予期せぬことが起こります。 本番環境でそのような状況を軽減するには、ツールを利用できる必要があります。 私たちはこれを見越して Quorum Fixer を構築しました。
Quorum Fixer は、Python で作成された手動修復ツールで、リング上の書き込みを抑制します。 帯域外チェックを実行して、最長のログ エンティティを特定します。 Raft 内のリーダー選挙の定足数の期待値を強制的に変更し、選択されたエンティティがリーダーになるようにします。 昇格が成功した後、クォーラムの期待値をリセットして戻すと、通常、リングは正常になります。
このツールを自動的に実行しないのは意識的な決定でした。なぜなら、クォーラム損失のすべてのケースの根本原因を特定して途中でバグを修正したいからです (自動化によって黙って修正されるのではありません)。
大規模なデプロイメントを介して半同期から MySQL Raft に移行するのは困難です。 このために、enable-raft というツール (Python で) を作成しました。 Enable-raft は、プラグインをロードし、各エンティティに適切な構成 (mysql sys-vars) を設定することにより、半同期から Raft への移行を調整します。 このプロセスでは、リングのわずかなダウンタイムが発生します。 このツールは時間をかけて堅牢に作られており、Raft を非常に迅速に大規模に展開できます。 Raft を安全に展開するためにこれを使用しました。
言うまでもなく、MySQL のコア レプリケーション パイプラインに変更を加えるのは非常に困難なプロジェクトです。 データの安全性が危機に瀕しているため、信頼性を得るにはテストが鍵でした。 私たちはプロジェクト中にシャドウ テストと障害挿入を大幅に活用しました。 RPM パッケージ マネージャーを展開するたびに、テスト リングに何千ものフェイルオーバーと選挙を注入します。 テストアセットの置換とメンバーシップの変更をトリガーして、重要なコードパスをトリガーします。
データの正確性チェックを伴う長時間にわたるテストも重要でした。 シャード上で毎晩実行される自動化があり、プライマリとレプリカの一貫性が確保されます。 このような不一致があれば警告を受け、デバッグします。
Raft の書き込みパス レイテンシーのパフォーマンスは、半同期と同等でした。 半同期機構はわずかに単純であるため、無駄が少ないことが期待されますが、半同期と同じレイテンシが得られるように Raft を最適化しました。 以前はサーバー バイナリの外にあった多くの責任を取り込んだにもかかわらず、フリートにこれ以上 CPU を追加しないように kuduraft を最適化しました。
Raft は、プロモーションとフェイルオーバー時間を桁違いに改善しました。 フリートのリーダーシップ変更の大部分を占めるグレースフル プロモーションが大幅に改善され、通常は 300 ミリ秒でプロモーションを完了できます。 半同期セットアップでは、サービス検出ストアが信頼できる情報源となるため、クライアントがプロモーションの終了に気づくまでの時間が大幅に長くなり、シャードでのエンド ユーザーのダウンタイムがさらに長くなる可能性があります。
Raft は通常、2 秒以内にフェイルオーバーを実行します。 これは、Raft の健全性を 500 ミリ秒ごとにハートビートし、3 回連続してハートビートが失敗したときに選挙を開始するためです。 半同期の世界では、このステップはオーケストレーションに負荷がかかり、20 ~ 40 秒かかりました。 これにより、Raft はフェイルオーバーの場合のダウンタイムを 10 倍改善しました。
Raft は、証明可能な安全性とシンプルさを提供することで、Meta での MySQL の運用管理に関する問題の解決に貢献してきました。 MySQL の一貫性を手動で管理できるようにし、まれな可用性の損失に対応するツールを用意するという私たちの目標は、ほぼ達成されています。 Raft により、MySQL を使用するサービスの提供強化に集中できるため、将来的に大きな機会が開かれます。 私たちのサービス所有者からの要望の 1 つは、構成可能な一貫性を持つことです。 構成可能な一貫性により、所有者はオンボーディング時に、サービスに X リージョンのクォーラムが必要か、それとも一部の特定の地域 (ヨーロッパや米国など) でコピーを要求するクォーラムが必要かを選択できます。 FlexiRaft は、このような構成可能なクォーラムをシームレスにサポートしており、将来的にはこのサポートの展開を開始する予定です。 このようなクォーラムは、それに応じてコミット レイテンシの増加につながりますが、ユース ケースでは一貫性とレイテンシの間でトレードオフを行うことができなければなりません (PACELC の定理など)。
プロキシ機能 (マルチホップ分散トポロジを使用してメッセージを送信する機能) により、Raft は大西洋全体のネットワーク帯域幅を節約することもできます。 Raft を使用して米国からヨーロッパに 1 回だけ複製し、その後 Raft のプロキシ機能を使用してヨーロッパ内で配布する予定です。 これによりレイテンシは増加しますが、レイテンシの大部分が大西洋横断転送にあり、余分なホップがはるかに短いことを考慮すると、名目程度です。
Meta のデータベース展開と分散コンセンサス空間におけるより投機的なアイデアのいくつかは、Epaxos のようなリーダーレス プロトコルの探索に関するものです。 当社の現在の展開とサービスは、強力なリーダー プロトコルに伴う前提に基づいて機能していますが、WAN での書き込み遅延がより均一になることでサービスが恩恵を受ける要件が少しずつ見え始めています。 私たちが検討しているもう 1 つのアイデアは、ログをステート マシン (データベース) から切り離して、細分化されたログ設定にすることです。 これにより、チームはログとレプリケーションの問題を、データベース ストレージや SQL 実行エンジンの問題とは別に管理できるようになります。
MySQL Raft をメタスケールで構築してデプロイするには、重要なチームワークと管理サポートが必要でした。 このプロジェクトの成功に貢献した次の方々に感謝いたします。 この旅の間、チームメンバーをサポートしてくれた Shrikanth Shankar 氏、Tobias Asplund 氏、Jim Carrig 氏、Affan Dar 氏、David Nagle 氏。 また、私たちを軌道に乗せてくれたこのプロジェクトの有能なプログラム マネージャーの Dan O 氏と Karthik Chidambaram 氏にも感謝します。
エンジニアリングの取り組みには、Vinaykumar Bhat、Xi Wang、Bartholomew Pelc、Chi Li、Yash Botadra、Alan Liang、Michael Percy、ヨシノリ マツノブ、Ritwik Yadav、Luqun Lou、Puschap Goyal、Anatoly Karp Igor を含む数人の現在および過去のチーム メンバーからの主要な貢献が含まれていました。ポズガイ。
前: