One self-hosted console to run your entire business — commerce, ERP, HRM, CRM & manufacturing

लोड में data tier: PostgreSQL, कनेक्शन pooling, Redis और pgvector

ट्रैफ़िक का उछाल आए तो पहली प्रतिक्रिया डेटाबेस बड़ा करने की होती है, और वह अक्सर ग़लत निकलती है। जानिए write throughput से साइज़ कैसे तय करें, PgBouncer से कनेक्शन pool कैसे करें, Redis या Valkey को भूमिका के हिसाब से कैसे बाँटें, और checkout धीमा किए बिना pgvector search कैसे चलाएँ।

Author

Anichur Rahaman

2 सप्ताह पहले12 min read1 views
लोड में data tier: PostgreSQL, कनेक्शन pooling, Redis और pgvector

फ़्लैश सेल की सुबह 9:02 बजे, एक काल्पनिक दृश्य में, ऑन-कॉल इंजीनियर dashboard पर देखता है कि 1,000 में से 940 database connection इस्तेमाल हो रहे हैं और checkout तीन सेकंड से ऊपर खिंच रहा है। कोई सुझाव देता है कि दूसरी लहर आने से पहले database को अगले साइज़ पर चढ़ा दिया जाए।

यह अंदाज़े पर चलना होगा। उन 940 connections में से ज़्यादातर idle हैं, दो request के बीच PHP process ने उन्हें पकड़ रखा है, और database का CPU 35 प्रतिशत के आसपास है। असली कतार ग्यारह worker की है, जो उन्हीं चंद पंक्तियों पर write के लिए इंतज़ार कर रहे हैं। connection की गिनती सिर्फ़ लक्षण है, और साइज़ बढ़ाना लक्षण का इलाज करना है।

इसका असली रूप मैंने देखा है। एक प्लैटफ़ॉर्म पर मैंने ऐसे तय समय वाले उछाल की तैयारी की थी जिसमें लगभग 1,000 user एक ही मिनट में सक्रिय होते; वहाँ managed database (16 GB RAM, 4 vCPU, करीब 1,000 connection की सीमा) कभी रुकावट नहीं बना। रुकावट सिर्फ़ एक serial worker था। इस लेख में मैं data tier को उसी तरह रखूँगा जैसे आज उसकी योजना बनाता हूँ: क्या मापना है, pool कैसे करना है, replica क्या कर सकता है और क्या नहीं, Redis या Valkey को साफ़ ज़िम्मेदारी कैसे देनी है, और transactional database पर vector query डाले बिना AI search कैसे जोड़ना है।

यह "हाई-वॉल्यूम system की इंजीनियरिंग" सीरीज़ का तीसरा भाग है। पहले भाग में edge और web tier है और दूसरे में queue और worker। यहाँ हम एक परत और नीचे, data तक उतरते हैं।

connection की गिनती से नहीं, write throughput से साइज़ तय करें

"1,000 max connections" लिखा देखकर पता चलता है कि कितने क्लाइंट जुड़ सकते हैं, यह नहीं कि server कितना काम कर सकता है। चार vCPU एक ही पल में गिनती की query चला सकते हैं। बाक़ी सब इंतज़ार करते हैं, जुड़े हों या नहीं।

इसलिए वही मापिए जो रिलेशनल database को सचमुच भर देता है: write throughput (प्रति सेकंड commit और WAL की मात्रा), CPU, डिस्क की latency और lock का इंतज़ार। connection की गिनती सिर्फ़ किसी और समस्या का लक्षण है, जैसे धीमी query जो connection रोके रखती हैं।

मेरा नियम सीधा है: जब तक load test साबित न कर दे कि database भर चुका है, उसे अपग्रेड न करें। ऊपर वाले सेटअप में हल यह था कि web और worker node अलग किए गए और करीब 12 worker process चलाए गए। database ने अतिरिक्त लोड आराम से संभाल लिया। लगभग 20 worker के बाद job सिर्फ़ database write की कतार में खड़े रहते थे, और असली छत वही है, connection नहीं। ये आँकड़े एक ही सेटअप के हैं, कोई सार्वभौमिक benchmark नहीं; अपना test ख़ुद चलाइए।

Data tier का नक़्शा: PostgreSQL primary और replica के आगे PgBouncer, भूमिका के हिसाब से बँटा Redis या Valkey, अलग pgvector database, और compute के उसी region में object storage
एक tier, कई store, हर एक का एक ही काम और अपनी सीमाएँ।

