المستوى المطلوب: متوسط — المقال مناسب لمن يبني تطبيق ويب فيه قاعدة بيانات علائقية (PostgreSQL أو MySQL) ويريد يفهم إزاي يدير اتصالاتها بكفاءة. الافتراض إن عندك تطبيق ويب عادي بترافيك متوسط وبتستخدم قاعدة بيانات واحدة.
حوض الاتصالات (Connection Pool) وإدارة اتصالات قاعدة البيانات
لو استعلام بسيط بياخد وقت زيادة والسيرفر فاضي، المشكلة غالبًا مش في الاستعلام نفسه، دي في إن كل طلب بيفتح اتصال جديد لقاعدة البيانات من الصفر. المقال ده هيوريك إزاي تعيد استخدام الاتصالات فتوفّر عشرات المللي ثانية في كل طلب وتحمي قاعدة بياناتك من الانهيار تحت الضغط.
المشكلة باختصار
فتح اتصال جديد لـ PostgreSQL مش عملية رخيصة. بيحصل فيها مصافحة TCP، وغالبًا مصافحة TLS، وبعدين مصادقة (authentication) وتجهيز جلسة على السيرفر. ده بياخد في المعتاد من 5 لـ 30 مللي ثانية لكل اتصال، وممكن يوصل أكتر لو السيرفر بعيد جغرافيًا. زيّ فتح استعلامك نفسه اللي بياخد 2 مللي ثانية، لكن تأسيس الاتصال قبله بياخد 20. يعني 90% من الوقت راح في حاجة مالهاش علاقة بشغلك الفعلي.
ليه فتح اتصال جديد بيخنق السيرفر
تخيّل بنك بيوظّف موظّف صرف جديد لكل عميل يدخل، ويطرده بمجرد ما العميل يمشي. توظيف وتدريب الموظّف كل مرة بياخد وقت أطول من الخدمة نفسها، ولو دخل 100 عميل في نفس اللحظة هتحتاج 100 مكتب. ده بالظبط اللي بيحصل لما كل طلب يفتح اتصال جديد.
علميًا: PostgreSQL بيعمل لكل اتصال عملية (process) مستقلة على السيرفر، وكل واحدة بتستهلك ذاكرة تقديرية من 5 لـ 10 ميجابايت. وفيه حد أقصى اسمه max_connections قيمته الافتراضية 100. يعني لو ترافيكك زاد وكل طلب بيفتح اتصال، هتضرب السقف بسرعة وتبدأ ترفض طلبات بخطأ "too many connections"، حتى لو المعالج مش مشغول.
الحل: حوض اتصالات ثابت يُعاد استخدامه
بدل ما تفتح وتقفل كل مرة، افتح عدد محدود من الاتصالات مرة واحدة وسيبهم مفتوحين، وكل طلب "يستعير" اتصال جاهز، يستخدمه، ثم يرجّعه للحوض. خطوات الفكرة:
- عند تشغيل التطبيق، افتح عدد ثابت من الاتصالات (مثلًا 10) وخزّنهم في الحوض.
- كل طلب ياخد اتصال جاهز من الحوض بدل ما يعمل مصافحة جديدة.
- بعد الاستعلام، رجّع الاتصال للحوض بدل ما تقفله.
- لو كل الاتصالات مشغولة، الطلب ينتظر في طابور بدل ما يفتح اتصال إضافي على السيرفر.
# مثال شغّال باستخدام psycopg2 pool على PostgreSQL
from psycopg2 import pool
# minconn=2, maxconn=10 -> نفتح الاتصالات مرة واحدة عند بدء التطبيق
db_pool = pool.SimpleConnectionPool(
2, 10,
host="localhost", dbname="app", user="app", password="secret"
)
def get_user(user_id):
conn = db_pool.getconn() # استعارة اتصال جاهز من الحوض
try:
with conn.cursor() as cur:
cur.execute("SELECT email FROM users WHERE id = %s", (user_id,))
return cur.fetchone()
finally:
db_pool.putconn(conn) # رجّع الاتصال بدل ما تقفله