خمسة طلاب وثلاثة مخازن ذات قيمة رئيسية موزعة

أو كيف قمنا بكتابة مكتبة C++ للعميل لـ ZooKeeper وetcd وConsul KV

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

خمسة طلاب وثلاثة مخازن ذات قيمة رئيسية موزعة

في جوهرها، جميع هذه الأنظمة عبارة عن مخازن ذات قيمة أساسية قابلة للخطية ومتسامحة مع الأخطاء. وعلى الرغم من أن نماذج البيانات الخاصة بهم بها اختلافات كبيرة، والتي سنناقشها لاحقًا، إلا أنها تحل نفس المشكلات العملية. ومن الواضح أن كل تطبيق يستخدم خدمة التنسيق يرتبط بأحدها، مما قد يؤدي إلى الحاجة إلى دعم عدة أنظمة في مركز بيانات واحد تحل نفس المشكلات لتطبيقات مختلفة.

فكرة حل هذه المشكلة نشأت في وكالة استشارات أسترالية، ووقع على عاتقنا، كفريق صغير من الطلاب، تنفيذها، وهو ما سأتحدث عنه.

تمكنا من إنشاء مكتبة توفر واجهة مشتركة للعمل مع ZooKeeper وetcd وConsul KV. المكتبة مكتوبة بلغة C++، ولكن هناك خطط لنقلها إلى لغات أخرى.

نماذج البيانات

لتطوير واجهة مشتركة لثلاثة أنظمة مختلفة، تحتاج إلى فهم ما هو مشترك بينها وكيفية اختلافها. دعونا معرفة ذلك.

حارس حديقة الحيوان

خمسة طلاب وثلاثة مخازن ذات قيمة رئيسية موزعة

يتم تنظيم المفاتيح في شجرة وتسمى العقد. وفقا لذلك، بالنسبة للعقدة، يمكنك الحصول على قائمة بأطفالها. يتم فصل عمليات إنشاء znode (إنشاء) وتغيير القيمة (setData): يمكن قراءة المفاتيح الموجودة فقط وتغييرها. يمكن ربط الساعات بعمليات التحقق من وجود العقدة وقراءة القيمة وإنجاب الأطفال. المراقبة عبارة عن مشغل لمرة واحدة يتم تشغيله عندما يتغير إصدار البيانات المقابلة على الخادم. يتم استخدام العقد المؤقتة للكشف عن حالات الفشل. وهي مرتبطة بجلسة العميل الذي قام بإنشائها. عندما يغلق العميل الجلسة أو يتوقف عن إخطار ZooKeeper بوجودها، يتم حذف هذه العقد تلقائيًا. يتم دعم المعاملات البسيطة - مجموعة من العمليات التي إما أن تنجح أو تفشل جميعها إذا لم يكن ذلك ممكنًا لواحدة منها على الأقل.

إلخ

خمسة طلاب وثلاثة مخازن ذات قيمة رئيسية موزعة

من الواضح أن مطوري هذا النظام استلهموا فكرة ZooKeeper، وبالتالي فعلوا كل شيء بطريقة مختلفة. لا يوجد تسلسل هرمي للمفاتيح، ولكنها تشكل مجموعة مرتبة معجميًا. يمكنك الحصول على أو حذف كافة المفاتيح التي تنتمي إلى نطاق معين. قد تبدو هذه البنية غريبة، لكنها في الواقع معبرة للغاية، ويمكن محاكاة النظرة الهرمية بسهولة من خلالها.

لا تحتوي etcd على عملية مقارنة وتعيين قياسية، ولكنها تحتوي على شيء أفضل: المعاملات. وبطبيعة الحال، فهي موجودة في جميع الأنظمة الثلاثة، ولكن المعاملات الخ هي جيدة بشكل خاص. وهي تتألف من ثلاث كتل: التحقق والنجاح والفشل. تحتوي الكتلة الأولى على مجموعة من الشروط، والثانية والثالثة - العمليات. يتم تنفيذ الصفقة ذريًا. إذا كانت جميع الشروط صحيحة، فسيتم تنفيذ كتلة النجاح، وإلا يتم تنفيذ كتلة الفشل. في API 3.3، يمكن أن تحتوي كتل النجاح والفشل على معاملات متداخلة. وهذا يعني أنه من الممكن تنفيذ بنيات شرطية ذات مستوى تداخل تعسفي تقريبًا. يمكنك معرفة المزيد حول ماهية عمليات التحقق والعمليات الموجودة توثيق.

