القفل الموزع باستخدام Redis

يا هبر!

نلفت انتباهكم اليوم إلى ترجمة مقالة معقدة حول تنفيذ القفل الموزع باستخدام Redis وندعوكم للحديث عن آفاق Redis كموضوع. تحليل خوارزمية Redlock المعنية من مارتن كليبمان، مؤلف كتاب "تطبيقات التحميل العالي"، منح هنا.

يعد القفل الموزع أداة بدائية مفيدة جدًا تُستخدم في العديد من البيئات حيث يجب أن تعمل العمليات المختلفة على الموارد المشتركة بطريقة حصرية متبادلة.

هناك عدد من المكتبات والمنشورات التي تصف كيفية تنفيذ DLM (Distributed Lock Manager) باستخدام Redis، لكن كل مكتبة تتبع نهجًا مختلفًا والضمانات التي تقدمها ضعيفة جدًا مقارنة بما يمكن تحقيقه بتصميم أكثر تعقيدًا قليلاً.

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

تطبيقات

قبل الانتقال إلى وصف الخوارزمية، نقدم عدة روابط للتطبيقات الجاهزة. يمكن استخدامها كمرجع.

ضمانات الأمن والتوافر

سنقوم بتصميم تصميمنا باستخدام ثلاث خصائص فقط نعتقد أنها توفر الحد الأدنى من الضمانات اللازمة لاستخدام القفل الموزع بشكل فعال.

  1. خاصية الأمان: الاستبعاد المتبادل. في أي وقت، يمكن لعميل واحد فقط أن يحمل القفل.
  2. خاصية التوفر أ: لا توجد حالات توقف تام. من الممكن دائمًا الحصول على قفل في النهاية، حتى إذا فشل العميل الذي قام بتأمين المورد أو وصل إلى مقطع قرص مختلف.
  3. خاصية التوفر ب: التسامح مع الخطأ. طالما أن غالبية عقد Redis قيد التشغيل، يستطيع العملاء الحصول على الأقفال وتحريرها.

لماذا التنفيذ على أساس التعافي من الفشل ليس كافيًا في هذه الحالة
لفهم ما سنقوم بتحسينه، دعنا نحلل الوضع الحالي مع معظم مكتبات القفل الموزعة المستندة إلى Redis.

إن أبسط طريقة لقفل مورد باستخدام Redis هي إنشاء مفتاح في المثيل. عادةً، يتم إنشاء المفتاح بعمر محدود، ويتم تحقيق ذلك باستخدام ميزة انتهاء الصلاحية المتوفرة في Redis، لذلك يتم إصدار هذا المفتاح عاجلاً أم آجلاً (الخاصية 2 في قائمتنا). عندما يحتاج العميل إلى تحرير المورد، فإنه يحذف المفتاح.

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

من الواضح أنه في مثل هذا النموذج تحدث حالة سباق:

  1. يحصل العميل "أ" على قفل على السيد.
  2. يفشل السيد قبل أن يتم نقل إدخال المفتاح إلى العبد.
  3. تتم ترقية التابع إلى القائد.
  4. يحصل العميل "ب" على قفل على نفس المورد الذي قام "أ" بتأمينه بالفعل. اختراق الامن!

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

التنفيذ الصحيح مع مثيل واحد

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

للحصول على القفل، قم بما يلي:

SET resource_name my_random_value NX PX 30000

يقوم هذا الأمر بتثبيت مفتاح فقط إذا لم يكن موجودًا بالفعل (خيار NX)، مع فترة صلاحية تبلغ 30000 مللي ثانية (خيار PX). تم ضبط المفتاح على "myrandomvalue" يجب أن تكون هذه القيمة فريدة عبر كافة العملاء وكافة طلبات القفل.
في الأساس، يتم استخدام قيمة عشوائية لتحرير القفل بأمان، مع برنامج نصي يخبر Redis: قم بإزالة المفتاح فقط إذا كان موجودًا وكانت القيمة المخزنة فيه هي بالضبط ما كان متوقعًا. يتم تحقيق ذلك باستخدام برنامج Lua النصي التالي:

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

