Esta biblioteca tiene como objetivo ayudar a Swift a ganar terreno en un nuevo espacio: los sistemas distribuidos agrupados de múltiples nodos.
Con esta biblioteca proporcionamos implementaciones de protocolos de membresía independientes del tiempo de ejecución reutilizables que se pueden adoptar en varios casos de uso de agrupación.
Los protocolos de membresía de clústeres son un componente crucial para los sistemas distribuidos, como clústeres de computación intensiva, programadores, bases de datos, almacenes de valores clave y más. Con el anuncio de este paquete, nuestro objetivo es simplificar la construcción de dichos sistemas, ya que ya no necesitan depender de servicios externos para gestionar la membresía del servicio. También nos gustaría invitar a la comunidad a colaborar y desarrollar protocolos de membresía adicionales.
En esencia, los protocolos de membresía deben proporcionar una respuesta a la pregunta "¿Quiénes son mis pares (vivos)?". Esta tarea aparentemente simple resulta no serlo en absoluto en un sistema distribuido donde los mensajes retrasados o perdidos, las particiones de la red y los nodos que no responden pero aún están "vivos" son el pan de cada día. Proporcionar una respuesta predecible y confiable a esta pregunta es lo que hacen los protocolos de membresía de clústeres.
Hay varias compensaciones que se pueden tomar al implementar un protocolo de membresía, y sigue siendo un área interesante de investigación y perfeccionamiento continuo. Como tal, el paquete de membresía del clúster pretende centrarse no en una única implementación, sino que sirve como un espacio de colaboración para varios algoritmos distribuidos en este espacio.
Para una discusión más profunda del protocolo y las modificaciones en esta implementación, sugerimos leer la documentación de SWIM API, así como los documentos asociados vinculados a continuación.
El algoritmo de membresía del grupo de procesos de estilo de infección escalable y débilmente consistente (también conocido como "SWIM"), junto con algunas extensiones de protocolo notables, como se documenta en el documento Lifeguard: Local Health Awareness for More Accurate Failure 2018 de 2018.
SWIM es un protocolo de chismes en el que los pares intercambian periódicamente bits de información sobre sus observaciones del estado de otros nodos, y eventualmente difunden la información a todos los demás miembros de un grupo. Esta categoría de algoritmos distribuidos es muy resistente a la pérdida arbitraria de mensajes, particiones de red y problemas similares.
En un nivel alto, SWIM funciona así:
.ack
. Vea cómo A
sondea B
inicialmente en el siguiente diagrama.payload
de chismes, que es información (parcial) sobre qué otros pares conoce el remitente del mensaje, junto con su estado de membresía ( .alive
, .suspect
, etc.)..ack
, se considera que el par todavía .alive
. De lo contrario, es posible que el par objetivo haya finalizado/fallado o no responda por otros motivos..pingRequest
a un número configurado de otros pares, que luego emiten pings directos a ese par (pares de sondeo). E en el diagrama siguiente)..suspect
,.nack
("reconocimiento negativo") adicionales en la situación para informar al origen de la solicitud de ping que el intermediario recibió esos mensajes .pingRequest
, sin embargo, el objetivo parece no haber respondido. Usamos esta información para ajustar un multiplicador de salud local, que afecta la forma en que se calculan los tiempos de espera. Para obtener más información sobre esto, consulte los documentos API y el documento Lifeguard.El mecanismo anterior sirve no sólo como mecanismo de detección de fallos, sino también como mecanismo de chismes, que transporta información sobre miembros conocidos del clúster. De esta manera, los miembros eventualmente aprenden sobre el estado de sus pares, incluso sin tenerlos a todos en una lista por adelantado. Sin embargo, vale la pena señalar que esta visión de la membresía es débilmente consistente, lo que significa que no hay garantía (o forma de saberlo, sin información adicional) si todos los miembros tienen exactamente la misma visión sobre la membresía en un momento dado. Sin embargo, es un excelente componente básico para que las herramientas y sistemas de nivel superior construyan garantías más sólidas.
Una vez que el mecanismo de detección de fallas detecta un nodo que no responde, eventualmente se marca como .dead, lo que resulta en su eliminación irrevocable del clúster. Nuestra implementación ofrece una extensión opcional, agregando un estado .unreachable a los estados posibles; sin embargo, la mayoría de los usuarios no lo encontrarán necesario y está deshabilitado de forma predeterminada. Para obtener detalles y reglas sobre transiciones de estatus legal, consulte SWIM.Status o el siguiente diagrama:
La forma en que Swift Cluster Membership implementa protocolos es ofreciendo " Instances
" de ellos. Por ejemplo, la implementación de SWIM está encapsulada en la instancia SWIM.Instance
independiente del tiempo de ejecución, que debe ser "impulsada" o "interpretada" por algún código adhesivo entre un tiempo de ejecución de red y la instancia misma. A esas piezas adhesivas de una implementación las llamamos " Shell
", y la biblioteca se envía con un SWIMNIOShell
implementado utilizando DatagramChannel
de SwiftNIO que realiza todos los mensajes de forma asincrónica a través de UDP. Las implementaciones alternativas pueden utilizar transportes completamente diferentes, o incorporar mensajes SWIM en algún otro sistema de chismes existente, etc.
La instancia SWIM también tiene soporte integrado para emitir métricas (usando métricas rápidas) y se puede configurar para registrar detalles sobre detalles internos pasando un Logger
de registro rápido.
El objetivo principal de esta biblioteca es compartir la implementación de SWIM.Instance
entre varias implementaciones que necesitan algún tipo de servicio de membresía en proceso. La implementación de un tiempo de ejecución personalizado está documentada en profundidad en el archivo README del proyecto (https://github.com/apple/swift-cluster-membership/), así que eche un vistazo allí si está interesado en implementar SWIM en algún transporte diferente.
La implementación de un nuevo transporte se reduce a un ejercicio de “llenar los espacios en blanco”:
Primero, hay que implementar los protocolos de pares (https://github.com/apple/swift-cluster-membership/blob/main/Sources/SWIM/Peer.swift) utilizando el transporte de destino:
/// 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
// ...
}
Lo que generalmente significa incluir alguna conexión, canal u otra identidad con la capacidad de enviar mensajes e invocar las devoluciones de llamada apropiadas cuando corresponda.
Luego, en el extremo receptor de un par, uno tiene que implementar la recepción de esos mensajes e invocar todas las devoluciones de llamada correspondientes on<SomeMessage>(...)
definidas en SWIM.Instance
(agrupadas en SWIMProtocol).
A continuación se enumera una parte del SWIMProtocol para darle una idea al respecto:
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 ]
// ...
}
Estas llamadas realizan todas las tareas específicas del protocolo SWIM internamente y devuelven directivas que son "comandos" fáciles de interpretar para una implementación sobre cómo debe reaccionar al mensaje. Por ejemplo, al recibir un mensaje .pingRequest
, la directiva devuelta puede indicarle a un shell que envíe un ping a algunos nodos. La directiva prepara todos los objetivos, tiempos de espera e información adicional adecuados que hacen que sea más sencillo seguir sus instrucciones e implementar la llamada correctamente, por ejemplo, así:
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 general, esto permite encapsular todo el complicado "qué hacer y cuándo" dentro de la instancia del protocolo, y un Shell solo tiene que seguir las instrucciones para implementarlo. Las implementaciones reales a menudo necesitarán realizar algunas tareas de red y concurrencia más complicadas, como esperar una secuencia de respuestas y manejarlas de una manera específica, etc. Sin embargo, el esquema general del protocolo está orquestado por las directivas de la instancia.
Para obtener documentación detallada sobre cada una de las devoluciones de llamada, cuándo invocarlas y cómo encaja todo esto, consulte la documentación de API .
El repositorio contiene un ejemplo de un extremo a otro y una implementación de ejemplo llamada SWIMNIOExample que utiliza SWIM.Instance
para habilitar un sistema de monitoreo de pares simple basado en UDP. Esto permite a los pares cotillear y notificarse entre sí sobre fallas de nodos utilizando el protocolo SWIM mediante el envío de datagramas impulsados por SwiftNIO.
La implementación
SWIMNIOExample
se ofrece solo como ejemplo y no se ha implementado con el uso en producción en mente; sin embargo, con algo de esfuerzo definitivamente podría funcionar bien en algunos casos de uso. Si está interesado en aprender más sobre los algoritmos de membresía de clústeres, la evaluación comparativa de escalabilidad y el uso del propio SwiftNIO, este es un excelente módulo para comenzar, y tal vez una vez que el módulo esté lo suficientemente maduro, podríamos considerar convertirlo no solo en un ejemplo, sino en un Componente reutilizable para aplicaciones en clúster basadas en Swift NIO.
En su forma más simple, combinando la instancia SWIM proporcionada y el shell NIO para construir un servidor simple, se pueden integrar los controladores proporcionados como se muestra a continuación, en una canalización de canal NIO típica:
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 )
El controlador de ejemplo puede luego recibir y controlar eventos de cambio de membresía del clúster 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 está interesado en contribuir y pulir la implementación de SWIMNIO, diríjase a los problemas y elija una tarea o proponga una mejora usted mismo.
En general, estamos interesados en fomentar debates e implementaciones de implementaciones de membresía adicionales utilizando un estilo de "Instancia" similar.
Si está interesado en dichos algoritmos y tiene un protocolo favorito que le gustaría que se implementara, no dude en comunicarse con Heve a través de Issues o en los foros de Swift.
¡Se recomiendan mucho los informes de experiencia, los comentarios, las ideas de mejora y las contribuciones! Esperamos saber de usted.
Consulte la guía CONTRIBUCIÓN para conocer el proceso de envío de solicitudes de extracción y consulte el MANUAL para obtener terminología y otros consejos útiles para trabajar con esta biblioteca.