- Postgres के बदलावों को दूसरे systems तक real-time में पहुंचाने के लिए CDC(Change Data Capture) की जरूरत होती है, और सरल notifications से लेकर WAL-आधारित replication तक हर विकल्प में reliability और operational burden काफी अलग होता है
- Listen/Notify सबसे हल्के तरीके से शुरू किया जा सकता है, लेकिन at-most-once delivery, temporary notifications और 8000-byte payload limit के कारण यह core CDC की बजाय एक सहायक signal जैसा है
- table polling और audit table(outbox pattern) standard tables और triggers से implement किए जा सकते हैं, लेकिन deletion detection, diff, commit order, write amplification और backpressure को खुद हल करना पड़ता है
- logical replication WAL से insert/update/delete stream करने का शक्तिशाली तरीका है, लेकिन replication slot, ack, restart और throughput handling तक application को manage करना पड़ता है
- Sequin, Postgres logical replication के आधार पर SQS, Kafka, Elasticsearch, Redis, HTTP endpoints आदि तक बदलाव पहुंचाता है और replication slot को सीधे handle करने का बोझ घटाता है
Postgres CDC की जरूरत कब पड़ती है
- Postgres stored data को handle करने में मजबूत है, लेकिन table changes से workflow trigger करने या data को दूसरे data stores, systems और services तक real-time streaming करने के लिए data movement को अलग से design करना पड़ता है
- Change Data Capture(CDC) database changes को identify और capture करने के बाद downstream systems तक real-time पहुंचाने की पद्धति है
- Postgres में changes पकड़ने के कई तरीके हैं, और implementation difficulty, reliability और operational burden एक-दूसरे से अलग हैं
Listen/Notify: सबसे सरल pub-sub
- Postgres का Listen/Notify inter-process communication feature है और publish-subscribe pattern पर काम करता है
- session किसी specific channel को
listenकरता है, और database activity या दूसरे sessions उस channel परnotifyभेज सकते हैं - change capture के लिए इसे trigger लगाकर इस्तेमाल किया जा सकता है
- example trigger
after insert or update or deleteसमय पर बदले हुए record केtable,id,actionको JSON में बनाता है औरpg_notify('table_changes', payload::text)call करता है
- example trigger
- सीमाएं स्पष्ट हैं
- इसमें at-most-once delivery semantics हैं, और notification publish होते समय listener connected होना चाहिए
- listener को subscribe करने के बाद की notifications ही मिलती हैं, इसलिए network issue से थोड़ी देर disconnect होने पर भी notifications miss हो सकती हैं
- payload size limit 8000 bytes है, और इससे ज्यादा होने पर
notifycommand fail हो जाता है - payload size में channel name भी शामिल होता है, और Postgres identifiers की तरह channel name maximum 64 bytes हो सकता है
- इसे basic change detection या table polling optimization के लिए इस्तेमाल किया जा सकता है, लेकिन complex CDC requirements के लिए यह पूरी तरह fit नहीं हो सकता
table polling: सरल, लेकिन deletion और diff में कमजोर
- सबसे सरल robust change capture तरीका table को सीधे poll करना है
- हर table में
updated_atजैसा column चाहिए जो row update होने पर हर बार update हो; जरूरत हो तो इसे trigger से बनाया जा सकता है updated_atऔरidके combination को cursor के रूप में इस्तेमाल किया जाता है, और application logic cursor को store और manage करती है- Notify subscription साथ में इस्तेमाल करने पर record insert/update होने की जानकारी application को दी जा सकती है, जिससे polling frequency घट सकती है
- Postgres notifications temporary होती हैं, इसलिए इन्हें polling के ऊपर optimization के रूप में ही इस्तेमाल करना बेहतर है
- मुख्य कमियां तीन हैं
- deleted rows table में नहीं बचतीं, इसलिए deletion detection संभव नहीं है
- workaround के तौर पर delete trigger
idऔर जरूरी columns कोdeleted_contactsजैसी अलग table में store कर सकता है, और application उस table को poll कर सकती है - यह पता चल सकता है कि record update हुआ है, लेकिन क्या बदला है यह नहीं पता चलता
- Postgres datetime और sequence में commit order mismatch हो सकता है, इसलिए
updated_atके आधार पर block पढ़ते समय अभी commit हो रही rows छूट सकती हैं
- deletion, diff और intermittent misses अगर बड़ी समस्या नहीं हैं, तो simple change tracking के लिए यह reasonable option है
audit table: outbox pattern से change log store करना
- audit table तरीका changes को अलग
changelogtable में record करता है, और इसे outbox pattern भी कहा जाता है changelogमें change से जुड़े columns हो सकते हैंaction:insert,update,deleteमें से क्या हैold: change से पहले record काjsonb, insert में emptyvalues: बदले हुए fields काjsonb, delete में emptyinserted_at: change होने का समय
- implement करने के लिए हर change होने पर
changelogमें insert करने वाला trigger function और monitor की जाने वाली हर table के लिए trigger चाहिए changelogको queue की तरह consume करना भी संभव है- application worker table से changes लाता है
- roughly exactly-once processing के लिए Postgres का
for update skip lockedइस्तेमाल किया जा सकता है - worker transaction खोलकर
order by timestamp limit 100 for update skip lockedसे batch lock कर सकता है, फिर process कर सकता है, processed records delete करके commit कर सकता है
- operational drawbacks हैं
- single table write audit table में multiple writes बनाता है, यानी write amplification होता है
- आम तौर पर audit table initial insert, processing के दौरान update, और processing के बाद delete तक कम से कम तीन writes होते हैं
- workers में fan-out करने का तरीका application के हिसाब से खुद design करना पड़ता है
- production-scale deployment से पहले trigger function और table design tune करने की संभावना रहती है
- worker कितनी देर तक change को checked out रख सकता है, ऐसी detailed policies पर भी विचार किया जा सकता है
- worker successfully process न करे तब भी audit table भरती रहती है, इसलिए backpressure management की कमी रहती है
Foreign Data Wrapper: specific Postgres-to-Postgres sync जैसा विकल्प
- Foreign Data Wrapper(FDW) Postgres database से external data sources को read और write करने की सुविधा है
- सबसे व्यापक रूप से supported FDW-based extension
postgres_fdwहै- यह दो Postgres databases को connect कर सकता है और एक database से दूसरे database की tables को reference करने वाला view जैसा structure बना सकता है
- internally एक Postgres database client बनता है और दूसरा database server
- foreign table पर query करने पर client database Postgres wire protocol के जरिए server database को query भेजता है
- FDW change capture method के रूप में common नहीं है, और बहुत specific situations के बाहर recommend करना कठिन है
- अगर एक Postgres database के changes को दूसरे Postgres database में लिखना हो, तो FDW fit हो सकता है
- example accounting database और application database अलग-अलग इस्तेमाल करने की स्थिति है
- बीच का change capture step छोड़कर
postgres_fdwसे databases के बीच direct reflection किया जा सकता है
- direct FDW बनाकर internal API पर changes POST करने का तरीका भी संभव है
- commit के अंदर API में write करने के कारण API change reject कर सकती है और commit rollback हो सकता है
- FDW powerful है, लेकिन CDC use case में यह शायद ही best choice बनता है, और खुद FDW लिखना change capture methods में सबसे बड़ा काम जैसा है
- खुद FDW लिखना Supabase wrappers जैसे tools से आसान हुआ है, लेकिन फिर भी यह बड़ा काम है
direct logical replication: WAL-based powerful CDC
- Postgres के पास database replication के लिए protocol है, और उनमें से एक logical replication है
- logical replication Postgres के WAL(write-ahead log) पर built है
- database के सभी insert, update, delete track होते हैं
- changes subscriber तक stream होते हैं
- user पहले primary पर replication slot बनाता है
pg_create_logical_replication_slot('<your_slot_name>', '<output_plugin>')form इस्तेमाल किया जाता है
output_pluginWAL changes decode करने वाला plugin specify करता हैpgoutputdefault plugin है, और client server द्वारा expected binary format में output देता हैtest_decodingWAL changes को human-readable form में देने वाला simple output plugin है- Postgres built-in plugin नहीं है, लेकिन popular plugin के रूप में
wal2jsonहै; JSON को Postgres binary format की तुलना में application starting point के रूप में handle करना आसान है
- replication slot बनाने के बाद उसे start और consume किया जा सकता है
- replication slot standard queries से अलग Postgres protocol area इस्तेमाल करता है
- कई client libraries replication slot operations में मदद करने वाले functions देती हैं
psycopg2example मेंcursor.start_replication(...)औरcursor.consume_stream(...)से WAL messages consume किए जाते हैं, औरcursor.send_feedback(flush_lsn=msg.wal_end)से ack भेजा जाता है
- client को मिले हुए WAL messages को ack करना होता है, और replication slot offset वाले Kafka जैसा काम करता है
- logical replication CDC के लिए बनाया गया robust तरीका है, लेकिन complex है
- replication slot और replication protocol developers को सामान्य tables और queries की तुलना में कम familiar होते हैं
- restart के दौरान messages miss न हों, इसके लिए strategy चाहिए
- Postgres से आने वाले large-volume messages को handle करने के लिए design करना पड़ता है
Sequin: logical replication को wrap करने वाला CDC tool
- Sequin Postgres changes और rows को queues, streams, search indexes, caches, HTTP endpoints आदि तक पहुंचाने वाला CDC tool है
- destinations में SQS, Kafka, Elasticsearch, Redis, HTTP endpoints आदि शामिल हैं
- Sequin internally Postgres logical replication इस्तेमाल करता है, लेकिन low-level protocol की complexity को abstract कर देता है
- insert, update, delete सभी capture किए जा सकते हैं, और update तथा delete में row के
newvalues औरoldvalues दोनों capture होते हैं - Sequin पर विचार करने की conditions ये हैं
- real-time CDC चाहिए
- SQS या webhook जैसे destination तक intermediate system के बिना सीधे stream करना चाहते हैं
- past data backfill और SQL
whereclause based change filtering जैसी capabilities चाहिए - replication slot directly manage करने की तुलना में सरल alternative चाहिए
- exactly-once processing guarantee चाहिए
- कमियां भी हैं
- Sequin Postgres internal extension नहीं, बल्कि database के बगल में चलने वाला third-party tool है
- extension न होने के कारण इसकी किसी भी Postgres database के साथ व्यापक compatibility है, लेकिन Sequin Cloud नहीं इस्तेमाल करते तो additional infrastructure खुद खड़ा करना पड़ता है
चयन के मानदंड
- शुरुआत के stage में Listen/Notify और table polling suitable हैं
- Listen/Notify non-critical event capture, prototyping और polling optimization के लिए अच्छा है
- polling simple use cases के लिए सहज और straightforward solution है
- थोड़ा ज्यादा serious stage में audit table middle option बन सकता है
- row के
newऔरoldpayload capture किए जा सकते हैं - ठीक से बनाया जाए तो exactly-once processing system मिल सकता है
- scale करने पर write amplification और backpressure की कमी समस्या बनते हैं, और manual configuration में गलती होने पर messages drop हो सकते हैं
- row के
- scaling stage में logical replication robust solution के सबसे करीब है
- हालांकि slot से directly read करने की बजाय Sequin जैसे tool का इस्तेमाल recommend किया जाता है
- FDW interesting feature है, लेकिन common CDC requirements को solve करने की संभावना कम है
1 टिप्पणियां
Hacker News की राय
ट्रिगर + हिस्ट्री टेबल (ऑडिट टेबल) 98% मामलों में सही जवाब है। अगर आप इसे पहले से इस्तेमाल नहीं कर रहे हैं, तो आज से शुरू कर सकते हैं। यह 30 साल से ज़्यादा समय से परखी हुई तकनीक है
इसे generic तरीके से लागू करने का एक सरल उदाहरण https://gist.github.com/slotrans/353952c4f383596e6fe8777db5d... पर है। यह तरीका space efficiency छोड़कर “आसान implementation” चुनता है
अगर immutable data स्टोर कर सकें तो बहुत अच्छा है, लेकिन database में शायद mutable data बहुत बड़ी मात्रा में होगा और आप रोज़ बहुत कुछ भूल रहे होंगे। भूलिए मत, हिस्ट्री टेबल इस्तेमाल करें
संदर्भ: https://github.com/matthiasn/talk-transcripts/blob/master/Hi...
Papertrail जैसी application layer की history tracking libraries या techniques का इस्तेमाल न करना बेहतर है। वे धीमी होती हैं, error-prone होती हैं, और app stack को bypass करके होने वाले DB changes को पकड़ नहीं पातीं। app में
updatedtimestamp लगाने की कोशिश भी मूल रूप से गलत है, क्योंकि हर web server की clock अलग होती है। DB clock इस्तेमाल करनी चाहिए, और वही एकमात्र सही clock हैnow()जैसी call डालकर DB clock इस्तेमाल करना सही हैलेकिन सिर्फ इस timestamp से synchronization करना पर्याप्त नहीं है। इसकी वजह यह है कि timestamp transaction commit time पर नहीं, बल्कि transaction start time पर बनता है
अगर आप table को poll करते हुए हालिया timestamp से filter करेंगे, तो commit order उलझे हुए कुछ transactions छूट सकते हैं। कुछ मिनट और पीछे तक query करके duplicates हटाने का buffer interval रखा जा सकता है, लेकिन PostgreSQL में transaction time असीमित हो सकता है और बहुत पीछे तक query करना काफी wasteful है। अगर accuracy और efficiency अहम हैं, तो यह तरीका सही नहीं है
log sequence number, DB time, और
REPLICA IDENTITY FULLइस्तेमाल करने पर change से पहले/बाद की state तक शामिल होती है। इसके बाद Snowflake जैसी जगह पर collections को materialize करने से source DB updates को follow करने वाली sync table मूल रूप से मिल जाती हैउसी underlying data lake से audit purpose के लिए full table history को transform या materialize भी किया जा सकता है, इसलिए source DB पर फिर से capture या WAL reader लगाने की जरूरत नहीं होती
इसी तरह के pattern को JSON के बजाय column-based तरीके से implement करने वाली SQLite approach भी अलग से समझाई है: https://simonwillison.net/2023/Apr/15/sqlite-history/
यह लेख Postgres की default features से संभव कई approaches को संक्षेप में अच्छी तरह समेटता है
“audit table में changes capture करना” वाले हिस्से में, पिछली company में हमने Temporal Tables pattern को अच्छे से इस्तेमाल किया था। दूसरे प्रमुख relational DBMS के विपरीत, Postgres में यह built-in नहीं है, लेकिन SQL functions के जरिए इस्तेमाल किया जा सकने वाला एक सरल pattern है: https://github.com/nearform/temporal_tables
इससे किसी खास समय पर table की state देखी जा सकती है, इसलिए “12 अगस्त को इस user की settings क्या थीं”, “कल रात 11:55 पर unprocessed records कितने थे”, “current और एक हफ्ते पहले के feature flags का अंतर दिखाओ” जैसे सवालों के जवाब मिल सकते हैं
पहले एक ऐसी कंपनी में कंसल्टिंग की थी जिसके पास बहुत बड़ा monolithic SQL Server था। Postgres नहीं था, लेकिन अगर मान लें कि वह Postgres होता तो भी बात लगभग वैसी ही रहती
दशकों तक चलने के दौरान वह कंपनी के भीतर हर तरह के कामों में इस्तेमाल हुआ था, और व्यावहारिक रूप से पूरी कंपनी के सभी applications और business processes इसी database में डेटा सेव कर रहे थे
समस्या यह थी कि इस DB को query करने वाले applications बहुत थे, और डेटा insert/modify करने वाली processes और procedures भी बेहद ज़्यादा थीं। इसलिए upstream insert/modify process बदलने या नई जुड़ने पर application-level invariants टूट जाते थे। खराब डेटा होने पर normal processes भी अलग तरह से behave करती थीं
root cause track करना बहुत मुश्किल था, क्योंकि जिन चीज़ों को देखा जा रहा था वे अक्सर 10 साल पहले लिखी गई थीं और वे कर्मचारी कंपनी छोड़ चुके थे
सोचता हूँ कि क्या Postgres database में होने वाले changes को किसी DAG जैसी form में capture किया जा सकता है, ताकि पता चले कि कौन-सी process डेटा insert/modify/delete करती है और historically कैसे behave करती रही है, कई applications इस डेटा को कैसे query करते हैं और query statistics समय के साथ कैसे बदलते हैं
नहीं पता कि ऐसे पहले से कोई examples हैं या ऐसा tool बनाने के लिए कौन-सा approach सही होगा। पहले कुछ ऐसा बनाने के बारे में सोचा था, लेकिन अच्छे choices करने के लिए यह Postgres core engineer-level समझ की जरूरत वाला area लगता है
हर change के साथ client-level source data नहीं मिलता
फिर भी workaround संभव है। logical replication stream में
pg_logical_emit_messagefunction के info messages भी शामिल हो सकते हैं, इसलिए client खुद metadata डाल सकता है। शायद हर transaction की शुरुआत में client identifier emit करने के लिए configure किया जा सकेlast updated by) track करने वाला column रखता हूँ। यह anti-pattern भी हो सकता है, इसलिए कोई ज्यादा robust solution हो तो अच्छा होगायह approach WAL या triggers इस्तेमाल करने वाले लगभग सभी SQL-family solutions में काम करता है
SQL Server में trigger वाला तरीका कई बार इस्तेमाल किया है, लेकिन सभी queries log करने पर performance slow होने की tendency होती है। operations को block न करने वाला insert mechanism design करना perfect नहीं होता, और sampling की जरूरत पड़ सकती है
अगर “audit table” वाले रास्ते पर जा रहे हैं, तो बस pgaudit इस्तेमाल करें। production में proven extension है, और AWS इस्तेमाल करते हैं तो RDS पर भी available है
https://github.com/pgaudit/pgaudit/blob/master/README.md
https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Appen...
https://github.com/arkhipov/temporal_tables
https://news.ycombinator.com/item?id=26748096
जरूरी नहीं कि यह किया ही जाए। इसे चाहने का मतलब है Postgres के relations को contract में बदल देना। कोई भी service अपनी internal state persist नहीं कर पाएगी
अगर आप domain-driven design के लिए सच में committed हैं तो संभव हो सकता है, लेकिन बेहतर है कि हल्का और practical event-driven system इस्तेमाल करें
event-driven कोई भी चीज़ 1000 गुना ज्यादा complex होती है
updated_atcolumn को poll करने वाला तरीका अपनी सबसे simple form में robust नहीं है। क्योंकि transactions के उसी order में commit होने की guarantee नहीं होतीupdated_at2023-09-22 12:00:01पर set हो जाता हैथोड़ी देर बाद transaction B शुरू होता है, Row 2 का
updated_at2023-09-22 12:00:02पर set होता है, और B पहले commit हो जाता हैpolling query चलती है, Row 2 को latest change मानकर cursor को
2023-09-22 12:00:02पर update कर देती है, फिर A बाद में commit होता है तो Row 1 छूट जाएगाइस समस्या से बचने का simple तरीका है कि लगभग real-time में polling न करें। order आखिरकार consistently align हो जाएगा
ज्यादा robust suggestion sequence इस्तेमाल करना हो सकता है। जैसे हर row बदलने पर increment होने वाला
updated_at_idxcolumn रखनाnow()डालने वाला before trigger इस्तेमाल करने पर भी क्या दोनों rows केupdated_attimestamps transaction commit order से अलग हो सकते हैं?updated_atऔर commit timestamp समान होना जरूरी नहीं है, लेकिनupdated_atको millisecond/microsecond level पर commit order accurately दिखाना चाहिएupdated_atकी जगह trigger द्वारा current transaction ID पर set किया गया_txidcolumn इस्तेमाल करता हूँ। बाद में poll करते समयtxid_current()से जांचता हूँ कि कौन-सा transaction commit हो चुका है और कौन-सा अभी नहींथोड़ा जोखिम भरा है और boundary-value errors करना बहुत आसान है, लेकिन कई सालों से production में अच्छी तरह चल रहा है
लेख शानदार है
अगर Elixir और Postgres इस्तेमाल करते हैं, तो इसी तरह के approach से WAL changes listen करने वाली एक छोटी library बनाई है: https://github.com/cpursley/walex
ये सभी तरीके कुछ खास नहीं लगते, और निजी तौर पर मुझे polling सबसे practical लगती है
अच्छा होगा अगर Postgres इस क्षेत्र में innovation करे
जब तक यह SQL standard में नहीं आता, relational DBMS के kernel space में इसके लिए momentum बनना मुश्किल लगता है। विकल्प बहुत हैं और जटिल भी, और user space में सफल solutions भी performance के लिहाज़ से बहुत ज़्यादा बोझ डालने वाले नहीं हैं
संदर्भ के लिए, इस क्षेत्र पर शोध करने वाले लोग आम तौर पर audit table approach की ओर झुकते हैं। क्योंकि इससे database के भीतर consistent ACID properties बनी रहती हैं, और proxy या polling job जोड़ने के बजाय Postgres को single point of failure ही रखा जाता है
डेटा की दुनिया में एक बड़ा खालीपन है। Data store से result पूछने के बजाय, अच्छा होगा अगर query results incrementally push किए जा सकें
Real-time और streaming analytics बहुत होते हैं, stream processing भी की जा सकती है और data store के अंदर materialized view के रूप में कुछ processing भी हो सकती है। लेकिन data DB या data lake में आ जाने के बाद, downstream में बदलाव देखने के लिए असल में फिर polling पर ही लौटना पड़ता है
डेटा में कोई स्थिति होने पर react करना हो, या page refresh किए बिना screen update करनी हो, तो साफ-सुथरे solutions बहुत कम हैं। इस लेख के solutions भी first-class features से ज़्यादा workaround जैसे लगते हैं
अगर page refresh किए बिना real-time में update होने वाली report बनानी हो, तो आम तौर पर DB से data load करके Kafka और WebSocket के ज़रिए GUI में changes भेजे जाते हैं। इससे कुछ analytics code में और कुछ DB में करने वाली अजीब lambda architecture चलानी पड़ती है
इस क्षेत्र में innovation है। KSQL और Kafka Streams changes emit कर सकते हैं, Materialize में subscriptions हैं, और ClickHouse में live views हैं। फिर भी कई features नए या preview stage में हैं और बिल्कुल fit नहीं बैठते। मैंने सभी आज़माए हैं, लेकिन लगता है कि ये developer पर बहुत ज़्यादा काम डाल देते हैं
अच्छा होगा अगर
[select * from orders with suscribe]जैसे option से सीधे change feed पाने वाली कोई library हो। यह काफी महत्वपूर्ण क्षेत्र है, लेकिन इस पर अपेक्षाकृत कम ध्यान मिला हैReplication की एक बड़ी pitfall है जिसे लेख में cover नहीं किया गया, और इसी वजह से मैं replication इस्तेमाल नहीं करता
Postgres बहुत मजबूत guarantee देने की कोशिश करता है कि replication slot का consumer data miss न करे। इसलिए अगर consumer slot से data consume नहीं करता, तो Postgres छूटे हुए data को विनम्रता से लगातार संभालकर रखता रहता है, और अंततः disk भर जाने तक DB गिर जाता है। Prototyping के दौरान दो अलग-अलग SaaS DBs में मेरे साथ ऐसा हुआ, और recovery के लिए support ticket डालने के अलावा कोई रास्ता नहीं था
अगर replication slot consumer पढ़ना बंद कर दे, तो alert ज़रूर बजना चाहिए
एक और वजह यह है कि table का initial snapshot लेने वाला code path और changes पढ़ने वाला code path पूरी तरह अलग होते हैं। कोई change miss न हो, इसके लिए replication slot reading initialize करना trivial नहीं है
अफसोस, change capture के नज़रिए से replication सबसे कम hacky solution है
मैं polling इस्तेमाल करता हूँ, लेकिन
updated_atके बजाय txid store करता हूँजिज्ञासा है कि आप किस behavior को ज़्यादा पसंद करेंगे
अगर बड़े data volume से dealing हो, तो initial snapshot और change reading को अलग तरह से handle करना चाहेंगे। क्योंकि parallel initialization या physical backup-based initialization जैसे काम संभव होने चाहिए। हालांकि यह बात समझ में आती है कि slot creation के बाद existing data को selectively stream करने वाला feature उपयोगी हो सकता है
Changes miss न हों, इसके लिए replication slot reading initialize करने वाला हिस्सा मुश्किल नहीं होना चाहिए, ऐसा लगता है; जानना चाहूँगा कि आप कहाँ अटके
जब सभी changes की ज़रूरत न हो, तो temporary replication slots भी उपयोगी होते हैं, जो connection टूटने पर खुद clean up हो जाते हैं। Server को गिरने से बचाने के लिए retained WAL की maximum limit set करने वाली configuration भी है
updated_atके बजाय txid कैसे इस्तेमाल करते हैं, इस पर थोड़ा और समझाएँ तो अच्छा होगा