يعد هذا أمرًا مهمًا لمنع إزالة القفل الذي يحتفظ به عميل آخر. على سبيل المثال، قد يحصل العميل على قفل، ثم يصبح مقفلاً في بعض العمليات التي تستمر لفترة أطول من القفل الأول (بحيث يكون للمفتاح وقت لتنتهي صلاحيته)، ثم يقوم لاحقًا بإزالة القفل الذي وضعه عميل آخر.
يعد استخدام DEL البسيط أمرًا غير آمن لأنه يمكن للعميل إزالة القفل الذي يحتفظ به عميل آخر. في المقابل، عند استخدام البرنامج النصي أعلاه، يتم "توقيع" كل قفل بسلسلة عشوائية، بحيث لا يتمكن من إزالته سوى العميل الذي وضعه مسبقًا.

ماذا يجب أن تكون هذه السلسلة العشوائية؟ أعتقد أنه يجب أن يكون 20 بايت من /dev/urandom، ولكن يمكنك العثور على طرق أقل تكلفة لجعل السلسلة فريدة بما يكفي لأغراضك. على سبيل المثال، سيكون من الجيد زرع RC4 باستخدام /dev/urandom ثم إنشاء دفق عشوائي زائف منه. يتضمن الحل الأبسط مزيجًا من وقت يونكس بدقة ميكروثانية بالإضافة إلى معرف العميل؛ إنه ليس آمنًا، لكنه على الأرجح يصل إلى مستوى المهمة في معظم السياقات.

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

لذلك ناقشنا طريقة جيدة للحصول على القفل وتحريره. النظام (إذا كنا نتحدث عن نظام غير موزع يتكون من نسخة واحدة ومتاحة دائمًا) آمن. دعونا نوسع هذا المفهوم ليشمل النظام الموزع، حيث ليس لدينا مثل هذه الضمانات.

خوارزمية ريدلوك

تفترض النسخة الموزعة من الخوارزمية أن لدينا N Redis masters. هذه العقد مستقلة تمامًا عن بعضها البعض، لذلك لا نستخدم النسخ المتماثل أو أي نظام تنسيق ضمني آخر. لقد تناولنا بالفعل كيفية الحصول على القفل وتحريره بشكل آمن في مثيل واحد. نحن نعتبر أنه من المسلم به أن الخوارزمية، عند العمل مع مثيل واحد، ستستخدم هذه الطريقة بالضبط. في أمثلةنا قمنا بتعيين N إلى 5، وهي قيمة معقولة. وبالتالي، سنحتاج إلى استخدام 5 Redis Masters على أجهزة كمبيوتر أو أجهزة افتراضية مختلفة للتأكد من أنها تعمل بشكل مستقل إلى حد كبير عن بعضها البعض.

للحصول على القفل، يقوم العميل بتنفيذ العمليات التالية:

  1. الحصول على الوقت الحالي بالمللي ثانية.
  2. يحاول بشكل تسلسلي الحصول على قفل لجميع مثيلات N، باستخدام نفس اسم المفتاح والقيم العشوائية في جميع الحالات. في المرحلة 2، عندما يقوم العميل بإعداد القفل على أساس كل مثيل، يستخدم العميل تأخيرًا للحصول عليه وهو قصير بما يكفي مقارنة بالوقت الذي يتم بعده تحرير القفل تلقائيًا. على سبيل المثال، إذا كانت مدة الحظر 10 ثوانٍ، فقد يكون التأخير في نطاق ~5-50 مللي ثانية. يؤدي هذا إلى التخلص من الموقف الذي قد يظل فيه العميل محظورًا لفترة طويلة أثناء محاولته الوصول إلى عقدة Redis الفاشلة: إذا كان المثيل غير متاح، فسنحاول الاتصال بمثيل آخر في أقرب وقت ممكن.
  3. للحصول على القفل، يحسب العميل مقدار الوقت المنقضي؛ للقيام بذلك، فإنه يطرح من القيمة الزمنية الفعلية الطابع الزمني الذي تم الحصول عليه في الخطوة 1. إذا وفقط إذا كان العميل قادرًا على الحصول على القفل في معظم المثيلات (3 على الأقل)، وإجمالي الوقت المستغرق الحصول على القفل، إذا كانت مدة القفل أقل من ذلك، فسيتم اعتبار القفل قد تم الحصول عليه.
  4. إذا تم الحصول على القفل، فسيتم اعتبار مدة القفل هي مدة القفل الأصلية مطروحًا منها الوقت المنقضي المحسوب في الخطوة 3.
  5. إذا فشل العميل في الحصول على القفل لسبب ما (إما أنه لم يتمكن من قفل مثيلات N/2+1، أو كانت مدة القفل سالبة)، فسيحاول فتح جميع المثيلات (حتى تلك التي يعتقد أنه لا يمكنه حظرها) ).

