Цель этой библиотеки — помочь Swift закрепиться в новом пространстве: кластерных распределенных системах с несколькими узлами.
С помощью этой библиотеки мы предоставляем многоразовые реализации протокола независимого членства во время выполнения, которые можно использовать в различных случаях использования кластеризации.
Протоколы членства в кластерах являются важнейшим строительным блоком для распределенных систем, таких как кластеры с интенсивными вычислениями, планировщики, базы данных, хранилища «ключ-значение» и многое другое. Анонсируя этот пакет, мы стремимся упростить создание таких систем, поскольку им больше не нужно полагаться на внешние сервисы для управления членством в сервисах. Мы также хотели бы пригласить сообщество к сотрудничеству и разработке дополнительных протоколов членства.
По своей сути протоколы членства должны давать ответ на вопрос «Кто мои (живые) коллеги?». Эта, казалось бы, простая задача оказывается совсем не такой простой в распределенной системе, где задержанные или потерянные сообщения, сетевые разделы и неотвечающие, но все еще «живые» узлы являются хлебом насущным. Предсказуемый и надежный ответ на этот вопрос — вот что делают протоколы членства в кластере.
При реализации протокола членства можно пойти на различные компромиссы, и он продолжает оставаться интересной областью исследований и постоянного совершенствования. Таким образом, пакет членства в кластере намерен сосредоточиться не на одной реализации, а служить пространством для совместной работы различных распределенных алгоритмов в этом пространстве.
Для более подробного обсуждения протокола и модификаций этой реализации мы предлагаем прочитать документацию SWIM API, а также соответствующие документы, ссылки на которые приведены ниже.
Масштабируемый слабосогласованный алгоритм членства в группе процессов в стиле заражения (также известный как «SWIM»), а также несколько заметных расширений протокола, как описано в документе Lifeguard: Local Health Awareness для более точного обнаружения сбоев 2018 года.
SWIM — это протокол сплетен, в котором одноранговые узлы периодически обмениваются битами информации о своих наблюдениях за статусами других узлов, в конечном итоге распространяя информацию среди всех остальных членов кластера. Эта категория распределенных алгоритмов очень устойчива к произвольной потере сообщений, разделам сети и подобным проблемам.
На высоком уровне SWIM работает следующим образом:
.ack
. Посмотрите, как A
первоначально исследует B
на диаграмме ниже.payload
сплетен, которая представляет собой (частичную) информацию о том, о каких других узлах знает отправитель сообщения, а также их статус членства ( .alive
, .suspect
и т. д.)..ack
, узел считается все еще .alive
. В противном случае целевой узел мог быть прерван или аварийно завершен или не отвечает по другим причинам..pingRequest
настроенному числу других узлов, которые затем отправляют прямые пинги этому узлу (зондирующий узел). E на схеме ниже)..suspect
,.nack
(«отрицательное подтверждение») в этой ситуации, чтобы сообщить источнику запроса ping о том, что посредник действительно получил эти сообщения .pingRequest
, однако цель, похоже, не ответила. Мы используем эту информацию для настройки локального множителя работоспособности, который влияет на расчет тайм-аутов. Чтобы узнать больше об этом, обратитесь к документации API и документу Lifeguard.Вышеупомянутый механизм служит не только механизмом обнаружения сбоев, но и механизмом распространения информации, который переносит информацию об известных членах кластера. Таким образом, участники в конечном итоге узнают о статусе своих коллег, даже не перечисляя их всех заранее. Однако стоит отметить, что это представление о членстве является слабо последовательным, что означает, что нет никакой гарантии (или способа узнать без дополнительной информации), что все члены имеют одинаковое точное представление о членстве в любой данный момент времени. Тем не менее, это отличный строительный блок для инструментов и систем более высокого уровня, позволяющий создавать более надежные гарантии.
Как только механизм обнаружения сбоев обнаруживает неотвечающий узел, он в конечном итоге помечается как .dead, что приводит к его безвозвратному удалению из кластера. Наша реализация предлагает дополнительное расширение, добавляющее состояние .unreachable к возможным состояниям, однако большинство пользователей не сочтут это необходимым, и по умолчанию оно отключено. Подробную информацию и правила перехода правового статуса см. в SWIM.Status или на следующей диаграмме:
Способ, которым Swift Cluster Membership реализует протоколы, заключается в предложении их « Instances
». Например, реализация SWIM инкапсулирована в независимый от среды выполнения SWIM.Instance
, который должен «управляться» или «интерпретироваться» неким связующим кодом между сетевой средой выполнения и самим экземпляром. Мы называем эти связующие части реализации « Shell
», и библиотека поставляется с SWIMNIOShell
реализованным с использованием SwiftNIO DatagramChannel
, который выполняет весь обмен сообщениями асинхронно через UDP. Альтернативные реализации могут использовать совершенно другие транспортные средства или совмещать сообщения SWIM с какой-либо другой существующей системой распространения сплетен и т. д.
Экземпляр SWIM также имеет встроенную поддержку отправки метрик (с использованием Swift-metrics) и может быть настроен для регистрации подробностей о внутренних деталях путем передачи Swift-log Logger
.
Основная цель этой библиотеки — совместно использовать реализацию SWIM.Instance
в различных реализациях, которым требуется та или иная форма внутрипроцессной службы членства. Реализация пользовательской среды выполнения подробно описана в README проекта (https://github.com/apple/swift-cluster-membership/), поэтому, пожалуйста, загляните туда, если вы заинтересованы во внедрении SWIM через какой-либо другой транспорт.
Реализация нового транспорта сводится к упражнению по «заполнению пробелов»:
Во-первых, необходимо реализовать одноранговые протоколы (https://github.com/apple/swift-cluster-membership/blob/main/Sources/SWIM/Peer.swift), используя целевой транспорт:
/// 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
// ...
}
Обычно это означает, что какое-то соединение, канал или другой идентификатор включает в себя возможность отправлять сообщения и вызывать соответствующие обратные вызовы, когда это применимо.
Затем на принимающей стороне однорангового узла необходимо реализовать получение этих сообщений и вызвать все соответствующие обратные вызовы on<SomeMessage>(...)
определенные в SWIM.Instance
(сгруппированные в SWIMProtocol).
Ниже приведен фрагмент SWIMProtocol, чтобы дать вам представление о нем:
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 ]
// ...
}
Эти вызовы выполняют все задачи, специфичные для протокола SWIM, и возвращают директивы, которые легко интерпретировать как «команды» реализации о том, как она должна реагировать на сообщение. Например, при получении сообщения .pingRequest
возвращаемая директива может дать указание оболочке отправить пинг некоторым узлам. Директива подготавливает всю соответствующую цель, время ожидания и дополнительную информацию, которая упрощает простое следование ее инструкциям и правильную реализацию вызова, например, так:
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
)
}
}
В общем, это позволяет инкапсулировать все сложные вопросы «что делать и когда» в экземпляре протокола, и командному интерпретатору остается только следовать инструкциям, реализующим их. Реальные реализации часто должны будут выполнять некоторые более сложные задачи параллелизма и сетевых операций, такие как ожидание последовательности ответов, их обработка определенным образом и т. д., однако общая схема протокола определяется директивами экземпляра.
Подробную документацию о каждом из обратных вызовов, о том, когда их вызывать и как все это сочетается друг с другом, можно найти в документации по API .
Репозиторий содержит сквозной пример и пример реализации под названием SWIMNIOExample, который использует SWIM.Instance
для включения простой системы однорангового мониторинга на основе UDP. Это позволяет узлам сплетничать и уведомлять друг друга о сбоях узлов с помощью протокола SWIM, отправляя дейтаграммы, управляемые SwiftNIO.
Реализация
SWIMNIOExample
предлагается только в качестве примера и не была реализована с учетом производственного использования, однако при некоторых усилиях она определенно может хорошо подойти для некоторых случаев использования. Если вы хотите узнать больше об алгоритмах членства в кластере, тестировании масштабируемости и использовании самого SwiftNIO, это отличный модуль, который поможет вам разобраться, и, возможно, как только модуль станет достаточно зрелым, мы могли бы рассмотреть возможность сделать его не просто примером, а многоразовый компонент для кластерных приложений на базе Swift NIO.
В простейшей форме, объединив предоставленный экземпляр SWIM и оболочку NIO для создания простого сервера, можно встроить предоставленные обработчики, как показано ниже, в типичный конвейер каналов NIO:
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 )
Затем пример обработчика может получать и обрабатывать события изменения членства в кластере 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 ) " ,
] )
}
}
Если вы заинтересованы в том, чтобы внести свой вклад и усовершенствовать реализацию SWIMNIO, перейдите к разделу «Проблемы» и выберите задачу или предложите улучшение самостоятельно!
Обычно мы заинтересованы в содействии обсуждению и реализации дополнительных реализаций членства с использованием аналогичного стиля «Экземпляр».
Если вы заинтересованы в таких алгоритмах и у вас есть любимый протокол, который вы хотели бы реализовать, не стесняйтесь обращаться к Heve через вопросы или на форумах Swift.
Отчеты об опыте, отзывы, идеи по улучшению и вклад очень приветствуются! Мы с нетерпением ждем вашего ответа.
Пожалуйста, обратитесь к руководству CONTRIBUTING, чтобы узнать о процессе отправки запросов на включение, а также обратитесь к РУКОВОДСТВУ за терминологией и другими полезными советами по работе с этой библиотекой.