ארכיטקטורת CDC: איך לוכדים שינויים בבסיס נתונים בזמן אמת בלי להרוג את ה-DB
מאת צוות מדיה דיל · 01.08.2026 · Data Engineering · 12 דק׳
מדריך מעמיק ל-Change Data Capture: איך log-based CDC עובד בפועל, מתי להעדיף אותו על polling, ומה החלטות הארכיטקטורה שקובעות אם הפרויקט יעבוד בפרודקשן או יקרוס תחת עומס.
תרחיש קלאסי: צוות פיתוח צריך לסנכרן טבלת הזמנות מ-PostgreSQL ל-Elasticsearch לצורך חיפוש, ובמקביל לשלוח אירועים למערכת אנליטיקס. הפתרון הראשוני, כמעט תמיד, הוא polling — job שרץ כל דקה, שואל SELECT * FROM orders WHERE updated_at > :last_run, ודוחף את התוצאות הלאה. זה עובד יפה בדמו. בפרודקשן, עם טבלה של עשרות מיליוני שורות, ה-query הזה הופך לסיוט: אינדקס על updated_at שלא תמיד נבחר, load על ה-DB הראשי בכל cycle, ובעיה מהותית יותר — polling לא רואה מחיקות, ולא רואה states ביניים (עדכון ואז עדכון נוסף באותה דקה). זו בדיוק הנקודה שבה CDC — Change Data Capture — נכנס לתמונה, ולמה הוא הפך לסטנדרט דה-פקטו בכל ארכיטקטורת דאטה מודרנית.
מה זה בעצם CDC, ולמה הוא שונה מ-polling
CDC הוא טכניקה ללכידת כל שינוי (insert, update, delete) שקורה בבסיס נתונים, ברגע שהוא קורה, בלי לשאול את ה-DB ישירות. הגישה הנפוצה והיציבה ביותר היא log-based CDC: קריאה ישירה מה-transaction log של בסיס הנתונים — ה-WAL (Write-Ahead Log) ב-PostgreSQL, ה-binlog ב-MySQL, redo log ב-Oracle. כל בסיס נתונים כותב לוג טרנזקציוני לפני שהוא מיישם שינוי בפועל, לצורכי recovery ו-replication. CDC פשוט 'מאזין' ללוג הזה ומפרסם כל שינוי כאירוע.
ההבדל המהותי מ-polling הוא שה-DB לא 'מרגיש' את ה-CDC. אין query נוסף, אין load על הטבלאות עצמן — הקריאה היא מהלוג, שממילא נכתב לצורך replication רגיל. זו הסיבה שכלים כמו Debezium, שהם היום הסטנדרט הפתוח המוביל, בנויים על העיקרון הזה: הם מתחזים ל-replica רגיל של בסיס הנתונים.
שלוש גישות ל-CDC, ולמה log-based מנצח
יש שלוש גישות עיקריות לביצוע CDC, וכדאי להבין את ה-trade-offs לפני שבוחרים:
- Query-based (polling) — כמו שתואר למעלה. פשוט להטמעה, אבל לא סקיילבילי, לא תופס מחיקות בקלות, ותלוי בעמודת timestamp אמינה שלעיתים פשוט לא קיימת בסכמה הקיימת.
- Trigger-based — יצירת triggers על הטבלאות שכותבים לטבלת shadow בכל שינוי. עובד, אבל מוסיף overhead כתיבה על כל טרנזקציה ב-DB המקורי (כל insert הופך לשתי כתיבות), ומסבך migrations כי כל שינוי סכמה דורש עדכון triggers.
- Log-based — קריאה מה-WAL/binlog. אין overhead כתיבה נוסף על ה-DB, לוכד את כל סוגי השינויים כולל מחיקות, ושומר על הסדר הכרונולוגי המדויק של הטרנזקציות. החיסרון: מורכבות תפעולית גבוהה יותר, ותלות בפורמט הלוג הספציפי של כל מנוע DB.
ברוב הפרויקטים הרציניים שאנחנו בונים, log-based הוא הבחירה הנכונה כמעט תמיד ברגע שהיקף הנתונים עולה על כמה מאות אלפי שורות ליום, או כשיש דרישה אמיתית ל-latency נמוך.
הארכיטקטורה בפועל: מ-WAL ועד consumer
ארכיטקטורת CDC טיפוסית עם Debezium ו-Kafka נראית כך: PostgreSQL עם logical replication מופעל (wal_level = logical) יוצר replication slot ייעודי. Debezium connector, שרץ בתוך Kafka Connect, מתחבר ל-slot הזה, קורא כל שינוי, ומתרגם אותו לפורמט Kafka event הכולל את הערך הישן (before) והחדש (after), metadata של הטרנזקציה, ו-LSN (Log Sequence Number) שמאפשר מעקב מדויק אחר המיקום בלוג.
כל טבלה הופכת לטופיק Kafka משלה, לרוב עם key מבוסס primary key כדי לשמור על partitioning עקבי — כל שינוי לאותה שורה מגיע לאותו partition, ובכך נשמר הסדר. משם, consumers שונים — job שמעדכן Elasticsearch, job שמעדכן data warehouse, שירות שמפעיל side-effects עסקיים — צורכים את האירועים באופן עצמאי, כל אחד בקצב שלו.
Snapshotting: מה קורה עם הנתונים ההיסטוריים
CDC לוכד שינויים מהרגע שהוא מתחיל להאזין — אבל מה עם המיליונים של שורות שכבר קיימות בטבלה? כאן נכנס שלב ה-initial snapshot. Debezium, לדוגמה, מבצע consistent snapshot של כל הטבלה (בדרך כלל תוך שימוש בטרנזקציה עם isolation level מתאים) לפני שהוא עובר למצב streaming מהלוג. הנקודה הקריטית כאן היא סנכרון: ה-snapshot חייב להיות עקבי עם הנקודה המדויקת בלוג שממנה מתחיל ה-streaming, אחרת נוצרים duplicates או, גרוע יותר, פערים בנתונים. פרויקטים רבים נכשלים דווקא בשלב הזה — snapshot על טבלה של מאות מיליוני שורות יכול לקחת שעות ולנעול resources, ולכן יש טכניקות כמו incremental snapshotting (Debezium תומך בכך) שמחלקות את ה-snapshot ל-chunks קטנים שלא חוסמים כתיבות.
Schema evolution: האויב השקט של כל pipeline
אחד הכשלים הנפוצים ביותר בפרודקשן הוא לא בעיית ביצועים אלא בעיית schema. מפתח מוסיף עמודה לטבלה, או משנה טיפוס נתונים, וה-pipeline שצורך את השינויים נשבר בשקט — או גרוע מכך, ממשיך לרוץ אבל עם נתונים פגומים. הפתרון הארכיטקטוני הנכון הוא Schema Registry (למשל Confluent Schema Registry או Apicurio) שאוכף compatibility rules — backward, forward, או full — על כל שינוי סכמה, ומונע פרסום אירוע שלא תואם את החוזה שה-consumers מצפים לו. זה לא nice-to-have, זו דרישת יסוד לכל מערכת CDC שרצה מעבר לפיילוט.
Delivery guarantees: at-least-once מול exactly-once
חשוב להיות כנים לגבי מה CDC מבטיח ומה לא. ברוב המימושים, הערבות היא at-least-once: אחרי כשל וחזרה לפעולה, connector עלול לשלוח מחדש אירועים שכבר נשלחו. הסיבה טכנית — ה-offset (מיקום ה-LSN שנקרא לאחרונה) מתעדכן אחרי commit, ואם ה-connector קורס בין קריאה לבין commit, הוא יתחיל שוב מהנקודה האחרונה שאושרה. כדי להגיע ל-exactly-once בפועל, ה-consumers חייבים להיות אידמפוטנטיים — למשל, upsert לפי primary key במקום insert, כך שקבלה כפולה של אותו אירוע לא יוצרת duplicate. זו החלטת עיצוב שצריך לקבל בשלב התכנון, לא לתקן בדיעבד.
The Outbox Pattern: כשצריך אמינות עסקית, לא רק טכנית
יש תרחיש נפוץ שבו CDC לבד לא מספיק: כשצריך להבטיח שאירוע עסקי (כמו 'הזמנה בוצעה') יישלח בדיוק כשהטרנזקציה שיצרה אותו הצליחה — לא לפני ולא בלי קשר לשינוי בטבלה עצמה. הפתרון הוא Transactional Outbox Pattern: השירות כותב, באותה טרנזקציה שמעדכנת את הטבלה העסקית, גם רשומה לטבלת outbox ייעודית. CDC אז לוכד רק את טבלת ה-outbox, ומבטיח שהאירוע יישלח אם ורק אם הטרנזקציה המקורית הצליחה — פותר את בעיית ה-dual write הקלאסית (כתיבה ל-DB וגם שליחה ישירה ל-message broker כשני צעדים נפרדים, שיכולים להיכשל אחד בלי השני).
מוניטורינג ותפעול: מה שבאמת שובר CDC בפרודקשן
הבעיה התפעולית הכי נפוצה היא replication slot שנתקע. אם ה-consumer של ה-CDC נופל או נעצר, ה-replication slot ב-PostgreSQL ממשיך להצטבר — כי בסיס הנתונים חייב לשמור את הלוג עד שהוא נצרך. התוצאה: WAL שגדל ללא הפסקה, שיכול למלא את הדיסק של ה-DB הראשי תוך שעות ולהפיל את כל המערכת. זו לא תיאוריה — זה הכשל הכי נפוץ שראינו בפרויקטי CDC בשטח. הפתרון: alerting אגרסיבי על slot lag (במונחי bytes או זמן), ומדיניות ברורה למחיקת slots שלא בשימוש.
מעבר לכך, יש לעקוב אחר lag בין הזמן שהשינוי קרה ב-DB לזמן שהוא הגיע ל-consumer, connector health (Kafka Connect חושף metrics ייעודיים), ו-dead letter queues לאירועים שנכשלים בעיבוד — כדי לא לחסום את כל ה-pipeline בגלל שורה בעייתית אחת.
CDC על בסיסי נתונים ניהוליים בענן: המגבלות שכדאי להכיר מראש
ברוב הפרויקטים היום ה-DB רץ כשירות מנוהל — RDS, Cloud SQL, Azure Database. זה משנה את התמונה, כי לא תמיד יש גישה מלאה להגדרות ה-replication ברמת ה-DB. ב-AWS RDS for PostgreSQL, למשל, צריך להפעיל במפורש logical replication דרך parameter group ייעודי, ולעיתים יש הגבלה על מספר ה-replication slots הפעילים בו-זמנית — מגבלה שקל לפספס בתכנון ראשוני ולהיתקל בה רק כשמנסים להוסיף connector שני. ב-MySQL על RDS, יש להפעיל binlog בפורמט ROW (לא STATEMENT או MIXED), אחרת Debezium לא יכול לשחזר את הערכים המדויקים של כל שינוי. שירותים כמו Amazon DMS או Aurora עם Kafka integration מובנה מציעים חלופה מנוהלת יותר, אבל בדרך כלל עם פחות שליטה על התנהגות ה-snapshot ועל ניטור ה-lag ברמת הפרטים שדרוש לדיבוג רציני. גם MongoDB דורש גישה שונה לחלוטין — הוא לא כותב WAL קלאסי אלא oplog, ו-Debezium's MongoDB connector עובד מול change streams הפנימיים שלו, עם מגבלות משלו על retention של האירועים בתוך replica set.
נקודה נוספת שחשוב להכיר: לא כל שינוי בסכמה נתמך equally. הוספת עמודה בדרך כלל בטוחה (עם ברירת מחדל ל-consumers ישנים), אבל שינוי טיפוס נתונים של עמודה קיימת, או rename, יכולים לשבור consumers שכבר קיימים בלי שום אזהרה מוקדמת אם אין schema registry שאוכף תאימות. לכן, בכל תכנון CDC רציני, שווה להגדיר מראש process ברור של code review לשינויי סכמה שכולל בדיקה מפורשת של ההשפעה על ה-CDC pipeline — לא לסמוך על כך שמישהו 'יזכור'.
מתי כן ומתי לא כדאי CDC
CDC הוא הכלי הנכון כשיש צורך אמיתי בסנכרון נתונים כמעט-בזמן-אמת בין מערכות, כשה-DB המקורי לא יכול לספוג load נוסף, או כשצריך audit trail מלא של כל שינוי. הוא פחות מתאים כשמדובר בנפח נתונים קטן שסנכרון batch פשוט (once a day) פותר, או כשהצוות אין לו את הבשלות התפעולית לתחזק Kafka/Debezium — כי המורכבות התפעולית אמיתית ולא ניתן להתעלם ממנה. אנחנו במדיה דיל בדרך כלל ממליצים להתחיל עם CDC רק כשיש לפחות שני צרכנים שונים לאותם שינויי נתונים; אם יש רק צרכן אחד, לפעמים webhook פשוט מהאפליקציה עצמה זול יותר לתחזוקה.
דוגמת קונפיגורציה: connector בסיסי ל-Debezium
כדי להמחיש את זה בפועל, כך נראית הגדרת connector בסיסי ל-Debezium שמאזין לטבלת orders ב-PostgreSQL ומפרסם ל-Kafka:
{
"name": "orders-connector",
"config": {
"connector.class": "io.debezium.connector.postgresql.PostgresConnector",
"database.hostname": "db.internal",
"database.dbname": "orders_db",
"table.include.list": "public.orders",
"plugin.name": "pgoutput",
"slot.name": "orders_slot",
"topic.prefix": "orders",
"snapshot.mode": "initial"
}
}
שימו לב לפרמטר slot.name — זה השם שדרכו עוקבים אחרי ה-lag של ה-replication slot, ו-snapshot.mode: initial קובע שה-connector יבצע consistent snapshot מלא בפעם הראשונה שהוא רץ, ואז יעבור למצב streaming. שינוי הפרמטר הזה בטעות ל-never על connector שרץ בפעם הראשונה יגרום לו לפספס את כל הנתונים הקיימים ולהתחיל רק משינויים עתידיים — טעות נפוצה שקל לעשות בזמן קונפיגורציה ראשונית.
סיכום
CDC פותר בעיה אמיתית — היכולת לשקף שינויים בין מערכות בלי להעמיס על מקור הנתונים ובלי לפספס אירועים — אבל הוא מגיע עם מחיר תפעולי: ניהול replication slots, schema evolution, אחריות ל-idempotency אצל הצרכנים, ומוניטורינג רציני. הארכיטקטורה הנכונה, עם outbox pattern במקומות הנכונים ו-schema registry שאוכף חוזים, היא ההבדל בין pipeline שרץ בשקט שנתיים לבין אחד שמעיר אתכם ב-3 בלילה.
תגיות: CDC · Change Data Capture · Debezium · Kafka Connect · WAL · outbox pattern · schema registry · replication slot