هل الخوارزمية غير متزامنة؟

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

في هذه المرحلة، يجب علينا صياغة قاعدة الاستبعاد المتبادل بعناية أكبر: يتم ضمان الاستبعاد المتبادل فقط إذا خرج العميل الذي يحمل القفل خلال الوقت الذي يكون فيه القفل صالحًا (تم الحصول على هذه القيمة في الخطوة 3)، مطروحًا منها بعض الوقت الإضافي (إجمالي بضعة أيام) ميلي ثانية للتعويض عن الفارق الزمني بين العمليات).

توضح المقالة المثيرة للاهتمام التالية المزيد عن هذه الأنظمة التي تتطلب تنسيق الفترات الزمنية: عقود الإيجار: آلية فعالة للتسامح مع الأخطاء من أجل تناسق ذاكرة التخزين المؤقت للملفات الموزعة.

أعد المحاولة عند الفشل

عندما يفشل العميل في الحصول على القفل، يجب عليه المحاولة مرة أخرى بعد تأخير عشوائي؛ يتم ذلك لإلغاء مزامنة العديد من العملاء الذين يحاولون الحصول على قفل على نفس المورد في نفس الوقت (مما قد يؤدي إلى حالة "انقسام الدماغ" حيث لا يوجد فائزون). بالإضافة إلى ذلك، كلما حاول العميل الحصول على قفل على غالبية مثيلات Redis بشكل أسرع، كلما كانت النافذة التي يمكن أن يحدث فيها موقف الانقسام الدماغي أضيق (وقلت الحاجة إلى إعادة المحاولة). لذلك، من الناحية المثالية، يجب أن يحاول العميل إرسال أوامر SET إلى مثيلات N في وقت واحد باستخدام تعدد الإرسال.

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

الافراج عن القفل

يعد تحرير القفل عملية بسيطة تتطلب ببساطة إلغاء قفل جميع المثيلات، بغض النظر عما إذا كان العميل قد نجح في قفل مثيل معين أم لا.

اعتبارات أمنية

هل الخوارزمية آمنة؟ دعونا نحاول أن نتخيل ما يحدث في سيناريوهات مختلفة.

في البداية، لنفترض أن العميل كان قادرًا على الحصول على القفل في معظم الحالات. سيحتوي كل مثيل على مفتاح له نفس العمر للجميع. ومع ذلك، تم تثبيت كل مفتاح من هذه المفاتيح في وقت مختلف، لذا ستنتهي صلاحيتها في أوقات مختلفة. ولكن، إذا تم تثبيت المفتاح الأول في وقت ليس أسوأ من T1 (الوقت الذي نختاره قبل الاتصال بالخادم الأول)، وتم تثبيت المفتاح الأخير في وقت ليس أسوأ من T2 (الوقت الذي تم فيه تلقي الاستجابة من الخادم الأخير)، فإننا على ثقة من أن المفتاح الأول في المجموعة الذي تنتهي صلاحيته سيبقى على قيد الحياة على الأقل MIN_VALIDITY=TTL-(T2-T1)-CLOCK_DRIFT. ستنتهي صلاحية جميع المفاتيح الأخرى لاحقًا، لذا يمكننا التأكد من أن جميع المفاتيح ستكون صالحة في نفس الوقت على الأقل هذه المرة.

خلال الوقت الذي تظل فيه معظم المفاتيح صالحة، لن يتمكن عميل آخر من الحصول على القفل، نظرًا لأن عمليات N/2+1 SET NX لا يمكن أن تنجح إذا كانت مفاتيح N/2+1 موجودة بالفعل. لذلك، بمجرد الحصول على القفل، فمن المستحيل الحصول عليه مرة أخرى في نفس اللحظة (وهذا من شأنه أن ينتهك خاصية الاستبعاد المتبادل).
ومع ذلك، نريد التأكد من أن العديد من العملاء الذين يحاولون الحصول على قفل في نفس الوقت لا يمكنهم النجاح في نفس الوقت.

