この記事の要点
- Swift サーバーエコシステム向けの新しいオープンソースプロジェクト Swift Cluster Membership(swift-cluster-membership)が発表されました。複数ノードからなる分散システムを Swift で構築するための、再利用可能な membership protocol(メンバーシッププロトコル)の実装を提供するライブラリです。
- membership protocol は「自分の(生きている)ピアは誰か?」という問いに答えるための仕組みです。メッセージの遅延・喪失やネットワーク分断、応答しないノードが日常的に起こる分散システムでは、この一見単純な問いに信頼できる答えを返すのは簡単ではありません。
- 初回リリースでは、その実装の 1 つとして SWIM(Scalable Weakly-consistent Infection-style process group Membership)プロトコルが提供されます。runtime 非依存の
SWIM.Instanceとして実装され、ネットワーク層と組み合わせる「Shell」を差し替えることで、さまざまなトランスポート上で利用できます。
何が発表されたのか
Swift Cluster Membership は、計算クラスタやスケジューラ、データベース、key-value ストアといった分散システムの構築を助けるためのオープンソースライブラリです。これらのシステムにとってクラスタのメンバーシップ管理は重要な構成要素であり、このライブラリを使うことで、メンバーシップを外部サービスに頼らずに扱えるようになります。
membership protocol の中心的な役割は「自分の(生きている)ピアは誰か?」という問いに答えることです。単一マシンであれば自明なこの問いも、分散システムでは難しくなります。メッセージは遅延・喪失し、ネットワークは分断され、ノードは応答しないのに「生きている」ように見えることもあるからです。membership protocol は、こうした状況でも予測可能で信頼できる答えを提供します。
membership protocol の実装には多くのトレードオフがあり、現在も研究と改良が続く領域です。そのため Swift Cluster Membership は単一の実装に絞らず、この分野のさまざまな分散アルゴリズムが集まる協働の場となることを目指しています。その最初の実装として、初回リリースと同時に SWIM プロトコルがオープンソース化されました。
何に使えるのか
SWIM プロトコルの概要
最初に提供される SWIM(Scalable Weakly-consistent Infection-style process group Membership)は gossip protocol の一種で、各ピアが他のノードの状態についての観測情報を定期的に交換し、最終的にクラスタ全体へ広めていきます。この種のアルゴリズムは、任意のメッセージ喪失やネットワーク分断に対して非常に強い耐性を持ちます。
SWIM はおおまかに次のように動作します。
- 各メンバーは、知っているピアの中からランダムに 1 つを選んで定期的に
.pingを送り、.ackが返ってくることを期待します。やり取りされるメッセージには gossip ペイロード(送信者が把握している他ピアとその状態.alive/.suspectなどの部分的な情報)も載せられます。 .ackが返ってくれば、そのピアはまだ.aliveとみなされます。返ってこない場合、対象のピアは停止・クラッシュしたか、何らかの理由で応答していない可能性があります。- 本当にダウンしているかを確認するため、起点ノードは設定された数の他ピアに
.pingRequestを送り、それらのピアから対象ピアへ直接 ping を送らせます。 - それらの ping も失敗すると
.nack(否定応答)が返り、.ackが得られないことから対象ピアは.suspectとしてマークされます。
この仕組みは障害検知であると同時に、クラスタの既知メンバー情報を運ぶ gossip 機構でもあります。これによりメンバーは、全ピアをあらかじめ列挙していなくても、互いの状態を最終的に学習できます。ただしこのメンバーシップの見え方は weakly-consistent(弱整合)であり、ある時点で全メンバーが完全に同じメンバーシップ像を持っている保証はありません。とはいえ、より上位のツールやシステムが強い保証を構築するための土台としては優れています。
障害検知が応答しないノードを検出すると、そのノードは最終的に .dead とマークされ、クラスタから取り除かれます。実装には任意の拡張として .unreachable 状態の追加も用意されていますが、多くの利用者には不要なため既定では無効です。
Instance と Shell
Swift Cluster Membership は、各プロトコルを「Instance」として提供します。たとえば SWIM の実装は runtime 非依存の SWIM.Instance にカプセル化されており、ネットワーク runtime と instance をつなぐグルーコードによって「駆動」されます。このグルー部分を「Shell」と呼びます。ライブラリには、SwiftNIO の DatagramChannel を使い UDP 上で非同期にメッセージをやり取りする SWIMNIOShell が同梱されています。別の Shell を用意すれば、まったく異なるトランスポートを使ったり、既存の gossip システムに SWIM メッセージを相乗りさせたりもできます。
SWIM.Instance は swift-metrics によるメトリクス出力に対応し、swift-log の Logger を渡して内部の詳細をログ出力するよう構成することもできます。
独自トランスポートへの組み込み
このライブラリの主目的は、何らかのプロセス内メンバーシップサービスを必要とする実装どうしで SWIM.Instance を共有することです。独自のトランスポートに組み込むには、まず対象トランスポートを使って Peer プロトコルを実装します。
public protocol SWIMPeer: SWIMAddressablePeer {
func ping(
payload: SWIM.GossipPayload,
from origin: SWIMAddressablePeer,
timeout: DispatchTimeInterval,
sequenceNumber: SWIM.SequenceNumber,
onComplete: @escaping (Result<SWIM.PingResponse, Error>) -> Void
)
// ...
}
これは通常、コネクションやチャネルといった識別子を包み、メッセージ送信と適切なコールバック呼び出しの能力を持たせることを意味します。受信側では、メッセージを受け取って SWIM.Instance が定義する on<SomeMessage> 系のコールバックを呼び出します。これらの呼び出しは SWIM 固有の処理を内部で行い、実装がどう反応すべきかを示す「ディレクティブ」(単純なコマンド)を返します。たとえば PingRequest を受け取ると、.sendPing(target:pingRequestOrigin:timeout:sequenceNumber) のように、対象ピアへ ping を送るよう指示するディレクティブが返ります。
SwiftNIO を使った例
リポジトリには、SWIM.Instance を使って UDP ベースのピア監視を行う実装例 SWIMNIOExample と、それを使ったエンドツーエンドのサンプルが含まれています。最も単純な形では、提供されている SWIM instance と SwiftNIO の Shell を組み合わせ、次のように典型的な SwiftNIO のチャネルパイプラインへハンドラを埋め込めます。
let bootstrap = DatagramBootstrap(group: group)
.channelInitializer { channel in
channel.pipeline
// まず SWIMNIOShell を含む SWIM ハンドラを設置する
.addHandler(SWIMNIOHandler(settings: settings)).flatMap {
// 続けてユーザーのハンドラを設置する。SWIM イベントを受け取る
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 = 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)",
]
)
}
}
なお SWIMNIOExample はあくまで実装例であり、本番利用を想定したものではないとされています。クラスタメンバーシップアルゴリズムやスケーラビリティのベンチマーク、SwiftNIO 自体に触れて学ぶには良い題材です。
導入・今後の位置づけ
発表時点でこのプロジェクトは pre-release の状態で、安定版をタグ付けする前に十分に練り上げる期間を取りたいとされています。主な注力対象は SWIM.Instance の品質と正しさであり、いくつかのユースケースで API への確信が得られた段階で 1.0 をタグ付けする方針です。その先では、既存実装のオーバーヘッド削減に加え、追加の membership protocol の実装も継続して検討するとされています。
Swift Cluster Membership は完全なオープンソースプロジェクトで、GitHub 上で開発されています。議論は Swift フォーラムの server カテゴリで行えます。