Esta biblioteca tem como objetivo ajudar o Swift a abrir caminho em um novo espaço: sistemas distribuídos em cluster com vários nós.
Com esta biblioteca, fornecemos implementações de protocolo de associação agnósticas em tempo de execução reutilizáveis que podem ser adotadas em vários casos de uso de cluster.
Os protocolos de associação de cluster são um alicerce crucial para sistemas distribuídos, como clusters de computação intensiva, agendadores, bancos de dados, armazenamentos de valores-chave e muito mais. Com o anúncio deste pacote, pretendemos simplificar a construção de tais sistemas, uma vez que já não necessitam de depender de serviços externos para lidar com a adesão ao serviço. Gostaríamos também de convidar a comunidade a colaborar e desenvolver protocolos adicionais de adesão.
Em sua essência, os protocolos de adesão precisam fornecer uma resposta para a pergunta “Quem são meus pares (vivos)?”. Essa tarefa aparentemente simples acaba não sendo tão simples em um sistema distribuído onde mensagens atrasadas ou perdidas, partições de rede e nós que não respondem, mas ainda "vivos", são o pão com manteiga de cada dia. Fornecer uma resposta previsível e confiável a essa pergunta é o que os protocolos de associação de cluster fazem.
Existem vários compromissos que podem ser assumidos durante a implementação de um protocolo de adesão, e esta continua a ser uma área interessante de investigação e refinamento contínuo. Como tal, o pacote de membros do cluster pretende focar não numa única implementação, mas servir como um espaço de colaboração para vários algoritmos distribuídos neste espaço.
Para uma discussão mais aprofundada do protocolo e das modificações nesta implementação, sugerimos a leitura da documentação da API SWIM, bem como dos documentos associados vinculados abaixo.
O algoritmo de associação de grupo de processo de estilo de infecção escalonável e fracamente consistente (também conhecido como "SWIM"), junto com algumas extensões de protocolo notáveis, conforme documentado no documento Lifeguard: Local Health Awareness for More Accurate Failure Detection de 2018.
SWIM é um protocolo de fofoca no qual os pares trocam periodicamente informações sobre suas observações do status de outros nós, eventualmente espalhando as informações para todos os outros membros de um cluster. Esta categoria de algoritmos distribuídos é muito resistente contra perda arbitrária de mensagens, partições de rede e problemas semelhantes.
Em alto nível, o SWIM funciona assim:
.ack
seja enviado de volta. Veja como A
sonda B
inicialmente no diagrama abaixo.payload
de fofoca, que é informação (parcial) sobre o que outros pares que o remetente da mensagem conhece, juntamente com seu status de membro ( .alive
, .suspect
, etc.).ack
, o par será considerado ainda .alive
. Caso contrário, o peer de destino pode ter encerrado/travado ou não responder por outros motivos..pingRequest
para um número configurado de outros peers, que então emitem pings diretos para esse peer (peer de sondagem E no diagrama abaixo)..suspect
,.nack
("reconhecimento negativo") adicionais na situação para informar a origem da solicitação de ping que o intermediário recebeu essas mensagens .pingRequest
, porém o alvo parece não ter respondido. Usamos essas informações para ajustar um Multiplicador de Saúde Local, que afeta a forma como os tempos limite são calculados. Para saber mais sobre isso, consulte a documentação da API e o artigo do Lifeguard.O mecanismo acima serve não apenas como um mecanismo de detecção de falhas, mas também como um mecanismo de fofoca, que carrega informações sobre membros conhecidos do cluster. Dessa forma, os membros eventualmente aprendem sobre o status de seus pares, mesmo sem ter todos listados antecipadamente. Vale a pena salientar, no entanto, que esta visão de adesão é fracamente consistente, o que significa que não há garantia (ou forma de saber, sem informações adicionais) se todos os membros têm a mesma opinião exata sobre a adesão num determinado momento. No entanto, é um excelente alicerce para que ferramentas e sistemas de nível superior construam garantias mais sólidas.
Depois que o mecanismo de detecção de falhas detecta um nó que não responde, ele eventualmente é marcado como .dead, resultando em sua remoção irrevogável do cluster. Nossa implementação oferece uma extensão opcional, adicionando um estado .unreachable aos estados possíveis, porém a maioria dos usuários não achará isso necessário e está desabilitado por padrão. Para obter detalhes e regras sobre transições de status legal, consulte SWIM.Status ou o diagrama a seguir:
A forma como o Swift Cluster Membership implementa protocolos é oferecendo " Instances
" deles. Por exemplo, a implementação SWIM é encapsulada no SWIM.Instance
independente de tempo de execução, que precisa ser “conduzido” ou “interpretado” por algum código cola entre um tempo de execução de rede e a própria instância. Chamamos essas peças adesivas de uma implementação de " Shell
s", e a biblioteca vem com um SWIMNIOShell
implementado usando DatagramChannel
do SwiftNIO que executa todas as mensagens de forma assíncrona por UDP. Implementações alternativas podem usar transportes completamente diferentes ou carregar mensagens SWIM em algum outro sistema de fofoca existente, etc.
A instância SWIM também possui suporte integrado para emissão de métricas (usando swift-metrics) e pode ser configurada para registrar detalhes sobre detalhes internos, passando um swift-log Logger
.
O objetivo principal desta biblioteca é compartilhar a implementação SWIM.Instance
entre várias implementações que precisam de alguma forma de serviço de associação em processo. A implementação de um tempo de execução personalizado está documentada detalhadamente no README do projeto (https://github.com/apple/swift-cluster-membership/), portanto, dê uma olhada lá se estiver interessado em implementar o SWIM em algum transporte diferente.
A implementação de um novo transporte resume-se a um exercício de “preencher as lacunas”:
Primeiro, é necessário implementar os protocolos Peer (https://github.com/apple/swift-cluster-membership/blob/main/Sources/SWIM/Peer.swift) usando o 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
// ...
}
O que geralmente significa envolver alguma conexão, canal ou outra identidade com a capacidade de enviar mensagens e invocar os retornos de chamada apropriados, quando aplicável.
Então, no lado receptor de um peer, é necessário implementar o recebimento dessas mensagens e invocar todos os retornos de chamada on<SomeMessage>(...)
correspondentes definidos no SWIM.Instance
(agrupados em SWIMProtocol).
Uma parte do protocolo SWIM está listada abaixo para você ter uma ideia:
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 ]
// ...
}
Essas chamadas executam todas as tarefas específicas do protocolo SWIM internamente e retornam diretivas que são “comandos” simples de interpretar para uma implementação sobre como ela deve reagir à mensagem. Por exemplo, ao receber uma mensagem .pingRequest
, a diretiva retornada pode instruir um shell a enviar um ping para alguns nós. A diretiva prepara todas as informações apropriadas de alvo, tempo limite e adicionais que tornam mais simples seguir suas instruções e implementar a chamada corretamente, por exemplo, assim:
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
)
}
}
Em geral, isso permite que todos os complicados "o que fazer quando" sejam encapsulados na instância do protocolo, e um Shell só precisa seguir as instruções para implementá-los. As implementações reais muitas vezes precisarão realizar algumas tarefas mais envolvidas de simultaneidade e rede, como aguardar uma sequência de respostas e tratá-las de uma maneira específica, etc., no entanto, o esboço geral do protocolo é orquestrado pelas diretivas da instância.
Para documentação detalhada sobre cada um dos retornos de chamada, quando invocá-los e como tudo isso se encaixa, consulte a Documentação da API .
O repositório contém um exemplo ponta a ponta e um exemplo de implementação chamado SWIMNIOExample que faz uso do SWIM.Instance
para habilitar um sistema simples de monitoramento de pares baseado em UDP. Isso permite que os pares fofocem e notifiquem uns aos outros sobre falhas de nós usando o protocolo SWIM, enviando datagramas conduzidos pelo SwiftNIO.
A implementação
SWIMNIOExample
é oferecida apenas como exemplo e não foi implementada com o uso em produção em mente; no entanto, com algum esforço, ela definitivamente poderia funcionar bem para alguns casos de uso. Se você estiver interessado em aprender mais sobre algoritmos de associação de cluster, benchmarking de escalabilidade e uso do próprio SwiftNIO, este é um ótimo módulo para começar, e talvez quando o módulo estiver maduro o suficiente, possamos considerar torná-lo não apenas um exemplo, mas um componente reutilizável para aplicativos em cluster baseados em Swift NIO.
Em sua forma mais simples, combinando a instância SWIM fornecida e o shell NIO para construir um servidor simples, pode-se incorporar os manipuladores fornecidos como mostrado abaixo, em um pipeline de canal NIO típico:
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 )
O manipulador de exemplo pode então receber e manipular eventos de alteração de associação de 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 ) " ,
] )
}
}
Se você estiver interessado em contribuir e aprimorar a implementação do SWIMNIO, vá até os problemas e escolha uma tarefa ou proponha você mesmo uma melhoria!
Geralmente estamos interessados em promover discussões e implementações de implementações de associação adicionais usando um estilo de "Instância" semelhante.
Se você estiver interessado em tais algoritmos e tiver um protocolo favorito que gostaria de ver implementado, não hesite em entrar em contato com heve por meio de problemas ou dos fóruns do Swift.
Relatos de experiência, feedback, ideias de melhoria e contribuições são altamente incentivados! Estamos ansiosos para ouvir de você.
Consulte o guia CONTRIBUTING para aprender sobre o processo de envio de pull requests e consulte o HANDBOOK para terminologia e outras dicas úteis para trabalhar com esta biblioteca.