مستوى المقال: مبتدئ
لو السيرفر بتاعك ماشي تمام بـ 80 مستخدم متزامن، وفجأة عند 200 المستخدمين بيشوفوا خطأ FATAL: sorry, too many clients already، المشكلة مش في حجم السيرفر ولا في الكود. المشكلة إن كل طلب بيفتح اتصال جديد على PostgreSQL، وPostgreSQL بيموت تحت ضغط الاتصالات اللي 95% منها بتنام مفيش بتعمل حاجة.
PgBouncer: حلّ مشكلة الـ Connections في 3 أسطر إعداد
المشكلة باختصار
كل اتصال (connection) على PostgreSQL بيشغّل عملية لينكس منفصلة بتحجز حوالي 9 ميجا من الذاكرة في الفاضي. 200 اتصال = 1.8 جيجا رام مستهلكة قبل ما أي query تشتغل. القيمة الافتراضية في PostgreSQL هي max_connections = 100، وأي حاجة فوق 200 على سيرفر بـ 4GB رام بتدخّلك في scenario الـ swapping ثم القتل العشوائي بـ OOM killer.
تخيّل الأسانسير قبل ما نشرح pooling علميًا
تخيّل عمارة فيها 30 دور وأسانسير واحد. لو كل ساكن لما يحتاج يطلع لازم يستنّى يبني أسانسير شخصي ليه، يستخدمه ثانيتين، وبعدها يهدّه — هتلاقي 200 عامل بيبنوا 200 أسانسير في نفس اللحظة. ده اللي بيحصل في PostgreSQL لما كل HTTP request بيفتح connection جديد، يعمل query واحدة، وبيقفله.
الحل المنطقي إن العمارة فيها 5 أسانسيرات جاهزة طول الوقت، والسكان بيتشاركوا فيهم. لما واحد يخلص، الأسانسير بيرجع متاح للي بعده. ده بالظبط اللي بيعمله PgBouncer: بيمسك مجموعة connections دائمة مع PostgreSQL، والتطبيق بتاعك بيتعامل معاه كأنه قاعدة البيانات.
الشرح العلمي بدون مثال — Connection Pooling فعليًا
الـ Connection Pool هو طبقة وسيطة بتحتفظ بعدد ثابت من اتصالات TCP المفتوحة مسبقًا مع قاعدة البيانات. لما التطبيق يطلب اتصال، الـ pool بيدّيله واحد من اللي عنده بدل ما يفتح اتصال جديد. هذا يلغي تكلفة الـ TCP handshake (حوالي 1-3ms)، تكلفة الـ TLS handshake (حوالي 30-80ms)، وتكلفة المصادقة على PostgreSQL.
PgBouncer بيشتغل بثلاث أنماط: session (اتصال للجلسة كاملة)، transaction (اتصال لكل transaction — الأشهر)، وstatement (لكل query). الـ transaction pooling بيخلّي 25 اتصال داخلي مع PostgreSQL يخدم 2000 اتصال خارجي من التطبيق.
الحل التنفيذي: PgBouncer في 3 أسطر إعداد
- ثبّت PgBouncer على نفس السيرفر اللي عليه PostgreSQL أو سيرفر منفصل.
- أنشئ ملف
/etc/pgbouncer/pgbouncer.iniبالقيم الأساسية. - غيّر connection string في تطبيقك من بورت 5432 إلى 6432.
[databases]
mydb = host=127.0.0.1 port=5432 dbname=mydb
[pgbouncer]
listen_port = 6432
listen_addr = 127.0.0.1
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
max_client_conn = 2000
default_pool_size = 25