توجد ساعات هنا أيضًا، على الرغم من أنها أكثر تعقيدًا وقابلة لإعادة الاستخدام. أي أنه بعد تثبيت الساعة على نطاق رئيسي، ستصلك كافة التحديثات في هذا النطاق حتى تقوم بإلغاء الساعة، وليس الأولى فقط. في إلخ، فإن جلسات عميل ZooKeeper التناظرية هي عقود إيجار.

القنصل ك.ف.

لا يوجد أيضًا هيكل هرمي صارم هنا، ولكن يمكن لـ Consul إنشاء المظهر الموجود: يمكنك الحصول على جميع المفاتيح ذات البادئة المحددة وحذفها، أي العمل مع "الشجرة الفرعية" للمفتاح. تسمى هذه الاستعلامات العودية. بالإضافة إلى ذلك، يمكن للقنصل تحديد المفاتيح التي لا تحتوي على الحرف المحدد بعد البادئة فقط، وهو ما يتوافق مع الحصول على "أبناء" فوريين. ولكن تجدر الإشارة إلى أن هذا هو بالضبط مظهر الهيكل الهرمي: من الممكن تمامًا إنشاء مفتاح في حالة عدم وجود الأصل أو حذف المفتاح الذي يحتوي على أطفال، بينما سيستمر تخزين الأطفال في النظام.

خمسة طلاب وثلاثة مخازن ذات قيمة رئيسية موزعة
بدلاً من المراقبة، قام القنصل بحظر طلبات HTTP. في جوهرها، هذه هي المكالمات العادية لطريقة قراءة البيانات، والتي تتم الإشارة إليها، إلى جانب المعلمات الأخرى، إلى آخر إصدار معروف من البيانات. إذا كان الإصدار الحالي من البيانات المقابلة على الخادم أكبر من المحدد، فسيتم إرجاع الاستجابة على الفور، وإلا - عندما تتغير القيمة. هناك أيضًا جلسات يمكن ربطها بالمفاتيح في أي وقت. تجدر الإشارة إلى أنه على عكس etcd وZooKeeper، حيث يؤدي حذف الجلسات إلى حذف المفاتيح المرتبطة، هناك وضع يتم فيه إلغاء ربط الجلسة بها ببساطة. متاح المعاملات، بدون فروع، ولكن بجميع أنواع الشيكات.

ضع كل شيء معا

يمتلك ZooKeeper نموذج البيانات الأكثر صرامة. لا يمكن محاكاة استعلامات النطاق التعبيرية المتوفرة في etcd بشكل فعال في ZooKeeper أو Consul. في محاولة لدمج الأفضل من جميع الخدمات، انتهى بنا الأمر بواجهة تعادل تقريبًا واجهة ZooKeeper مع الاستثناءات المهمة التالية:

  • عقد التسلسل والحاوية وTTL غير مدعوم
  • قوائم ACL غير مدعومة
  • تقوم طريقة set بإنشاء مفتاح إذا لم يكن موجودًا (في ZK setData تُرجع خطأً في هذه الحالة)
  • يتم فصل أساليب set وcas (في ZK هما نفس الشيء بشكل أساسي)
  • تقوم طريقة المسح بحذف عقدة مع الشجرة الفرعية الخاصة بها (في حذف ZK يُرجع خطأ إذا كانت العقدة بها أطفال)
  • يوجد إصدار واحد فقط لكل مفتاح - إصدار القيمة (في ZK هناك ثلاثة منهم)

يرجع رفض العقد التسلسلية إلى حقيقة أن etcd وConsul ليس لديهما دعم مدمج لها، ويمكن للمستخدم تنفيذها بسهولة أعلى واجهة المكتبة الناتجة.

يتطلب تنفيذ سلوك مشابه لـ ZooKeeper عند حذف قمة ما الحفاظ على عداد فرعي منفصل لكل مفتاح في etcd وConsul. وبما أننا حاولنا تجنب تخزين معلومات التعريف، فقد تقرر حذف الشجرة الفرعية بأكملها.

الدقيقة من التنفيذ

