Diese Bibliothek soll Swift dabei helfen, in einem neuen Bereich Fuß zu fassen: geclusterte verteilte Systeme mit mehreren Knoten.
Mit dieser Bibliothek stellen wir wiederverwendbare laufzeitunabhängige Mitgliedschaftsprotokollimplementierungen bereit, die in verschiedenen Clustering-Anwendungsfällen übernommen werden können.
Cluster-Mitgliedschaftsprotokolle sind ein entscheidender Baustein für verteilte Systeme wie rechenintensive Cluster, Scheduler, Datenbanken, Schlüsselwertspeicher und mehr. Mit der Ankündigung dieses Pakets möchten wir den Aufbau solcher Systeme vereinfachen, da sie nicht mehr auf externe Dienste angewiesen sind, um die Servicemitgliedschaft für sie zu verwalten. Wir möchten die Community auch dazu einladen, an zusätzlichen Mitgliedschaftsprotokollen mitzuarbeiten und diese zu entwickeln.
Im Kern müssen Mitgliedschaftsprotokolle eine Antwort auf die Frage „Wer sind meine (lebenden) Kollegen?“ geben. Diese scheinbar einfache Aufgabe erweist sich in einem verteilten System, in dem verzögerte oder verlorene Nachrichten, Netzwerkpartitionen und nicht reagierende, aber immer noch „lebende“ Knoten an der Tagesordnung sind, als gar nicht so einfach. Cluster-Mitgliedschaftsprotokolle liefern eine vorhersehbare und zuverlässige Antwort auf diese Frage.
Bei der Implementierung eines Mitgliedschaftsprotokolls können verschiedene Kompromisse eingegangen werden, und es handelt sich weiterhin um ein interessantes Forschungsgebiet und eine kontinuierliche Weiterentwicklung. Daher soll sich das Cluster-Mitgliedschaftspaket nicht auf eine einzelne Implementierung konzentrieren, sondern als Raum für die Zusammenarbeit verschiedener verteilter Algorithmen in diesem Bereich dienen.
Für eine ausführlichere Diskussion des Protokolls und der Änderungen in dieser Implementierung empfehlen wir die Lektüre der SWIM-API-Dokumentation sowie der unten verlinkten zugehörigen Dokumente.
Der Scalable Weakly-consistent Infection-style Process Group Membership Algorithmus (auch bekannt als „SWIM“), zusammen mit einigen bemerkenswerten Protokollerweiterungen, wie im Papier „Lifeguard: Local Health Awareness for More Accurate Failure Detection“ von 2018 dokumentiert.
SWIM ist ein Klatschprotokoll, bei dem Peers regelmäßig Informationen über ihre Beobachtungen des Status anderer Knoten austauschen und diese Informationen schließlich an alle anderen Mitglieder in einem Cluster weitergeben. Diese Kategorie verteilter Algorithmen ist sehr widerstandsfähig gegen willkürlichen Nachrichtenverlust, Netzwerkpartitionen und ähnliche Probleme.
Auf hohem Niveau funktioniert SWIM folgendermaßen:
.ack
zurückgesendet wird. Sehen Sie im Diagramm unten, wie A
B
zunächst prüft.payload
, bei der es sich um (teilweise) Informationen darüber handelt, welche anderen Peers dem Absender der Nachricht bekannt sind, zusammen mit ihrem Mitgliedschaftsstatus ( .alive
, .suspect
usw.)..ack
erhält, gilt der Peer als noch .alive
. Andernfalls ist der Ziel-Peer möglicherweise beendet/abgestürzt oder reagiert aus anderen Gründen nicht..pingRequest
-Nachrichten an eine konfigurierte Anzahl anderer Peers sendet, die dann direkte Pings an diesen Peer (prüfenden Peer) senden E im Diagramm unten)..suspect
markiert wird,.nack
Nachrichten („negative Bestätigung“), um den Ursprung der Ping-Anfrage darüber zu informieren, dass der Vermittler diese .pingRequest
Nachrichten erhalten hat, das Ziel jedoch offenbar nicht geantwortet hat. Wir verwenden diese Informationen, um einen lokalen Gesundheitsmultiplikator anzupassen, der sich darauf auswirkt, wie Zeitüberschreitungen berechnet werden. Weitere Informationen hierzu finden Sie in den API-Dokumenten und im Lifeguard-Papier.Der obige Mechanismus dient nicht nur als Fehlererkennungsmechanismus, sondern auch als Klatschmechanismus, der Informationen über bekannte Mitglieder des Clusters übermittelt. Auf diese Weise erfahren die Mitglieder schließlich etwas über den Status ihrer Kollegen, auch ohne dass sie alle im Voraus aufgelistet werden müssen. Es ist jedoch darauf hinzuweisen, dass diese Ansicht über die Mitgliedschaft nur schwach konsistent ist, was bedeutet, dass es keine Garantie (oder keine Möglichkeit, ohne zusätzliche Informationen herauszufinden) gibt, ob alle Mitglieder zu einem bestimmten Zeitpunkt die gleiche exakte Ansicht über die Mitgliedschaft haben. Es ist jedoch ein hervorragender Baustein für Tools und Systeme auf höherer Ebene, um darauf stärkere Garantien aufzubauen.
Sobald der Fehlererkennungsmechanismus einen nicht reagierenden Knoten erkennt, wird dieser schließlich als .dead markiert, was zur unwiderruflichen Entfernung aus dem Cluster führt. Unsere Implementierung bietet eine optionale Erweiterung, die den möglichen Zuständen den Status „.unreachable“ hinzufügt. Die meisten Benutzer werden dies jedoch nicht für notwendig halten und sind standardmäßig deaktiviert. Einzelheiten und Regeln zu Rechtsstatusübergängen finden Sie in SWIM.Status oder im folgenden Diagramm:
Die Art und Weise, wie Swift Cluster Membership Protokolle implementiert, besteht darin, „ Instances
“ davon anzubieten. Beispielsweise ist die SWIM-Implementierung in der laufzeitunabhängigen SWIM.Instance
gekapselt, die durch einen Verbindungscode zwischen einer Netzwerklaufzeit und der Instanz selbst „gesteuert“ oder „interpretiert“ werden muss. Wir nennen diese Verbindungselemente einer Implementierung „ Shell
“, und die Bibliothek wird mit einer SWIMNIOShell
ausgeliefert, die mit DatagramChannel
von SwiftNIO implementiert wird und alle Nachrichten asynchron über UDP ausführt. Alternative Implementierungen können völlig andere Transporte verwenden oder SWIM-Nachrichten auf ein anderes bestehendes Klatschsystem usw. übertragen.
Die SWIM-Instanz verfügt außerdem über eine integrierte Unterstützung für die Ausgabe von Metriken (mithilfe von Swift-Metriken) und kann so konfiguriert werden, dass Details zu internen Details protokolliert werden, indem ein Swift-Log Logger
übergeben wird.
Der Hauptzweck dieser Bibliothek besteht darin, die SWIM.Instance
-Implementierung für verschiedene Implementierungen freizugeben, die eine Form von In-Process-Mitgliedschaftsdienst benötigen. Die Implementierung einer benutzerdefinierten Laufzeit wird ausführlich in der README-Datei des Projekts (https://github.com/apple/swift-cluster-membership/) dokumentiert. Schauen Sie also bitte dort nach, wenn Sie daran interessiert sind, SWIM über einen anderen Transport zu implementieren.
Die Implementierung eines neuen Transportmittels läuft auf eine „Lückenausfüllen“-Übung hinaus:
Zunächst muss man die Peer-Protokolle (https://github.com/apple/swift-cluster-membership/blob/main/Sources/SWIM/Peer.swift) mit dem eigenen Zieltransport implementieren:
/// 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
// ...
}
Dies bedeutet normalerweise, dass eine Verbindung, ein Kanal oder eine andere Identität mit der Möglichkeit versehen wird, Nachrichten zu senden und gegebenenfalls die entsprechenden Rückrufe aufzurufen.
Dann muss man auf der Empfangsseite eines Peers den Empfang dieser Nachrichten implementieren und alle entsprechenden on<SomeMessage>(...)
-Rückrufe aufrufen, die auf der SWIM.Instance
definiert sind (gruppiert unter SWIMProtocol).
Ein Teil des SWIMProtocol ist unten aufgeführt, um Ihnen eine Vorstellung davon zu geben:
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 ]
// ...
}
Diese Aufrufe führen alle SWIM-Protokoll-spezifischen Aufgaben intern aus und geben Anweisungen zurück, die einfach zu interpretierende „Befehle“ an eine Implementierung darüber sind, wie diese auf die Nachricht reagieren soll. Beispielsweise kann die zurückgegebene Direktive beim Empfang einer .pingRequest
Nachricht eine Shell anweisen, einen Ping an einige Knoten zu senden. Die Direktive bereitet alle entsprechenden Ziel-, Timeout- und Zusatzinformationen vor, die es einfacher machen, einfach ihrer Anweisung zu folgen und den Aufruf korrekt umzusetzen, z. B. so:
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
)
}
}
Im Allgemeinen ermöglicht dies, alle kniffligen „Was ist wann zu tun?“ in der Protokollinstanz zu kapseln, und eine Shell muss nur den Anweisungen zur Implementierung folgen. Die eigentlichen Implementierungen müssen oft einige aufwändigere Parallelitäts- und Netzwerkaufgaben ausführen, wie etwa das Warten auf eine Folge von Antworten und deren spezifische Handhabung usw. Der allgemeine Überblick über das Protokoll wird jedoch durch die Anweisungen der Instanz orchestriert.
Eine ausführliche Dokumentation zu den einzelnen Rückrufen, wann sie aufgerufen werden sollen und wie alles zusammenpasst, finden Sie in der API-Dokumentation .
Das Repository enthält ein End-to-End-Beispiel und eine Beispielimplementierung namens SWIMNIOExample, die die SWIM.Instance
nutzt, um ein einfaches UDP-basiertes Peer-Überwachungssystem zu ermöglichen. Dies ermöglicht es Peers, mithilfe des SWIM-Protokolls zu klatschen und sich gegenseitig über Knotenausfälle zu benachrichtigen, indem sie von SwiftNIO gesteuerte Datagramme senden.
Die
SWIMNIOExample
Implementierung wird nur als Beispiel angeboten und wurde nicht für den Produktionseinsatz implementiert. Mit etwas Aufwand könnte sie jedoch für einige Anwendungsfälle durchaus gut funktionieren. Wenn Sie daran interessiert sind, mehr über Cluster-Mitgliedschaftsalgorithmen, Skalierbarkeits-Benchmarking und die Verwendung von SwiftNIO selbst zu erfahren, ist dies ein großartiges Modul, um erste Erfahrungen zu sammeln wiederverwendbare Komponente für Swift NIO-basierte Clusteranwendungen.
In der einfachsten Form, bei der die bereitgestellte SWIM-Instanz und die NIO-Shell kombiniert werden, um einen einfachen Server zu erstellen, kann man die bereitgestellten Handler wie unten gezeigt in eine typische NIO-Kanalpipeline einbetten:
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 )
Der Beispielhandler kann dann Änderungsereignisse der SWIM-Clustermitgliedschaft empfangen und verarbeiten:
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 ) " ,
] )
}
}
Wenn Sie daran interessiert sind, einen Beitrag zu leisten und die SWIMNIO-Implementierung zu verbessern, gehen Sie bitte zu den Themen und übernehmen Sie eine Aufgabe oder schlagen Sie selbst eine Verbesserung vor!
Wir sind im Allgemeinen daran interessiert, Diskussionen und Implementierungen zusätzlicher Mitgliedschaftsimplementierungen unter Verwendung eines ähnlichen „Instanz“-Stils zu fördern.
Wenn Sie an solchen Algorithmen interessiert sind und ein Lieblingsprotokoll haben, das Sie gerne implementiert sehen würden, zögern Sie bitte nicht, heve über Issues oder die Swift-Foren zu kontaktieren.
Erfahrungsberichte, Feedback, Verbesserungsideen und Beiträge sind herzlich willkommen! Wir freuen uns, von Ihnen zu hören.
Bitte lesen Sie den Leitfaden „CONTRIBUTING“, um mehr über den Prozess der Übermittlung von Pull-Requests zu erfahren. Die Terminologie und andere nützliche Tipps für die Arbeit mit dieser Bibliothek finden Sie im HANDBUCH.