- Postgres LISTEN/NOTIFY का global exclusive lock सरल implementations की throughput को सीमित करता है, लेकिन notifications को buffer करके batch में भेजने पर एक single server पर प्रति सेकंड अधिकतम 60,000 stream writes संभाले जा सकते हैं
NOTIFYकॉल करने वाला transaction notification के commit order की गारंटी देने के लिए commit औरfsync()पूरा होने तक global lock पकड़े रखता है, जिससे commits serialize हो जाते हैं और group commit का भी लाभ नहीं मिल पाता- शुरुआती implementation में stream table की हर write पर trigger के जरिए
NOTIFYकॉल किया जाता था, जिसने low latency दी, लेकिन CPU·memory·IOPS का पर्याप्त उपयोग किए बिना ही यह प्रति सेकंड 2,900 पर bottleneck में फँस गया - notifications की बजाय database table को source of truth मानकर, memory में इकट्ठी की गई notifications को समय-समय पर एक transaction में भेजने से lock acquisition की संख्या काफी कम की जा सकती है
- process failure से buffer में मौजूद notifications खोने की संभावना को low-frequency polling से पूरा किया जाता है, और concurrent read environment में भी 15~100ms latency तथा पहले की तुलना में 20 गुना throughput हासिल होता है
Postgres से बनाया गया low-latency stream
- Postgres-आधारित stream, हर stream chunk को
streamstable में एक नई row के रूप में store करता है, और LLM response token भी एक chunk हो सकता है - read side पर यह पता नहीं होता कि अगला chunk कब आएगा, इसलिए केवल simple query से efficiently wait करना मुश्किल है
- periodic polling में interval लंबा हो तो online chat जैसे interactive use case में latency बढ़ जाती है, और छोटा हो तो concurrent pollers database पर अत्यधिक load डाल देते हैं
- LISTEN/NOTIFY का उपयोग करने पर read process blocked state में wait कर सकता है और नया chunk लिखे जाने का notification मिलते ही तुरंत जाग सकता है, जिससे अनावश्यक polling से बचा जा सकता है
हर write पर भेजे गए NOTIFY का bottleneck
- शुरुआती implementation में
streamstable में नया chunk लिखे जाने पर trigger function चलाकर हर बार एक NOTIFY भेजा जाता था, और read process notification का इंतज़ार करने के बाद नया chunk पढ़ता था - correctness और low latency तो मिली, लेकिन बड़े Postgres database पर भी प्रति सेकंड 2,900 से अधिक stream writes लगातार sustain नहीं हो सकीं
- bottleneck के दौरान CPU·memory·IOPS utilization उल्लेखनीय रूप से ऊँचा नहीं था, और कारण था NOTIFY commit path का global lock
commit order की गारंटी देने वाला global lock
NOTIFYकॉल करने वाला transaction commit शुरू होते ही global exclusive lock प्राप्त करता है, और पूरी तरह commit होकर उसकी सामग्रीfsync()से disk पर लिखे जाने तक उसे छोड़ता नहीं है- Postgres यह गारंटी देता है कि notifications transaction commit order के अनुसार deliver हों, और सभी outgoing notifications को एक global internal queue में store करता है जो commit order से बिल्कुल मेल खाना चाहिए
- queue में notification जोड़ने का काम भी commit के हिस्से के रूप में transactional तरीके से होना चाहिए, लेकिन हर transaction का commit time अलग होने से commit पूरा होने से पहले order तय नहीं किया जा सकता
- global lock, notifications वाले transactions के commit को serialize करके order पहले से तय करता है और internal notification queue में भी वही order बनाए रखता है
commit serialization throughput को कैसे सीमित करता है
- क्योंकि हर stream write trigger के जरिए
NOTIFYकॉल करती है, इसलिए हर write transaction पूरे commit और disk flush के दौरान global lock पकड़े रखता है - transactions बारी-बारी से commit होने के कारण, Postgres के group commit का लाभ नहीं मिल पाता, जो कई transactions को एक ही
fsync()में process कर सकता है - throughput, Postgres द्वारा individual transactions commit करने की गति से आगे नहीं जा सकती, और jobs lock पर wait करती रहती हैं, इसलिए CPU और disk भी पूरी तरह उपयोग नहीं हो पाते
- Postgres 19 में शामिल होने वाला patch global lock को हटाता नहीं है, इसलिए यह इस bottleneck को दूर नहीं कर सकता
- इसके बजाय यह ऐसे सीमित case को optimize करता है जहाँ बहुत से notification channels हों और हर listener केवल एक specific channel का इंतज़ार करता हो
notification buffering और batch dispatch
- stream समेत कई LISTEN/NOTIFY use cases में notifications source of truth नहीं होतीं, बल्कि सिर्फ यह signal होती हैं कि actual data वाले table को check किया जाए
- ऐसे design में notifications को perfect global order या complete durability देने की ज़रूरत नहीं होती, इसलिए उन्हें memory में buffer करके समय-समय पर एक batch transaction में भेजा जा सकता है
- global lock, हर individual stream write पर नहीं बल्कि केवल buffer flush करते समय acquire किया जाता है
- individual writes, background notification dispatch से अलग होकर तेज़ी से पूरी हो सकती हैं, और group commit जैसी Postgres optimizations का उपयोग करके throughput बढ़ाया जा सकता है
notification loss को पूरा करने वाली low-frequency polling
- अगर notifications memory में रहते समय process बंद हो जाए, तो वे notifications deliver नहीं हो सकतीं
- read process notifications का इंतज़ार करने के साथ-साथ database को समय-समय पर query करके यह भी देखता है कि कहीं बिना notification के लिखा गया stream data तो नहीं है
- यह polling केवल lost notifications को recover करने का supplementary तरीका है, इसलिए इसे low frequency पर चलाया जा सकता है और performance पर इसका असर भी बड़ा नहीं होता
throughput और latency
- optimized implementation concurrent read processes वाले environment में प्रति सेकंड अधिकतम 60,000 stream writes process करती है, जो शुरुआती implementation की तुलना में 20 गुना अधिक throughput है
- throughput बढ़ाने के बाद भी latency 15~100ms की range में बनी रहती है
- maximum throughput पर Postgres CPU पूरी तरह उपयोग हो जाता है, जो दिखाता है कि यह lock contention नहीं बल्कि database की वास्तविक saturation है
- पूरा benchmark code dbos-postgres-benchmark में देखा जा सकता है
1 टिप्पणियां
Hacker News टिप्पणियाँ
स्केलेबिलिटी एक सतत स्पेक्ट्रम है, कोई द्विआधारी चीज़ नहीं। प्रति सेकंड 60,000 इवेंट कुछ सिस्टमों की ज़रूरत से 100,000 गुना ज़्यादा हो सकते हैं, और दूसरे सिस्टमों के लिए 100,000 गुना कम। डेवलपरों की आम गलतियों में मैं “premature optimization” से ज़्यादा गलत scalability characteristics वाली तकनीक चुनने को रखूँगा
बहुत छोटी तकनीक अपनी सीमा पार होने पर साफ़ तौर पर फेल हो जाती है, लेकिन ज़रूरत से ज़्यादा scalable तकनीक भी operational burden और constraints लाती है। किसी छोटे सिस्टम में, जहाँ ज़्यादा समृद्ध मॉडल development effort को बहुत घटा सकता है, ऐसी तकनीक लाना भी खराब विकल्प है
LISTEN/NOTIFY की सीमाएँ इतनी कम हैं कि सावधानी बरतना ज़रूरी है, इसलिए pessimistic maximum load निकालने के बाद भी कम-से-कम 10x हेडरूम रखना बेहतर है, लेकिन बहुत-से प्रोजेक्ट्स के लिए यह काफ़ी है। database integration, availability, और अलग service चलाने की ज़रूरत न होना इसके फ़ायदे हैं, इसलिए इसे सीधे ख़ारिज नहीं करना चाहिए, और पहले बताई गई 2,000 प्रति सेकंड की दर भी ऐसे सिस्टम के लिए बड़ी संख्या है जो एक message को सेकंड-स्तर पर प्रोसेस करता है
असली अनुमानित scale के हिसाब से थोड़ा margin रखकर design करना बेहतर है, और उससे ज़्यादा तभी चुनना चाहिए जब extra scalability लगभग free हो। अगर कुछ हज़ार डॉलर में बड़ा hardware मिल रहा हो, या scalability के अलावा बाकी विकल्प समान हों, तो बड़ा वाला चुन सकते हैं
LISTEN/NOTIFY और Rust GraphQL subscription broker को मिलाकर हमें काफ़ी बड़ी सफलता मिली। subscriptions तो दसियों हज़ार थीं, लेकिन LISTEN connections हर host पर सिर्फ़ एक थीं, कुल मिलाकर 3–4
हम हर बदलाव को हर host तक भेजते थे, और host असली user subscriptions को मैनेज करके तय करता था कि क्या publish करना है। अगर सैकड़ों Ruby या Node hosts की जगह कुछ Rust hosts ले आएँ, तो architecture बहुत सरल हो सकता है, और जिसे लोग non-scalable मानते हैं वह तरीका भी काफ़ी अच्छा चल सकता है
जब मैं CTO था, तब हम सभी services में रोज़ लगभग 100,000 इवेंट संभालते थे, फिर वह बढ़कर millions और आखिर में tens of millions तक पहुँचा। उस दौरान एक engineer ने data model के साथ strong consistency का फ़ायदा लेने के लिए LISTEN/NOTIFY semantics के ऊपर एक queue बना दी। उसे समझना कठिन नहीं था और अलग storage/transport layer भी हट जाती थी, इसलिए उस समय यह तर्कसंगत लगा
लेकिन जैसे-जैसे हमने अपने बनाए फ़ीचर को scale किया, PostgreSQL के अंदरूनी कामकाज को bypass करना पड़ा और यह बहुत असुविधाजनक हो गया; हमें काफ़ी पहले किसी दूसरे सिस्टम पर जाना चाहिए था। scalability भी अच्छी नहीं थी, इसलिए RDS पर source identify करना मुश्किल होने वाली disk contention बहुत बढ़ गई, और उस table का VACUUM भी एक दुःस्वप्न था। एक परिचित queue को PostgreSQL की अंदरूनी सुविधाओं से अपरिचित ढंग से लागू करने पर दूसरे engineers डरते थे और debugging व ownership लेने से बचते थे
schema और index जैसी बारीकियों को छोड़कर मुख्य सीख यह थी कि हमेशा सरल और अनुमानित तकनीक से शुरुआत करनी चाहिए। अगर बेहद मज़बूत data consistency सचमुच ज़रूरी न हो, तो infrastructure में एक component बढ़ जाने पर भी SQS या Redis queue जैसी API contract के स्तर पर सरल queue इस्तेमाल करना बेहतर है और बाकी चीज़ों को उसी के हिसाब से ढालना चाहिए। एक मुख्य data store पर mechanical responsibility जितनी कम हो, उतना बेहतर है
distributed systems ecosystem के विकसित होने के साथ हमें समझ आया कि हर component क्या कर सकता है। कम components से शुरुआत करना और सच में ज़रूरत पड़ने पर ही और जोड़ना, ज़्यादा स्वस्थ सिस्टम बनाता है
Postgres और अब SQLite तक का सही इस्तेमाल करने वाला DBOS मुझे लगातार पसंद आ रहा है। इसे मौजूदा CRUD stack में भी लगभग बिना मेहनत जोड़ा जा सकता है
durable workflows इस्तेमाल करना शुरू करें तो बार-बार नई जगहें दिखती रहती हैं जहाँ इन्हें लगाया जा सकता है। हाल में मैं यह प्रयोग कर रहा हूँ कि हर email को एक durable workflow माना जाए, और user, counterparty, agent, तथा GitHub या Attio जैसे tools बारी-बारी से उस flow में भाग लें
https://housecat.com/blog/gmail-durable-workflows-sandbox-vm
ऐसे लेख आम तौर पर लोगों की अपनी-अपनी समस्याओं, समझ और समाधानों के स्वतंत्र आकलन का नतीजा होते हैं। सिर्फ़ इसलिए कि किसी ने tool की default settings पर एक खास performance की उम्मीद की, उसे विशेषज्ञता की कमी कहना ठीक नहीं; हर कोई असफलताओं से सीखता रहता है
इस experiment में 96-core·384GB RAM database server का इस्तेमाल हुआ था (https://github.com/dbos-inc/dbos-postgres-benchmark/blob/mai...), यह बहुत महत्वपूर्ण बात है और इसे साफ़-साफ़ बताया जाना चाहिए था। database vertical scaling कर सकता है, लेकिन उसकी भी सीमा है। कौन कहाँ से connect कर रहा है, इसका असर performance और overall latency दोनों पर पड़ता है
प्रति सेकंड 60,000 बड़ा लग सकता है, लेकिन असल सिस्टमों को गिराने वाली चीज़ सामान्य traffic नहीं बल्कि अचानक आने वाले traffic spikes होते हैं। अगर आप कोई बड़ी enterprise नहीं हैं, तो शायद ऐसे बड़े server से शुरुआत नहीं करेंगे। read replicas और cross-region redundancy जोड़ें, तो एक production database cluster की लागत 100,000 डॉलर से ऊपर चली जाती है
संबंधित लेख लगता है: Postgres LISTEN/NOTIFY does not scale - https://news.ycombinator.com/item?id=44490510 - जुलाई 2025, 321 टिप्पणियाँ
लगता है कि लेख का सबसे महत्वपूर्ण हिस्सा छूट गया है: offset या sequence number assign करने का तरीका, जिससे यह ट्रैक किया जा सके कि consumer कहाँ तक पढ़ चुका है और fallback path के नए messages query किए जा सकें। इसके कई तरीके हैं, लेकिन complexity या lock contention के बिना इसे हल करना आसान नहीं है, और गलत implementation में consumer के साथ race condition भी हो सकती है। आमतौर पर लेखक event topic के अगले नंबर को assign करने के लिए state table की किसी single row पर lock लगाएगा
सबसे अच्छा तरीका क्या है, यह जानने की जिज्ञासा है। Change Data Capture (CDC) पढ़कर assigned event number को किसी दूसरी table में लिखने वाला tool ठीक हो सकता है, लेकिन latency बढ़ सकती है, और उस CDC processor को NOTIFY भी करना होगा
consumer side पर batch processing से throughput काफ़ी बढ़ाया जा सकता है। इस स्थिति में LISTEN/NOTIFY के बिना consumer को बार-बार चलाकर हर बार सभी नए unprocessed messages process किए जा सकते हैं, और हर iteration के बीच आख़िरी sequence number save किया जा सकता है
जैसा मुझे याद है, जिस release में पहली बार LISTEN/NOTIFY support आया था, उसमें lock implementation अच्छा नहीं था, इसलिए performance problems थीं। यहाँ आलोचना किए गए पुराने लेख ने भी पहले paragraph के तुरंत बाद correction में यह बात ठीक की थी
अगर correction की तारीख 8 मई है, तो 24 जुलाई के लेख के बारे में यह मानना होगा कि इस feature के scale न होने की बात करने वाला मशहूर लेख दुर्भावना से लिखा गया नहीं था, और उस समय के हिसाब से ग़लत भी नहीं रहा होगा
बल्कि यह उस अधिक सीमित case को optimize करता है जहाँ notification channels बहुत ज़्यादा हों और हर receiver सिर्फ़ एक specific channel का इंतज़ार कर रहा हो
आख़िरी बार जब मैंने देखा था, LISTEN/NOTIFY में notification data के लिए 8,000 bytes की upper limit थी, इसलिए एक मायने में यह साफ़ तौर पर scale नहीं करता था। अगर data को row के रूप में store करके सिर्फ़ ID pass नहीं की जा सकती, तो इसे इस्तेमाल करना मुश्किल है
web game के events state changes को describe करने वाला ephemeral data थे, इसलिए उन्हें database में store करने का कारण नहीं था, और वे 8,000 bytes से बड़े भी हो सकते थे, इसलिए यह use case इसके लिए उपयुक्त नहीं था
लेख में global queue की lock contention की बात है, लेकिन लगता है कि fixed-size global queue की एक दूसरी समस्या का ज़िक्र नहीं है। एक channel का एक slow receiver सभी channels के लिए writes को रोक सकता था। कम-से-कम कुछ साल पहले तक ऐसी failure mode संभव थी; अब यह बदल गया हो सकता है