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

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

... مفيد إذا:
- لديك مصادر قليلة (مئات الآلاف)
- غالبًا ما يكون هناك حمل منهم ، وأحيانًا يصل إلى 1Krps
- يتطلب معالجة معقدة مع تجزئة حسب المصدر
مثال: مُحمل / محلل للصفقات في سياق كل منصة تداول.
... يسبب مشاكل:
- تحديد الموارد لمعالجة مصدر واحد بعملية واحدة (وحدة المعالجة المركزية الأساسية) ، حيث لا يمكن "نشرها" عبر مستلمين
- هناك حاجة إلى منسق يعيد توزيع الحمل ديناميكيًا من المصادر عبر العمليات / المواضيع / الموارد الموجودة
نظرًا لأن نموذج التحميل الخاص بنا عند مراقبة PostgreSQL ينجذب بوضوح نحو خوارزمية السحب ، وموارد عملية واحدة ونواة وحدة المعالجة المركزية الحديثة كافية بالنسبة لنا لمصدر واحد ، فقد توقفنا عند ذلك.
سجلات السحب
اتصالاتنا مع خادم المنصوص عليها جدا كثير عمليات الشبكة والعمل مع السلاسل النصية slaboformatirovannymi ، وذلك نواة جامع ذهب جافا سكريبت الكمال في تجسده كخادم نود.جي إس .
تبين أن أبسط حل للحصول على البيانات من سجل الخادم هو "عكس" ملف السجل بالكامل إلى وحدة التحكم باستخدام أمر لينكس بسيط
tail -F <current.log>. وحدة التحكم الخاصة بنا فقط ليست بسيطة ، ولكنها افتراضية - داخل اتصال آمن بالخادم الممتد عبر بروتوكول SSH .

لذلك ، عند الجلوس على الجانب الثاني من اتصال SSH ، يتلقى المُجمع نسخة كاملة من كل حركة مرور السجل عند المدخل. وإذا لزم الأمر ، يطلب من الخادم معلومات موسعة عن النظام حول الوضع الحالي.
لماذا لا يتم تسجيل النظام
هناك سببان رئيسيان:
syslogيعمل على نموذج الدفع ، لذلك من المستحيل إدارة حمل المعالجة للتيار المتولد عنه بسرعة عند نقطة الاستلام. بمعنى ، إذا بدأ زوج من المضيفين فجأة في "صب" آلاف الخطط للطلبات البطيئة ، فمن الصعب للغاية فصل معالجتهم عبر عقد مختلفة.
المعالجة هنا لا تعني الكثير من الاستقبال / التحليل "الغبي" للسجل ، مثل تحليل الخطط وحساب كثافة الموارد الحقيقية لكل من العقد .- PostgreSQL, , «» (relation/page/tuple/...).
«DBA: ».
-
من حيث المبدأ ، يمكن استخدام الحلول الأخرى كنظام DBMS لتخزين البيانات التي تم تحليلها من السجل ، لكن حجم المعلومات الواردة من 150 إلى 200 جيجابايت / يوم لا يترك مساحة كبيرة للمناورة. لذلك ، اخترنا أيضًا PostgreSQL كمخزن.
- PostgreSQL لتخزين السجلات؟ بجدية؟
- أولاً ، لا توجد سجلات فقط وليست كثيرة مثل التمثيلات التحليلية المختلفة . ثانيًا ، "أنت فقط لا تعرف كيف تطبخها!" :)

