شهية كبيرة ليتل المخازن المؤقتة في Node.js

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



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

لقد غرقنا في أحشاء المحلل ووجدنا بعض ميزات العمل مع BufferNode.js ، والتي يمكن أن توفر معرفتها وقتك وموارد الخادم بشكل كبير.

تحميل وحدة المعالجة المركزية




تم إنفاق معظم وقت المعالج في معالجة دفق السجل الوارد ، وهو أمر مفهوم. ولكن ما لم يكن واضحًا هو كثافة الموارد "للتقطيع" البدائي للتيار الوارد من الكتل الثنائية إلى خطوط بواسطة \r\n:



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

جاري محاولة القراءة


أوصلتنا مراجعة سريعة للحلول المتاحة إلى وحدة القراءة العادية المنتظمة تمامًا مع الوظائف الضرورية للتقطيع إلى خطوط:



بعد تنفيذ ذلك ، أصبح "التشريح" من الجزء العلوي لملف التعريف أعمق:



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

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



تعديل الدراجة


إنها مشكلة ... لا يزال من الضروري إجراء القطع بنفسك ، ولكن مع تحسينات من النوع Buffer.indexOf()بدلاً من "فحص البايت":



يبدو أن كل شيء على ما يرام ، والحمل في دائرة الاختبار لم يزد ، وتم إصلاح أسماء win1251 ، وانتقلنا إلى المعركة ... Ta-dam! يقطع استخدام وحدة المعالجة المركزية بشكل دوري الحد الأقصى بنسبة 100٪ :



كيف يتم ذلك .. اتضح أنه خطأنا Buffer.concatالذي "نلصقه الذيل" المتبقي من الكتلة السابقة:



ولكن لدينا فقط اللصق عندما نمر خطًا عبر الكتلة ، ولكن لا ينبغي أن يكون كثيرًا - حقًا حقًا؟ .. حسنًا ، تقريبًا. فقط في بعض الأحيان تأتي "سلاسل" عدة مئات من مقاطع 16 كيلو بايت :



بفضل زملائي المطورين الذين اهتموا بإنشاء هذا. يحدث ذلك "نادرًا ، ولكن بدقة" ، لذلك لم يكن من الممكن الرؤية مسبقًا في دائرة الاختبار.

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



الآن وقد عاد الحمل إلى مؤشرات يقوم readline.

استهلاك الذاكرة


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



"التقلبات" التقليدية حقًا ... "إذا فشل كل شيء آخر ، تفريغ!" ولم تكن الحكمة الشعبية مخيبة للآمال - لقد رأينا سحابة من المخزن المؤقت 8360 بايت بحجم إجمالي 520 ميجا بايت ...



وتم إنشاؤها داخل CopyBinaryStream - بدأ الوضع يتضح ...

نسخة ... من ستدين مع ثنائي


لتقليل كمية حركة المرور المرسلة إلى قاعدة البيانات ، نستخدم التنسيق الثنائي COPY . في الواقع ، لكل سجل تحتاج إلى إرسال مخزن مؤقت إلى الدفق ، يتكون من "قطع" - عدد الحقول في السجل (2 بايت) ثم التمثيل الثنائي لقيمة كل عمود (4 بايت لكل نوع ID + البيانات).

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

مذكرة


حسنًا ، نظرًا لأن لدينا العديد من القطع التي يتم تكرارها مرارًا وتكرارًا (على سبيل المثال ، عدد الحقول في سجلات الجدول نفسه) - فلنتذكرها فقط ، ثم نأخذ الحقول الجاهزة التي تم إنشاؤها مرة واحدة في المكالمة الأولى. استنادًا إلى تنسيق COPY للخيارات ، هناك عدد قليل جدًا من الخيارات - تأتي القطع النموذجية بحجم 1 أو 2 أو 4 بايت:



و ... بام ، وصلت المدخنة!



أي نعم ، في كل مرة تقوم فيها بإنشاء مخزن مؤقت ، يتم تخصيص جزء من الذاكرة بحجم 8 كيلو بايت افتراضيًا ، بحيث يمكن تكديس المخازن المؤقتة الصغيرة التي يتم إنشاؤها في صف "بجوار" في الذاكرة المخصصة بالفعل. وقد عمل تخصيصنا "عند الطلب" ، وتبين أنه ليس "قريبًا" على الإطلاق - وهذا هو السبب في أن كل من المخزن المؤقت 1-2-4 بايت الذي يشغله فعليًا رأس 8 كيلوبايت + - هنا ، لدينا 520 ميجابايت!

مذكرة ذكية


هم ... لماذا علينا أن ننتظر حتى هذا المخزن أو ذاك الذي يحتاج إلى 1/2 بايت؟ مع 4 بايت قضية منفصلة، ولكن بعض هذه الخيارات المختلفة ليصبح المجموع 256 + 65536. لذلك دعونا nagenerim على التوالي في كل مرة ! في الوقت نفسه ، قمنا بقطع شرط وجود كل فحص - سيعمل أيضًا بشكل أسرع ، حيث تتم التهيئة فقط في بداية العملية.



هذا ، بالإضافة إلى المخازن المؤقتة 1/2 بايت ، نقوم على الفور بتهيئة القيم الأكثر تشغيلًا (أقل من 2 بايت و -1) للقيم 4 بايت. و- ساعد ، فقط 10 ميجا بايت بدلا من 520 ميجا بايت!


All Articles