مستوى المقال: محترف — يفترض إنك تعرف PostgreSQL backend model، الفرق بين process و thread، وبتشتغل بـ database/sql أو psycopg2 أو asyncpg في الإنتاج.
لو فريق الـ ops رفع max_pool_size من 20 لـ 100 علشان "نحل مشكلة الـ latency" ولقيتم الـ P99 طلع من 18ms لـ 84ms، انتم مش لوحدكم. السلوك ده مش غريب على PostgreSQL، وله سبب معماري واضح هنشرحه بأرقام مقاسة على pgbench.
Connection Pooling في PostgreSQL — الـ pool الأكبر مش دائمًا أسرع
المشكلة باختصار
الافتراض الشائع: connections أكتر = throughput أعلى. الواقع: PostgreSQL بيستخدم process-per-connection model (مش thread-per-connection زي MySQL/InnoDB)، وكل connection بيستهلك من 5 لـ 12MB RAM، وبيعمل lock contention على هياكل داخلية زي ProcArray و LWLock. بعد عتبة معينة، كل connection جديد بيبطّأ كل الباقي.
مثال مبسّط قبل الدخول في التفاصيل
تخيّل مطعم فيه 8 طباخين (cores) و 200 طاولة (clients). لو فتحت 200 شباك طلبات، الطباخين هيقضّوا وقتهم في التنقل بين الطلبات أكثر من الطبخ. لو فتحت 17 شباك بس، كل طباخ هيشتغل بتركيز ولو في طلب مستني هيخلص في ثواني. PostgreSQL بيشتغل بنفس المنطق: الـ CPU هو الـ bottleneck في معظم workloads الـ OLTP، والـ connections الزيادة بتعمل context switching بدون قيمة.
المعادلة اللي PgBouncer FAQ بيوصي بيها
المعادلة المرجعية المذكورة في PostgreSQL Wiki و PgBouncer FAQ و مقال Brandur Leach:
connections ≈ ((core_count × 2) + effective_spindle_count)على سيرفر بـ 8 cores مع SSD واحد (spindle=1)، العدد الأنسب حوالي 17 connection. على 16 cores، 33 connection. الأرقام دي مش قاعدة مقدسة، لكنها نقطة بداية أحسن بكتير من 100 أو 200 اللي بيكتبهم الناس بالـ "اضرب في سقف عالٍ علشان نأمن".
ليه المعادلة دي بالظبط؟
السبب نظرية الـ queueing و Little's Law. لو الـ CPU هو الـ bottleneck، الـ connections اللي بتقعد تستنى الـ CPU بتعمل overhead بدون فائدة. زيادة الـ connections بعد cores × 2 بتدخل في diminishing returns، وبعد cores × 5 تقريبًا بتدخل في negative returns بسبب lock contention.
تكوين سليم في Go
import (
"database/sql"
"time"
_ "github.com/jackc/pgx/v5/stdlib"
)
db, err := sql.Open("pgx", dsn)
if err != nil {
log.Fatal(err)
}
// 8-core machine, SSD storage
db.SetMaxOpenConns(20)
db.SetMaxIdleConns(20)
db.SetConnMaxLifetime(5 * time.Minute)
db.SetConnMaxIdleTime(2 * time.Minute)