اعدادات الخادم
هذه النقطة ذاتية وتعتمد بشدة على أجهزتك ، لكننا وضعنا المبادئ التالية لأنفسنا لتهيئة مضيف PostgreSQL للتسجيل النشط.
إعدادات نظام الملفات
العامل الأكثر أهمية الذي يؤثر على أداء الكتابة هو [عدم] التركيب الصحيح لقسم البيانات. لقد اخترنا القواعد التالية:
- يتم تحميل دليل PGDATA (في حالة ext4) مع المعلمات
noatime,nodiratime,barrier=0,errors=remount-ro,data=writeback,nobh - تم نقل الدليل PGDATA / pg_stat_tmp إلى
tmpfs - و PGDATA / دليل pg_wal و انتقل إلى وسيلة أخرى، إذا كان من المعقول
راجع PostgreSQL File System Tuning
اختيار برنامج جدولة الإدخال / الإخراج الأمثل
بشكل افتراضي ، اختارت العديد من التوزيعات جدولة الإدخال / الإخراج
cfq ، التي تم شحذها لاستخدام "سطح المكتب" ، في RedHat و CentOS - noop. لكن اتضح أنه أكثر فائدة لنا deadline.
انظر PostgreSQL مقابل. جدولة الإدخال / الإخراج (cfq ، noop ، الموعد النهائي)
تقليل حجم ذاكرة التخزين المؤقت "المتسخة" تحدد
هذه المعلمة
vm.dirty_background_bytesحجم ذاكرة التخزين المؤقت بالبايت ، عند الوصول إلى النظام الذي يبدأ عملية الخلفية لمسحها على القرص. توجد معلمة مماثلة ، ولكنها حصريةvm.dirty_background_ratio - تحدد نفس القيمة كنسبة مئوية من إجمالي حجم الذاكرة - افتراضيًا ، يتم تعيينها ، وليس "... بايت".
في معظم التوزيعات تبلغ 10٪ وفي CentOS 5٪. هذا يعني أنه مع وجود ذاكرة خادم إجمالية تبلغ 16 جيجابايت ، قد يحاول النظام كتابة أكثر من 850 ميجابايت على القرص مرة واحدة ، مما يؤدي إلى تحميل ذروة IOps.
نقوم بتقليلها بشكل تجريبي حتى تبدأ قمم التسجيل بالتلاشي. من التجربة ، لتجنب الارتفاعات ، يجب أن يكون الحجم أقل من الحد الأقصى لسرعة نقل الوسائط (في عمليات الإدخال / الإخراج) مضروبًا في حجم صفحة الذاكرة. هذا ، على سبيل المثال ، لـ 7K IOps (~ 7000 × 4096) - حوالي 28 ميجابايت.
راجع تكوين خيارات Linux Kernel لإعدادات تحسين PostgreSQL
في postgresql.conf
ما هي المعلمات التي يجب رؤيتها ، الملتوية لتسريع التسجيل. كل شيء هنا فردي تمامًا ، لذلك سأقدم بعض الأفكار فقط حول الموضوع:
shared_buffers- يجب أن تكون أصغر ، لأنه مع التسجيل المستهدف لبيانات "شائعة" متداخلة بشكل خاص ، لا تظهر العملياتsynchronous_commit = off- يمكنك دائمًا تعطيل انتظار كتابة الالتزام إذا كنت تثق في بطارية وحدة تحكم RAID الخاصة بكfsync- إذا لم تكن البيانات حرجة على الإطلاق ، يمكنك محاولة إيقاف تشغيلها - "في حدود" يمكنك حتى الحصول على قاعدة بيانات في الذاكرة
هيكل جدول قاعدة البيانات
لقد قمت بالفعل بنشر بعض المقالات حول تحسين تخزين البيانات المادية:
- حول تقسيم الجداول - "نكتب في PostgreSQL على ضوء فرعي: مضيف واحد ، يوم واحد ، 1 تيرابايت"
- حول إعداد التوست - "وفر الكثير من المال على كميات كبيرة في PostgreSQL"
ولكن حول مفاتيح مختلفة في البيانات - لم يكن هناك بعد. سأخبرك عنهم.
تعتبر المفاتيح الأجنبية شريرة للأنظمة المثقلة بالكتابة. في الواقع ، هذه "عكازات" لا تسمح للمبرمج المهمل أن يكتب إلى قاعدة البيانات ما يفترض ألا يكون هناك.
اعتاد العديد من المطورين على حقيقة أن كيانات الأعمال ذات الصلة منطقيًا على مستوى وصف جداول قاعدة البيانات يجب ربطها من خلال FK. ولكن هذا ليس هو الحال!
بالطبع ، تعتمد هذه النقطة كثيرًا على الأهداف التي تحددها عند كتابة البيانات في قاعدة البيانات. إذا لم تكن بنكًا (وإذا كنت أيضًا بنكًا ، فأنت لا تقوم بمعالجة البيانات!) ، فإن الحاجة إلى FK في قاعدة بيانات مكتوبة بكثرة هي سؤال كبير.
"من الناحية الفنية" ، يقوم كل FK بعمل SELECT منفصل عند إدخال سجلمن الجدول المشار إليه. انظر الآن إلى الجدول الذي تكتب فيه بنشاط ، حيث لديك 2-3 FKs معلقة ، وقم بتقييم ما إذا كان الأمر يستحق لمهمتك المحددة أن توفر نوعًا من انخفاض التكامل في الأداء بمقدار 3-4 مرات ... أم أن الاتصال المنطقي بالقيمة كافٍ؟ لقد أزلنا جميع FKs هنا.
مفاتيح UUID جيدة . نظرًا لأن احتمال حدوث تضارب في UUIDs الذي تم إنشاؤه في نقاط مختلفة غير مرتبطة صغير للغاية ، يمكن إزالة هذا الحمل (عن طريق إنشاء بعض المعرفات البديلة) بأمان من قاعدة البيانات إلى "المستهلك". يعد استخدام UUIDs ممارسة جيدة في الأنظمة الموزعة المتصلة وغير المتزامنة.
يمكنك أن تقرأ عن المتغيرات الأخرى للمعرفات الفريدة في PostgreSQL في المقالة "PostgreSQL Antipatterns: المعرفات الفريدة ".
المفاتيح الطبيعية جيدة أيضًا ، حتى لو كانت تتكون من حقول متعددة. يجب ألا يخاف المرء من المفاتيح المركبة ، ولكن من حقل PK بديل إضافي وفهرس عليه في جدول محمل ، والذي يمكنك الاستغناء عنه بسهولة.
في الوقت نفسه ، لا أحد يمنع الجمع بين النهج. على سبيل المثال ، لدينا UUID بديل مخصص لـ "دفعة" من سجلات السجل المتسلسلة ذات الصلة بمعاملة أصلية واحدة (نظرًا لعدم وجود مفتاح طبيعي) ، ولكن يتم استخدام الزوج باعتباره PK
(pack::uuid, recno::int2)، حيث recnoيكون رقم التسلسل "الطبيعي" للسجل داخل الدفعة.
تيارات نسخ "لا نهاية لها"
PostgreSQL ، مثل OC ، "لا تحب" عندما تتم كتابة البيانات عليها على دفعات ضخمة (مثل
INSERT1000 سطر ). لكنها COPYأكثر تسامحًا مع تدفقات الكتابة المتوازنة (من خلال ). لكن يجب أن يكونوا قادرين على الطهي بعناية فائقة.