connection pool करें

PHP बहुत सारे छोटे connection खोलता है: हर request, और हर queue job भी, जुड़ता और कटता है। PostgreSQL हर connection के लिए अलग process चलाता है, इसलिए एक हज़ार क्लाइंट एक भी काम की query चलाने से पहले काफ़ी मेमोरी खा सकते हैं। PgBouncer जैसा pooler एप्लिकेशन और database के बीच बैठता है। सैकड़ों क्लाइंट connection कुछ दर्जन असली server connections को साझा करते हैं।

एक काल्पनिक हिसाब। दो web node, हर एक में 60 PHP-FPM child, 120 connection खोल सकते हैं; 12 worker process 12 और जोड़ते हैं, और scheduler व कुछ admin session मिलाकर करीब 8। कुल करीब 140 connection, यानी 4 vCPU के लिए 140 PostgreSQL process की खींचतान। PgBouncer को transaction mode में रखकर 20 server connection का pool दें तो वही 140 क्लाइंट इन 20 को साझा करते हैं। अगर औसत transaction अपना connection 5 ms रोकता है, तो 20 connection ज़्यादा से ज़्यादा 20 ÷ 0.005 = 4,000 transaction प्रति सेकंड उठा सकते हैं। CPU आपको इससे काफ़ी नीचे रोक देगा, और यही मक़सद है: pool असली सीमा को connection की आवाजाही के नीचे छिपाने के बजाय सामने ला देता है।

PgBouncer के तीन pool mode हैं, और आप कौन-सा चुनते हैं उसी से तय होता है कि आपका app क्या कर पाएगा।

Pool modeserver connection रुका रहता हैकहाँ सबसे अच्छाध्यान रखें
Sessionक्लाइंट connection के पूरे समयलंबे समय तक चलने वाले listener, session state चाहने वाले टूलसबसे कम बचत; idle क्लाइंट भी server connection पकड़े रहते हैं
Transactionएक transaction के लिएweb request और queue job; high volume में आम पसंदsession state पर टिकी हर चीज़ टूट जाती है
Statementएक statement के लिएसरल, सिर्फ़ autocommit वाला कामकई statement वाले transaction नहीं चलते

लोड में चल रहे web app के लिए transaction mode सबसे बड़ा फ़ायदा देता है। लेकिन स्विच करने से पहले इसकी कुछ सीमाएँ जान लेना ठीक रहेगा।

Transaction mode में क्या टूटता है

एक ही क्लाइंट का अगला transaction किसी दूसरे server connection पर जा सकता है, इसलिए session में रखी state पर भरोसा नहीं किया जा सकता:

  • Session-level setting। साधारण SET आगे नहीं टिकता; transaction के अंदर SET LOCAL टिकता है।
  • Session-level advisory lock और LISTEN/NOTIFY। इनके लिए direct connection या session mode इस्तेमाल करें।
  • Temporary table और WITH HOLD cursor जो transaction के बाद भी जीवित रहते हैं।
  • Prepared statement। PgBouncer के पुराने वर्ज़न transaction mode में इन्हें नहीं सँभाल पाते थे। वर्ज़न 1.21 से, max_prepared_statements विकल्प शून्य से ऊपर रखने पर PgBouncer इन्हें ट्रैक कर सकता है। मानकर न चलें, अपना वर्ज़न और ड्राइवर जाँचें।

व्यावहारिक हल है दो connection रास्ते: app के लिए pooled रास्ता, और migration, schema टूल व हर उस चीज़ के लिए direct रास्ता जो सुनती रहती है। pool size और timeout समेत सारे विकल्प PgBouncer configuration reference में हैं।

Replica: क्या ठीक करता है, और क्या primary पर ही रहेगा

Read replica primary की एक कॉपी है जो थोड़ी देरी से उसके पीछे चलती है। जो काम थोड़ा पीछे रहकर भी चल जाता है, उसके लिए यह बढ़िया है: report, export, search listing, dashboard और analytics query, जो वरना checkout से होड़ करते।

यह हर जगह रफ़्तार बढ़ाने का नुस्ख़ा नहीं है। Replication lag सामान्य बात है, और ज़्यादा write होने पर बढ़ता है। इन्हें primary पर ही रखें:

  • जो write के ठीक बाद पढ़ता है, जैसे checkout के बाद order पेज दिखाना।
  • stock रिज़र्वेशन, payment, authentication और हर वह चीज़ जो पैसे या एक्सेस का फ़ैसला करती है।
  • ऐसे transaction के अंदर की कोई भी query जिसमें write भी है।