إذا قام العميل بتأمين غالبية المثيلات لمدة تقريبية أو أكثر من الحد الأقصى لمدة القفل، فسوف يعتبر القفل غير صالح ويفتح المثيلات. لذلك، علينا فقط أن نأخذ في الاعتبار الحالة التي تمكن فيها العميل من حظر غالبية الحالات في وقت أقل من تاريخ انتهاء الصلاحية. في هذه الحالة، فيما يتعلق بالحجة المذكورة أعلاه، خلال الوقت MIN_VALIDITY يجب ألا يتمكن أي عميل من استعادة القفل. لذلك، سيتمكن العديد من العملاء من قفل مثيلات N/2+1 في نفس الوقت (والذي ينتهي في نهاية المرحلة 2) فقط عندما يكون وقت قفل الأغلبية أكبر من وقت TTL، مما يجعل القفل غير صالح.

هل يمكنك تقديم دليل رسمي على الأمان، أو الإشارة إلى خوارزميات مماثلة موجودة، أو العثور على خطأ فيما ورد أعلاه؟

اعتبارات إمكانية الوصول

يعتمد توفر النظام على ثلاث خصائص رئيسية:

  1. تحرير الأقفال تلقائيًا (مع انتهاء صلاحية المفاتيح): ستكون المفاتيح متاحة مرة أخرى في النهاية لاستخدامها في الأقفال.
  2. حقيقة أن العملاء عادة ما يساعدون بعضهم البعض عن طريق إزالة الأقفال عندما لا يتم الحصول على القفل المطلوب، أو تم الحصول عليه واكتملت المهمة؛ لذلك فمن المحتمل ألا نضطر إلى الانتظار حتى تنتهي صلاحية المفاتيح لاستعادة القفل.
  3. الحقيقة هي أنه عندما يحتاج العميل إلى إعادة محاولة الحصول على القفل، فإنه ينتظر وقتًا أطول نسبيًا من الفترة المطلوبة للحصول على معظم الأقفال. وهذا يقلل من احتمال حدوث حالة انقسام الدماغ عند التنافس على الموارد.

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

من حيث المبدأ، نظرًا لوجود عدد لا نهائي من قطاعات الشبكة المتجاورة، يمكن أن يظل النظام غير متاح لفترة غير محدودة من الوقت.

الأداء وتجاوز الفشل وfsync

يستخدم العديد من الأشخاص Redis لأنهم يحتاجون إلى أداء خادم قفل عالي من حيث زمن الوصول المطلوب للحصول على الأقفال وتحريرها، وعدد عمليات الاستحواذ/الإصدارات التي يمكن إكمالها في الثانية. لتلبية هذا المطلب، توجد استراتيجية للتواصل مع خوادم N Redis لتقليل زمن الوصول. هذه هي استراتيجية تعدد الإرسال (أو "تعدد إرسال الرجل الفقير"، حيث يتم وضع المقبس في وضع عدم الحظر، ويرسل جميع الأوامر، ويقرأ الأوامر لاحقًا، على افتراض أن وقت الذهاب والإياب بين العميل وكل مثيل متشابه) .

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

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

إذا قمت بتمكين البيانات المقبلة (AOF)، فسوف يتحسن الوضع قليلاً. على سبيل المثال، يمكنك ترقية خادم عن طريق إرسال أمر SHUTDOWN وإعادة تشغيله. نظرًا لأن عمليات انتهاء الصلاحية في Redis يتم تنفيذها لغويًا بحيث يستمر الوقت في التدفق حتى عند إيقاف تشغيل الخادم، فإن جميع متطلباتنا جيدة. وهذا أمر طبيعي طالما تم ضمان إيقاف التشغيل العادي. ماذا تفعل في حالة انقطاع التيار الكهربائي؟ إذا تم تكوين Redis افتراضيًا، مع مزامنة fsync على القرص كل ثانية، فمن المحتمل أنه بعد إعادة التشغيل لن يكون لدينا مفتاحنا. من الناحية النظرية، إذا أردنا ضمان أمان القفل أثناء إعادة تشغيل أي مثيل، فيجب علينا تمكينه fsync=always في إعدادات تخزين البيانات على المدى الطويل. سيؤدي هذا إلى قتل الأداء تمامًا، وصولاً إلى مستوى أنظمة CP التي تُستخدم تقليديًا لتنفيذ الأقفال الموزعة بشكل آمن.

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

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

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

نحن نزيد من توافر الخوارزمية: نقوم بتمديد الحظر

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

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

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

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

إضافة تعليق