- نظرًا لأننا في المرحلة السابقة أزلنا جميع FKs ، يمكننا الآن كتابة معلومات عن نفسها
packومجموعة من المعلومات ذات الصلةreordبترتيب تعسفي ، بشكل غير متزامن . في هذه الحالة ، من الأكثر فاعلية الاحتفاظ بقناة نشطة باستمرارCOPYلكل جدول هدف . - , , «», ( —
COPY-) . , — 100, . - , , . . .
, , «» , . , . - , node-pg, PostgreSQL Node.js, API —
stream.write(data)COPY-true, ,false, .

, , « »,COPY. -
COPY- LRU «». .

هنا تجدر الإشارة إلى الميزة الرئيسية التي حصلنا عليها من خلال هذا المخطط لقراءة السجلات وكتابتها - أصبحت "الحقائق" في قاعدة بياناتنا متاحة للتحليل عبر الإنترنت تقريبًا ، بعد بضع ثوانٍ.
صقل مع ملف
يبدو أن كل شيء على ما يرام. أين هو "الخليع" في المخطط السابق؟ لنبدأ بسيط ...
تزامن أكثر من اللازم
واحدة من المشاكل الكبيرة للأنظمة المحملة هي زيادة التزامن لبعض العمليات التي لا تتطلب ذلك. في بعض الأحيان "لأنهم لم يلاحظوا" ، وأحيانًا "كان الأمر أسهل بهذه الطريقة" ، ولكن عاجلاً أم آجلاً عليك التخلص منها.
هذا سهل التحقيق لقد قمنا بالفعل بإعداد ما يقرب من 1000 خادم للمراقبة ، تتم معالجة كل منها بواسطة مؤشر ترابط منطقي منفصل ، ويقوم كل مؤشر ترابط بتفريغ المعلومات المتراكمة لإرسالها إلى قاعدة البيانات بتردد معين ، مثل هذا:
setInterval(writeDB, interval)
تكمن المشكلة هنا تحديدًا في حقيقة أن جميع التدفقات تبدأ في نفس الوقت تقريبًا ، لذا فإن لحظات الإرسال تتزامن دائمًا تقريبًا "إلى النقطة".

