كلاود
development

Connection Pooling: لماذا تنهار قاعدة بياناتك عند 200 اتصال وكيف تمنعه

كلاود|٢٠ سبتمبر ٢٠٢٦|6 دقائق قراءة|١٤٢ مشاهدة
Connection Pooling: لماذا تنهار قاعدة بياناتك عند 200 اتصال وكيف تمنعه

كل اتصال جديد بقاعدة البيانات يكلّف ذاكرة وميلي ثانية للتفاوض، وعند مئات الاتصالات تلتهم الذاكرة وتنهار القاعدة. الحل تجميع الاتصالات: بركة صغيرة معاد استعمالها، وProxy مثل PgBouncer عند الحاجة.

قاعدة بياناتك لا تنهار لأنها تعالج استعلامات كثيرة، بل غالباً لأنها تفتح اتصالات كثيرة في وقت واحد. كل اتصال جديد ليس مجانياً: فتحه يتطلب مصافحة شبكة، ثم مصادقة، ثم تخصيص ذاكرة داخل القاعدة، وكل هذا يكلّف من 1 إلى 3 ميلي ثانية قبل أن ينفّذ أي استعلام. الأخطر أن كل اتصال مفتوح يحجز ذاكرة دائمة: في MySQL كل خيط اتصال (thread) يحجز عادة من 256 كيلوبايت إلى عدة ميغابايت بحسب إعدادات المخازن المؤقتة لكل جلسة، وفي PostgreSQL كل اتصال عملية (process) منفصلة تكلّف نحو 5 إلى 10 ميغابايت. اضرب ذلك في 200 اتصال متزامن، فتجد قاعدة PostgreSQL وحدها التهمت 1 إلى 2 غيغابايت من الذاكرة على الاتصالات فقط، قبل أي بيانات. الجواب المباشر: لا تفتح اتصالاً جديداً لكل طلب، بل استخدم تجميع الاتصالات (connection pooling)، وهو بركة صغيرة من الاتصالات الجاهزة يعاد استعمالها. رفع max_connections وحده حل خاطئ لأنه يؤجّل الانهيار إلى رقم أكبر ثم يجعله أسوأ حين يقع، فالذاكرة تنفد أسرع. البركة الصحيحة صغيرة عمداً، وقاعدة عملية شهيرة: عدد الاتصالات الأمثل يقارب (عدد الأنوية × 2) زائد عدد الأقراص، أي أن خادماً بـ4 أنوية قد يكون أسرع بـ10 اتصالات نشطة منه بـ200.

القاعدة الجوهرية: الاتصالات مورد نادر وثمين، لا رخيص ولا لانهائي. البركة الصغيرة المعاد استعمالها أسرع وأثبت من فيض اتصالات يتنافس على معالج محدود.

لماذا الاتصال الجديد مكلف

حين يطلب تطبيقك اتصالاً جديداً، تجري سلسلة خطوات قبل أول استعلام:

  • مصافحة TCP: تبادل حزم لتأسيس القناة.
  • المصادقة: تبادل بيانات الدخول والتحقق منها.
  • تخصيص الموارد: القاعدة تنشئ خيطاً أو عملية وتحجز لها مخازن مؤقتة في الذاكرة.

هذا كله من 1 إلى 3 ميلي ثانية لكل اتصال. إن كان تطبيقك يفتح ويغلق اتصالاً لكل طلب HTTP، فأنت تدفع هذه الضريبة آلاف المرات في الدقيقة. البركة تدفعها مرة عند بدء التطبيق، ثم كل طلب يستعير اتصالاً جاهزاً من البركة وينفّذ استعلامه فوراً ويعيده، فيسقط زمن الحصول على اتصال من ميلي ثانية إلى ميكروثانية.

لماذا رفع max_connections وحده خطأ

الحل الغريزي عند خطأ «too many connections» هو رفع max_connections من 151 (الافتراضي في MySQL) إلى 500 أو 1000. هذا يعالج العرض ويفاقم المرض. السبب أن كل اتصال مسموح به يحجز ذاكرة كامنة، فترفع سقف استهلاك الذاكرة إلى ما يتجاوز ذاكرة السيرفر نفسه، وحين تمتلئ الاتصالات فعلاً ينهار السيرفر كلياً (OOM kill) بدل أن يرفض اتصالاً واحداً بأدب.

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

الأصح أن تُبقي max_connections معقولاً بما يناسب ذاكرة سيرفرك، وتضع بركة أمامه تدير الطلبات بعدد اتصالات صغير.

البركة داخل التطبيق: Laravel وNode

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

Laravel وPHP

نموذج PHP-FPM يفتح اتصالاً لكل عملية عامل (worker)، فعدد الاتصالات يقارب عدد عمّال FPM. إن كان لديك 100 عامل FPM على كل خادم تطبيق، وثلاثة خوادم، فأنت عند 300 اتصال محتمل. الضبط الصحيح: قلّل عمّال FPM إلى ما يناسب أنويتك فعلاً، ولمنصات serverless أو عمّال كثيرين استخدم Proxy خارجياً (سيأتي). PHP بطبيعته لا يحتفظ ببركة دائمة بين الطلبات إلا عبر الاتصالات الدائمة (persistent connections) التي تحتاج حذراً.

Node.js

