مقدمة
أثناء تنقيح مشروع واحد ، أصبح من الضروري تخزين البيانات المطلوبة بشكل متكرر. يمكن تنفيذ التخزين المؤقت بطرق مختلفة ، لكنني أردت تنفيذه مع الحد الأدنى من التغييرات في المشروع الأصلي. والنتيجة ، إيجابيات وسلبيات موصوفة أدناه.
كيف كان كل شيء؟
في البداية ، لكل استعلام يحتوي على معرف الكائن المطلوب ، تم تنفيذ استعلام في قاعدة بيانات PostgreSQL (DB). بتعبير أدق ، استفسارات متعددة ، لأنه من أجل تشكيل إجابة كاملة ، كان من الضروري تطبيقه على عدة جداول لقواعد البيانات. نتيجة لمعالجة الطلبات ، تم تكوين كائن معقد إلى حد ما ، حيث يتم تمثيل بعض الحقول به بواجهات. في الذاكرة ، يشغل هذا الكائن حوالي 250 كيلو بايت.
لم يكن الأداء مع هذا التطبيق رائعًا ، إذ لم يتجاوز 3500 RPS (طلب في الثانية) عند طلب نفس البيانات مع 1000 مؤشر ترابط متنافس.
السؤال الذي يطرح نفسه على الفور ، ولكن كيفية زيادة RPS: تغيير جهاز التوجيه ، وتحسين قاعدة البيانات ، وبيانات ذاكرة التخزين المؤقت؟ تم استخدام جهاز التوجيه جيدًا ( github.com/julienschmidt/httprouter ) ، وسوف يتطلب استبدال جهاز التوجيه في مشروع كبير الكثير من الوقت وهناك خطر كبير لحدوث شيء ما. لتحسين العمل باستخدام قاعدة البيانات ، ستحتاج أيضًا إلى إعادة كتابة جزء كبير من الكود (يتم الآن استخدام github.com/jmoiron/sqlx ). من الواضح أن التخزين المؤقت هو الطريقة المثلى لزيادة RPS.
حل بسيط
إن أبسط ما يتبادر إلى الذهن هو استخدام ذاكرة التخزين المؤقت في الذاكرة. عند استخدام ذاكرة التخزين المؤقت هذه ، تم الحصول على حوالي 20000 RPS. أداء ذاكرة التخزين المؤقت في الذاكرة ممتاز ، لكن لا يمكنك استخدام ذاكرة التخزين المؤقت هذه مع العديد من مثيلات الخدمة. لا تعرف أبدًا أي مثيل للخدمة سيتم نقل الطلب إليه ، وقد تكون هناك طلبات ليس فقط لتلقي البيانات ، ولكن أيضًا للحذف / التحديث.
تم اتخاذ الأداء الذي تم الحصول عليه باستخدام ذاكرة التخزين المؤقت في الذاكرة كمعيار في البحث عن حل.
فكرة ، فكرة سيئة
هل من الممكن وضع نتيجة الاستعلام كما هي في قاعدة بيانات NoSQL Redis؟ هذا هو الحل النموذجي للتخزين المؤقت لطلبات الاستجابة. يتم تخزين البيانات في الذاكرة ، عند استخدام مثيلات متعددة للخدمة ، يمكن لجميعهم استخدام ذاكرة تخزين مؤقت شائعة. تم تنفيذ هذا الحل بسرعة. وأظهرت الاختبارات ... وأظهرت الاختبارات أن الأداء لم يرتفع كثيرا.
أظهرت الأبحاث الإضافية أن الخسائر الرئيسية في الأداء مرتبطة بالتنظيم والانتشار. يتطلب تحويل بنية إلى JSON والعكس بالعكس استخدام التفكير ، وهو مكلف للغاية في الأداء. من المستحيل رفض التنظيم / unmarshaling ، لأنه من الضروري الحصول على كائن كامل من ذاكرة التخزين المؤقت مع القدرة على استدعاء أساليب الهياكل ، وليس فقط الحصول على قيم الحقول الفردية. استخدام مكتبات مختلفة مع تحسين التنظيم / unmhalhaling أيضًا لم ينقذ ، كان هناك نمو ، ولكن ذاكرة التخزين المؤقت في الذاكرة كانت بعيدة جدًا. لذلك ، تقرر عدم تكوين صداقات مع "القنفذ والأفعى" وإنشاء ذاكرة تخزين مؤقت مختلطة.
هجين "ثعبان وقنفذ"
لا يمكنك أن تسميها هجينة كاملة (انظر الشكل.) ، في الواقع ، تحولت إلى ذاكرة تخزين مؤقت في الذاكرة ، ولكن مع التزامن من خلال Redis ( تم استخدام مكتبة github.com/go-redis/redis ). سيتم تخزين المعرّف الفريد للكائن المطلوب من قاعدة البيانات (كائن المعرف) في Redis. ستتم إضافته إلى Redis أثناء معالجة طلب لإنشاء كائن ، أو طلب للحصول على كائن موجود من قاعدة البيانات. سيعمل معرف الكائن كمفتاح للقيمة في Redis ، وستكون القيمة هي UUID (معرف فريد عالميًا ، معرف فريد عالمي "). سيتم إنشاء UUID فقط عند إضافة الكائن إلى Redis. لماذا هناك حاجة لهذا UUID سيتم وصفها لاحقا.
مخطط كتلة التفاعل المكون لمزامنة ذاكرة التخزين المؤقت من خلال Redis
يتم تنفيذ ذاكرة التخزين المؤقت في الذاكرة على أساس sync.Map. بالنسبة لعناصر ذاكرة التخزين المؤقت المختلطة ، يتم ضبط TTL (الوقت للعيش ، وعمر الحياة) ، وإذا قام Redis بتنظيف العناصر "الخاطئة" ، فسيتم تنظيف ذاكرة التخزين المؤقت في الذاكرة بواسطة مؤقت (time.AfterFunc). يمر عبر جميع عناصر ذاكرة التخزين المؤقت ويتحقق مما إذا كان العنصر "فاسدًا". إذا تم الوصول إلى عنصر ذاكرة التخزين المؤقت ، فسيتم تمديد عمره ؛ ويتم إجراء عملية مماثلة باستخدام مفاتيح في Redis.
لذلك ، الآن وفقا للخوارزمية. في حالة وصول طلب وتحتاج إلى استرداد الكائن ، يتم تنفيذ تسلسل الإجراءات التالي:
- ننظر إلى ما إذا كان هناك كائن له معرّف معرّف معين في Redis ، إذا كان الأمر كذلك ، فيمكننا إذن أخذ نسخة ذاكرة التخزين المؤقت لمثيل الخدمة من الذاكرة الداخلية:
- إذا لم يكن الكائن في ذاكرة التخزين المؤقت في الذاكرة ، فسنأخذه من قاعدة البيانات ونضيف ذاكرة التخزين المؤقت مع UUID من Redis إلى ذاكرة التخزين المؤقت في الذاكرة وتحديث TTL للمفتاح في Redis.
- إذا كان الكائن في ذاكرة التخزين المؤقت في الذاكرة ، فسنأخذه من ذاكرة التخزين المؤقت ، وتحقق مما إذا كان UUID في ذاكرة التخزين المؤقت وفي مطابقة Redis ، وإذا كان الأمر كذلك ، فقم بتحديث TTL في ذاكرة التخزين المؤقت وفي Redis. إذا لم يتطابق UUID ، ثم احذف الكائن من ذاكرة التخزين المؤقت في الذاكرة ، فاخذه من قاعدة البيانات ، أضف ذاكرة التخزين المؤقت مع UUID من Redis إلى الذاكرة.
- إذا لم يكن الكائن في Redis ، فإذا كان الكائن في ذاكرة التخزين المؤقت ، فقم بإزالته من ذاكرة التخزين المؤقت. أخذ كائن من قاعدة البيانات وإضافته إلى ذاكرة التخزين المؤقت وإلى Redis. لإزالة الموقف عندما يكون تحديث / حذف إدخال أسرع من الإضافة إلى ذاكرة التخزين المؤقت ( تعليق andreyverbin ) ، أضف كائنًا بدون معرف مستخدم صفري إلى ذاكرة التخزين المؤقت. بعد ذلك ، عند أول وصول إلى ذاكرة التخزين المؤقت ، سيتم الكشف عن الفرق في UUID مع Redis ، وسيتم طلب البيانات من قاعدة البيانات مرة أخرى.
في حالة وصول طلب لحذف كائن ، يتم حذفه على الفور من قاعدة البيانات ، ثم عمليات التخزين المؤقت:
- حذف الكائن في Redis.
- حذف الكائن في ذاكرة التخزين المؤقت في الذاكرة.
الآن ، إذا وصل طلب مماثل إلى مثيل آخر من الخدمة ، فعلى الرغم من أنه لا يزال من الممكن وجود الكائن في ذاكرة التخزين المؤقت في الذاكرة ، فلن يتم استخدامه.
تحديث الكائن ، بعد التحديث في قاعدة البيانات:
- حذف الكائن في Redis.
- حذف الكائن في ذاكرة التخزين المؤقت في الذاكرة.
عندما تطلب كائنًا في مثيل آخر من الخدمة ، فسيتم الكشف عن عدم وجوده في Redis ، لذلك تحتاج إلى أخذه من قاعدة البيانات. إذا كان هناك مثيل آخر للخدمة ، وتم نقل الطلب إليه بعد تحديث الكائن وبعد إضافته بواسطة المثيل الثاني في Redis ، عندئذٍ ، عند التحقق من UUID ، سيتم الكشف عن اختلاف ، وستأخذ المثيل الثالث للخدمة أيضًا الكائن من قاعدة البيانات.
أي في الواقع ، في أي موقف غير مفهوم ، نعتقد أن ذاكرة التخزين المؤقت لدينا غير صحيحة ، ونحن بحاجة إلى أخذ البيانات من قاعدة البيانات.
استنتاج
الحل المتقدمة لديها كل من إيجابيات وسلبيات.
الأشياء الجيدة
- مكّن نظام التخزين المؤقت المطوّر من تحقيق حوالي 19000 RPS ، وهو ما يعادل تقريباً الاختبارات مع ذاكرة التخزين المؤقت في الذاكرة.
- يحتوي رمز المشروع الأصلي على عدد أدنى من التغييرات.
سلبيات
- في حالة تعطل Redis ، تتعطل الخدمة بشكل كبير في الأداء وتعتمد على العمل مع قاعدة البيانات.
- سيتطلب كل مثيل من الخدمة مزيدًا من الذاكرة لأنه يحتوي على ذاكرة التخزين المؤقت الخاصة به في الذاكرة.
نظرًا لأن الأداء العالي كان أكثر أهمية ، فأنا لا أعتبر أن السلبيات مهمة. في المستقبل ، توجد فكرة لكتابة مكتبة لتبسيط تنفيذ ذاكرة التخزين المؤقت المختلطة ، نظرًا لأن هناك حاجة إلى استخدام ذاكرة تخزين مؤقت مماثلة في مشاريع أخرى.