أو كما كتبنا مكتبة عميل C ++ لـ ZooKeeper و etcd و Consul KV
في عالم الأنظمة الموزعة ، هناك عدد من المهام النموذجية: تخزين المعلومات حول تكوين الكتلة ، وإدارة تكوين العقد ، واكتشاف العقد الفاشلة ، واختيار زعيم ،
وغيرها . لحل هذه المشكلات ، تم إنشاء أنظمة موزعة خاصة - خدمات التنسيق. الآن سنهتم بثلاثة منهم: ZooKeeper و etcd و القنصل. من بين جميع وظائف القنصل الغنية ، سنركز على القنصل KV.

في الواقع ، كل هذه الأنظمة عبارة عن مخازن ذات قيمة خطية تتحمل الأخطاء. على الرغم من أن نماذج البيانات الخاصة بهم لها اختلافات كبيرة ، والتي سنناقشها لاحقًا ، فإنها تسمح لنا بحل المشكلات العملية نفسها. من الواضح أن كل تطبيق يستخدم خدمة التنسيق مرتبط بأحدها ، مما قد يؤدي إلى الحاجة إلى دعم العديد من الأنظمة التي تحل نفس المهام في مركز بيانات واحد لتطبيقات مختلفة.
نشأت فكرة صممت لحل هذه المشكلة في وكالة استشارية أسترالية ، وكان علينا ، نحن فريق صغير من الطلاب ، تنفيذها ، والتي سأخبركم بها.
تمكنا من إنشاء مكتبة توفر واجهة مشتركة للعمل مع ZooKeeper و etcd و Consul KV. المكتبة مكتوبة بلغة C ++ ، ولكن هناك خطط للانتقال إلى لغات أخرى.
نماذج البيانات
لتطوير واجهة مشتركة لثلاثة أنظمة مختلفة ، تحتاج إلى فهم ما هو مشترك بينهم وكيف تختلف. هيا بنا
حارس الحديقة
يتم تنظيم المفاتيح في شجرة وتسمى العقد (znodes). وفقًا لذلك ، بالنسبة للموقع ، يمكنك الحصول على قائمة بأطفاله. عمليات إنشاء znode (إنشاء) وتغيير القيمة (setData) منفصلة: فقط المفاتيح الموجودة يمكنها قراءة وتغيير القيم. يمكن إرفاق الساعات بعمليات التحقق من وجود عقدة ، وقراءة القيمة ، وإنجاب الأطفال. Watch هي مشغل يتم تشغيله لمرة واحدة ويتم إطلاقه عند تغيير إصدار البيانات المطابقة على الخادم. تستخدم العقد المؤقتة للكشف عن الفشل. يتم إرفاقها بجلسة العميل الذي قام بإنشائها. عندما يغلق العميل جلسة ما أو يتوقف عن إخطار ZooKeeper بوجودها ، يتم حذف هذه العقد تلقائيًا. يتم دعم المعاملات البسيطة - مجموعة من العمليات التي تنجح أو تفشل جميعها ، إذا كان واحدًا منها على الأقل مستحيلًا.
etcd
استلهم مطورو هذا النظام بشكل واضح من ZooKeeper ، وبالتالي فعلوا كل شيء بطريقة مختلفة. التسلسل الهرمي للمفتاح ليس هنا ، لكنهم يشكلون مجموعة مرتبة معجميًا. يمكنك الحصول على أو حذف جميع المفاتيح التي تنتمي إلى نطاق معين. قد يبدو هذا الهيكل غريبًا ، لكنه في الحقيقة معبر جدًا ، ويتم محاكاة وجهة النظر الهرمية من خلاله بسهولة.
لا توجد عملية مقارنة وضبط معيارية في etcd ، ولكن هناك شيء أفضل - المعاملات. بطبيعة الحال ، فهي في جميع النظم الثلاثة ، ولكن في المعاملات etcd جيدة بشكل خاص. وهي تتألف من ثلاث كتل: الاختيار ، النجاح ، الفشل. الكتلة الأولى تحتوي على مجموعة من الشروط ، الثانية والثالثة - العمليات. يتم تنفيذ الصفقة ذريًا. إذا كانت جميع الشروط صحيحة ، فسيتم تنفيذ كتلة النجاح ، وإلا - الفشل. في الإصدار 3.3 من API ، يمكن أن تحتوي كتل النجاح والفشل على المعاملات المتداخلة. وهذا يعني أنه من الممكن تنفيذ الإنشاءات الشرطية بمستوى تعسفي تقريبًا من التعشيش. يمكنك معرفة المزيد حول عمليات الفحص والعمليات الموجودة من
الوثائق .
الساعات موجودة أيضًا هنا ، على الرغم من أنها أكثر تعقيدًا وقابلة لإعادة الاستخدام. أي بعد تثبيت الساعة على مجموعة من المفاتيح ، ستتلقى جميع التحديثات في هذا النطاق حتى تقوم بإلغاء المشاهدة ، وليس فقط الأولى. في etcd ، تعادل عقود الإيجار جلسات عمل عميل ZooKeeper.
القنصل KVلا يوجد أيضًا هيكل هرمي صارم ، لكن القنصل يمكنه إنشاء المظهر الموجود: يمكنك استلام وحذف جميع المفاتيح بالبادئة المحددة ، أي العمل مع "الشجرة الفرعية" للمفتاح. وتسمى هذه الاستفسارات العودية. بالإضافة إلى ذلك ، يمكن القنصل فقط تحديد المفاتيح التي لا تحتوي على الحرف المحدد بعد البادئة ، والتي تتوافق مع استلام "الأطفال" الفوري. ولكن تجدر الإشارة إلى أن هذا هو بالضبط مظهر الهيكل الهرمي: من الممكن تمامًا إنشاء مفتاح إذا كان والده غير موجود أو حذف مفتاح به أطفال ، في حين سيستمر تخزين الأطفال في النظام.