دعونا نلقي نظرة فاحصة على بعض جوانب تنفيذ واجهة المكتبة في أنظمة مختلفة.

التسلسل الهرمي في الخ

تبين أن الحفاظ على عرض هرمي في إلخ هو أحد المهام الأكثر إثارة للاهتمام. تسهل استعلامات النطاق استرداد قائمة المفاتيح ببادئة محددة. على سبيل المثال، إذا كنت بحاجة إلى كل ما يبدأ "/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/u00", "/very/u01").

إزالة المفتاح في ZooKeeper

كما ذكرت سابقًا، في ZooKeeper لا يمكنك حذف عقدة إذا كانت بها أطفال. نريد حذف المفتاح مع الشجرة الفرعية. ماذا علي أن أفعل؟ نحن نفعل هذا بتفاؤل. أولاً، نقوم باجتياز الشجرة الفرعية بشكل متكرر، للحصول على الأطفال من كل قمة باستخدام استعلام منفصل. ثم نقوم بإنشاء معاملة تحاول حذف جميع عقد الشجرة الفرعية بالترتيب الصحيح. بالطبع، يمكن أن تحدث تغييرات بين قراءة الشجرة الفرعية وحذفها. في هذه الحالة، سوف تفشل الصفقة. علاوة على ذلك، قد تتغير الشجرة الفرعية أثناء عملية القراءة. قد يُرجع طلب أبناء العقدة التالية خطأً، على سبيل المثال، إذا تم حذف هذه العقدة بالفعل. وفي كلتا الحالتين، نكرر العملية برمتها مرة أخرى.

هذا الأسلوب يجعل حذف المفتاح غير فعال للغاية إذا كان لديه أطفال، وأكثر من ذلك إذا استمر التطبيق في العمل مع الشجرة الفرعية، وحذف المفاتيح وإنشائها. ومع ذلك، فقد سمح لنا هذا بتجنب تعقيد تنفيذ الأساليب الأخرى في إلخد والقنصل.

تم تعيينه في ZooKeeper

في ZooKeeper هناك طرق منفصلة تعمل مع بنية الشجرة (إنشاء، حذف، getChildren) وتعمل مع البيانات الموجودة في العقد (setData، getData). علاوة على ذلك، جميع الطرق لها شروط مسبقة صارمة: سوف يُرجع الإنشاء خطأ إذا كانت العقدة قد تم بالفعل تم إنشاؤها أو حذفها أو تعيين البيانات - إذا لم تكن موجودة بالفعل. كنا بحاجة إلى طريقة محددة يمكن استدعاؤها دون التفكير في وجود المفتاح.

أحد الخيارات هو اتباع نهج متفائل، كما هو الحال مع الحذف. تحقق من وجود عقدة. إذا كان موجودًا، فاتصل بـ setData، وإلا قم بإنشائه. إذا أعادت الطريقة الأخيرة خطأ، كرر ذلك مرة أخرى. أول شيء يجب ملاحظته هو أن اختبار الوجود لا معنى له. يمكنك الاتصال على الفور بإنشاء. سيعني الإكمال الناجح أن العقدة لم تكن موجودة وتم إنشاؤها. بخلاف ذلك، سيُرجع الإنشاء الخطأ المناسب، وبعد ذلك ستحتاج إلى استدعاء setData. بالطبع، بين الاستدعاءات، يمكن حذف قمة بواسطة مكالمة منافسة، وسيقوم setData أيضًا بإرجاع خطأ. في هذه الحالة، يمكنك أن تفعل ذلك من جديد، ولكن هل يستحق كل هذا العناء؟

إذا أعادت كلتا الطريقتين خطأً، فإننا نعرف على وجه اليقين حدوث عملية حذف منافسة. لنتخيل أن هذا الحذف حدث بعد استدعاء المجموعة. ومن ثم فإن أي معنى نحاول ترسيخه قد تم محوه بالفعل. هذا يعني أنه يمكننا أن نفترض أن المجموعة قد تم تنفيذها بنجاح، حتى لو لم تتم كتابة أي شيء في الواقع.

مزيد من التفاصيل الفنية

