Cette bibliothèque vise à aider Swift à s'imposer dans un nouvel espace : les systèmes distribués multi-nœuds en cluster.
Avec cette bibliothèque, nous fournissons des implémentations de protocoles d'adhésion réutilisables et indépendantes du temps d'exécution qui peuvent être adoptées dans divers cas d'utilisation de clustering.
Les protocoles d'appartenance aux clusters constituent un élément essentiel des systèmes distribués, tels que les clusters à calcul intensif, les planificateurs, les bases de données, les magasins clé-valeur, etc. Avec l'annonce de ce package, nous visons à simplifier la construction de tels systèmes, car ils n'ont plus besoin de s'appuyer sur des services externes pour gérer l'adhésion aux services à leur place. Nous aimerions également inviter la communauté à collaborer et à développer des protocoles d'adhésion supplémentaires.
Fondamentalement, les protocoles d’adhésion doivent fournir une réponse à la question « Qui sont mes pairs (en direct) ? ». Cette tâche apparemment simple s'avère ne pas être si simple du tout dans un système distribué où les messages retardés ou perdus, les partitions réseau et les nœuds qui ne répondent pas mais toujours « vivants » constituent le pain quotidien. Fournir une réponse prévisible et fiable à cette question est le rôle des protocoles d’adhésion aux clusters.
Il existe divers compromis à faire lors de la mise en œuvre d’un protocole d’adhésion, et cela continue d’être un domaine de recherche intéressant et de perfectionnement continu. En tant que tel, le package d’adhésion au cluster n’a pas l’intention de se concentrer sur une seule implémentation, mais de servir d’espace de collaboration pour divers algorithmes distribués dans cet espace.
Pour une discussion plus approfondie du protocole et des modifications apportées à cette implémentation, nous vous suggérons de lire la documentation de l'API SWIM, ainsi que les articles associés liés ci-dessous.
L'algorithme d'adhésion au groupe de processus de style infection faiblement cohérent (également connu sous le nom de « SWIM »), ainsi que quelques extensions de protocole notables, comme documenté dans le document Lifeguard 2018 : Sensibilisation locale à la santé pour une détection plus précise des pannes .
SWIM est un protocole de potins dans lequel les pairs échangent périodiquement des informations sur leurs observations des statuts des autres nœuds, diffusant finalement les informations à tous les autres membres d'un cluster. Cette catégorie d'algorithmes distribués est très résistante aux pertes de messages arbitraires, aux partitions réseau et à des problèmes similaires.
À un niveau élevé, SWIM fonctionne comme ceci :
.ack
lui soit renvoyé. Voyez comment A
sonde B
initialement dans le diagramme ci-dessous.payload
utile de potins, qui est des informations (partielles) sur les autres pairs dont l'expéditeur du message est conscient, ainsi que sur leur statut d'adhésion ( .alive
, .suspect
, etc.).ack
, le homologue est considéré comme toujours .alive
. Sinon, l'homologue cible pourrait s'être arrêté/planté ou ne répond plus pour d'autres raisons..pingRequest
à un nombre configuré d'autres homologues, qui émettent ensuite des pings directs à cet homologue (sondant l'homologue E dans le schéma ci-dessous)..suspect
,.nack
(« accusé de réception négatif ») supplémentaires dans la situation pour informer l'origine de la requête ping que l'intermédiaire a reçu ces messages .pingRequest
, mais la cible ne semble pas avoir répondu. Nous utilisons ces informations pour ajuster un multiplicateur de santé local, qui affecte la façon dont les délais d'attente sont calculés. Pour en savoir plus à ce sujet, reportez-vous à la documentation API et au document Lifeguard.Le mécanisme ci-dessus sert non seulement de mécanisme de détection de pannes, mais également de mécanisme de potins, qui transporte des informations sur les membres connus du cluster. De cette façon, les membres finissent par connaître le statut de leurs pairs, même sans les avoir tous répertoriés au préalable. Il convient toutefois de souligner que cette vision de l’adhésion est faiblement cohérente, ce qui signifie qu’il n’y a aucune garantie (ou moyen de savoir, sans informations supplémentaires) si tous les membres ont exactement la même vision de l’adhésion à un moment donné. Cependant, il s’agit d’un excellent élément de base permettant aux outils et systèmes de niveau supérieur de renforcer leurs garanties.
Une fois que le mécanisme de détection de panne détecte un nœud qui ne répond pas, il est finalement marqué comme .dead, ce qui entraîne sa suppression irrévocable du cluster. Notre implémentation propose une extension facultative, ajoutant un état .unreachable aux états possibles, mais la plupart des utilisateurs ne la trouveront pas nécessaire et elle est désactivée par défaut. Pour plus de détails et les règles concernant les transitions de statut juridique, reportez-vous à SWIM.Status ou au schéma suivant :
La façon dont Swift Cluster Membership implémente les protocoles consiste à en proposer des « Instances
». Par exemple, l'implémentation de SWIM est encapsulée dans l'instance SWIM.Instance
indépendante du runtime qui doit être « pilotée » ou « interprétée » par un code de liaison entre un runtime réseau et l'instance elle-même. Nous appelons ces éléments de colle d'une implémentation " Shell
s ", et la bibliothèque est livrée avec un SWIMNIOShell
implémenté à l'aide du DatagramChannel
de SwiftNIO qui effectue toute la messagerie de manière asynchrone via UDP. Des implémentations alternatives peuvent utiliser des transports complètement différents, ou superposer des messages SWIM sur un autre système de potins existant, etc.
L'instance SWIM dispose également d'une prise en charge intégrée pour l'émission de métriques (à l'aide de Swift-metrics) et peut être configurée pour enregistrer des détails sur les détails internes en transmettant un Swift-log Logger
.
L'objectif principal de cette bibliothèque est de partager l'implémentation SWIM.Instance
entre diverses implémentations qui nécessitent une certaine forme de service d'adhésion en cours. La mise en œuvre d'un runtime personnalisé est documentée en détail dans le README du projet (https://github.com/apple/swift-cluster-membership/), alors n'hésitez pas à y jeter un œil si vous souhaitez implémenter SWIM sur un transport différent.
Mettre en œuvre un nouveau transport se résume à un exercice de « remplir les blancs » :
Tout d'abord, il faut implémenter les protocoles Peer (https://github.com/apple/swift-cluster-membership/blob/main/Sources/SWIM/Peer.swift) en utilisant son transport cible :
/// 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
// ...
}
Ce qui signifie généralement envelopper une connexion, un canal ou une autre identité avec la possibilité d'envoyer des messages et d'invoquer les rappels appropriés le cas échéant.
Ensuite, à la réception d'un homologue, il faut implémenter la réception de ces messages et appeler tous les rappels on<SomeMessage>(...)
correspondants définis sur SWIM.Instance
(regroupés sous SWIMProtocol).
Un morceau du SWIMProtocol est répertorié ci-dessous pour vous donner une idée :
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 ]
// ...
}
Ces appels effectuent toutes les tâches spécifiques au protocole SWIM en interne et renvoient des directives simples à interpréter comme des « commandes » à une implémentation sur la façon dont elle doit réagir au message. Par exemple, lors de la réception d'un message .pingRequest
, la directive renvoyée peut demander à un shell d'envoyer un ping à certains nœuds. La directive prépare toutes les informations appropriées sur la cible, le délai d'attente et les informations supplémentaires qui permettent de simplement suivre ses instructions et d'implémenter l'appel correctement, par exemple comme ceci :
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
)
}
}
En général, cela permet d'encapsuler tous les « quoi faire quand » délicats dans l'instance de protocole, et un Shell n'a qu'à suivre les instructions les mettant en œuvre. Les implémentations réelles devront souvent effectuer des tâches de concurrence et de mise en réseau plus complexes, comme attendre une séquence de réponses et les gérer d'une manière spécifique, etc., mais les grandes lignes du protocole sont orchestrées par les directives de l'instance.
Pour une documentation détaillée sur chacun des rappels, quand les appeler et comment tout cela s'articule, veuillez vous référer à la documentation de l'API .
Le référentiel contient un exemple de bout en bout et un exemple d'implémentation appelé SWIMNIOExample qui utilise SWIM.Instance
pour activer un système simple de surveillance par les pairs basé sur UDP. Cela permet aux pairs de bavarder et de s'informer mutuellement des pannes de nœuds à l'aide du protocole SWIM en envoyant des datagrammes pilotés par SwiftNIO.
L'implémentation
SWIMNIOExample
est proposée uniquement à titre d'exemple et n'a pas été implémentée en vue d'une utilisation en production. Cependant, avec un certain effort, elle pourrait certainement bien fonctionner pour certains cas d'utilisation. Si vous souhaitez en savoir plus sur les algorithmes d'appartenance aux clusters, l'analyse comparative de l'évolutivité et l'utilisation de SwiftNIO lui-même, c'est un excellent module pour vous familiariser, et peut-être qu'une fois le module suffisamment mature, nous pourrions envisager d'en faire non seulement un exemple, mais un composant réutilisable pour les applications en cluster basées sur Swift NIO.
Dans sa forme la plus simple, combinant l'instance SWIM fournie et le shell NIO pour créer un serveur simple, on peut intégrer les gestionnaires fournis comme indiqué ci-dessous, dans un pipeline de canal NIO typique :
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 )
L'exemple de gestionnaire peut ensuite recevoir et gérer les événements de changement d'appartenance au cluster 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 ) " ,
] )
}
}
Si vous souhaitez contribuer et peaufiner la mise en œuvre de SWIMNIO, veuillez vous rendre sur les problèmes et choisir une tâche ou proposer vous-même une amélioration !
Nous souhaitons généralement favoriser les discussions et la mise en œuvre d'implémentations d'adhésion supplémentaires en utilisant un style « Instance » similaire.
Si vous êtes intéressé par de tels algorithmes et que vous avez un protocole favori que vous aimeriez voir implémenté, n'hésitez pas à nous contacter via les problèmes ou les forums Swift.
Les rapports d’expérience, les retours, les idées d’amélioration et les contributions sont grandement encouragés ! Nous sommes impatients de vous entendre.
Veuillez vous référer au guide CONTRIBUTION pour en savoir plus sur le processus de soumission des demandes d'extraction, et vous référer au MANUEL pour la terminologie et d'autres conseils utiles pour travailler avec cette bibliothèque.