لحسن الحظ ، من السهل إصلاح هذا - عن طريق إضافة فاصل زمني "عشوائي" لكل من لحظة البدء والفاصل الزمني:
setInterval(writeDB, interval * (1 + 0.1 * (Math.random() - 0.5)))

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

دعنا "نقلب" الموقف - أي السماح للعمليات الفرعية بجمع ومعالجة المعلومات من مصادرها ، لكن لا تكتب إلى قاعدة البيانات! بدلاً من ذلك ، دعهم يرسلون رسالة عبر IPC لإتقانها ، وقد كتب بالفعل شيئًا حيث يجب أن يكون:

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

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

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

أولويات قائمة الانتظار
القارئ اليقظ ، بعد أن نظر إلى الصورة السابقة ، أصيب بالحيرة مرة أخرى ، "ماذا سيحدث عندما تنفد الذاكرة تمامًا ؟ .." هناك بالفعل خيارات قليلة - يجب التضحية بشخص ما.
ولكن ليست كل السجلات التي نريد تسليمها إلى قاعدة البيانات "مفيدة بنفس القدر". من مصلحتنا تدوينها قدر الإمكان من الناحية الكمية. سيساعدنا "تحديد الأولويات الأسية" البدائي بحجم السلسلة المكتوبة في:
let priority = Math.trunc(Math.log2(line.length));
queue[priority].push(line);
وفقًا لذلك ، عند الكتابة إلى قناة ، نبدأ دائمًا في البحث عن قوائم الانتظار "السفلية" - كل سطر منفصل يكون أقصر هناك ، ويمكننا إرسالهم كميًا أكثر:
let qkeys = Object.keys(queue);
qkeys.sort((x, y) => x.valueOf() - y.valueOf()); // - - !
هزيمة العوائق
لنعد الآن خطوتين إلى الوراء. بحلول الوقت الذي قررنا فيه ترك 16 مؤشر ترابط كحد أقصى لعنوان جدول واحد. إذا كان الجدول الهدف "متدفقًا" ، أي أن السجلات لا ترتبط ببعضها البعض ، فكل شيء على ما يرام. الحد الأقصى - سيكون لدينا أقفال "فعلية" على مستوى القرص.
ولكن إذا كان هذا جدولًا للتجمعات أو حتى "قاموسًا" ، فعندما نحاول كتابة صفوف باستخدام نفس PK من تدفقات مختلفة ، فسوف نتلقى انتظارًا على القفل ، أو حتى طريق مسدود. إنه لأمر محزن ...
ولكن بعد كل شيء ، ماذا نكتب - نحدد أنفسنا! النقطة الأساسية هي عدم محاولة كتابة PK من أماكن مختلفة .
أي ، عند تمرير قائمة الانتظار ، فإننا ننظر على الفور لنرى ما إذا كانت بعض الخيوط تكتب بالفعل على نفس الجدول (نتذكر أنها كلها في مساحة العنوان المشتركة لعملية واحدة) مع PK. إذا لم يكن الأمر كذلك ، فإننا نأخذه لأنفسنا ونكتبه في القاموس الموجود في الذاكرة "لأنفسنا" ، وإذا كان بالفعل يخص شخصًا آخر ، فإننا نضعه في قائمة الانتظار.
في نهاية المعاملة ، نقوم ببساطة "بتنظيف" المرفق "بأنفسنا" من القاموس.
دليل قليل
أولاً ، مع LRU ، فإن الاتصالات "الأولى" وعمليات PostgreSQL التي تخدمها تعمل دائمًا طوال الوقت تقريبًا. هذا يعني أن نظام التشغيل يقوم بتبديلها بين أنوية وحدة المعالجة المركزية بشكل أقل كثيرًا ، مما يقلل من وقت التوقف عن العمل.

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

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