في هذا القسم سنأخذ استراحة من الأنظمة الموزعة ونتحدث عن البرمجة.
كان أحد المتطلبات الرئيسية للعميل هو تعدد الأنظمة الأساسية: يجب أن تكون إحدى الخدمات على الأقل مدعومة على Linux وMacOS وWindows. في البداية، قمنا بتطوير نظام التشغيل Linux فقط، ثم بدأنا في الاختبار على الأنظمة الأخرى لاحقًا. وقد تسبب هذا في الكثير من المشاكل التي لم تكن واضحة لبعض الوقت كيفية التعامل معها. ونتيجة لذلك، أصبحت جميع خدمات التنسيق الثلاث مدعومة الآن على نظامي التشغيل Linux وMacOS، بينما يتم دعم Consul KV فقط على نظام التشغيل Windows.

منذ البداية، حاولنا استخدام المكتبات الجاهزة للوصول إلى الخدمات. وفي حالة ZooKeeper، وقع الاختيار حارس حديقة الحيوان C ++، والذي فشل في النهاية في التجميع على Windows. ومع ذلك، هذا ليس مفاجئًا: فالمكتبة تعمل بنظام التشغيل Linux فقط. بالنسبة للقنصل كان الخيار الوحيد com.ppconsul. وكان لا بد من إضافة الدعم إليها الجلسات и المعاملات. بالنسبة إلى إلخ، لم يتم العثور على مكتبة كاملة تدعم أحدث إصدار من البروتوكول، لذلك قمنا ببساطة عميل grpc الذي تم إنشاؤه.

مستوحاة من الواجهة غير المتزامنة لمكتبة ZooKeeper C++، قررنا أيضًا تنفيذ واجهة غير متزامنة. يستخدم ZooKeeper C++ بدايات المستقبل/الوعد لهذا الغرض. ولسوء الحظ، يتم تنفيذها في المحكمة الخاصة بلبنان بشكل متواضع للغاية. على سبيل المثال، لا ثم الطريقة، والذي يطبق الدالة التي تم تمريرها على نتيجة المستقبل عندما تصبح متاحة. في حالتنا، هذه الطريقة ضرورية لتحويل النتيجة إلى تنسيق مكتبتنا. للتغلب على هذه المشكلة، كان علينا تنفيذ تجمع مؤشرات الترابط البسيط الخاص بنا، نظرًا لأنه بناءً على طلب العميل، لم نتمكن من استخدام مكتبات الطرف الثالث الثقيلة مثل Boost.

تنفيذنا ثم يعمل مثل هذا. عند الاستدعاء، يتم إنشاء زوج وعد/مستقبل إضافي. يتم إرجاع المستقبل الجديد، ويتم وضع المستقبل الذي تم تمريره مع الوظيفة المقابلة والوعد الإضافي في قائمة الانتظار. يختار مؤشر ترابط من التجمع العديد من العقود الآجلة من قائمة الانتظار ويستقصيها باستخدام wait_for. عندما تصبح النتيجة متاحة، يتم استدعاء الوظيفة المقابلة ويتم تمرير قيمتها المرجعة إلى الوعد.

استخدمنا نفس تجمع مؤشرات الترابط لتنفيذ الاستعلامات إلى etcd وConsul. وهذا يعني أنه يمكن الوصول إلى المكتبات الأساسية من خلال عدة سلاسل رسائل مختلفة. ppconsul ليس مؤشر ترابط آمن، لذا فإن الاستدعاءات إليه محمية بواسطة الأقفال.
يمكنك العمل مع grpc من عدة سلاسل رسائل، ولكن هناك بعض التفاصيل الدقيقة. في etcd يتم تنفيذ الساعات عبر تدفقات grpc. هذه قنوات ثنائية الاتجاه للرسائل من نوع معين. تقوم المكتبة بإنشاء موضوع واحد لجميع الساعات وموضوع واحد يعالج الرسائل الواردة. لذلك يحظر grpc الكتابة المتوازية للتدفق. وهذا يعني أنه عند تهيئة الساعة أو حذفها، يجب عليك الانتظار حتى اكتمال إرسال الطلب السابق قبل إرسال الطلب التالي. نحن نستخدم للمزامنة المتغيرات الشرطية.

مجموع

انظر لنفسك: liboffkv.

فريقنا: رائد رومانوف, إيفان جلوشينكوف, ديمتري كمالدينوف, فيكتور كرابيفنسكي, فيتالي إيفانين.

المصدر: www.habr.com

إضافة تعليق