المستوى: متوسط
لو Postgres بتاعك بيرفض اتصالات جديدة عند 100 مستخدم ويرمي خطأ FATAL: sorry, too many clients already، السيرفر مش ضعيف. المشكلة في طريقة Postgres نفسه بيتعامل مع كل اتصال. PgBouncer بـ ملف config مكوّن من 12 سطر بيخلّيك تشغّل 5,000 client متزامن على نفس السيرفر بـ 512MB RAM إضافية فقط، بدون أي تعديل في كود تطبيقك.
المشكلة باختصار: ليه Postgres بينهار عند 100 اتصال
Postgres بيتبع نموذج process-per-connection. يعني لكل client يفتح اتصال، Postgres بيـ fork عملية منفصلة في الـ kernel، بياخد بين 5MB و 10MB RAM في الـ idle حتى لو الاتصال مش بيعمل أي query فعلي. ده تصميم متعمد عشان العزل الأمني، لكنه بيخلّي scaling الاتصالات مكلف جدًا.
الـ default في Postgres 16 هو max_connections=100. لو عندك تطبيق Node.js شغّال على 8 instances وكل واحد فاتح pool بـ 20 اتصال، يبقى عندك 160 اتصال نشط طول الوقت، وأغلبهم بيقعد فاضي 92% من الوقت بينتظر طلب جديد من المستخدم. ولما تيجي تعمل scaling لـ 20 instance بسبب زيادة في الترافيك، فجأة بتلاقي 400 اتصال محجوزة على سيرفر مش قادر يستحمل أكتر من 100.
مثال مبسّط: شبابيك البنك والعملاء
تخيّل بنك فيه 100 شبّاك فقط (هي الـ max_connections) و 2,000 عميل بيدخلوا الفرع يوميًا. كل شبّاك بياخد كرسي ومكتب وموظف ثابت، حتى لو الشبّاك مفتوح لعميل بيدوّر على ورقة في شنطته. اللي بيحصل: الـ 1,900 عميل التانيين بيقفوا في الطابور برّا، وأول ما يخلص شبّاك من الواحد بيدخل عميل جديد.
PgBouncer هو شخص محترف بيقف عند باب البنك. بياخد طلب العميل، يدخل بسرعة شبّاك متاح، ينفّذ المعاملة، ويرجع للعميل بالنتيجة. الـ 100 شبّاك بيكفوا فعلاً 2,000 عميل لو كل معاملة بتاخد ثانيتين والعميل بيستلم نتيجته في 10 ثواني. السر إن الشبّاك مش محجوز للعميل طول الوقت، هو محجوز فقط أثناء المعاملة.
التعريف العلمي: PgBouncer هو lightweight connection pooler بيشتغل في طبقة TCP بين تطبيقك و Postgres. بيحتفظ بـ pool ثابت من الاتصالات الحقيقية لـ Postgres (مثلاً 50)، ويعرض على التطبيق آلاف الـ pseudo-connections. الـ reuse للـ pool بيحصل بثلاث modes حسب اختيارك، وكل mode له trade-offs مختلفة.
الـ pooling modes الثلاثة وقاعدة الاختيار
- Session pooling: الاتصال محجوز للـ client من أول ما يتصل لحد ما يقفل الاتصال. بيشيل overhead الـ TCP handshake والـ authentication، لكن مش بيوفر اتصالات على الـ DB. مفيد لو تطبيقك بيفتح اتصال لكل request ويقفله فورًا.
- Transaction pooling: الاتصال بيتحجز من بداية transaction (BEGIN) لحد نهايته (COMMIT/ROLLBACK). بعد كده الاتصال بيرجع للـ pool ويقدر يخدم client تاني. ده الـ mode الافتراضي لتطبيقات الويب الحديثة.
- Statement pooling: الاتصال بيتحجز لكل query فردي. أعلى throughput ممكن، لكن بيكسر تمامًا أي transaction متعدد الـ statements و prepared statements. نادر الاستخدام إلا في analytics workloads بسيطة.