व्यवहार में यह एक routing नियम बन जाता है, जो एक पन्ने पर लिखा जा सके इतना छोटा है।

तीन फ़ैसलों वाला flowchart: session state चाहिए तो direct connection, write करे या अपना write पढ़े तो primary, lag सह ले तो replica, वरना primary
कौन-सी query कहाँ चलती है, और उसे तय करने वाले तीन सवाल।

Index और N+1 query: सबसे सस्ती क्षमता

हार्डवेयर जोड़ने से पहले पता कीजिए कि कौन-सी query सबसे ज़्यादा समय खाती हैं। PostgreSQL का pg_stat_statements एक्सटेंशन query को कुल समय के हिसाब से रैंक करता है, और ANALYZE व BUFFERS विकल्पों के साथ EXPLAIN दिखाता है कि query कुछ पन्ने पढ़ रही है या दस लाख।

बर्बादी की ज़्यादातर वजह दो समस्याएँ हैं:

  • index न होना या ग़लत index। जिन कॉलमों पर मेल खाता index नहीं है, वहाँ filter और sort मिलीसेकंड के lookup को पूरे table scan में बदल देते हैं। अपने असली filter और ordering से मेल खाते composite index बनाइए।
  • N+1 query। लिस्ट पेज जो लिस्ट के लिए एक query चलाता है और फिर हर पंक्ति के लिए एक और। पचास पंक्तियाँ यानी इक्यावन बार database तक दौड़। जो relation दिखा रहे हैं, उन्हें eager-load कीजिए।

Redis और Valkey: एक server, कई काम

नामों पर एक छोटी बात। मार्च 2024 में Redis source-available लाइसेंस पर चला गया, और Linux Foundation ने Valkey शुरू किया, जो Redis 7.2.4 से बना fork है और उदार BSD लाइसेंस पर ही है। Valkey वही protocol बोलता है, इसलिए नीचे की हर बात दोनों पर लागू होती है।

सबसे आम ग़लती है cache, session, API token और queue को एक ही keyspace में रख देना। तब एक flush या मेमोरी की एक तंगी सबको एक साथ बिगाड़ देती है। हर भूमिका को अलग logical database या अलग instance दीजिए, और अलग key prefix। Cache को सुरक्षित ढंग से ख़ाली किया जा सकता है। Queue को नहीं।

भूमिकाखो जाए तो क्या होता हैEviction policyनियम
Cacheकुछ देर धीमापन; data फिर बन जाता हैallkeys-lruहर चीज़ पर TTL; evict होना ठीक है
Session और API tokenलोग logout हो जाते हैंvolatile-ttl या volatile-lruइतना बड़ा रखें कि eviction कभी न हो
Queuejob खो जाते हैंnoevictionकाम चुपचाप गिराने के बजाय write ज़ोर से फ़ेल हो; मेमोरी पर alert रखें

हर policy का ब्योरा Redis eviction दस्तावेज़ों में है। दो बातें सबसे ज़रूरी हैं। allkeys-lru में Redis मेमोरी सीमा के भीतर रहने के लिए कोई भी key हटा सकता है; cache के लिए यही चाहिए, queue के लिए यही कभी नहीं चाहिए। noeviction में भरा हुआ server data मिटाने के बजाय नए write लौटा देता है, इसलिए queue सही रहती है और आपको error व alert से पता चलता है।

FLUSHDB कभी नहीं

अपने runbook और app, दोनों में FLUSHDB और FLUSHALL पर रोक लगाइए। Cache साफ़ करना हो तो prefix के हिसाब से मिटाइए। तब सबसे बुरे वक़्त पर चूक से भी cache flush queue के job नहीं मिटा सकेगा।

सही आकार में रखें

उसी प्लैटफ़ॉर्म पर managed Valkey instance 16 GB provision था और इस्तेमाल हो रहा था करीब 62 MB। असली लोड में मेमोरी का वास्तविक उपयोग देखिए, burst के दौरान queue के लिए उदार headroom रखिए, और उसी नाप तक घटाइए।

AI search के लिए pgvector, अपने database में

