このライブラリは、Swift がクラスター化されたマルチノード分散システムという新しい領域に進出できるよう支援することを目的としています。
このライブラリでは、さまざまなクラスタリングのユースケースで採用できる、再利用可能なランタイムに依存しないメンバーシップ プロトコル実装を提供します。
クラスター メンバーシップ プロトコルは、計算集約型クラスター、スケジューラー、データベース、キーバリュー ストアなどの分散システムにとって重要な構成要素です。このパッケージの発表により、サービス メンバーシップを処理するために外部サービスに依存する必要がなくなるため、そのようなシステムの構築がより簡単になることを目指しています。また、追加のメンバーシップ プロトコルに協力して開発するようコミュニティに呼びかけたいと考えています。
メンバーシップ プロトコルの中核では、「私の (ライブ) ピアは誰ですか?」という質問に対する答えを提供する必要があります。この一見単純なタスクは、メッセージの遅延や消失、ネットワークの分断、応答しないがまだ「生きている」ノードが日々の糧となる分散システムでは、まったく単純ではないことがわかります。この質問に対して予測可能で信頼できる答えを提供するのが、クラスター メンバーシップ プロトコルの役割です。
メンバーシッププロトコルを実装する際にはさまざまなトレードオフがあり、これは引き続き興味深い研究分野であり、改良が続けられています。そのため、クラスター メンバーシップ パッケージは、単一の実装に焦点を当てるのではなく、この領域のさまざまな分散アルゴリズムのコラボレーション スペースとして機能することを目的としています。
この実装におけるプロトコルと変更についてさらに詳しく説明するには、SWIM API ドキュメントと、以下にリンクされている関連文書を読むことをお勧めします。
スケーラブルな弱一貫性感染スタイルのプロセス グループ メンバーシップアルゴリズム (「SWIM」とも呼ばれる) と、2018 年のLifeguard: Local Health Affairs for More Accurate Failure Detection の論文に記載されているいくつかの注目すべきプロトコル拡張機能。
SWIM は、ピアが他のノードのステータスの観察に関する情報を定期的に交換し、最終的にはその情報をクラスタ内の他のすべてのメンバーに広めるゴシップ プロトコルです。このカテゴリの分散アルゴリズムは、任意のメッセージ損失、ネットワーク分割、および同様の問題に対して非常に回復力があります。
大まかに言うと、SWIM は次のように機能します。
.ack
が返されることを期待して、そのピアに .ping メッセージを送信することによって行われます。以下の図で、 A
最初にB
どのように調査するかを見てください。payload
と、そのメンバーシップ ステータス ( .alive
、 .suspect
など) も含まれます。.ack
受信した場合、ピアはまだ.alive
であるとみなされます。そうしないと、ターゲット ピアが終了/クラッシュしたか、他の理由で応答しなくなった可能性があります。.pingRequest
メッセージを送信することによって、応答しないピアの状態について他のいくつかのピアに問い合わせます。その後、ピアはそのピアに直接 ping を発行します (ピアの調査)下図のE)。.suspect
としてマークされます。.nack
(「否定応答」) メッセージも使用して、仲介者がそれらの.pingRequest
メッセージを受信したが、ターゲットが応答していないようであることを ping リクエストの発信元に通知します。この情報を使用して、タイムアウトの計算方法に影響を与えるローカル ヘルス乗数を調整します。これについて詳しくは、API ドキュメントと Lifeguard の論文を参照してください。上記のメカニズムは、障害検出メカニズムとしてだけでなく、クラスターの既知のメンバーに関する情報を伝えるゴシップ メカニズムとしても機能します。このようにして、メンバーは、すべてのメンバーを事前にリストにしなくても、最終的には仲間のステータスを知ることができます。ただし、このメンバーシップの見解は一貫性が低いことを指摘する価値があります。つまり、すべてのメンバーが特定の時点でメンバーシップについてまったく同じ見解を持っているかどうかを保証することはできません (追加情報がなければ知る方法もありません)。ただし、これは、上位レベルのツールやシステムがその上に強力な保証を構築するための優れた構成要素です。
障害検出メカニズムが応答しないノードを検出すると、そのノードは最終的に .dead としてマークされ、その結果、クラスターから取り消し不能に削除されます。私たちの実装では、可能な状態に .unreachable 状態を追加するオプションの拡張機能が提供されていますが、ほとんどのユーザーはその必要性を感じず、デフォルトでは無効になっています。法的ステータスの移行に関する詳細とルールについては、SWIM.Status または次の図を参照してください。
Swift Cluster Membership がプロトコルを実装する方法は、その「 Instances
」を提供することです。たとえば、SWIM 実装はランタイムに依存しないSWIM.Instance
にカプセル化されており、ネットワーク ランタイムとインスタンス自体の間のグルー コードによって「駆動」または「解釈」される必要があります。実装のこれらの接着部分を「 Shell
」と呼びます。ライブラリには、すべてのメッセージングを UDP 経由で非同期に実行する SwiftNIO のDatagramChannel
を使用して実装されたSWIMNIOShell
が同梱されています。代替実装では、まったく異なるトランスポートを使用したり、他の既存のゴシップ システムなどに SWIM メッセージを便乗させたりすることができます。
SWIM インスタンスには、(swift-metrics を使用した) メトリクスの出力のサポートも組み込まれており、 swift-log Logger
渡すことで内部の詳細をログに記録するように構成できます。
このライブラリの主な目的は、何らかの形式のインプロセス メンバーシップ サービスを必要とするさまざまな実装間でSWIM.Instance
実装を共有することです。カスタム ランタイムの実装については、プロジェクトの README (https://github.com/apple/swift-cluster-membership/) に詳しく記載されているため、別のトランスポート上で SWIM を実装することに興味がある場合は、そこを参照してください。
新しいトランスポートの実装は、「空白を埋める」作業を要約すると次のようになります。
まず、ターゲット トランスポートを使用してピア プロトコル (https://github.com/apple/swift-cluster-membership/blob/main/Sources/SWIM/Peer.swift) を実装する必要があります。
/// SWIM peer which can be initiated contact with, by sending ping or ping request messages.
public protocol SWIMPeer : SWIMAddressablePeer {
/// Perform a probe of this peer by sending a `ping` message.
///
/// <... more docs here - please refer to the API docs for the latest version ...>
func ping (
payload : SWIM . GossipPayload ,
from origin : SWIMPingOriginPeer ,
timeout : DispatchTimeInterval ,
sequenceNumber : SWIM . SequenceNumber
) async throws -> SWIM . PingResponse
// ...
}
これは通常、メッセージを送信し、該当する場合は適切なコールバックを呼び出す機能を備えた接続、チャネル、またはその他の ID をラップすることを意味します。
次に、ピアの受信側で、これらのメッセージの受信を実装し、 SWIM.Instance
(SWIMProtocol の下にグループ化されている) で定義されている、対応するすべてのon<SomeMessage>(...)
コールバックを呼び出す必要があります。
SWIMProtocol についてのアイデアを提供するために、SWIMProtocol の一部を以下にリストします。
public protocol SWIMProtocol {
/// MUST be invoked periodically, in intervals of `self.swim.dynamicLHMProtocolInterval`.
///
/// MUST NOT be scheduled using a "repeated" task/timer", as the interval is dynamic and may change as the algorithm proceeds.
/// Implementations should schedule each next tick by handling the returned directive's `scheduleNextTick` case,
/// which includes the appropriate delay to use for the next protocol tick.
///
/// This is the heart of the protocol, as each tick corresponds to a "protocol period" in which:
/// - suspect members are checked if they're overdue and should become `.unreachable` or `.dead`,
/// - decisions are made to `.ping` a random peer for fault detection,
/// - and some internal house keeping is performed.
///
/// Note: This means that effectively all decisions are made in interval sof protocol periods.
/// It would be possible to have a secondary periodic or more ad-hoc interval to speed up
/// some operations, however this is currently not implemented and the protocol follows the fairly
/// standard mode of simply carrying payloads in periodic ping messages.
///
/// - Returns: `SWIM.Instance.PeriodicPingTickDirective` which must be interpreted by a shell implementation
mutating func onPeriodicPingTick ( ) -> [ SWIM . Instance . PeriodicPingTickDirective ]
mutating func onPing ( ... ) -> [ SWIM . Instance . PingDirective ]
mutating func onPingRequest ( ... ) -> [ SWIM . Instance . PingRequestDirective ]
mutating func onPingResponse ( ... ) -> [ SWIM . Instance . PingResponseDirective ]
// ...
}
これらの呼び出しは、すべての SWIM プロトコル固有のタスクを内部で実行し、メッセージにどのように反応するかについて実装に「コマンド」を解釈するのが簡単なディレクティブを返します。たとえば、 .pingRequest
メッセージを受信すると、返されたディレクティブはシェルにいくつかのノードに ping を送信するように指示する場合があります。このディレクティブは、適切なターゲット、タイムアウト、および追加情報をすべて準備し、その指示に従って単純に呼び出しを正しく実装することを容易にします。たとえば、次のようになります。
self . swim . onPingRequest (
target : target ,
pingRequestOrigin : pingRequestOrigin ,
payload : payload ,
sequenceNumber : sequenceNumber
) . forEach { directive in
switch directive {
case . gossipProcessed ( let gossipDirective ) :
self . handleGossipPayloadProcessedDirective ( gossipDirective )
case . sendPing ( let target , let payload , let pingRequestOriginPeer , let pingRequestSequenceNumber , let timeout , let sequenceNumber ) :
self . sendPing (
to : target ,
payload : payload ,
pingRequestOrigin : pingRequestOriginPeer ,
pingRequestSequenceNumber : pingRequestSequenceNumber ,
timeout : timeout ,
sequenceNumber : sequenceNumber
)
}
}
一般に、これにより、すべての難しい「いつ何をするか」をプロトコル インスタンス内にカプセル化することが可能になり、シェルはそれらを実装する指示に従うだけで済みます。実際の実装では、一連の応答を待機したり、それらを特定の方法で処理したりするなど、より複雑な同時実行性やネットワーキング タスクを実行する必要があることがよくありますが、プロトコルの概要はインスタンスのディレクティブによって調整されます。
各コールバック、コールバックを呼び出すタイミング、およびこれらすべてがどのように組み合わされるかについての詳細なドキュメントについては、 API ドキュメントを参照してください。
リポジトリには、エンドツーエンドのサンプルと、 SWIM.Instance
を利用して単純な UDP ベースのピア監視システムを有効にする SWIMNIOExample と呼ばれるサンプル実装が含まれています。これにより、ピアは、SwiftNIO によって駆動されるデータグラムを送信することで、SWIM プロトコルを使用してノードの障害についてお互いにゴシップしたり通知したりすることができます。
SWIMNIOExample
実装は例としてのみ提供されており、運用環境での使用を念頭に置いて実装されていませんが、ある程度の努力をすれば、いくつかのユースケースでは確実にうまく機能する可能性があります。クラスター メンバーシップ アルゴリズム、スケーラビリティ ベンチマーク、および SwiftNIO 自体の使用について詳しく学ぶことに興味がある場合、これはすぐに始めるのに最適なモジュールです。おそらくモジュールが十分に成熟したら、これを単なる例ではなく、 Swift NIO ベースのクラスター化アプリケーション用の再利用可能なコンポーネント。
最も単純な形式では、提供された SWIM インスタンスと NIO シェルを組み合わせて単純なサーバーを構築し、以下に示すような提供されたハンドラーを一般的な NIO チャネル パイプラインに埋め込むことができます。
let bootstrap = DatagramBootstrap ( group : group )
. channelOption ( ChannelOptions . socketOption ( . so_reuseaddr ) , value : 1 )
. channelInitializer { channel in
channel . pipeline
// first install the SWIM handler, which contains the SWIMNIOShell:
. addHandler ( SWIMNIOHandler ( settings : settings ) ) . flatMap {
// then install some user handler, it will receive SWIM events:
channel . pipeline . addHandler ( SWIMNIOExampleHandler ( ) )
}
}
bootstrap . bind ( host : host , port : port )
サンプル ハンドラーは、SWIM クラスター メンバーシップ変更イベントを受信して処理できます。
final class SWIMNIOExampleHandler : ChannelInboundHandler {
public typealias InboundIn = SWIM . MemberStatusChangedEvent
let log = Logger ( label : " SWIMNIOExampleHandler " )
public func channelRead ( context : ChannelHandlerContext , data : NIOAny ) {
let change : SWIM . MemberStatusChangedEvent = self . unwrapInboundIn ( data )
self . log . info ( " Membership status changed: [ ( change . member . node ) ] is now [ ( change . status ) ] " , metadata : [
" swim/member " : " ( change . member . node ) " ,
" swim/member/status " : " ( change . status ) " ,
] )
}
}
SWIMNIO の実装に貢献して磨き上げることに興味がある場合は、問題にアクセスしてタスクを選択するか、自分で改善を提案してください。
私たちは一般に、同様の「インスタンス」スタイルを使用して追加のメンバーシップ実装の議論と実装を促進することに関心を持っています。
このようなアルゴリズムに興味があり、実装してほしいお気に入りのプロトコルがある場合は、Issue または Swift フォーラムを通じて遠慮なく heve に連絡してください。
体験レポート、フィードバック、改善のアイデア、貢献を大いに歓迎します。ご連絡をお待ちしております。
プル リクエストを送信するプロセスについては CONTRIBUTING ガイドを参照し、用語やこのライブラリを使用する際のその他の役立つヒントについてはハンドブックを参照してください。