تهدف هذه المكتبة إلى مساعدة Swift على تحقيق تقدم في مساحة جديدة: الأنظمة الموزعة المجمعة متعددة العقد.
من خلال هذه المكتبة، نقدم تطبيقات بروتوكول العضوية الحيادية القابلة لإعادة الاستخدام والتي يمكن اعتمادها في حالات استخدام التجميع المختلفة.
تعد بروتوكولات عضوية المجموعة بمثابة لبنة بناء مهمة للأنظمة الموزعة، مثل المجموعات الحسابية المكثفة والمجدولات وقواعد البيانات ومخازن القيمة الرئيسية والمزيد. ومع الإعلان عن هذه الحزمة، نهدف إلى جعل بناء مثل هذه الأنظمة أكثر بساطة، حيث لم تعد بحاجة إلى الاعتماد على الخدمات الخارجية للتعامل مع عضوية الخدمة الخاصة بها. نود أيضًا أن ندعو المجتمع للتعاون في تطوير بروتوكولات عضوية إضافية.
في جوهرها، تحتاج بروتوكولات العضوية إلى تقديم إجابة للسؤال "من هم زملائي (الأحياء)؟". تبين أن هذه المهمة التي تبدو بسيطة ليست بهذه البساطة على الإطلاق في نظام موزع حيث تكون الرسائل المتأخرة أو المفقودة وأقسام الشبكة والعقد غير المستجيبة ولكنها لا تزال "حية" هي الخبز والزبدة اليومية. إن تقديم إجابة يمكن التنبؤ بها وموثوقة لهذا السؤال هو ما تفعله بروتوكولات عضوية المجموعة.
هناك العديد من المقايضات التي يمكن للمرء القيام بها أثناء تنفيذ بروتوكول العضوية، ولا يزال هذا مجالًا مثيرًا للاهتمام للبحث والتحسين المستمر. على هذا النحو، تهدف حزمة عضوية المجموعة إلى التركيز ليس على تنفيذ واحد، ولكن بمثابة مساحة تعاون لمختلف الخوارزميات الموزعة في هذا الفضاء.
لإجراء مناقشة أكثر تعمقًا حول البروتوكول والتعديلات في هذا التنفيذ، نقترح قراءة وثائق SWIM API، بالإضافة إلى الأوراق المرتبطة المرتبطة أدناه.
خوارزمية عضوية مجموعة العمليات القابلة للتطوير والمتسقة بشكل ضعيف (المعروفة أيضًا باسم "SWIM")، إلى جانب عدد قليل من امتدادات البروتوكول البارزة كما هو موثق في ورقة الإنقاذ لعام 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 البروتوكولات هي من خلال تقديم " Instances
" منها. على سبيل المثال، يتم تغليف تطبيق SWIM في SWIM.Instance
الحيادي لوقت التشغيل والذي يحتاج إلى "إدارته" أو "تفسيره" بواسطة بعض التعليمات البرمجية اللاصقة بين وقت تشغيل الشبكة والمثيل نفسه. نطلق على هذه الأجزاء اللاصقة من التنفيذ " Shell
s"، وتأتي المكتبة مزودة بـ SWIMNIOShell
الذي تم تنفيذه باستخدام DatagramChannel
الخاص بـ SwiftNIO والذي ينفذ جميع الرسائل بشكل غير متزامن عبر UDP. يمكن أن تستخدم التطبيقات البديلة وسائل نقل مختلفة تمامًا، أو تحمل رسائل SWIM على بعض أنظمة القيل والقال الموجودة الأخرى وما إلى ذلك.
يحتوي مثيل SWIM أيضًا على دعم مدمج لإصدار المقاييس (باستخدام المقاييس السريعة) ويمكن تهيئته لتسجيل تفاصيل حول التفاصيل الداخلية عن طريق تمرير 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).
تم إدراج جزء من بروتوكول السباحة أدناه لإعطائك فكرة عنه:
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
، قد يأمر التوجيه الذي تم إرجاعه الصدفة بإرسال اختبار ping إلى بعض العقد. يقوم التوجيه بإعداد كل الهدف المناسب والمهلة والمعلومات الإضافية التي تجعل من السهل اتباع تعليماته وتنفيذ المكالمة بشكل صحيح، على سبيل المثال كما يلي:
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
)
}
}
بشكل عام، يسمح هذا بتغليف جميع الأسئلة الصعبة "ما يجب فعله ومتى" داخل مثيل البروتوكول، ويجب على Shell فقط اتباع التعليمات التي تنفذها. غالبًا ما تحتاج التطبيقات الفعلية إلى تنفيذ بعض المهام المتزامنة والشبكية الأكثر تعقيدًا، مثل انتظار سلسلة من الاستجابات، والتعامل معها بطريقة محددة وما إلى ذلك، ومع ذلك، يتم تنسيق المخطط العام للبروتوكول من خلال توجيهات المثيل.
للحصول على وثائق مفصلة حول كل من عمليات الاسترجاعات، ومتى يتم استدعاؤها، وكيف يتناسب كل ذلك معًا، يرجى الرجوع إلى وثائق واجهة برمجة التطبيقات (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 وتحسينه، فيرجى التوجه إلى المشكلات واختيار مهمة أو اقتراح تحسين بنفسك!
نحن مهتمون عمومًا بتعزيز المناقشات وتنفيذ عمليات تنفيذ العضوية الإضافية باستخدام نمط "مثيل" مشابه.
إذا كنت مهتمًا بمثل هذه الخوارزميات، ولديك بروتوكول مفضل ترغب في تنفيذه، فيرجى عدم التردد في التواصل معنا عبر المشكلات أو منتديات Swift.
يتم تشجيع تقارير الخبرة والتعليقات وأفكار التحسين والمساهمات بشكل كبير! ونحن نتطلع إلى سماع منك.
يرجى الرجوع إلى دليل المساهمة للتعرف على عملية إرسال طلبات السحب، والرجوع إلى الدليل للحصول على المصطلحات والنصائح المفيدة الأخرى للعمل مع هذه المكتبة.