अगर आपके स्टोर या ERP में AI search, recommendation या दस्तावेज़ खोजकर लाने वाला असिस्टेंट है, तो आप embedding रख रहे हैं: अर्थ को दर्शाने वाली संख्याओं की लंबी सूचियाँ। pgvector एक्सटेंशन PostgreSQL में vector टाइप और nearest-neighbour search जोड़ता है, इसलिए शुरू करने के लिए अलग vector product नहीं चाहिए।

मेरा डिज़ाइन नियम है इसे अलग PostgreSQL database में चलाना, अपने connection और अपने migration रास्ते के साथ। मेरा सबसे ताज़ा सेटअप PostgreSQL 18 पर pgvector 0.8 इस्तेमाल करता था; PostgreSQL 18 सितंबर 2025 में जारी हुआ। Vector query CPU और मेमोरी पर भारी होती हैं, और index बनाना और भी भारी। अलग database में AI का लोड checkout को धीमा नहीं कर सकता, और आप उसका साइज़, backup और अपग्रेड अपने कार्यक्रम से तय कर सकते हैं।

product में बदलाव से queue में job, embedding बनाने वाले worker, pgvector में भंडारण और filter के साथ nearest-neighbour query तक का प्रवाह
Embedding बदलाव के बाद worker बनाते हैं, web request के अंदर कभी नहीं।

Embedding worker में बनाएँ

Embedding बनाने का मतलब है किसी model को कॉल करना, जो धीमा है और फ़ेल हो सकता है। इसे database commit के बाद शुरू होने वाले queue job से कीजिए, web request में नहीं। Job को idempotent रखिए: मूल पाठ का hash रखिए और कुछ न बदला हो तो काम छोड़ दीजिए; हर पंक्ति के साथ model का वर्ज़न रखिए ताकि बाद में बैकग्राउंड में दोबारा embed किया जा सके। यह दूसरे भाग वाला वही worker अनुशासन है।

HNSW या IVFFlat

pgvector दो तरह के approximate index देता है, और दोनों थोड़ी सटीकता के बदले काफ़ी रफ़्तार देते हैं।

HNSWIVFFlat
रफ़्तार और recallआमतौर पर बेहतर संतुलनअच्छा, इस पर निर्भर कि कितनी list probe करते हैं
बनने का समय और मेमोरीबनने में धीमा, ज़्यादा मेमोरीबनने में तेज़, कम मेमोरी
पहले data चाहिए?नहीं, ख़ाली table पर भी शुरू हो सकता हैहाँ, प्रतिनिधि data लोड करने के बाद बनाएँ
आम इस्तेमालज़्यादातर live search का डिफ़ॉल्टबहुत बड़े, कम बदलने वाले सेट, जहाँ बनाने की लागत मायने रखती है

ज़्यादातर कॉमर्स और ERP कामों में मैं HNSW से शुरू करता हूँ। दोनों के ट्यूनिंग विकल्प pgvector प्रोजेक्ट के दस्तावेज़ों में हैं।

Filter पर आकर search चुपचाप टूटता है

असली query कभी सिर्फ़ "इस पाठ के सबसे क़रीब" नहीं होती। वह होती है "सबसे क़रीब, stock में, इस स्टोर का, इस भाषा में"। Approximate index में filter index scan के बाद लगता है। अगर कोई filter 10% पंक्तियों से मेल खाता है और hnsw.ef_search का डिफ़ॉल्ट 40 है, तो दस माँगने पर भी औसतन चार ही मेल खाती पंक्तियाँ मिलेंगी।

2024 के अंत में जारी pgvector 0.8.0 ने ठीक इसी समस्या के लिए iterative index scan जोड़ा। जब filter के बाद बहुत कम पंक्तियाँ बचती हैं, scan तब तक चलता रहता है जब तक काफ़ी पंक्तियाँ न मिल जाएँ या कोई सीमा न आ जाए।

Settingक्या करती है
hnsw.iterative_scanHNSW के लिए iterative scan चालू करती है: strict_order दूरी का सटीक क्रम रखती है, relaxed_order थोड़ी बेतरतीबी की छूट देकर बेहतर recall देती है
hnsw.max_scan_tuplesiterative scan अधिकतम कितनी index entry देख सकता है, उसकी सीमा
ivfflat.iterative_scanIVFFlat के लिए यही विचार, relaxed ordering के साथ
ivfflat.max_probesiterative scan में अधिकतम कितनी list probe होंगी, उसकी सीमा

