كيف ولماذا كتبنا خدمة قابلة للتطوير ومحملة للغاية لـ 1C: Enterprise: Java و PostgreSQL و Hazelcast

في هذا المقال سنتحدث عن كيف ولماذا تطورنا نظام التفاعل - آلية تنقل المعلومات بين تطبيقات العميل و 1 C: خوادم المؤسسة - من تحديد مهمة إلى التفكير في البنية وتفاصيل التنفيذ.

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

يستخدم برنامج SW التخزين الموزع البندق ومحرك البحث Elasticsearch. سنتحدث أيضًا عن Java وكيف نوسع PostgreSQL أفقيًا.
كيف ولماذا كتبنا خدمة قابلة للتطوير ومحملة للغاية لـ 1C: Enterprise: Java و PostgreSQL و Hazelcast

صياغة المشكلة

لتوضيح سبب إنشاء نظام التفاعل ، سأخبرك قليلاً عن كيفية عمل تطوير تطبيقات الأعمال في 1C.

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

نموذج تطوير خادم العميل

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

في كود التطبيق ، يجب أن تشير رؤوس الإجراءات والوظائف صراحة إلى مكان تنفيذ الكود - باستخدام التوجيهات & AtClient / & AtServer (& AtClient / & AtServer في النسخة الإنجليزية من اللغة). سيقوم مطورو 1C الآن بتصحيح لي بالقول أن التوجيهات هي في الواقع أكثر، ولكن بالنسبة لنا هذا ليس مهمًا الآن.

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

كيف ولماذا كتبنا خدمة قابلة للتطوير ومحملة للغاية لـ 1C: Enterprise: Java و PostgreSQL و Hazelcast
الرمز الذي يتعامل مع نقرة زر: سيعمل استدعاء إجراء الخادم من العميل ، ولن يتم استدعاء إجراء العميل من الخادم

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

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

وضع في الواقع

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

تصميم النظام قابل للتطوير أفقيًا. يجب تغطية الحمل المتزايد من خلال زيادة عدد العقد.

تطبيق

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

