تحسين PostgreSQL لخدمة تطبيق Rails

كمهندس برامج أقدم في شركة بناء منصة الرسائل لصناعة الرعاية الصحية أنا مسؤول ، بما في ذلك واجبات أخرى ، عن أداء تطبيقنا. نقوم بتطوير خدمة ويب قياسية إلى حد كبير باستخدام تطبيق Ruby on Rails لمنطق الأعمال و API ، React + Redux للمستخدمين الذين يواجهون تطبيق صفحة واحدة ، كقاعدة بيانات نستخدم PostgreSQL. تتمثل الأسباب الشائعة لمشكلات الأداء في المداخن المشابهة في الاستعلامات الثقيلة لقاعدة البيانات ، وأود أن أخبر القصة كيف طبقنا تحسينات غير قياسية ولكن بسيطة إلى حد ما لتحسين الأداء.


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


الحلول القياسية


في بداية المقال ، ذكرت عبارة "تحسينات غير قياسية" لأن كل الفواكه المنخفضة المعلقة تم اختيارها بالفعل:


  • أزلنا جميع استفسارات N + 1. كان روبي جوهرة رصاصة الأداة الأساسية
  • تمت إضافة جميع المؤشرات المطلوبة في قاعدة البيانات ، وتمت إزالة جميع البيانات غير المطلوبة ، وذلك بفضل pg_stat_statements
  • تمت إعادة كتابة بعض الاستعلامات ذات الصلات المتعددة لتحسين الكفاءة
  • قمنا بفصل الاستعلامات لجلب مجموعات ترقيم الصفحات من استعلامات الديكور. على سبيل المثال ، قمنا في البداية بإضافة عداد الرسائل لكل مربع حوار من خلال ربط الجداول ولكن تم استبداله باستعلام إضافي لزيادة النتائج. الاستعلام التالي يقوم بـ Index Only Scan ورخيص جدًا:

SELECT COUNT(1), dialog_id FROM messages WHERE dialog_id IN (1, 2, 3, 4) GROUP BY dialog_id; 

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

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


الحلول المتقدمة


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


حلول مخصصة


ضغط البيانات


قررنا التركيز على الأعراض الرئيسية - القرص IO. نظرًا لأن البيانات الأقل التي نقوم بتخزينها ، ونقص سعة الإدخال / الإخراج التي نحتاجها ، كانت هذه هي الفكرة الأساسية. بدأنا نبحث عن فرص لضغط البيانات وكان المرشحون الأوائل عبارة عن أعمدة مثل نوع المستخدم الذي تم تزويده بجمعيات متعددة الأشكال بواسطة ActiveRecord. في التطبيق ، نستخدم الوحدات النمطية التي تقودنا إلى الحصول على سلاسل طويلة مثل Module::SubModule::ModelName للجمعيات متعددة الأشكال. ما فعلناه - تحويل جميع أنواع هذه الأعمدة من varchar إلى ENUM. تبدو هجرة السكك الحديدية كما يلي:


 class AddUserEnumType < ActiveRecord::Migration[5.2] disable_ddl_transaction! def up ActiveRecord::Base.connection.execute <<~SQL CREATE TYPE user_type_enum AS ENUM ( 'Module::Submodule::UserModel1', 'Module::Submodule::UserModel2', 'Module::Submodule::UserModel3' ); SQL add_column :messages, :sender_type_temp, :user_type_enum Message .in_batches(of: 10000) .update_all('sender_type_temp = sender_type::user_type_enum') safety_assured do rename_column :messages, :sender_type, :sender_type_old rename_column :messages, :sender_type_temp, :sender_type end end end 

بعض الملاحظات حول هذا الترحيل للأشخاص الذين ليسوا على دراية بـ Rails:


  • disable_ddl_transaction! تعطيل ترحيل المعاملات. هذا أمر محفوف بالمخاطر للغاية ولكن أردنا تجنب المعاملات الطويلة. يرجى التأكد من عدم تعطيل المعاملات عند الترحيل دون الحاجة إليها.
  • في الخطوة الأولى ، نقوم بإنشاء نوع بيانات ENUM جديد على PostgreSQL. أفضل ميزة في ENUM هي حجم صغير ، وهو صغير جدًا مقارنة بـ varchar. تواجه ENUM بعض الصعوبات في إضافة قيم جديدة ولكن عادةً لا نضيف أنواع مستخدمين جديدة كثيرًا.
  • أضف عمودًا جديدًا sender_type_temp مع user_type_enum
  • ملء القيم في العمود الجديد in_batches لتجنب قفل طويل على رسائل الجدول
  • الخطوة الأخيرة مبادلة العمود القديم مع الجديد. هذه هي الخطوة الأكثر خطورة لأنه إذا تحول العمود sender_type إلى sender_type_old لكن sender_type_temp فشل في أن يصبح sender_type ، فسنواجه الكثير من المشكلات.
  • وتأتي السلامة من الهجرة القوية التي تساعد على تجنب الأخطاء عند كتابة الهجرات. إعادة تسمية العمود ليست عملية آمنة ، لذلك كان علينا أن نؤكد أننا نفهم ما كنا نفعله. في الواقع ، هناك طريقة أكثر أمانًا ولكنها أطول بما في ذلك عمليات النشر المتعددة.

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