بدلاً من الساعات ، يتم حظر طلبات HTTP في القنصل. في جوهرها ، هذه هي مكالمات عادية لطريقة قراءة البيانات ، والتي ، إلى جانب غيرها من المعالم ، تتم الإشارة إلى آخر نسخة معروفة من البيانات. إذا كان الإصدار الحالي من البيانات المطابقة على الخادم أكبر من الإصدار المحدد ، يتم إرجاع الاستجابة على الفور ، وإلا ، عندما تتغير القيمة. هناك أيضًا جلسات هنا يمكن إرفاقها بالمفاتيح في أي وقت. تجدر الإشارة إلى أنه على عكس etcd و ZooKeeper ، حيث يؤدي حذف الجلسات إلى إزالة المفاتيح ذات الصلة ، هناك وضع يتم فيه فصل الجلسة ببساطة عنهم.
المعاملات المتاحة ، دون المتفرعة ، ولكن مع جميع أنواع الشيكات.
أحضرها جميعًا معًا
يحتوي نموذج البيانات الأكثر صرامة على ZooKeeper. لا يمكن محاكاة طلبات النطاق التعبيرية المتوفرة في etcd بكفاءة في ZooKeeper أو القنصل. في محاولة للاستفادة القصوى من جميع الخدمات ، حصلنا على واجهة مكافئة تقريبًا لواجهة ZooKeeper مع الاستثناءات المهمة التالية:
- لا يتم دعم عقد تسلسل وحاوية و TTL
- قوائم ACL غير مدعومة
- تنشئ الطريقة المحددة مفتاحًا في حالة عدم وجوده (في ZK setData تُرجع خطأً في هذه الحالة)
- مجموعة وطرق cas منفصلة (في ZK ، فهي في الأساس نفس الشيء)
- طريقة المسح تحذف قمة الرأس مع الشجرة الفرعية (في ZK ، تقوم الحذف بإرجاع خطأ إذا كانت القمة تحتوي على أطفال)
- لكل مفتاح هناك إصدار واحد فقط - إصدار القيمة (في ZK هناك ثلاثة منهم )
يرجع رفض العقد المتسلسلة إلى حقيقة أنه لا يوجد دعم مدمج لها في etcd و Consul ، بالإضافة إلى واجهة المكتبة الناتجة يمكن للمستخدم تنفيذها بسهولة.
يتطلب تنفيذ نفس السلوك عند إزالة أعلى ZooKeeper الحفاظ على عداد الأطفال منفصلة في etcd و القنصل لكل مفتاح. نظرًا لأننا حاولنا تجنب تخزين معلومات التعريف ، فقد تقرر حذف الشجرة الفرعية بأكملها.
دقة التنفيذ
دعونا نفكر بمزيد من التفصيل في بعض جوانب تطبيق واجهة المكتبة في أنظمة مختلفة.
التسلسل الهرمي في الخكانت المحافظة على النظرة الهرمية في etcd واحدة من أكثر المهام إثارة للاهتمام. تسهل طلبات النطاق الحصول على قائمة بالمفاتيح ذات بادئة محددة. على سبيل المثال ، إذا كنت تريد كل شيء يبدأ بـ
"/foo"
، فإنك تطلب النطاق
["/foo", "/fop")
. لكن هذا من شأنه أن يعيد الشجرة بأكملها كاملة من المفتاح ، والتي قد لا تكون مقبولة إذا كانت الشجرة الفرعية كبيرة. في البداية ، خططنا لاستخدام آلية التحويل الرئيسية
المطبقة في zetcd . يتضمن إضافة بايت واحد في بداية المفتاح ، مساويًا لعمق العقدة في الشجرة. سأقدم مثالا.
"/foo" -> "\u01/foo" "/foo/bar" -> "\u02/foo/bar"
ثم يمكنك الحصول على جميع الأطفال المباشرين لمفتاح
"/foo"
خلال الاستعلام عن النطاق
["\u02/foo/", "\u02/foo0")
. نعم ، في ASCII ،
"0"
يتبع مباشرة
"/"
.
ولكن كيف ، إذن ، لحذف قمة الرأس؟ اتضح أنك بحاجة إلى حذف جميع نطاقات النموذج
["\uXX/foo/", "\uXX/foo0")
لـ XX من 01 إلى FF. ثم واجهنا
حدًا لعدد العمليات داخل معاملة واحدة.
نتيجة لذلك ، تم اختراع نظام بسيط لتحويل المفتاح ، مما سمح لنا بالتنفيذ الفعال لكل من إزالة المفتاح وتلقي قائمة بالأطفال. يكفي لإضافة رمز خاص قبل الرمز المميز الأخير. على سبيل المثال:
"/very" -> "/\u00very" "/very/long" -> "/very/\u00long" "/very/long/path" -> "/very/long/\u00path"
ثم يتحول حذف المفتاح
"/very"
إلى حذف
"/\u00very"
والنطاق
["/very/", "/very0")
،
["/very/", "/very0")
جميع الأطفال على طلب مفاتيح من النطاق
["/very/\u00", "/very/\u01")
.
إزالة مفتاح في ZooKeeperكما ذكرت بالفعل ، لا يمكنك حذف عقدة في ZooKeeper إذا كان لديها أطفال. نريد حذف المفتاح مع الشجرة الفرعية. كيف تكون نحن نفعل ذلك بتفاؤل. أولاً ، نجتاز الشجرة الفرعية بشكل متكرر ، ونضع أطفال كل قمة في استعلام منفصل. ثم نقوم ببناء معاملة تحاول حذف جميع عقد الشجرة الفرعية بالترتيب الصحيح. بالطبع ، يمكن أن تحدث التغييرات بين قراءة الشجرة الفرعية وحذفها. في هذه الحالة ، ستفشل المعاملة. علاوة على ذلك ، قد تتغير الشجرة الفرعية أثناء عملية القراءة. قد يُرجع استعلام للأطفال التابع للعقدة التالية خطأ إذا تم حذف قمة الرأس هذه ، على سبيل المثال. في كلتا الحالتين ، نكرر العملية بأكملها مرة أخرى.
هذا النهج يجعل حذف مفتاح غير فعال للغاية إذا كان لديه أطفال ، وأكثر من ذلك إذا استمر التطبيق في العمل مع الشجرة الفرعية وحذف وإنشاء المفاتيح. ومع ذلك ، سمح لنا هذا بعدم تعقيد تنفيذ أساليب أخرى في الخ والقنصل.
المنصوص عليها في ZooKeeperفي ZooKeeper ، هناك طرق منفصلة تعمل مع بنية الشجرة (الإنشاء ، الحذف ، getChildren) والتي تعمل مع البيانات في العقد (setData ، getData). جميع الطرق لها شروط مسبقة صارمة: إنشاء سيعود خطأ إذا تم إنشاء العقدة بالفعل ، حذف أو setData - إذا لم يكن موجودا بعد. كنا بحاجة إلى طريقة المجموعة ، والتي يمكن أن يطلق عليها دون التفكير في المفتاح.
خيار واحد هو تطبيق نهج متفائل ، كما هو الحال عند الحذف. تحقق من وجود العقدة. إذا كان موجودًا ، اتصل بـ setData ، وإلا ، قم بإنشاء. إذا أرجعت الطريقة الأخيرة خطأً ، كرر الأمر مرة أخرى. أول شيء يجب ملاحظته هو عدم جدوى التحقق من وجوده. يمكنك الاتصال على الفور إنشاء. الإكمال الناجح سيعني أن العقدة لم تكن موجودة وتم إنشاؤها. خلاف ذلك ، سيعود إنشاء الخطأ المقابل ، وبعد ذلك يجب استدعاء setData. بطبيعة الحال ، بين المكالمات ، يمكن إزالة الرأس من خلال مكالمة منافسة ، وسيعيد setData أيضًا خطأ. في هذه الحالة ، يمكنك تكرار كل شيء مرة أخرى ، ولكن هل يستحق كل هذا العناء؟
إذا أرجعت كلتا الطريقتين خطأ ، فنحن نعرف بالتأكيد أن هناك حذفًا منافسًا. تخيل أن هذا الحذف حدث بعد استدعاء مجموعة. ثم بغض النظر عن القيمة التي نحاول تأسيسها ، تم مسحها بالفعل. لذلك يمكنك أن تفترض أن المجموعة كانت ناجحة ، حتى لو كان في الحقيقة لم يكتب شيء.
مزيد من التفاصيل الفنية
في هذا القسم ، نتخلص من الأنظمة الموزعة ونتحدث عن الترميز.
أحد المتطلبات الرئيسية للعميل كان عبر الأنظمة الأساسية: في Linux و MacOS و Windows ، يجب دعم واحدة على الأقل من الخدمات. في البداية ، أجرينا التطوير فقط في نظام Linux ، وفي الأنظمة الأخرى بدأنا الاختبار لاحقًا. تسبب هذا في الكثير من المشاكل ، والتي لبعض الوقت كان من غير الواضح تماما كيفية التعامل معها. ونتيجة لذلك ، أصبحت خدمات التنسيق الثلاثة مدعومة الآن على نظامي Linux و MacOS ، و Consul KV فقط على نظام Windows.
منذ البداية ، حاولنا استخدام مكتبات جاهزة للوصول إلى الخدمات. في حالة ZooKeeper ، وقع الاختيار على
ZooKeeper C ++ ، والذي في النهاية لا يمكن تجميعه على Windows. هذا ، مع ذلك ، ليس مفاجئًا: يتم وضع المكتبة في نظام التشغيل Linux فقط. بالنسبة للقنصل ، كان
ppconsul هو الخيار الوحيد. اضطررت إلى إضافة دعم
للجلسات والمعاملات لذلك . بالنسبة إلى etcd ، لم يتم العثور على مكتبة كاملة تدعم أحدث إصدار من البروتوكول ، لذلك أنشأنا للتو
عميل grpc .
مستوحاة من الواجهة غير المتزامنة لمكتبة ZooKeeper C ++ ، قررنا تطبيق الواجهة غير المتزامنة أيضًا. في ZooKeeper C ++ ، يتم استخدام بدايات المستقبل / الوعد لهذا الغرض. في STL ، لسوء الحظ ، يتم تنفيذها بشكل متواضع للغاية. على سبيل المثال ، لا توجد
طريقة تطبق الوظيفة التي تم تمريرها على النتيجة المستقبلية عندما تصبح متاحة. في حالتنا ، مثل هذه الطريقة ضرورية لتحويل النتيجة إلى تنسيق مكتبتنا. للتغلب على هذه المشكلة ، كان علينا تطبيق مجموعة مؤشرات الترابط البسيطة الخاصة بنا ، لأنه بناءً على طلب العميل ، لم نتمكن من استخدام مكتبات الجهات الخارجية الثقيلة ، مثل Boost.
تنفيذ لدينا ثم يعمل على النحو التالي. عند الاتصال ، يتم إنشاء وعد إضافي / زوج في المستقبل. يتم إرجاع المستقبل الجديد ، ويتم وضع المستقبل المنقول مع الوظيفة المقابلة والوعد الإضافي في قائمة الانتظار. يحدد مؤشر ترابط من التجمع العديد من العقود المستقبلية من قائمة الانتظار ويستقصيها باستخدام wait_for. عندما تصبح النتيجة متاحة ، يتم استدعاء الوظيفة المقابلة ، ويتم تمرير قيمة الإرجاع إلى الوعد.
استخدمنا تجمع مؤشرات الترابط نفسه لتنفيذ الطلبات إلى etcd و القنصل. هذا يعني أن العديد من مؤشرات الترابط المختلفة يمكن أن تعمل مع المكتبات الأساسية. ppconsul ليس خيطًا آمنًا ، لذا فإن المكالمات إليه محمية بواسطة الأقفال.
يمكنك العمل مع grpc من عدة مؤشرات ترابط ، ولكن هناك تفاصيل دقيقة. يتم تنفيذ الساعات Etcd من خلال تدفقات grpc. هذه قنوات ثنائية الاتجاه لأنواع معينة من الرسائل. تنشئ المكتبة دفقًا واحدًا لجميع الساعات ودفقًا واحدًا يعالج الرسائل الواردة. لذلك يحظر grpc التسجيلات المتوازية لتيار. هذا يعني أنه عند تهيئة أو حذف الساعة ، تحتاج إلى الانتظار حتى يتم الانتهاء من إرسال الطلب السابق قبل إرسال الطلب التالي. نحن نستخدم
المتغيرات الشرطية للمزامنة.
يؤدي
انظر لنفسك:
liboffkv .
فريقنا:
رائد رومانوف ،
إيفان غلوشنكوف ،
ديمتري كامالدينوف ،
فيكتور كرابينسكي ،
فيتالي إيفانين .