“بس اس کے آگے PgBouncer لگا دیں” Postgres کنکشن کے تقریباً ہر مسئلے کا ایک خودکار جواب بن چکا ہے، اور یہ اتنی بار درست ثابت ہوتا ہے کہ اکثر کوئی یہ پوچھنے کی زحمت ہی نہیں کرتا کہ PgBouncer کے تین pooling modes میں سے کون سا ابھی فعال کیا گیا ہے، یا آیا ان کے ORM کا اپنا connection handling اب pooler کے ساتھ تعاون کرنے کے بجائے اس سے ٹکرا رہا ہے۔ پھر کوئی migration ایک prepared statement شامل کر دیتی ہے، یا کوئی session search_path سیٹ کرتا ہے، اور وہ چیز جو ہر test میں کام کر رہی تھی production میں ایسے error کے ساتھ ٹوٹ جاتی ہے جو بظاہر pooler کی طرف اشارہ بھی نہیں کرتا۔
“too many connections” کے نیچے اصل مسئلہ یہ ہے کہ Postgres connection مہنگا ہوتا ہے — ہر connection ایک مکمل backend process fork کرتا ہے جس کا اپنا memory overhead ہوتا ہے — جبکہ ایک جدید Python web app آسانی سے ہزاروں concurrent منطقی database operations چاہ سکتی ہے۔ Pooling انہی دو حقیقتوں میں توازن پیدا کرنے کے لیے موجود ہے، لیکن آپ جو مخصوص mode منتخب کرتے ہیں وہ یہ طے کرتا ہے کہ آپ کی application کو session-level رویّے میں کس چیز پر انحصار کرنے کی اجازت ہے۔ اگر mode اور pool sizing دونوں ہی غلط ہوں، تو آپ ایک failure mode (too many connections) کو ایک زیادہ باریک failure mode سے بدل دیتے ہیں (خاموشی سے ٹوٹے ہوئے prepared statements، یا ایسے workers جو ایک ایسے connection کے لیے قطار میں ہوں جو پہلے ہی دو mode settings دور بیکار پڑا ہو)۔
آپ یہ سیکھیں گے:
- کیوں ایک raw Postgres connection اتنا مہنگا ہے کہ شروع سے ہی pooling کی ضرورت پڑتی ہے
- PgBouncer کے session، transaction، اور statement pooling modes میں اصل فرق کیا ہے
- خاص طور پر transaction mode میں کیا ٹوٹتا ہے، اور کیوں
- اندازے سے کوئی گول عدد چننے کے بجائے pool کو درست طریقے سے size کیسے کریں
- async Python frameworks کس طرح pool-sizing کی گنتی کو sync frameworks کے مقابلے میں بدل دیتے ہیں
- FastAPI اور Django کے لیے PgBouncer کو framework کے اپنے connection handling سے ٹکراؤ کے بغیر کیسے configure کریں
- صرف limit بڑھانے کے بجائے “too many connections” error کی واقعی تشخیص کیسے کریں
فہرستِ مضامین
- کیوں Postgres Connections مہنگے ہوتے ہیں
- PgBouncer کے تین Pooling Modes
- Transaction Mode میں کیا ٹوٹتا ہے
- Pool کو درست طریقے سے Size کرنا
- Async Python اور Pool Sizing
- FastAPI اور Django کے لیے PgBouncer Configure کرنا
- “Too Many Connections” کی تشخیص
- PgBouncer کے وہ متبادل جنہیں جاننا مفید ہے
- عام غلطیاں
کیوں Postgres Connections مہنگے ہوتے ہیں
Postgres process-per-connection model استعمال کرتا ہے — ہر نیا connection ایک dedicated backend process fork کرتا ہے، جس کے ساتھ query execution، sort buffers، اور cached catalog lookups کے لیے اپنی memory بھی ہوتی ہے۔ یہ سادہ اور مضبوط ہے، لیکن اس کا مطلب یہ ہے کہ ہر connection کی حقیقی، غیر معمولی memory cost ہوتی ہے (عام طور پر ہر idle connection کے لیے کئی megabytes، active query load میں اس سے زیادہ) اور fork کرنے کے لیے حقیقی CPU بھی درکار ہوتا ہے۔ postgresql.conf میں max_connections کی default value 100 خاص طور پر اس لیے ہے کیونکہ server چند سو connections سے آگے خوبصورتی سے scale نہیں کرتا — application concurrency سے براہِ راست مطابقت کے لیے اسے ہزاروں تک بڑھا دینا عموماً مجموعی throughput بہتر کرنے کے بجائے خراب کر دیتا ہے، کیونکہ server backend processes کے درمیان context-switching میں query work سے زیادہ وقت گزارنے لگتا ہے۔
دوسری طرف، حقیقی traffic کے تحت ایک Python web app کے پاس سینکڑوں یا ہزاروں concurrent requests ہو سکتی ہیں، اور ہر ایک بظاہر database connection چاہتی ہے۔ browser tab کے برابر traffic کو 1:1 تناسب سے Postgres backend processes کے ساتھ جوڑنا scale نہیں کرتا — یہی عدم مطابقت، نہ کہ کوئی مخصوص bug، اصل وجہ ہے کہ connection pooling موجود ہے۔ ایک pooler application اور Postgres کے درمیان بیٹھتا ہے، حقیقی backend connections کی ایک چھوٹی، مقررہ تعداد کھلی رکھتا ہے، اور اس چھوٹے pool پر اس سے کہیں زیادہ منطقی client connections کو multiplex کرتا ہے۔
driver-level pool (جیسے SQLAlchemy کا QueuePool، asyncpg.create_pool()، یا Django کا CONN_MAX_AGE) اسی مسئلے کا ایک محدود ورژن حل کرتا ہے: یہ ایک application process کے اندر connections کو reuse کرتا ہے، جو ایک single-process script کے لیے مددگار ہے لیکن اس صورتحال میں کچھ نہیں کرتا جو production میں واقعی “too many connections” کا سبب بنتی ہے — درجنوں آزادانہ طور پر scale ہونے والے application pods یا worker processes، جن میں سے ہر ایک اپنا driver-level pool چلا رہا ہو، اور سب ایک ہی Postgres instance سے بیک وقت connect کر رہے ہوں۔ اگر سو pods ہر ایک بیس driver-level connections کھولیں تو یہ دو ہزار حقیقی Postgres backends بنتے ہیں، چاہے ہر individual pod کا pool کتنا ہی اچھی طرح tune کیا گیا ہو۔ PgBouncer جیسے external pooler ان تمام pods کے نیچے ایک shared layer کے طور پر بیٹھتے ہیں، اور یہی واحد جگہ ہے جہاں پورے fleet میں کل connection count کو حقیقتاً محدود کیا جا سکتا ہے۔
PgBouncer کے تین Pooling Modes
PgBouncer Postgres کے لیے de facto standard external pooler ہے، اور اس کا رویّہ تقریباً مکمل طور پر ایک setting سے متعین ہوتا ہے: pool_mode۔
Session pooling — ایک client اپنی session کی پوری مدت تک وہی server connection برقرار رکھتا ہے، جو صرف disconnect ہونے پر release ہوتا ہے۔ application کے نقطۂ نظر سے یہ عملی طور پر Postgres سے براہِ راست connect ہونے جیسا ہے؛ ہر session-level feature (prepared statements، session variables، advisory locks، LISTEN/NOTIFY) بالکل توقع کے مطابق کام کرتا ہے۔ اس کا tradeoff یہ ہے کہ یہ connection-count کے مسئلے کو زیادہ حل نہیں کرتا — ایک long-lived client جب تک connected رہتا ہے ایک حقیقی Postgres connection پکڑے رکھتا ہے، اور یہی وہ چیز ہے جسے pooling اصل میں ٹھیک کرنے کے لیے تھی۔
Transaction pooling — ایک server connection صرف ایک single transaction کی مدت کے لیے client کو دیا جاتا ہے، پھر transaction commit یا rollback ہوتے ہی فوراً pool میں واپس چلا جاتا ہے۔ یہی وہ mode ہے جو واقعی scaling benefit فراہم کرتا ہے: سینکڑوں idle client connections حقیقی server connections کی صرف چند تعداد کو share کر سکتی ہیں، کیونکہ ہر ایک صرف اسی مختصر وقفے کے لیے server connection پکڑتا ہے جب وہ واقعۃً کوئی transaction چلا رہا ہو۔ اور اسی وجہ سے production میں یہی mode بہت بڑے فرق سے سب سے زیادہ استعمال ہوتا ہے۔
Statement pooling — سب سے aggressive mode، جس میں ہر statement کے بعد server connection release ہو جاتا ہے، حتیٰ کہ transaction کے اندر بھی۔ عملی طور پر یہ کم استعمال ہوتا ہے کیونکہ یہ multi-statement transactions کو سرے سے support نہیں کرتا — ایک statement میں BEGIN اور اگلے میں COMMIT دو مختلف server connections پر جا سکتے ہیں، جو transactional semantics کو مکمل طور پر توڑ دیتا ہے۔ زیادہ تر حقیقی deployments اس mode کو ہاتھ بھی نہیں لگاتیں۔
; pgbouncer.ini
[databases]
mydb = host=127.0.0.1 port=5432 dbname=mydb
[pgbouncer]
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 20
یہ configuration application سے بیک وقت ایک ہزار تک client connections قبول کرتی ہے، جبکہ Postgres کے ساتھ صرف 20 حقیقی connections ہی کبھی کھولتی ہے — یہی وہ تناسب ہے جو پہلی جگہ pooling کو deploy کرنے کے قابل بناتا ہے۔
Transaction Mode میں کیا ٹوٹتا ہے
Transaction mode کی رفتار براہِ راست اسی چیز سے آتی ہے جو اسے خطرناک بناتی ہے: کیونکہ ایک client کا server connection transactions کے درمیان بدل سکتا ہے، ہر وہ چیز جو اس بات پر منحصر ہو کہ server-side session state transactions کے درمیان برقرار رہے، خاموشی سے ٹوٹ جاتی ہے یا غیر مستقل رویّہ اختیار کرتی ہے۔
Prepared statements۔ PREPARE، اور اس کے اوپر بننے والی کوئی بھی ORM یا driver feature (Django کے persistent connections یہاں اثر انداز ہوتے ہیں، اور asyncpg default کے طور پر statements prepare کرتا ہے)، یہ فرض کرتی ہے کہ statement اسی backend پر prepared رہے گی جس پر اسے تیار کیا گیا تھا۔ Transaction mode میں اگلا transaction کسی بالکل مختلف backend پر جا سکتا ہے جس نے وہ PREPARE کبھی دیکھا ہی نہ ہو — نتیجتاً “prepared statement does not exist” جیسے errors آتے ہیں جو کبھی کبھار اور load-dependent محسوس ہوتے ہیں، کیونکہ وہ اس بات پر منحصر ہوتے ہیں کہ pool کس backend کو واپس دیتا ہے۔
Session-level SET statements۔ transaction کے باہر SET search_path = tenant_42 server connection پر برقرار رہتا ہے، client کی منطقی session پر نہیں — چنانچہ جو اگلا client pool سے وہی server connection حاصل کرے گا وہ پچھلے client کی آخری SET inheritance میں لے لے گا، جب تک اسے واضح طور پر reset نہ کیا گیا ہو۔ اگر search_path (یا کوئی بھی session-scoped setting) tenant isolation کے لیے استعمال ہو رہی ہو تو یہ ایک حقیقی اور خاموش multi-tenant data leak risk ہے۔ Transaction کے اندر SET LOCAL اسی transaction تک محدود رہتا ہے اور commit پر خودکار reset ہو جاتا ہے، اور transaction mode میں یہی اسی خیال کا محفوظ ورژن ہے۔
Advisory locks۔ transaction کے باہر رکھا گیا pg_advisory_lock() connection پر explicit release تک برقرار رہتا ہے — لیکن transaction mode میں آپ کے transaction کے ختم ہوتے ہی وہ connection کسی بالکل مختلف client کو دیا جا سکتا ہے، لہٰذا lock یا تو ارادے سے پہلے release ہو جاتا ہے یا، اس سے بھی بدتر، بعد کے کسی غیر متعلقہ client کے پاس موجود دکھائی دیتا ہے۔ اس کے بجائے pg_advisory_xact_lock() استعمال کریں، جو transaction تک محدود ہوتا ہے اور commit یا rollback پر ہمیشہ صاف طور پر release ہو جاتا ہے۔
LISTEN/NOTIFY۔ ایک backend پر register کیا گیا LISTEN اس وقت بے معنی ہو جاتا ہے جب وہ connection واپس pool میں چلا جائے اور کسی دوسرے کو دے دیا جائے — transaction mode بنیادی طور پر long-lived listeners کے ساتھ مطابقت نہیں رکھتا، کیونکہ اس بات کی کوئی ضمانت نہیں ہوتی کہ وہی backend، یا حتیٰ کہ وہی client، اس subscription کو زندہ رکھے گا۔
Temporary tables۔ CREATE TEMP TABLE session (یعنی physical backend connection) کے scope میں ہوتا ہے، client کی منطقی connection کے scope میں نہیں — ایک transaction میں بنائی گئی temp table اگلے transaction میں موجود نہ ہو سکتی ہے، یا غیر متوقع طور پر پرانے data کے ساتھ ابھی بھی موجود ہو سکتی ہے، اس backend پر جہاں اگلا transaction جا گرے۔
یہاں متحد کرنے والا اصول یہ ہے: transaction pooling کے تحت ہر connection کو transactions کے درمیان stateless سمجھیں۔ ہر وہ چیز جسے statements کے درمیان برقرار رہنا ہو، اسے یا تو واضح طور پر ایک ہی transaction کے اندر scope کرنا ہوگا (SET LOCAL، pg_advisory_xact_lock) یا مکمل طور پر avoid کرنا ہوگا (long-lived LISTEN، session-level prepared statements) — یہی pgbouncer fastapi اور pgbouncer django setups کے خراب ہونے کی عملی بنیاد ہے، ایسے طریقوں سے جو random لگتے ہیں جب تک آپ کو خاص طور پر اسی چیز کو دیکھنا نہ آ جائے۔
Pool کو درست طریقے سے Size کرنا
یہ فطری خیال کہ default_pool_size کو متوقع concurrent requests کے برابر رکھ دیا جائے، غلط ہے — اور ایک خاص، معروف وجہ سے غلط ہے: PostgreSQL کی اپنی guidance (جسے PgBouncer project اور متعدد production postmortems بھی دہرाते ہیں) یہ ہے کہ optimal pool size عموماً intuition کے مقابلے میں کہیں کم ہوتی ہے، کیونکہ database connection اپنے زیادہ تر “busy” وقت میں CPU استعمال نہیں بلکہ I/O کا انتظار کر رہا ہوتا ہے — اور ایک چھوٹا pool جس کے ساتھ مختصر queue ہو، اکثر ایک بڑے مگر contention والے pool سے بہتر کارکردگی دیتا ہے، کیونکہ Postgres کے پاس queries چلانے کے لیے حقیقی طور پر محدود CPU cores ہوتے ہیں۔
ایک عام طور پر حوالہ دی جانے والی ابتدائی formula، جو PostgreSQL performance guidance سے اخذ کی گئی ہے، یہ ہے:
pool_size = ((core_count * 2) + effective_spindle_count)
SSD-backed storage والے ایک جدید server کے لیے (جہاں spindle count کو مؤثر طور پر کم سمجھا جاتا ہے)، یہ ایک single database کے لیے مناسب ابتدائی pool size عموماً (cores * 2) + 1 کی حد میں لاتا ہے، پھر اسے صرف اس وقت اوپر adjust کیا جاتا ہے جب actual queue wait time کو measure کر لیا جائے، نہ کہ پہلے سے اندازے کی بنیاد پر۔ اصل tuning loop یہ ہے: ایک محتاط pool size سیٹ کریں، PgBouncer کے SHOW POOLS output میں ان clients کو monitor کریں جو connection کے انتظار میں ہیں، اور pool صرف اسی وقت بڑھائیں جب wait time حقیقی اور مستقل ہو، نہ کہ ایک سست query کے connection کو بہت دیر تک پکڑے رکھنے کا وقتی اثر۔
SHOW POOLS;
-- cl_waiting column: clients currently queued for a server connection
معمول کے load میں nonzero اور مستقل بڑھتا ہوا cl_waiting ہی وہ اصل signal ہے کہ یا تو pool size بڑھائی جائے یا ان سست queries کی تحقیق کی جائے جو connections کو ضرورت سے زیادہ دیر تک پکڑے رکھتی ہیں — نہ کہ کوئی اندازہ، اور نہ ہی pool size کو max_client_conn کے برابر کر دینا۔
Async Python اور Pool Sizing
Sync frameworks (classic Django views، یا threaded WSGI server کے ساتھ Flask) تقریباً ایک worker process یا thread کو ایک in-flight request کے ساتھ map کرتے ہیں، اس لیے connection کی گنتی تقریباً workers * pool_size_per_worker کے قریب ہوتی ہے۔ Async frameworks اس بات کو بنیادی طور پر بدل دیتے ہیں: ایک single async worker سینکڑوں concurrent منطقی requests کو in flight رکھ سکتا ہے، اور ممکن ہے کہ ان میں سے ہر ایک اسی لمحے database connection چاہتی ہو، جبکہ یہ سب بہت کم OS threads پر multiplex ہو رہی ہوتی ہیں۔
اس کا مطلب یہ ہے کہ async app کا application-level connection pool (asyncpg.create_pool()، یا SQLAlchemy async engine pool) اس سے آزادانہ طور پر size کیا جانا چاہیے کہ ASGI server کتنی concurrent requests سنبھال سکتا ہے — اور اسے PgBouncer کے max_client_conn سے خاصا کم رہنا چاہیے، کیونکہ PgBouncer اس کے اوپر pooling کی دوسری layer ہے، اس کا متبادل نہیں۔
import asyncpg
pool = await asyncpg.create_pool(
dsn="postgresql://user:pass@pgbouncer-host:6432/mydb",
min_size=5,
max_size=20,
statement_cache_size=0, # required under PgBouncer transaction mode
)
statement_cache_size=0 transaction-mode PgBouncer کے تحت اختیاری نہیں ہے — asyncpg default کے طور پر ہر physical connection کے حساب سے statements prepare اور cache کرتا ہے، اور یہی وہ رویّہ ہے جو اس وقت ٹوٹ جاتا ہے جب underlying backend calls کے درمیان بدل سکتا ہو۔ اسے disable کرنا correctness کے بدلے میں کچھ per-query overhead لے آتا ہے؛ اس قدم کو چھوڑ دینا PgBouncer کے خلاف asyncpg کے “locally works, under load breaks” errors کی سب سے عام وجہ ہے۔
FastAPI اور Django کے لیے PgBouncer Configure کرنا
FastAPI کے لیے SQLAlchemy async engine کے ساتھ، engine کو Postgres کے 5432 کے بجائے PgBouncer کے port (عام طور پر 6432) کی طرف point کریں، اور SQLAlchemy کی اپنی statement caching کو بھی اوپر asyncpg والی ہی وجہ سے disable کریں:
from sqlalchemy.ext.asyncio import create_async_engine
engine = create_async_engine(
"postgresql+asyncpg://user:pass@pgbouncer-host:6432/mydb",
pool_size=10,
max_overflow=5,
connect_args={"statement_cache_size": 0},
)
Django کے لیے، CONN_MAX_AGE Django کے اپنے persistent-connection رویّے کو کنٹرول کرتا ہے، اور اسے PgBouncer کے ساتھ duplicate ہونے کے بجائے تعاون کرنا چاہیے — transaction-mode PgBouncer کے اوپر Django کی connection persistence چلانے کا مطلب ہے کہ دو آزاد pooling layers ایک ہی connections کے بارے میں فیصلے کر رہی ہوں۔ CONN_MAX_AGE = 0 سیٹ کرنا (تاریخی default) ہر request کو اپنی منطقی connection صاف طور پر کھولنے اور بند کرنے دیتا ہے، اور اصل pooling کا تمام کام نیچے PgBouncer پر چھوڑ دیتا ہے:
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"HOST": "pgbouncer-host",
"PORT": "6432",
"NAME": "mydb",
"DISABLE_SERVER_SIDE_CURSORS": True, # required under transaction mode
}
}
DISABLE_SERVER_SIDE_CURSORS بھی اوپر statement_cache_size=0 والی ہی نوعیت کی وجہ سے اہم ہے — .iterator() کے لیے Django کی server-side cursor support یہ فرض کرتی ہے کہ متعدد fetch calls کے دوران backend connection مستحکم رہے گا، جس کی ضمانت transaction mode نہیں دیتا۔
“Too Many Connections” کی تشخیص
FATAL: too many connections for role یا remaining connection slots are reserved کا مطلب تقریباً کبھی یہ نہیں ہوتا کہ حل max_connections بڑھانا ہے۔ اس کے بجائے یہ ترتیب اختیار کریں:
- دیکھیں کہ
pg_stat_activityمیں اصل میں کیا چیز connections کو کھلا رکھے ہوئے ہے۔SELECT state, count(*) FROM pg_stat_activity GROUP BY state;— اگرidle in transactionکی تعداد زیادہ ہے تو اس کا مطلب ہے application code transactions کھول رہا ہے اور بند نہیں کر رہا، جسے pooling اکیلے حل نہیں کر سکتی۔ - یہ چیک کریں کہ آیا PgBouncer واقعی راستے میں موجود بھی ہے۔ اکثر ایسا ہوتا ہے کہ ایک service کے لیے PgBouncer شامل کر دیا جاتا ہے مگر دوسری service، cron job، یا analytics tool براہِ راست Postgres کے port 5432 سے connect کر رہا ہوتا ہے اور مکمل طور پر pooler کو bypass کر رہا ہوتا ہے۔
- Pool mode کو حقیقی usage کے خلاف چیک کریں۔ زیادہ concurrency میں session mode وہی مسئلہ دوبارہ پیدا کر دیتا ہے جسے pooling حل کرنے کے لیے بنائی گئی تھی — اگر connections تیزی سے واپس نہیں آ رہے تو transaction mode، اور اس کے ساتھ طویل
idle in transactionsessions کو ٹھیک کرنا، تقریباً ہمیشہ اصل حل ہوتا ہے۔ - اسی کے بعد
max_connectionsبڑھانے پر غور کریں، اور یہ سمجھتے ہوئے کریں کہ ہر اضافی slot Postgres server پر حقیقی memory cost رکھتا ہے، محض configuration-level نہیں۔
یہاں اگلا فطری قدم EXPLAIN ANALYZE output پڑھنا ہے تاکہ معلوم ہو سکے کون سی specific queries زیادہ دیر تک چل رہی ہیں اور connections کو ضرورت سے زیادہ وقت تک کھلا رکھ رہی ہیں — تشخیص کے اس حصے کے لیے Postgres query plans پڑھنے کی گائیڈ دیکھیں۔
PgBouncer کے وہ متبادل جنہیں جاننا مفید ہے
PgBouncer default recommendation ہے کیونکہ یہ mature، lightweight، اور اچھی طرح documented ہے، لیکن یہ فی instance single-threaded ہے (multi-core scaling کے لیے SO_REUSEPORT کے پیچھے متعدد instances چلائیں) اور یہ جاننا مفید ہے کہ اور کیا موجود ہے۔ Odyssey، جو Yandex نے بنایا، اور PgCat، جو Rust میں لکھا گیا، دونوں box سے باہر multi-threaded pooling فراہم کرتے ہیں اور read replicas پر load-balancing بھی — اگر ایک single PgBouncer process بھاری connection churn کے تحت CPU-bound ہو جائے تو یہ واقعی مفید ہے۔ Managed Postgres providers (RDS Proxy، Supabase کا built-in pooler، Neon’s pooler) تیزی سے transaction-mode pooling کے مساوی چیزیں براہِ راست platform میں bundle کر رہے ہیں، اس لیے self-managed PgBouncer instance کھڑا کرنے سے پہلے یہ دیکھ لینا مفید ہے — اس post کے وہی mode tradeoffs پھر بھی لاگو ہوتے ہیں، بس pgbouncer.ini file کے بجائے provider کے dashboard کے ذریعے configure کیے جاتے ہیں۔
عام غلطیاں
غلطی: PgBouncer pool size کو max_client_conn کے برابر set کرنا۔ یہ pooling کے مقصد کو مکمل طور پر شکست دیتا ہے — اصل فائدہ ایک چھوٹے server-side pool سے آتا ہے جو client connections کی کہیں بڑی تعداد کو serve کرے۔ حل: server pool کو core count اور measured wait time کی بنیاد پر size کریں، اس سے آزاد کہ کتنے clients connect کر سکتے ہیں۔
غلطی: transaction mode کو بغیر حفاظت والے session state کے ساتھ استعمال کرنا۔ SET، advisory locks، اور prepared statements سب خاموشی سے غلط برتاؤ کرتے ہیں۔ حل: SET LOCAL اور pg_advisory_xact_lock استعمال کریں، اور client-side statement caching disable کریں۔
غلطی: دو آزاد pooling layers کو بغیر coordination کے چلانا۔ Django کا CONN_MAX_AGE یا SQLAlchemy کا engine pool جب نیچے PgBouncer کے اپنے pool سے ٹکراتا ہے تو الجھا دینے والی، دوبارہ پیدا کرنا مشکل connection exhaustion سامنے آتی ہے۔ حل: application-level pool کو محدود رکھیں اور بھاری multiplexing PgBouncer کو کرنے دیں۔
غلطی: connection errors کے پہلے جواب کے طور پر max_connections بڑھا دینا۔ یہ symptom کا علاج کرتا ہے اور server پر حقیقی memory pressure بڑھاتا ہے۔ حل: limit کو چھیڑنے سے پہلے pg_stat_activity کے ذریعے معلوم کریں کہ اصل میں connections کو کیا چیز پکڑے ہوئے ہے۔
غلطی: یہ بھول جانا کہ async connection pools اور PgBouncer ایک دوسرے کی جگہ نہیں لیتے بلکہ ایک دوسرے کے اوپر stack ہوتے ہیں۔ پہلے سے sized PgBouncer pool کے اوپر asyncpg کے pool max_size کو بہت زیادہ set کرنا مسئلہ حل کرنے کے بجائے صرف queueing point کو دوسری جگہ منتقل کرتا ہے۔ حل: application pool کو محدود رکھیں اور تصدیق کریں کہ connection bursts کو جذب کرنے والی layer app نہیں بلکہ PgBouncer ہے۔
اختتامیہ
Connection pooling کوئی ایک سادہ on/off فیصلہ نہیں ہے — یہ تین واقعی مختلف معاہدوں میں سے ایک انتخاب ہے کہ statements کے درمیان کون سا session state برقرار رہتا ہے، اور transaction mode کی رفتار اس کی statelessness سے الگ نہیں کی جا سکتی۔ اسے درست کرنے کا مطلب ہے pool mode کو ان چیزوں کے مطابق چننا جن پر آپ کی application حقیقتاً انحصار کرتی ہے، pool کو core count اور measured wait time کی بنیاد پر size کرنا نہ کہ کسی اندازاً گول عدد سے، اور async connection pools اور PgBouncer کو ایک دوسرے کی نقل سمجھنے کے بجائے دو باہمی تعاون کرنے والی layers سمجھنا۔
کیا آپ کا موجودہ pool size وہ عدد ہے جسے آپ نے measure کیا ہے، یا وہ جو بس محفوظ محسوس ہوا؟