Object storage को compute के पास रखें

फ़ाइलें भी state हैं। product की तस्वीरें, invoice और export S3-compatible object storage में रहने चाहिए, web node की डिस्क पर नहीं; वरना load balancer के पीछे दो node नहीं चल सकते।

Bucket को servers के उसी region में रखिए। एक load test में दूसरे region का bucket हर बनी हुई फ़ाइल के upload में लगभग 50 से 80 ms जोड़ रहा था। फ़ाइलों को पहले लोकल डिस्क पर उतारने के बजाय सीधे storage से stream करने पर एक job की पीक मेमोरी भी करीब 288 MB से घटकर करीब 128 MB रह गई।

event से पहले data tier की चेकलिस्ट

  1. लक्ष्य rate पर load test चलाइए और database का CPU, डिस्क latency, lock का इंतज़ार और प्रति सेकंड commit दर्ज कीजिए। उसके बाद ही तय कीजिए कि साइज़ बढ़ाना है या नहीं।
  2. app के आगे PgBouncer को transaction mode में रखिए, और migration व listener के लिए direct connection अलग रखिए।
  3. Prepared statement सपोर्ट के लिए अपना PgBouncer वर्ज़न और ड्राइवर जाँचिए।
  4. pg_stat_statements से query रैंक कीजिए, सबसे भारी ठीक कीजिए, और सबसे व्यस्त पेजों से N+1 हटाइए।
  5. लिख लीजिए कि कौन-सी read replica पर जा सकती है, और पक्का कीजिए कि checkout, stock और payment primary पर ही रहें।
  6. Redis या Valkey को भूमिका के हिसाब से अलग prefix के साथ बाँटिए, हर भूमिका की eviction policy तय कीजिए, और FLUSHDB पर रोक लगाइए।
  7. Queue की मेमोरी पर alert रखिए ताकि noeviction चुपचाप विफलता में न बदल जाए।
  8. AI search को अपने PostgreSQL database में चलाइए, filter वाली query test कीजिए, और recall गिरे तो iterative scan चालू कीजिए।
  9. पक्का कीजिए कि object storage आपके compute के उसी region में है।

वापस 9:02 पर। Pooler transaction mode में हो तो 940 क्लाइंट connection 20 server connections में सिमट जाते हैं, और dashboard पर डराने वाला आँकड़ा दिखना बंद हो जाता है। सेल्स report replica पर चलती है, cache और queue अलग keyspace में रहते हैं, और ऑन-कॉल इंजीनियर पूछता है "क्या saturated है?", "अगला साइज़ क्या है?" नहीं। Load test पहले ही जवाब दे चुका है: write, और database आरामदेह margin पर।

Data tier ठीक हो जाए तब भी event के दिन यह देखना होगा कि वह कर क्या रहा है। चौथे भाग में यही है: काम आने लायक log, metric और trace।

मुख्य बातें

  • database का साइज़ write throughput और मापी गई saturation से तय करें, connection की सीमा से नहीं, और load test ज़रूरत न दिखाए तो कभी अपग्रेड न करें।
  • app के लिए PgBouncer को transaction mode में चलाएँ, और जानें कि वह क्या तोड़ता है: session state, session-level lock, LISTEN/NOTIFY और पुराने prepared-statement सेटअप।
  • Replica lag सह सकने वाली read सँभालता है; जो write के बाद पढ़े, या पैसे और stock को छुए, वह primary पर रहे।
  • Cache, session और queue को अलग Redis या Valkey database और prefix दीजिए, cache के लिए allkeys-lru और queue के लिए noeviction, और पूरा database कभी flush न करें।
  • pgvector को अपने PostgreSQL database में रखें, embedding worker में बनाएँ, शुरुआत HNSW से करें, और filter वाली search में iterative scan इस्तेमाल करें।
  • Cache की मेमोरी असली उपयोग के हिसाब से रखें, और object storage compute के उसी region में रखें।

अनिचुर रहमान सॉफ़्टवेयर आर्किटेक्ट और StoreConsole के संस्थापक हैं। वे बढ़ते कारोबारों के लिए कॉमर्स और ERP सिस्टम डिज़ाइन करते हैं — ख़ास ध्यान event-driven आर्किटेक्चर, डेटा की शुद्धता और अपने सर्वर पर चलने वाले सिस्टम पर।

About the Author

Anichur Rahaman

Continue Reading