この記事では、Raft コンセンサス アルゴリズムのログから始めて、etcd の Raft の Raft ログ モジュールの設計と実装を紹介し、分析します。目標は、読者が etcd の Raft の実装をより深く理解し、同様のシナリオを実装するための可能なアプローチを提供することです。
Raft コンセンサス アルゴリズムは、本質的には 複製されたステート マシン であり、サーバー クラスター全体で同じ方法で一連のログを複製することを目的としています。これらのログにより、クラスター内のサーバーが一貫した状態に達することができます。
この文脈では、ログは Raft Log を指します。クラスター内の各ノードには独自の Raft ログがあり、一連のログ エントリで構成されます。通常、ログ エントリには次の 3 つのフィールドが含まれます:
Raft ログのインデックスは 1 で始まり、リーダー ノードのみが Raft ログを作成してフォロワー ノードに複製できることに注意することが重要です。
ログ エントリがクラスタ内の大部分のノード (例: 2/3、3/5、4/7) に永続的に保存されている場合、そのエントリは コミット済みとみなされます。
ログ エントリがステート マシンに適用されると、それは 適用されたとみなされます。
etcd raft は Go で書かれた Raft アルゴリズム ライブラリであり、etcd、Kubernetes、CockroachDB などのシステムで広く使用されています。
etcd raft の主な特徴は、Raft アルゴリズムのコア部分のみを実装していることです。ユーザーは、Raft プロセスに関係するネットワーク送信、ディスク ストレージ、その他のコンポーネントを自分で実装する必要があります (ただし、etcd はデフォルトの実装を提供します)。
etcd raft ライブラリとの対話はいくぶん簡単です。どのデータを永続化する必要があるか、どのメッセージを他のノードに送信する必要があるかがわかります。あなたの責任は、ストレージおよびネットワーク送信プロセスを処理し、それに応じて通知することです。これらの操作の実装方法の詳細には関係ありません。ユーザーが送信したデータを処理し、Raft アルゴリズムに基づいて次のステップを通知するだけです。
etcd raft のコードの実装では、この対話モデルが Go の独自のチャネル機能とシームレスに結合され、etcd raft ライブラリが真に特徴的なものになります。
etcd raft では、Raft ログの主な実装は log.go および log_unstable.go ファイルにあり、主な構造は raftLog で不安定です。不安定構造も raftLog 内のフィールドです。
etcd raft は、raftLog と不安定版を連携させてアルゴリズム内のログを管理します。
議論を簡単にするために、この記事では etcd raft でのスナップショットの処理については触れず、ログ エントリの処理ロジックのみに焦点を当てます。
type raftLog struct { storage Storage unstable unstable committed uint64 applying uint64 applied uint64 }
raftLog のコアフィールド:
type unstable struct { entries []pb.Entry offset uint64 offsetInProgress uint64 }
不安定なコアフィールド:
raftLog のコア フィールドは単純であり、Raft 論文の実装に簡単に関連付けることができます。ただし、不安定なフィールドはより抽象的に見えるかもしれません。次の例は、これらの概念を明確にすることを目的としています。
Raft ログにすでに 5 つのログ エントリが保存されていると仮定します。現在、不安定版に 3 つのログ エントリが保存されており、これらの 3 つのログ エントリは現在永続化されています。状況は以下のとおりです。
offset=6 は、unstable.entries の位置 0、1、および 2 のログ エントリが、実際の Raft ログの位置 6 (0 6)、7 (1 6)、および 8 (2 6) に対応することを示します。 offsetInProgress=9 の場合、位置 0、1、および 2 の 3 つのログ エントリを含む不安定な.entries[:9-6] がすべて永続化されていることがわかります。
不安定版で offset と offsetInProgress が使用される理由は、不安定版ではすべての Raft ログ エントリが保存されないためです。
ここでは Raft ログ処理ロジックのみに焦点を当てているため、ここでの「対話するタイミング」とは、ユーザーが保持する必要があるログ エントリを etcd raft が渡すタイミングを指します。
etcd raft は、主に Node インターフェースのメソッドを通じてユーザーと対話します。 Ready メソッドは、ユーザーが etcd raft からデータまたは命令を受信できるようにするチャネルを返します。
type raftLog struct { storage Storage unstable unstable committed uint64 applying uint64 applied uint64 }
このチャネルから受信した Ready 構造体には、処理が必要なログ エントリ、他のノードに送信する必要があるメッセージ、ノードの現在の状態などが含まれます。
Raft Log についての説明では、Entries フィールドと CommittedEntries フィールドのみに注目する必要があります。
type unstable struct { entries []pb.Entry offset uint64 offsetInProgress uint64 }
Ready を介して渡されたログ、メッセージ、その他のデータを処理した後、Node インターフェイスの Advance メソッドを呼び出して、etcd raft に指示が完了したことを通知し、次の Ready を受信して処理できるようにします。
etcd raft は、ノードのパフォーマンスをある程度向上させることができる AsyncStorageWrites オプションを提供します。ただし、ここではこのオプションは考慮しません。
ユーザー側では、受信した Ready 構造体のデータを処理することに重点が置かれます。 etcd raft 側では、いつ Ready 構造体をユーザーに渡すか、そしてその後どのようなアクションを実行するかを決定することに重点が置かれています。
このプロセスに関係する主なメソッドを次の図にまとめました。これはメソッド呼び出しの一般的なシーケンスを示しています (これは呼び出しのおおよその順序を表しているだけであることに注意してください)。
プロセス全体がループであることがわかります。ここでは、これらのメソッドの一般的な機能の概要を説明し、その後の書き込みフロー分析では、これらのメソッドが raftLog と不安定なコア フィールドでどのように動作するかを詳しく説明します。
ここで考慮すべき重要な点が 2 つあります:
1.永続化 ≠ コミット済み
最初に定義したように、ログ エントリは、Raft クラスタ内の大部分のノードによって永続化されている場合にのみコミットされたとみなされます。したがって、etcd raft によって返されたエントリを Ready を通じて永続化しても、これらのエントリはまだコミット済みとしてマークできません。
ただし、Advance メソッドを呼び出して永続化ステップが完了したことを etcd raft に通知すると、etcd raft はクラスター内の他のノード全体の永続化ステータスを評価し、一部のログ エントリをコミット済みとしてマークします。これらのエントリは、Ready 構造体の CommittedEntries フィールドを通じて提供されるため、ステート マシンに適用できます。
したがって、etcd raft を使用する場合、エントリをコミット済みとしてマークするタイミングは内部で管理され、ユーザーは永続性の前提条件を満たすだけで済みます。
内部的には、コミットメントは raftLog.commitTo メソッドを呼び出すことによって達成されます。これにより、Raft ペーパーの commitIndex に対応する raftLog.committed が更新されます。
2.コミット済み ≠ 適用済み
etcd raft 内で raftLog.commitTo メソッドが呼び出された後、raft.committed インデックスまでのログ エントリがコミットされたとみなされます。ただし、インデックスが lastApplied
エントリを適用済みとしてマークするタイミングも etcd raft の内部で処理されます。ユーザーは、コミットされたエントリを Ready からステート マシンに適用するだけで済みます。
もう 1 つの微妙な点は、Raft ではリーダーのみがエントリをコミットできるが、すべてのノードがエントリを適用できることです。
ここでは、書き込みリクエストを処理する etcd raft のフローを分析することで、これまでに説明したすべての概念を結び付けます。
より一般的なシナリオについて説明するために、すでに 3 つのログ エントリがコミットされ適用されている Raft ログから始めます。
この図では、緑は raftLog フィールドとストレージに保存されたログ エントリを表し、赤は不安定なフィールドとエントリに保存された永続化されていないログ エントリを表します。
3 つのログ エントリをコミットして適用したため、コミットと適用の両方が 3 に設定されます。適用フィールドには、前のアプリケーションからの最も高いログ エントリのインデックスが保持されます。この場合も 3 です。
この時点ではリクエストは開始されていないため、unstable.entries は空です。 Raft ログの次のログ インデックスは 4 で、オフセット 4 になります。現在永続化されているログがないため、offsetInProgress も 4 に設定されます。
ここで、Raft ログに 2 つのログ エントリを追加するリクエストを開始します。
図に示すように、追加されたログ エントリはunstable.entriesに保存されます。この段階では、コアフィールドに記録されたインデックス値は変更されません。
HasReady メソッドを覚えていますか? HasReady は、保存されていないログ エントリがあるかどうかを確認し、存在する場合は true を返します。
永続化されていないログ エントリの存在を判断するロジックは、unstable.entries[offsetInProgress-offset:] の長さが 0 より大きいかどうかに基づいています。明らかに、この例では次のようになります。
type raftLog struct { storage Storage unstable unstable committed uint64 applying uint64 applied uint64 }
保持されていないログ エントリが 2 つあることを示しているため、HasReady は true を返します。
readyWithoutAccept の目的は、ユーザーに返される Ready 構造体を作成することです。 2 つの非永続ログ エントリがあるため、readyWithoutAccept は返された Ready の Entries フィールドにこれら 2 つのログ エントリを含めます。
acceptReady は、Ready 構造体がユーザーに渡された後に呼び出されます。
acceptReady は、永続化処理中のログ エントリのインデックスを 6 に更新します。これは、範囲 [4, 6) 内のログ エントリが永続化されているものとしてマークされることを意味します。
ユーザーはエントリを Ready 状態に保持した後、Node.Advance を呼び出して etcd raft に通知します。これで、etcd raft は acceptReady で作成された「コールバック」を実行できるようになります。
この「コールバック」は、unstable.entries にすでに保存されているログ エントリをクリアし、オフセットを Storage.LastIndex 1、つまり 6 に設定します。
これら 2 つのログ エントリは Raft クラスター内の大部分のノードによってすでに永続化されていると想定しているため、これら 2 つのログ エントリをコミット済みとしてマークできます。
ループを続けると、HasReady はコミットされているがまだ適用されていないログ エントリの存在を検出するため、true を返します。
readyWithoutAccept は、コミットされたがステート マシンに適用されていないログ エントリ (4、5) を含む Ready を返します。
これらのエントリは、左開き、右閉じの間隔で 1、コミット 1 を適用する low、high := として計算されます。
acceptReady は、Ready で返されたログ エントリ [4, 5] をステート マシンに適用されるものとしてマークします。
ユーザーが Node.Advance を呼び出した後、etcd raft は「コールバック」を実行し、更新を 5 に適用します。これは、インデックス 5 以前のログ エントリがすべてステート マシンに適用されたことを示します。
これで書き込みリクエストの処理フローは完了です。最終状態は次のようになり、初期状態と比較できます。
私たちは Raft Log の概要から始めて、その基本概念を理解し、続いて etcd raft 実装について最初に見ていきました。次に、etcd raft 内の Raft Log のコア モジュールをさらに詳しく調べ、重要な質問について検討しました。最後に、書き込みリクエスト フローの完全な分析を通じてすべてを結び付けました。
このアプローチが etcd raft 実装を明確に理解し、Raft ログに対する独自の洞察を発展させるのに役立つことを願っています。
これでこの記事は終わりです。間違いや質問がある場合は、プライベートメッセージを介してご連絡いただくか、コメントを残してください。
ところで、raft-foiver は私が実装した etcd raft の簡易バージョンであり、Raft のすべてのコア ロジックを保持し、Raft 論文のプロセスに従って最適化されています。このライブラリを紹介する別の記事は今後公開する予定です。ご興味がございましたら、お気軽にスター、フォーク、PR をしてください!
以上がetcd の Raft 実装を理解する: Raft ログの詳細の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。