لذلك ، قررنا أن نجعل CB كمنتج منفصل. بالنسبة للشركات الأصغر ، نوصي باستخدام خادم CB الذي قمنا بتثبيته في السحابة الخاصة بنا (wss: //1cdialog.com) لتجنب النفقات الزائدة المرتبطة بتثبيت الخادم المحلي وتكوينه. ومع ذلك ، قد يعتبر كبار العملاء أنه من المناسب تثبيت خادم CB الخاص بهم في منشآتهم. استخدمنا نهجًا مشابهًا في منتج SaaS السحابي الخاص بنا. 1c - تم إصداره كمنتج إنتاجي للتثبيت بواسطة العملاء ، كما تم نشره في السحابة الخاصة بنا https://1cfresh.com/.

تطبيق

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

التواصل بين العميل والخادم - عبر مقبس الويب. إنه مناسب تمامًا لأنظمة الوقت الفعلي.

ذاكرة التخزين المؤقت الموزعة

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

بالنسبة لنا ، فإن Hazelcast هي:

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

conversationKeyChecker.check("БЕНЗОКОЛОНКА");

      doInClusterLock("БЕНЗОКОЛОНКА", () -> {

          conversationKeyChecker.check("БЕНЗОКОЛОНКА");

          createChannel("БЕНЗОКОЛОНКА");
      });

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

اختيار نظم إدارة قواعد البيانات

لدينا خبرة واسعة وناجحة مع PostgreSQL والتعاون مع مطوري نظم إدارة قواعد البيانات (DBMS).

مع الكتلة ، فإن PostgreSQL ليس بالأمر السهل - فهناك XL, XC, سيتوس، ولكن ، بشكل عام ، ليس noSQL هو الذي يتطور خارج الصندوق. لم يتم اعتبار NoSQL على أنه التخزين الرئيسي ، وكان يكفي أن نأخذ Hazelcast ، والذي لم نقم بالعمل معه من قبل.

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

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

يمكنك أن تقرأ عن تعدد المستأجرين ، على سبيل المثال ، على الموقع الإلكتروني بيانات Citus.

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

لدينا قاعدة بيانات رئيسية حيث يتم تخزين جدول التوجيه مع معلومات حول موقع جميع قواعد بيانات المشتركين.

كيف ولماذا كتبنا خدمة قابلة للتطوير ومحملة للغاية لـ 1C: Enterprise: Java و PostgreSQL و Hazelcast

لمنع قاعدة البيانات الرئيسية من أن تكون عنق زجاجة ، نحتفظ بجدول التوجيه (والبيانات الأخرى المطلوبة بشكل متكرر) في ذاكرة التخزين المؤقت.

إذا بدأت قاعدة بيانات المشترك في التباطؤ ، فسنقطعها إلى أقسام بالداخل. في مشاريع أخرى ، نستخدم ملفات pg_pathman.

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

في حالة فقدان النسخة المتماثلة المتزامنة ، تصبح النسخة المتماثلة غير المتزامنة متزامنة.
في حالة فقدان قاعدة البيانات الرئيسية ، تصبح النسخة المتماثلة المتزامنة هي قاعدة البيانات الرئيسية ، وتصبح النسخة المتماثلة غير المتزامنة نسخة متماثلة متزامنة.

Elasticsearch للبحث

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

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

[te، tech، tex، text، text، ek، eks، ext، exts، ks، kst، ksty، st، sty، you] ،

وأيضًا سيتم حفظ جذر كلمة "نص". يتيح لك هذا الأسلوب البحث في بداية الكلمة وفي منتصفها وفي نهايتها.

الصورة الكبيرة

كيف ولماذا كتبنا خدمة قابلة للتطوير ومحملة للغاية لـ 1C: Enterprise: Java و PostgreSQL و Hazelcast
تكرار الصورة من بداية المقال ولكن مع الإيضاحات:

  • الموازن يتعرض للإنترنت. لدينا nginx ، يمكن أن يكون أيًا منها.
  • تتواصل طبعات تطبيق Java مع بعضها البعض عبر Hazelcast.
  • للعمل مع مقبس الويب ، نستخدم Netty.
  • تطبيق Java مكتوب بلغة Java 8 ، يتكون من حزم OSGi. الخطط هي الترحيل إلى Java 10 والتبديل إلى الوحدات النمطية.

التطوير والاختبار

أثناء تطوير واختبار CB ، واجهنا عددًا من الميزات المثيرة للاهتمام للمنتجات التي نستخدمها.

اختبار الحمل وتسريبات الذاكرة

إن إطلاق كل إصدار CB هو اختبار حمل. مر بنجاح عندما:

  • نجح الاختبار لعدة أيام ولم يكن هناك أي رفض للخدمة
  • لم يتجاوز وقت الاستجابة للعمليات الرئيسية حدًا مريحًا
  • تدهور الأداء مقارنة بالإصدار السابق لا يزيد عن 10٪

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

نجري اختبار الحمل لنظام التفاعل في ثلاثة تكوينات:

  1. اختبار الإجهاد
  2. التوصيلات فقط
  3. تسجيل المشترك

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

على سبيل المثال ، هذا هو الجزء الذي يبدو عليه اختبار التحمل:

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

    • يدخل روبوت المحادثة إلى النظام (يحاكي الرسائل من كود الحلول المطبقة)
      • 50٪ فرصة لإنشاء قناة بيانات جديدة (مناقشة خاصة)
      • 50٪ فرصة لكتابة رسالة في أي من القنوات الموجودة

ظهر سيناريو "الاتصالات فقط" لسبب ما. هناك موقف: قام المستخدمون بتوصيل النظام ، لكن لم يتم إشراكهم بعد. يقوم كل مستخدم في الصباح في الساعة 09:00 بتشغيل الكمبيوتر ، وإنشاء اتصال بالخادم وهو صامت. هؤلاء الأشخاص خطرون ، وهناك الكثير منهم - لديهم فقط PING / PONG من الحزم ، لكنهم يحتفظون بالاتصال بالخادم (لا يمكنهم الاحتفاظ به - وفجأة رسالة جديدة). يعيد الاختبار إنتاج الموقف عندما يحاول عدد كبير من هؤلاء المستخدمين تسجيل الدخول إلى النظام خلال نصف ساعة. يبدو أنه اختبار إجهاد ، لكن تركيزه ينصب تحديدًا على هذا الإدخال الأول - حتى لا تكون هناك إخفاقات (لا يستخدم الشخص النظام ، لكنه يتراجع بالفعل - من الصعب الخروج بشيء أسوأ).

سيناريو تسجيل المشترك ينشأ من الإطلاق الأول. أجرينا اختبار تحمّل وتأكدنا من أن النظام لم يتباطأ في المراسلات. لكن المستخدمين ذهبوا وبدأ التسجيل في التراجع مع انتهاء المهلة. عند التسجيل ، استخدمنا / dev / عشوائي، وهو مرتبط بانتروبيا النظام. لم يكن لدى الخادم الوقت لتجميع ما يكفي من الانتروبيا ، وعندما تم طلب SecureRandom جديد ، تم تجميده لعشرات الثواني. هناك العديد من الطرق للخروج من هذا الموقف ، على سبيل المثال: قم بالتبديل إلى أقل أمانًا / dev / urandom ، قم بتثبيت لوحة خاصة تولد إنتروبيا ، وتوليد أرقام عشوائية مقدمًا وتخزينها في التجمع. لقد أغلقنا مشكلة التجمع مؤقتًا ، لكننا منذ ذلك الحين نجري اختبارًا منفصلاً لتسجيل المشتركين الجدد.

كمولد تحميل نستخدمه JMeter. إنه لا يعرف كيفية العمل مع مقبس الويب ، هناك حاجة إلى مكون إضافي. الأولى في نتائج البحث عن الاستعلام "jmeter websocket" هي مقالات مع BlazeMeterالتي يوصون بها البرنامج المساعد Maciej Zaleski.

من هنا قررنا أن نبدأ.

بعد بدء الاختبار الجاد تقريبًا ، اكتشفنا أن تسرب الذاكرة بدأ في JMeter.

المكون الإضافي عبارة عن قصة كبيرة منفصلة ، مع 176 نجمة بها 132 شوكة على جيثب. لم يلتزم المؤلف نفسه بها منذ عام 2015 (أخذناه في عام 2015 ، ثم لم يثير الشك) ، والعديد من مشكلات github حول تسرب الذاكرة ، و 7 طلبات سحب غير مغلقة.
إذا اخترت تحميل الاختبار باستخدام هذا البرنامج المساعد ، فيرجى ملاحظة المناقشات التالية:

  1. في بيئة متعددة الخيوط ، تم استخدام LinkedList المعتادة ، ونتيجة لذلك ، حصلنا على NPE في وقت التشغيل. يتم حلها إما عن طريق التبديل إلى ConcurrentLinkedDeque ، أو عن طريق الكتل المتزامنة. اخترنا الخيار الأول لأنفسناhttps://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/43).
  2. تسرب الذاكرة ، لا يتم حذف معلومات الاتصال عند فصل (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/44).
  3. في وضع التدفق (عندما لا يتم إغلاق مقبس الويب في نهاية العينة ، ولكن يتم استخدامه بشكل أكبر في الخطة) ، لا تعمل أنماط الاستجابة (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/19).

هذا هو واحد من هؤلاء على جيثب. ماذا فعلنا:

  1. أخذ شوكة اليران كوجان (elyrank) - يعمل على إصلاح المشكلتين 1 و 3
  2. مشكلة محلولة 2
  3. تم تحديث الرصيف من 9.2.14 إلى 9.3.12
  4. ملفوفة SimpleDateFormat في ThreadLocal ؛ SimpleDateFormat ليس مؤشر ترابط آمن مما يؤدي إلى NPE في وقت التشغيل
  5. تم إصلاح تسرب آخر للذاكرة (تم إغلاق الاتصال بشكل غير صحيح عند قطع الاتصال)

ومع ذلك فهي تتدفق!

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

لقد مر يومان ...

الآن تنفد ذاكرة Hazelcast. أظهرت السجلات أنه بعد يومين من الاختبار ، يبدأ Hazelcast في الشكوى من نقص الذاكرة ، وبعد فترة تنهار الكتلة ، وتستمر العقد في الموت واحدة تلو الأخرى. لقد قمنا بتوصيل JVisualVM بالبندق ورأينا "المنشار التصاعدي" - يطلق عليه بانتظام GC ، لكن لم يستطع مسح الذاكرة بأي شكل من الأشكال.

كيف ولماذا كتبنا خدمة قابلة للتطوير ومحملة للغاية لـ 1C: Enterprise: Java و PostgreSQL و Hazelcast

اتضح أنه في Hazelcast 3.4 ، عند حذف خريطة / multiMap (map.destroy ()) ، لا يتم تحرير الذاكرة بالكامل:

github.com/hazelcast/hazelcast/issues/6317
github.com/hazelcast/hazelcast/issues/4888

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

public void join(Authentication auth, String sub) {
    MultiMap<UUID, Authentication> sessions = instance.getMultiMap(sub);
    sessions.put(auth.getUserId(), auth);
}

public void leave(Authentication auth, String sub) {
    MultiMap<UUID, Authentication> sessions = instance.getMultiMap(sub);
    sessions.remove(auth.getUserId(), auth);

    if (sessions.size() == 0) {
        sessions.destroy();
    }
}

يتصل:

service.join(auth1, "НОВЫЕ_СООБЩЕНИЯ_В_ОБСУЖДЕНИИ_UUID1");
service.join(auth2, "НОВЫЕ_СООБЩЕНИЯ_В_ОБСУЖДЕНИИ_UUID1");

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

public void join(Authentication auth, String sub) {
    addValueToMap(sub, auth.getSessionId());
}

public void leave(Authentication auth, String sub) { 
    removeValueFromMap(sub, auth.getSessionId());
}

لقد تحسنت الرسوم البيانية.

كيف ولماذا كتبنا خدمة قابلة للتطوير ومحملة للغاية لـ 1C: Enterprise: Java و PostgreSQL و Hazelcast

ما الذي تعلمناه أيضًا عن اختبار الحمل

  1. يجب كتابة JSR223 بطريقة رائعة وتضمين ذاكرة التخزين المؤقت للترجمة - إنها أسرع بكثير. رابط.
  2. الرسوم البيانية Jmeter-Plugins أسهل في الفهم من الرسوم البيانية القياسية. رابط.

حول تجربتنا مع Hazelcast

كان Hazelcast منتجًا جديدًا بالنسبة لنا ، بدأنا العمل معه من الإصدار 3.4.1 ، والآن لدينا الإصدار 3.9.2 على خادم الإنتاج لدينا (في وقت كتابة هذا التقرير ، كان أحدث إصدار من Hazelcast هو 3.10).

توليد الهوية

بدأنا بمعرفات الأعداد الصحيحة. لنتخيل أننا بحاجة إلى فترة طويلة أخرى لكيان جديد. التسلسل في قاعدة البيانات غير مناسب ، والجداول متضمنة في التجزئة - اتضح أن هناك معرف الرسالة = 1 في DB1 ومعرف الرسالة = 1 في DB2 ، لا يمكنك وضع هذا المعرف في Elasticsearch ، في Hazelcast أيضًا ، ولكن أسوأ شيء هو إذا كنت تريد تقليل البيانات من قاعدتي بيانات إلى واحدة (على سبيل المثال ، تحديد أن قاعدة بيانات واحدة كافية لهؤلاء المشتركين). يمكنك الحصول على العديد من AtomicLongs في Hazelcast والاحتفاظ بالعداد هناك ، ثم يتم زيادة أداء الحصول على معرف جديد بالإضافة إلى وقت الاستعلام في Hazelcast. لكن لدى Hazelcast شيء أفضل - FlakeIdGenerator. عند الاتصال ، يتم منح كل عميل نطاقًا من المعرفات ، على سبيل المثال ، الأول - من 1 إلى 10 ، والثاني - من 000 إلى 10 ، وهكذا. الآن يمكن للعميل إصدار معرفات جديدة من تلقاء نفسه حتى ينتهي النطاق الصادر له. يعمل بسرعة ، ولكن إعادة تشغيل التطبيق (وعميل Hazelcast) يبدأ تسلسلًا جديدًا - ومن ثم عمليات التخطي ، وما إلى ذلك. بالإضافة إلى ذلك ، ليس من الواضح تمامًا للمطورين سبب كون المعرفات أعدادًا صحيحة ، لكنهم متناقضون كثيرًا. قمنا بوزن كل شيء وتحولنا إلى UUIDs.

بالمناسبة ، بالنسبة لأولئك الذين يريدون أن يكونوا مثل Twitter ، توجد مكتبة Snowcast - هذا هو تنفيذ Snowflake أعلى Hazelcast. يمكنك أن ترى هنا:

github.com/noctarius/snowcast
github.com/twitter/snowflake

لكننا لم نتعرف عليها بعد.

استبدال خريطة المعاملات

مفاجأة أخرى: TransactionalMap.replace لا يعمل. هذا اختبار:

@Test
public void replaceInMap_putsAndGetsInsideTransaction() {

    hazelcastInstance.executeTransaction(context -> {
        HazelcastTransactionContextHolder.setContext(context);
        try {
            context.getMap("map").put("key", "oldValue");
            context.getMap("map").replace("key", "oldValue", "newValue");
            
            String value = (String) context.getMap("map").get("key");
            assertEquals("newValue", value);

            return null;
        } finally {
            HazelcastTransactionContextHolder.clearContext();
        }        
    });
}

Expected : newValue
Actual : oldValue

اضطررت إلى كتابة الاستبدال الخاص بي باستخدام getForUpdate:

protected <K,V> boolean replaceInMap(String mapName, K key, V oldValue, V newValue) {
    TransactionalTaskContext context = HazelcastTransactionContextHolder.getContext();
    if (context != null) {
        log.trace("[CACHE] Replacing value in a transactional map");
        TransactionalMap<K, V> map = context.getMap(mapName);
        V value = map.getForUpdate(key);
        if (oldValue.equals(value)) {
            map.put(key, newValue);
            return true;
        }

        return false;
    }
    log.trace("[CACHE] Replacing value in a not transactional map");
    IMap<K, V> map = hazelcastInstance.getMap(mapName);
    return map.replace(key, oldValue, newValue);
}

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

إرفاق JAR جديد دون توقف

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

IMap<UUID, Application> map = hazelcastInstance.getMap("application");
map.set(id, application);

قراءة:

IMap<UUID, Application> map = hazelcastInstance.getMap("application");
return map.get(id);

كل شيء يعمل. ثم قررنا إنشاء فهرس في Hazelcast للبحث فيه:

map.addIndex("subscriberId", false);

وعند كتابة كيان جديد ، بدأوا في تلقي ClassNotFoundException. حاول Hazelcast الإضافة إلى الفهرس ، لكنه لم يعرف شيئًا عن فصلنا وأراد وضع JAR مع هذا الفصل فيه. لقد فعلنا ذلك بالضبط ، كل شيء نجح ، ولكن ظهرت مشكلة جديدة: كيف يتم تحديث JAR دون إيقاف الكتلة تمامًا؟ لا يلتقط Hazelcast JAR جديدًا على تحديث لكل عقدة. في هذه المرحلة ، قررنا أنه يمكننا العيش بدون عمليات بحث الفهرس. بعد كل شيء ، إذا كنت تستخدم Hazelcast كمتجر ذي قيمة رئيسية ، فكل شيء سيعمل؟ ليس حقيقيًا. هنا مرة أخرى سلوك مختلف لـ IMap و TransactionalMap. عندما لا يهتم IMap ، يعرض TransactionalMap خطأ.

IMap. نكتب 5000 عنصر ونقرأ. كل شيء متوقع.

@Test
void get5000() {
    IMap<UUID, Application> map = hazelcastInstance.getMap("application");
    UUID subscriberId = UUID.randomUUID();

    for (int i = 0; i < 5000; i++) {
        UUID id = UUID.randomUUID();
        String title = RandomStringUtils.random(5);
        Application application = new Application(id, title, subscriberId);
        
        map.set(id, application);
        Application retrieved = map.get(id);
        assertEquals(id, retrieved.getId());
    }
}

لكنها لا تعمل في معاملة ، نحصل على ClassNotFoundException:

@Test
void get_transaction() {
    IMap<UUID, Application> map = hazelcastInstance.getMap("application_t");
    UUID subscriberId = UUID.randomUUID();
    UUID id = UUID.randomUUID();

    Application application = new Application(id, "qwer", subscriberId);
    map.set(id, application);
    
    Application retrievedOutside = map.get(id);
    assertEquals(id, retrievedOutside.getId());

    hazelcastInstance.executeTransaction(context -> {
        HazelcastTransactionContextHolder.setContext(context);
        try {
            TransactionalMap<UUID, Application> transactionalMap = context.getMap("application_t");
            Application retrievedInside = transactionalMap.get(id);

            assertEquals(id, retrievedInside.getId());
            return null;
        } finally {
            HazelcastTransactionContextHolder.clearContext();
        }
    });
}

في 3.8 ، ظهرت آلية نشر فئة المستخدم. يمكنك تعيين عقدة رئيسية واحدة وتحديث ملف JAR عليها.

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

كيف نقدم أداء عالي

أربع رحلات إلى Hazelcast جيدة ، ورحلتان إلى قاعدة البيانات سيئة

دائمًا ما يكون البحث عن البيانات في ذاكرة التخزين المؤقت أفضل مما هو عليه في قاعدة البيانات ، ولكنك لا تريد تخزين السجلات التي لم تتم المطالبة بها أيضًا. يُترك تحديد ما سيتم تخزينه مؤقتًا للمرحلة الأخيرة من التطوير. عندما يتم ترميز الوظيفة الجديدة ، نقوم بتشغيل تسجيل PostgreSQL لجميع الاستعلامات (log_min_duration_statement إلى 0) وتشغيل اختبار التحميل لمدة 20 دقيقة.يمكن لأدوات مثل pgFouine و pgBadger إنشاء تقارير تحليلية بناءً على السجلات المجمعة. في التقارير ، نبحث بشكل أساسي عن الطلبات البطيئة والمتكررة. للاستعلامات البطيئة ، نقوم ببناء خطة تنفيذ (شرح) وتقييم ما إذا كان يمكن تسريع مثل هذا الاستعلام. الطلبات المتكررة لنفس المدخلات تتناسب جيدًا مع ذاكرة التخزين المؤقت. نحاول إبقاء الاستعلامات "ثابتة" ، جدول واحد لكل استعلام.

استغلال

تم إطلاق CB كخدمة عبر الإنترنت في ربيع عام 2017 ، حيث تم إصدار منتج CB منفصل في نوفمبر 2017 (في ذلك الوقت في حالة تجريبية).

لأكثر من عام من التشغيل ، لم تكن هناك مشاكل خطيرة في تشغيل خدمة CB عبر الإنترنت. نحن نراقب الخدمة عبر الإنترنت من خلال Zabbix، وجمع ونشر من خيزران.

يأتي توزيع خادم CB في شكل حزم أصلية: RPM ، DEB ، MSI. بالإضافة إلى ذلك ، بالنسبة لنظام التشغيل Windows ، نقدم أداة تثبيت واحدة في شكل EXE واحد يقوم بتثبيت الخادم ، Hazelcast و Elasticsearch على جهاز واحد. في البداية أطلقنا على هذا الإصدار من التثبيت "عرض توضيحي" ، ولكن أصبح من الواضح الآن أن هذا هو خيار النشر الأكثر شيوعًا.

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

إضافة تعليق