مكتبات مثل pg وmysql2 تعطيك بركة صريحة. اضبط max (أقصى حجم للبركة) بعناية، فقيمة معقولة لكل نسخة تطبيق تتراوح غالباً من 10 إلى 20 اتصالاً، لا 100. تذكّر أن عدة نسخ من تطبيق Node تضرب هذا الرقم في عددها، فاحسب الإجمالي مقابل سقف القاعدة. اضبط أيضاً مهلة الخمول (idle timeout) لإغلاق الاتصالات الزائدة، ومهلة الاتصال (connection timeout) حتى يفشل الطلب بسرعة بدل التعليق.

PgBouncer وProxySQL: متى تحتاج Proxy خارجياً

حين تتعدد نسخ التطبيق (خوادم كثيرة، حاويات، دوال serverless)، تصبح كل بركة داخلية جزيرة لا تعرف الأخريات، فيتضخم مجموع الاتصالات على القاعدة. هنا يأتي دور Proxy مركزي للتجميع:

  • PgBouncer (لـPostgreSQL): خفيف جداً، يجلس بين تطبيقاتك والقاعدة، ويجمّع آلاف اتصالات التطبيق في عشرات الاتصالات الفعلية للقاعدة. في وضع transaction pooling يعيد استعمال الاتصال بين المعاملات، فيخدم عدداً هائلاً من العملاء بموارد ضئيلة.
  • ProxySQL (لـMySQL): يجمّع الاتصالات ويضيف توجيه الاستعلامات وتوزيع القراءة على النسخ وتخزين النتائج مؤقتاً.

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

كم اتصالاً تتحمل باقتك

لأن كل اتصال يحجز ذاكرة، عدد الاتصالات الآمن مرتبط بذاكرة سيرفرك. تقدير عملي على باقات الخادم السحابي:

الباقةRAMحجم بركة معقول
أساس2 GBمن 10 إلى 20 اتصالاً نشطاً
نمو4 GBمن 20 إلى 40 اتصالاً
أعمال8 GBمن 40 إلى 80 اتصالاً، أو PgBouncer للأكثر
مؤسسة16 GBبرك أوسع مع متسع للتخزين المؤقت

لاحظ أن هذه الأرقام أصغر بكثير مما يتوقع كثيرون، وهذا مقصود: القاعدة تعمل أفضل بعدد اتصالات نشطة صغير من عدد كبير متنافس. إن احتجت ذاكرة أوسع للقاعدة، إضافة +2GB RAM بـ600 ريال سنوياً حل مباشر.

أوضاع التجميع الثلاثة: اعرف الفرق قبل أن تختار

حين تستخدم Proxy مثل PgBouncer، يقدّم ثلاثة أوضاع للتجميع تختلف جذرياً في الكفاءة والقيود، وسوء الاختيار يكسر تطبيقك بصمت:

الوضعمتى يُعاد الاتصال للبركةالكفاءة
session poolingعند انتهاء جلسة العميل كاملةالأقل، اتصال محجوز طوال جلسة العميل
transaction poolingعند انتهاء كل معاملةالأعلى، الأكثر استعمالاً للويب
statement poolingعند انتهاء كل جملةالأقصى، لكن بقيود صارمة

الوضع الأشيع والأكفأ لتطبيقات الويب هو transaction pooling، لأن كل طلب HTTP غالباً معاملة قصيرة، فيُعاد الاتصال فوراً بعدها لعميل آخر، ويخدم عدد صغير من الاتصالات الفعلية آلاف العملاء. لكن انتبه للقيد المهم: في transaction pooling لا يمكنك الاعتماد على حالة تعيش عبر معاملات مثل prepared statements على مستوى الجلسة أو المتغيرات المؤقتة (SET) أو الجداول المؤقتة، لأن الاتصال قد يذهب لعميل آخر بين معاملة وأخرى. لهذا اضبط برنامج التشغيل (driver) على عدم استخدام prepared statements الدائمة، أو استخدم إعداداً متوافقاً مع البركة. أما وضع session pooling فهو أضمن توافقاً لكنه يهدر الفائدة لأنه يحجز الاتصال طوال جلسة العميل، فلا تستخدمه إلا إن كان تطبيقك يعتمد فعلاً على حالة الجلسة.

تجنّب الطابور الخانق

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

خاتمة عملية

ابدأ بقياس: راقب عدد الاتصالات المتزامنة الفعلي (SHOW STATUS LIKE 'Threads_connected' في MySQL، أو pg_stat_activity في PostgreSQL). إن رأيت عشرات أو مئات الاتصالات الخاملة، فأنت تهدر ذاكرة. اضبط حجم بركة التطبيق إلى رقم صغير مبني على قاعدة (أنوية × 2)، لا على الأمل. أبقِ max_connections معقولاً بما يناسب ذاكرتك ولا ترفعه كحل للأعراض. إن تعددت نسخ تطبيقك أو استخدمت serverless، ضع PgBouncer أو ProxySQL كبركة مركزية. القاعدة النهائية: الاتصال مورد نادر تديره بعناية، لا عدّاد ترفعه حتى ينهار السيرفر، وإن أردت خادماً بذاكرة تكفي بركة مريحة، ابدأ من سيرفرات الرياض بزمن استجابة نحو 8ms.

مشاركة