قمنا بتحويل جميع الأعمدة المتعددة الأشكال إلى ENUM ، وقمنا بإسقاط الأعمدة القديمة بعد بضعة أيام من المراقبة وأخيراً قمنا بتشغيل VACUUM لتقليل التفتت. هذا أنقذنا ما يقرب من 10 ٪ من إجمالي مساحة القرص ولكن
تم ضغط بعض الجداول مع عدد قليل من الأعمدة مرتين! ما هو أكثر أهمية - بدأت بعض الجداول في التخزين المؤقت في الذاكرة (تذكر ، لا يمكننا بسهولة إضافة المزيد من ذاكرة الوصول العشوائي) بواسطة PostgreSQL وهذا أدى إلى انخفاض كبير في إدخال القرص المطلوب.


لا تثق بمزود الخدمة الخاص بك


تم العثور على شيء آخر في المقالة: كيف أدى تكوين PostgreSQL واحد إلى تحسين أداء الاستعلام البطيء بمقدار 50 مرة - يقوم موفر PostgreSQL الخاص بنا بعمل تهيئة تلقائية للخادم استنادًا إلى وحدة التخزين المطلوبة من ذاكرة الوصول العشوائي (RAM) والقرص ووحدة المعالجة المركزية (CPU) ولكن بأي سبب من الأسباب تركوا المعلمة random_page_cost مع القيمة الافتراضية التي هي 4 الأمثل لل HDD. إنهم يفرضون علينا تشغيل قواعد البيانات على SSD لكنهم لم يقوموا بتكوين PostgreSQL بشكل صحيح. بعد الاتصال بهم ، وصلنا إلى خطط تنفيذ أفضل:


 EXPLAIN ANALYSE SELECT COUNT(*) AS count_dialog_id, dialog_id as dialog_id FROM messages WHERE sender_type = 'Module::Submodule::UserModel1' AND sender_id = 1234 GROUP BY dialog_id; db=# SET random_page_cost = 4; QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- HashAggregate (cost=76405.45..76411.92 rows=647 width=12) (actual time=2428.412..2428.420 rows=12 loops=1) Group Key: dialog_id -> Bitmap Heap Scan on messages (cost=605.90..76287.72 rows=23545 width=4) (actual time=82.442..2376.033 rows=79466 loops=1) Recheck Cond: ((sender_id = 1234) AND (sender_type = 'Module::Submodule::UserModel1'::user_type_enum)) Heap Blocks: exact=23672 -> Bitmap Index Scan on index_messages_on_sender_id_and_sender_type_and_message_type (cost=0.00..600.01 rows=23545 width=0) (actual time=76.067..76.068 rows=79466 loops=1) Index Cond: ((sender_id = 1234) AND (sender_type = 'Module::Submodule::UserModel1'::user_type_enum)) Planning time: 3.849 ms Execution time: 2428.691 ms (9 rows) db=# SET random_page_cost = 1; QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ HashAggregate (cost=21359.54..21366.01 rows=647 width=12) (actual time=97.905..97.913 rows=12 loops=1) Group Key: dialog_id -> Index Scan using index_messages_on_sender_id_and_sender_type_and_message_type on messages (cost=0.56..21241.81 rows=23545 width=4) (actual time=0.058..60.444 rows=79466 loops=1) Index Cond: ((sender_id = 1234) AND (sender_type = 'Module::Submodule::UserModel1'::user_type_enum)) Planning time: 0.277 ms Execution time: 98.070 ms (6 rows) 

نقل البيانات بعيدا


انتقلنا جدول ضخم إلى قاعدة بيانات أخرى. يتعين علينا الحفاظ على عمليات التدقيق لكل تغيير في النظام بموجب القانون ويتم تنفيذ هذا المطلب باستخدام PaperTrail الأحجار الكريمة. تنشئ هذه المكتبة جدولًا في قاعدة بيانات الإنتاج حيث يتم حفظ جميع التغييرات في الكائنات تحت المراقبة. نحن نستخدم مكتبة الكون المتعددة لدمج مثيل قاعدة بيانات أخرى في تطبيق Rails لدينا. بالمناسبة - ستكون ميزة قياسية لـ Rails 6. هناك بعض التكوينات:


وصف الاتصال في ملف config/database.yml


 external_default: &external_default url: "<%= ENV['AUDIT_DATABASE_URL'] %>" external_development: <<: *external_default 

الفئة الأساسية لنماذج ActiveRecord من قاعدة بيانات أخرى:


 class ExternalRecord < ActiveRecord::Base self.abstract_class = true establish_connection :"external_#{Rails.env}" end 

النموذج الذي ينفذ إصدارات PaperTrail:


 class ExternalVersion < ExternalRecord include PaperTrail::VersionConcern end 

استخدام حالة في النموذج قيد التدقيق:


 class Message < ActiveRecord::Base has_paper_trail class_name: "ExternalVersion" end 

ملخص


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


PS: نوصي بشدة إضافة DBA كبيرة ل psql .

Source: https://habr.com/ru/post/ar445426/


All Articles