إدارة الاتصالات والتجميع
إدارة الاتصالات والتجميع
كل اتصال بـ PostgreSQL أو MySQL هو عملية نظام تشغيل ثقيلة (أو خيط). عند ذروة حركة المرور، تفتح التصميمات التطبيقية السطحية اتصالًا جديدًا لكل طلب — وعلى نطاق واسع، يتحول ذلك إلى توقف في الإنتاج أسرع من أي نمط مضاد آخر لقواعد البيانات. فهم كيفية إدارة الاتصالات فعليًا، ولماذا يجب أن يجلس موزّع الاتصالات بين تطبيقك وقاعدة بياناتك، مهارة لا غنى عنها لأي مهندس يشغّل قواعد بيانات على نطاق إنتاجي.
ما الذي يعنيه max_connections فعلًا
في PostgreSQL، max_connections هو سقف صارم على إجمالي عدد عمليات الخادم الخلفي التي سيقبلها الخادم — بما يشمل تطبيقك، ونظيراتك الثانوية للنسخ المتماثل، والعمال الخلفيين، وجلساتك الإدارية. الافتراضي هو 100. كل اتصال يستهلك ما بين 5 إلى 10 ميغابايت من ذاكرة الوصول العشوائي لعملية الخادم الخلفي الخاصة به بالإضافة إلى حمل الذاكرة المشتركة. على نسخة بـ 16 غيغابايت، السماح بـ 500 اتصال يستهلك من 2.5 إلى 5 غيغابايت قبل تنفيذ أي استعلام واحد.
الفهم الجوهري: معظم الاتصالات خاملة معظم الوقت. تطبيق ويب نموذجي به 200 مستخدم متزامن لا يحتاج إلى 200 اتصال بقاعدة بيانات نشط — بل يحتاج إلى جزء صغير من ذلك في أقصى الأحوال، لأن عمليات قاعدة البيانات تستغرق ميلي ثوانٍ. لكن بدون موزّع، تفتح معظم الأطر اتصالًا واحدًا لكل خيط أو goroutine وتحتفظ به، مما يعني أن 200 خيط يعني 200 اتصال.
تستخدم MySQL max_connections بنفس الطريقة، لكن التكلفة لكل اتصال تختلف لأن MySQL تستخدم نموذج الخيوط بدلًا من نموذج العمليات المتفرعة. العمليات الحسابية للذاكرة لا تزال حاسمة — تخصص MySQL مخازن مؤقتة لكل خيط (sort_buffer_size، join_buffer_size، إلخ) عند الطلب، لذا فإن 1000 اتصال تنفّذ عمليات فرز كبيرة يمكن أن تستنفد ذاكرة الوصول العشوائي فجأة.
max_connections، تُرفض محاولات الاتصال الجديدة بخطأ — لا تُوضع في قائمة انتظار. يرمي تطبيقك الآن استثناءات لكل طلب. تولّد الاستثناءات عمليات إعادة محاولة، والتي تولّد المزيد من محاولات الاتصال، والتي تُرفض أيضًا. هذه الحلقة المرتدة الإيجابية يمكن أن تُوقف الخدمة لدقائق حتى بعد حل السبب الجذري. موزّع الاتصالات يمنع ذلك بوضع الطلبات الجديدة في قائمة انتظار على مستوى الموزّع بدلًا من قصف قاعدة البيانات.
طبقة الموزّع: PgBouncer و ProxySQL
PgBouncer هو الموزّع الافتراضي الأكثر استخدامًا لـ PostgreSQL. إنه عملية C أحادية الخيط تجلس بين تطبيقك وـ PostgreSQL. تحافظ PgBouncer على عدد صغير من اتصالات الخادم طويلة الأمد وتُوزّع آلاف الاتصالات قصيرة الأمد من العملاء عليها. إنها خفيفة بما يكفي للتشغيل على نفس مضيف قاعدة البيانات دون تأثير معنوي على وحدة المعالجة المركزية.
تعمل PgBouncer في ثلاثة أوضاع:
- تجميع الجلسة: يُخصص اتصال خادم للعميل طوال مدة جلسة العميل. هذا هو الوضع الأأمن ومتوافق مع جميع ميزات PostgreSQL (بما في ذلك
SETوLISTENوالجمل المحضّرة)، لكنه يوفر أقل فائدة تعددية. - تجميع المعاملات: يُخصص اتصال خادم فقط لمدة معاملة، ثم يُعاد إلى التجمّع. هذا هو الوضع الإنتاجي الموصى به لمعظم أعباء العمل — يسمح لمئات اتصالات العملاء بمشاركة تجمّع خوادم صغير. القيود: يجب إدارة الجمل المحضّرة والأقفال الاستشارية على مستوى التطبيق، أو استخدام PgBouncer 1.21+ مع تتبع الجمل المحضّرة من جانب الخادم.
- تجميع الجمل: يُعاد اتصال الخادم بعد كل جملة. نادرًا ما يُستخدم لأنه يكسر المعاملات متعددة الجمل.
ProxySQL هو المعادل لـ MySQL وMariaDB، لكن بمجموعة ميزات أوسع بكثير: قواعد توجيه الاستعلامات، وتقسيم القراءة/الكتابة، وإعادة كتابة الاستعلامات، والاتصالات المتعددة، وإمكانية المراقبة الغنية عبر جداول الإحصاءات الداخلية. تُستخدم ProxySQL بشكل شائع أمام مجموعات Galera وMySQL Group Replication لتوجيه الكتابات إلى الأساسي والقراءات إلى النسخ المتماثلة بشكل شفاف.
إعداد PgBouncer للإنتاج
الإعداد الأدنى لـ PgBouncer في الإنتاج لأساسي PostgreSQL. هذا الملف يقع في /etc/pgbouncer/pgbouncer.ini:
echo "md5$(echo -n 'passwordusername' | md5sum | awk '{print $1}')". بالنسبة لـ SCRAM، استخدم psql -c "SELECT rolpassword FROM pg_authid WHERE rolname = 'appuser';" على قاعدة البيانات وانسخ السلسلة SCRAM-SHA-256$... مباشرة إلى userlist.txt. في الإنتاج، استخدم auth_query لجعل PgBouncer تستعلم من قاعدة البيانات عن بيانات الاعتماد مباشرة، مما يلغي مشكلة المزامنة كليًا.
إعداد ProxySQL لـ MySQL
تُعدَّل ProxySQL عبر واجهتها الإدارية (اتصال بروتوكول MySQL على المنفذ 6032) بدلًا من ملف إعداد، مما يجعلها مناسبة للإدارة البرمجية وTerraform. الجداول الرئيسية هي mysql_servers وmysql_users وmysql_query_rules.
تحديد حجم التجمّع: المعادلة والواقع
أكثر قاعدة تحديد حجم تجمّع استشهادًا تأتي من وثائق HikariCP، والتي تستمد بدورها من أبحاث اختبار قواعد البيانات: حجم التجمّع = (عدد الأنوية × 2) + عدد الأقراص الفعّال. بالنسبة لخادم تطبيق نموذجي بـ 8 أنوية وتخزين SSD، ينتج ذلك تجمّعًا من حوالي 17 اتصالًا. هذا الرقم يصدم المهندسين الذين يفترضون أن الأكبر أفضل.
المنطق: وحدة المعالجة المركزية يمكنها تنفيذ خيط واحد فقط في وقت واحد لكل نواة. إذا كان لديك اتصالات قاعدة بيانات أكثر من أنوية وحدة المعالجة المركزية على مضيف قاعدة البيانات، فأنت ببساطة تتسلسل العمل على جانب قاعدة البيانات بمزيد من الحمل الزائد (تبديل السياق، وضغط الذاكرة) مما كانت ستستهلكه اتصالات أقل. إشباع وحدة المعالجة المركزية لقاعدة البيانات هو وضع الفشل، ليس الهدف.
في الممارسة العملية، حدّد حجم تجمّعك بالبدء من المعادلة والتحقق من الإشارات التالية في الإنتاج:
- عدد الاتصالات النشطة في
pg_stat_activity— كم عدد الاتصالات التي تنفّذ استعلامات فعلًا (state = 'active') مقابل الخاملة؟ cl_waitingفي PgBouncer — عملاء ينتظرون اتصال خادم. إذا كان هذا غير صفري، فحجم تجمّعك أصغر من اللازم لذروة الحمل.- استخدام وحدة المعالجة المركزية لقاعدة البيانات — إذا كان أقل من 60% في الذروة، فلديك مساحة؛ إذا كان أعلى من 80%، فإضافة اتصالات تجمّع ستجعل زمن الاستجابة أسوأ، لا أفضل.
- زمن استجابة الاستعلام p99 — يجب ألا يتدهور مع زيادة حجم التجمّع. إذا تدهور، فأنت تُرهق قاعدة البيانات.
max_connections بناءً على ذاكرة الوصول العشوائي للنسخة باستخدام المعادلة LEAST({DBInstanceClassMemory/9531392}, 5000). تحصل db.t3.micro (1 غيغابايت ذاكرة وصول عشوائي) على ما يقرب من 87 اتصالًا. مع 3 خوادم تطبيق تحتفظ كل منها بتجمّع من 30، تكون قد استخدمت جميعها. يوجد RDS Proxy (مبني على معمارية PgBouncer/ProxySQL) لحل هذه المشكلة بالضبط، وهو الحل الموصى به لأعباء Lambda أو ECS التي تفتح اتصالات كثيرة قصيرة الأمد.
أوضاع الفشل الإنتاجي وكيفية اكتشافها
ثلاثة أوضاع فشل متعلقة بالاتصالات تتكرر عبر حوادث الإنتاج على كل المقاييس:
1. تسرّب الاتصالات: يحصل كود التطبيق على اتصال ويفشل في تحريره (استثناء غير ملتقَط، كتلة finally مفقودة، خلل في ORM). تتراكم الاتصالات حتى ينضب التجمّع. اكتشفه بـ: SELECT count(*), state FROM pg_stat_activity GROUP BY state; — عدد خامل متزايد بدون حمل مقابل هو تسرب.
2. الخمول في المعاملة لفترة طويلة: يبدأ الاتصال معاملة، يقوم ببعض العمل، ثم يتوقف (ينتظر إدخال المستخدم، أو محجوب على استدعاء API خارجي). المعاملة تحتجز الأقفال. تنتظر الاستعلامات الأخرى خلفها. التجمّع يبدو "ممتلئًا" رغم أن معظم الاتصالات خاملة. اكتشفه بـ:
3. نضوب التجمّع تحت حركة مرور متفجرة: طفرة مفاجئة في معدل الطلبات تجعل جميع اتصالات التجمّع مشغولة في وقت واحد. تنتظر الطلبات الجديدة عند الموزّع. إذا تجاوزت قائمة الانتظار query_wait_timeout، يُعيد PgBouncer خطأً للعميل. هذا سلوك صحيح — الموزّع يحمي قاعدة البيانات — لكن على التطبيق معالجة هذا الخطأ بلطف (تراجع أسي، قاطع دارة) بدلًا من التعطل.
إدارة الاتصالات ليست بنية تحتية جذابة. نادرًا ما تُطرح في مراجعات المعمارية. لكن استنفاد التجمّع هو أحد أكثر أسباب الانقطاعات الناجمة عن قواعد البيانات شيوعًا على نطاق واسع — وموزّع اتصالات مُضبط بشكل صحيح أمام قاعدة بياناتك هو أحد أعلى تحسينات الموثوقية أثرًا يمكنك تنفيذه في بعد ظهيرة واحدة.