High-Performance Data Ingestion and CDC Implementation: Real-Time Change Data Capture at Scale
High-Performance Data Ingestion and CDC Implementation: Real-Time Change Data Capture at Scale
Change Data Capture (CDC) has become a critical component of modern data architectures, enabling real-time data integration, event-driven architectures, and maintaining data consistency across distributed systems. High-performance CDC implementation requires sophisticated approaches to handling schema evolution, conflict resolution, backpressure management, and fault tolerance.
This comprehensive guide explores advanced techniques for implementing enterprise-grade CDC systems, covering streaming architectures, optimization strategies, and production deployment patterns that can handle millions of events per second with minimal latency.
Understanding CDC Fundamentals and Architecture
CDC Architecture Patterns and Components
Change Data Capture systems must handle diverse data sources, transformation requirements, and delivery guarantees while maintaining high throughput and low latency.
# Advanced CDC framework and streaming infrastructure
import asyncio
import logging
import json
import time
from abc import ABC, abstractmethod
from typing import Dict, List, Any, Optional, Union, Callable, AsyncGenerator
from dataclasses import dataclass, field
from datetime import datetime, timezone
from enum import Enum
import uuid
import hashlib
class ChangeEventType(Enum):
INSERT = "INSERT"
UPDATE = "UPDATE"
DELETE = "DELETE"
TRUNCATE = "TRUNCATE"
SCHEMA_CHANGE = "SCHEMA_CHANGE"
class CDCSourceType(Enum):
DATABASE_LOG = "database_log"
TRIGGER_BASED = "trigger_based"
TIMESTAMP_BASED = "timestamp_based"
SNAPSHOT_BASED = "snapshot_based"
@dataclass
class ChangeEvent:
"""Standardized change event structure"""
event_id: str = field(default_factory=lambda: str(uuid.uuid4()))
source_system: str = ""
database: str = ""
table: str = ""
event_type: ChangeEventType = ChangeEventType.INSERT
timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
transaction_id: Optional[str] = None
lsn: Optional[str] = None # Log Sequence Number
before_image: Optional[Dict[str, Any]] = None
after_image: Optional[Dict[str, Any]] = None
primary_key: Dict[str, Any] = field(default_factory=dict)
schema_version: str = "1.0"
metadata: Dict[str, Any] = field(default_factory=dict)
def get_partition_key(self) -> str:
"""Generate partition key for event distribution"""
if self.primary_key:
key_str = "_".join(str(v) for v in sorted(self.primary_key.values()))
return hashlib.md5(key_str.encode()).hexdigest()
return hashlib.md5(f"{self.database}_{self.table}".encode()).hexdigest()
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary for serialization"""
return {
"event_id": self.event_id,
"source_system": self.source_system,
"database": self.database,
"table": self.table,
"event_type": self.event_type.value,
"timestamp": self.timestamp.isoformat(),
"transaction_id": self.transaction_id,
"lsn": self.lsn,
"before_image": self.before_image,
"after_image": self.after_image,
"primary_key": self.primary_key,
"schema_version": self.schema_version,
"metadata": self.metadata
}
class CDCSource(ABC):
"""Abstract base class for CDC sources"""
def __init__(self, config: Dict[str, Any]):
self.config = config
self.source_type = CDCSourceType(config.get("source_type", "database_log"))
self.running = False
@abstractmethod
async def start(self) -> None:
"""Start CDC capture"""
pass
@abstractmethod
async def stop(self) -> None:
"""Stop CDC capture"""
pass
@abstractmethod
async def get_events(self) -> AsyncGenerator[ChangeEvent, None]:
"""Get stream of change events"""
pass
@abstractmethod
async def get_current_position(self) -> str:
"""Get current position in the change stream"""
pass
@abstractmethod
async def set_position(self, position: str) -> None:
"""Set position in the change stream"""
pass
class PostgreSQLCDCSource(CDCSource):
"""PostgreSQL CDC source using logical replication"""
def __init__(self, config: Dict[str, Any]):
super().__init__(config)
self.connection_config = config["connection"]
self.replication_slot = config.get("replication_slot", "cdc_slot")
self.publication = config.get("publication", "cdc_publication")
self.current_lsn = None
async def start(self) -> None:
"""Start PostgreSQL CDC capture"""
self.running = True
# Initialize replication connection
await self._setup_replication()
logging.info(f"Started PostgreSQL CDC source for {self.connection_config['host']}")
async def stop(self) -> None:
"""Stop PostgreSQL CDC capture"""
self.running = False
# Cleanup replication connection
await self._cleanup_replication()
logging.info("Stopped PostgreSQL CDC source")
async def get_events(self) -> AsyncGenerator[ChangeEvent, None]:
"""Get PostgreSQL change events"""
while self.running:
try:
# Simulate reading from PostgreSQL logical replication
# In a real implementation, this would use psycopg2 or asyncpg
events = await self._read_replication_stream()
for event_data in events:
event = self._parse_postgres_event(event_data)
if event:
yield event
await asyncio.sleep(0.01) # Small delay to prevent busy waiting
except Exception as e:
logging.error(f"Error reading PostgreSQL events: {e}")
await asyncio.sleep(1) # Wait before retry
async def _setup_replication(self):
"""Setup PostgreSQL logical replication"""
# Implementation would:
# 1. Create replication slot if not exists
# 2. Create publication if not exists
# 3. Establish replication connection
pass
async def _cleanup_replication(self):
"""Cleanup PostgreSQL replication resources"""
# Implementation would cleanup replication connections
pass
async def _read_replication_stream(self) -> List[Dict[str, Any]]:
"""Read from PostgreSQL replication stream"""
# Simulate replication stream data
events = [
{
"lsn": "0/1234567",
"transaction_id": "tx_001",
"table": "customers",
"action": "INSERT",
"data": {"id": 123, "name": "John Doe", "email": "john@example.com"},
"timestamp": datetime.now(timezone.utc)
},
{
"lsn": "0/1234568",
"transaction_id": "tx_002",
"table": "orders",
"action": "UPDATE",
"data": {"id": 456, "status": "shipped", "updated_at": datetime.now(timezone.utc)},
"old_data": {"id": 456, "status": "pending"},
"timestamp": datetime.now(timezone.utc)
}
]
return events
def _parse_postgres_event(self, event_data: Dict[str, Any]) -> Optional[ChangeEvent]:
"""Parse PostgreSQL replication event"""
try:
event_type_map = {
"INSERT": ChangeEventType.INSERT,
"UPDATE": ChangeEventType.UPDATE,
"DELETE": ChangeEventType.DELETE
}
event_type = event_type_map.get(event_data["action"])
if not event_type:
return None
# Extract primary key
primary_key = {}
data = event_data.get("data", {})
if "id" in data:
primary_key["id"] = data["id"]
event = ChangeEvent(
source_system="postgresql",
database=self.connection_config.get("database", ""),
table=event_data["table"],
event_type=event_type,
timestamp=event_data["timestamp"],
transaction_id=event_data.get("transaction_id"),
lsn=event_data.get("lsn"),
before_image=event_data.get("old_data"),
after_image=event_data.get("data"),
primary_key=primary_key
)
self.current_lsn = event_data.get("lsn")
return event
except Exception as e:
logging.error(f"Error parsing PostgreSQL event: {e}")
return None
async def get_current_position(self) -> str:
"""Get current LSN position"""
return self.current_lsn or "0/0"
async def set_position(self, position: str) -> None:
"""Set LSN position"""
self.current_lsn = position
class MySQLCDCSource(CDCSource):
"""MySQL CDC source using binlog"""
def __init__(self, config: Dict[str, Any]):
super().__init__(config)
self.connection_config = config["connection"]
self.server_id = config.get("server_id", 1)
self.current_position = None
async def start(self) -> None:
"""Start MySQL CDC capture"""
self.running = True
# Initialize binlog reader
await self._setup_binlog_reader()
logging.info(f"Started MySQL CDC source for {self.connection_config['host']}")
async def stop(self) -> None:
"""Stop MySQL CDC capture"""
self.running = False
# Cleanup binlog reader
await self._cleanup_binlog_reader()
logging.info("Stopped MySQL CDC source")
async def get_events(self) -> AsyncGenerator[ChangeEvent, None]:
"""Get MySQL binlog events"""
while self.running:
try:
# Simulate reading from MySQL binlog
events = await self._read_binlog_stream()
for event_data in events:
event = self._parse_mysql_event(event_data)
if event:
yield event
await asyncio.sleep(0.01)
except Exception as e:
logging.error(f"Error reading MySQL events: {e}")
await asyncio.sleep(1)
async def _setup_binlog_reader(self):
"""Setup MySQL binlog reader"""
# Implementation would setup python-mysql-replication or similar
pass
async def _cleanup_binlog_reader(self):
"""Cleanup MySQL binlog reader"""
pass
async def _read_binlog_stream(self) -> List[Dict[str, Any]]:
"""Read from MySQL binlog"""
# Simulate binlog events
events = [
{
"log_file": "mysql-bin.000001",
"log_pos": 12345,
"timestamp": datetime.now(timezone.utc),
"event_type": "WRITE_ROWS_EVENT",
"table": "products",
"rows": [{"id": 789, "name": "Widget", "price": 29.99}]
}
]
return events
def _parse_mysql_event(self, event_data: Dict[str, Any]) -> Optional[ChangeEvent]:
"""Parse MySQL binlog event"""
try:
event_type_map = {
"WRITE_ROWS_EVENT": ChangeEventType.INSERT,
"UPDATE_ROWS_EVENT": ChangeEventType.UPDATE,
"DELETE_ROWS_EVENT": ChangeEventType.DELETE
}
event_type = event_type_map.get(event_data["event_type"])
if not event_type:
return None
# Process each row in the event
for row in event_data.get("rows", []):
primary_key = {}
if "id" in row:
primary_key["id"] = row["id"]
event = ChangeEvent(
source_system="mysql",
database=self.connection_config.get("database", ""),
table=event_data["table"],
event_type=event_type,
timestamp=event_data["timestamp"],
after_image=row,
primary_key=primary_key,
metadata={
"log_file": event_data["log_file"],
"log_pos": event_data["log_pos"]
}
)
self.current_position = f"{event_data['log_file']}:{event_data['log_pos']}"
return event
except Exception as e:
logging.error(f"Error parsing MySQL event: {e}")
return None
async def get_current_position(self) -> str:
"""Get current binlog position"""
return self.current_position or "mysql-bin.000001:0"
async def set_position(self, position: str) -> None:
"""Set binlog position"""
self.current_position = position
class CDCProcessor:
"""Process and transform CDC events"""
def __init__(self, config: Dict[str, Any]):
self.config = config
self.transformations: List[Callable] = []
self.filters: List[Callable] = []
self.enrichers: List[Callable] = []
def add_transformation(self, transformation: Callable[[ChangeEvent], ChangeEvent]):
"""Add event transformation function"""
self.transformations.append(transformation)
def add_filter(self, filter_func: Callable[[ChangeEvent], bool]):
"""Add event filter function"""
self.filters.append(filter_func)
def add_enricher(self, enricher: Callable[[ChangeEvent], ChangeEvent]):
"""Add event enrichment function"""
self.enrichers.append(enricher)
async def process_event(self, event: ChangeEvent) -> Optional[ChangeEvent]:
"""Process a single change event"""
try:
# Apply filters
for filter_func in self.filters:
if not filter_func(event):
return None # Event filtered out
# Apply transformations
for transformation in self.transformations:
event = transformation(event)
# Apply enrichments
for enricher in self.enrichers:
event = enricher(event)
return event
except Exception as e:
logging.error(f"Error processing event {event.event_id}: {e}")
return None
class ConflictResolver:
"""Resolve conflicts in CDC events"""
def __init__(self, strategy: str = "last_write_wins"):
self.strategy = strategy
self.event_cache: Dict[str, ChangeEvent] = {}
async def resolve_conflict(self, events: List[ChangeEvent]) -> List[ChangeEvent]:
"""Resolve conflicts between multiple events"""
if len(events) <= 1:
return events
if self.strategy == "last_write_wins":
return self._last_write_wins(events)
elif self.strategy == "merge":
return self._merge_events(events)
elif self.strategy == "version_vector":
return self._version_vector_resolution(events)
else:
raise ValueError(f"Unknown conflict resolution strategy: {self.strategy}")
def _last_write_wins(self, events: List[ChangeEvent]) -> List[ChangeEvent]:
"""Last write wins conflict resolution"""
# Sort by timestamp and return the latest event
sorted_events = sorted(events, key=lambda e: e.timestamp)
return [sorted_events[-1]]
def _merge_events(self, events: List[ChangeEvent]) -> List[ChangeEvent]:
"""Merge multiple events into a single event"""
if not events:
return []
# Start with the first event as base
merged_event = events[0]
# Merge data from subsequent events
for event in events[1:]:
if event.after_image:
if merged_event.after_image:
merged_event.after_image.update(event.after_image)
else:
merged_event.after_image = event.after_image.copy()
# Update timestamp to latest
if event.timestamp > merged_event.timestamp:
merged_event.timestamp = event.timestamp
return [merged_event]
def _version_vector_resolution(self, events: List[ChangeEvent]) -> List[ChangeEvent]:
"""Version vector based conflict resolution"""
# Simplified version vector implementation
# In practice, this would use proper vector clocks
latest_version = 0
latest_event = None
for event in events:
version = event.metadata.get("version", 0)
if version > latest_version:
latest_version = version
latest_event = event
return [latest_event] if latest_event else []
# Advanced streaming infrastructure for CDC
class StreamingCDCPipeline:
"""High-performance streaming CDC pipeline"""
def __init__(self, config: Dict[str, Any]):
self.config = config
self.sources: Dict[str, CDCSource] = {}
self.processor = CDCProcessor(config.get("processing", {}))
self.conflict_resolver = ConflictResolver(config.get("conflict_resolution", "last_write_wins"))
self.sinks: List[CDCSink] = []
self.running = False
self.metrics = CDCMetrics()
def add_source(self, name: str, source: CDCSource):
"""Add CDC source"""
self.sources[name] = source
def add_sink(self, sink: 'CDCSink'):
"""Add CDC sink"""
self.sinks.append(sink)
async def start(self):
"""Start CDC pipeline"""
self.running = True
# Start all sources
for name, source in self.sources.items():
await source.start()
logging.info(f"Started CDC source: {name}")
# Start all sinks
for sink in self.sinks:
await sink.start()
# Start processing tasks
tasks = []
for name, source in self.sources.items():
task = asyncio.create_task(self._process_source(name, source))
tasks.append(task)
logging.info("Started CDC pipeline")
# Wait for all tasks
await asyncio.gather(*tasks)
async def stop(self):
"""Stop CDC pipeline"""
self.running = False
# Stop all sources
for name, source in self.sources.items():
await source.stop()
logging.info(f"Stopped CDC source: {name}")
# Stop all sinks
for sink in self.sinks:
await sink.stop()
logging.info("Stopped CDC pipeline")
async def _process_source(self, source_name: str, source: CDCSource):
"""Process events from a specific source"""
async for event in source.get_events():
if not self.running:
break
try:
start_time = time.time()
# Process event
processed_event = await self.processor.process_event(event)
if not processed_event:
continue # Event was filtered out
# Send to all sinks
for sink in self.sinks:
await sink.send_event(processed_event)
# Record metrics
processing_time = time.time() - start_time
self.metrics.record_event_processed(source_name, processing_time)
except Exception as e:
logging.error(f"Error processing event from {source_name}: {e}")
self.metrics.record_error(source_name, str(e))
class CDCSink(ABC):
"""Abstract base class for CDC sinks"""
@abstractmethod
async def start(self) -> None:
"""Start the sink"""
pass
@abstractmethod
async def stop(self) -> None:
"""Stop the sink"""
pass
@abstractmethod
async def send_event(self, event: ChangeEvent) -> None:
"""Send event to sink"""
pass
class KafkaCDCSink(CDCSink):
"""Kafka CDC sink"""
def __init__(self, config: Dict[str, Any]):
self.config = config
self.topic_template = config.get("topic_template", "{database}.{table}")
self.producer = None
async def start(self) -> None:
"""Start Kafka producer"""
# Initialize Kafka producer
# In real implementation, would use aiokafka or similar
self.producer = "kafka_producer" # Placeholder
logging.info("Started Kafka CDC sink")
async def stop(self) -> None:
"""Stop Kafka producer"""
if self.producer:
# Close Kafka producer
self.producer = None
logging.info("Stopped Kafka CDC sink")
async def send_event(self, event: ChangeEvent) -> None:
"""Send event to Kafka"""
try:
# Generate topic name
topic = self.topic_template.format(
database=event.database,
table=event.table
)
# Serialize event
message = json.dumps(event.to_dict())
# Send to Kafka (simulated)
await self._send_to_kafka(topic, event.get_partition_key(), message)
except Exception as e:
logging.error(f"Error sending event to Kafka: {e}")
raise
async def _send_to_kafka(self, topic: str, key: str, message: str):
"""Send message to Kafka (simulated)"""
# In real implementation, would use producer.send()
logging.debug(f"Sent to Kafka topic {topic}: {message[:100]}...")
class ElasticsearchCDCSink(CDCSink):
"""Elasticsearch CDC sink"""
def __init__(self, config: Dict[str, Any]):
self.config = config
self.index_template = config.get("index_template", "{database}_{table}")
self.client = None
async def start(self) -> None:
"""Start Elasticsearch client"""
# Initialize Elasticsearch client
self.client = "elasticsearch_client" # Placeholder
logging.info("Started Elasticsearch CDC sink")
async def stop(self) -> None:
"""Stop Elasticsearch client"""
if self.client:
self.client = None
logging.info("Stopped Elasticsearch CDC sink")
async def send_event(self, event: ChangeEvent) -> None:
"""Send event to Elasticsearch"""
try:
# Generate index name
index = self.index_template.format(
database=event.database,
table=event.table
)
# Prepare document
doc = {
"timestamp": event.timestamp.isoformat(),
"event_type": event.event_type.value,
"data": event.after_image or event.before_image,
"primary_key": event.primary_key,
"metadata": event.metadata
}
# Index document (simulated)
await self._index_document(index, event.event_id, doc)
except Exception as e:
logging.error(f"Error sending event to Elasticsearch: {e}")
raise
async def _index_document(self, index: str, doc_id: str, document: Dict[str, Any]):
"""Index document in Elasticsearch (simulated)"""
logging.debug(f"Indexed document in {index}: {doc_id}")
class CDCMetrics:
"""CDC pipeline metrics collection"""
def __init__(self):
self.events_processed = {}
self.processing_times = {}
self.errors = {}
self.start_time = time.time()
def record_event_processed(self, source: str, processing_time: float):
"""Record event processing metrics"""
if source not in self.events_processed:
self.events_processed[source] = 0
self.processing_times[source] = []
self.events_processed[source] += 1
self.processing_times[source].append(processing_time)
# Keep only recent processing times
if len(self.processing_times[source]) > 1000:
self.processing_times[source] = self.processing_times[source][-1000:]
def record_error(self, source: str, error: str):
"""Record error metrics"""
if source not in self.errors:
self.errors[source] = []
self.errors[source].append({
"timestamp": datetime.now(timezone.utc),
"error": error
})
# Keep only recent errors
if len(self.errors[source]) > 100:
self.errors[source] = self.errors[source][-100:]
def get_metrics_summary(self) -> Dict[str, Any]:
"""Get metrics summary"""
summary = {
"uptime_seconds": time.time() - self.start_time,
"sources": {}
}
for source in self.events_processed:
processing_times = self.processing_times.get(source, [])
summary["sources"][source] = {
"events_processed": self.events_processed[source],
"error_count": len(self.errors.get(source, [])),
"avg_processing_time_ms": (
sum(processing_times) / len(processing_times) * 1000
if processing_times else 0
),
"throughput_eps": (
self.events_processed[source] / (time.time() - self.start_time)
if (time.time() - self.start_time) > 0 else 0
)
}
return summary
Schema Evolution and Management
Advanced Schema Handling
# Advanced schema evolution and compatibility management
from typing import Dict, List, Any, Optional, Tuple
import json
import logging
from enum import Enum
from dataclasses import dataclass
class SchemaCompatibilityType(Enum):
BACKWARD = "backward"
FORWARD = "forward"
FULL = "full"
NONE = "none"
class SchemaChangeType(Enum):
ADD_FIELD = "add_field"
REMOVE_FIELD = "remove_field"
MODIFY_FIELD = "modify_field"
RENAME_FIELD = "rename_field"
CHANGE_TYPE = "change_type"
@dataclass
class SchemaChange:
"""Schema change description"""
change_type: SchemaChangeType
field_name: str
old_definition: Optional[Dict[str, Any]] = None
new_definition: Optional[Dict[str, Any]] = None
compatibility_impact: Optional[str] = None
@dataclass
class SchemaVersion:
"""Schema version with metadata"""
version: str
schema: Dict[str, Any]
timestamp: datetime
compatibility_type: SchemaCompatibilityType
changes: List[SchemaChange]
metadata: Dict[str, Any]
class SchemaRegistry:
"""Centralized schema registry for CDC events"""
def __init__(self, config: Dict[str, Any]):
self.config = config
self.schemas: Dict[str, List[SchemaVersion]] = {}
self.compatibility_checker = SchemaCompatibilityChecker()
def register_schema(self, subject: str, schema: Dict[str, Any],
compatibility_type: SchemaCompatibilityType = SchemaCompatibilityType.BACKWARD) -> str:
"""Register a new schema version"""
# Generate version
if subject not in self.schemas:
self.schemas[subject] = []
version = "1.0.0"
else:
latest_version = self.schemas[subject][-1].version
version = self._increment_version(latest_version)
# Check compatibility with previous version
if self.schemas[subject]:
previous_schema = self.schemas[subject][-1]
changes = self.compatibility_checker.detect_changes(
previous_schema.schema, schema
)
is_compatible = self.compatibility_checker.check_compatibility(
previous_schema.schema, schema, compatibility_type
)
if not is_compatible:
raise ValueError(f"Schema is not compatible with previous version")
else:
changes = []
# Create schema version
schema_version = SchemaVersion(
version=version,
schema=schema,
timestamp=datetime.now(timezone.utc),
compatibility_type=compatibility_type,
changes=changes,
metadata={}
)
self.schemas[subject].append(schema_version)
logging.info(f"Registered schema version {version} for subject {subject}")
return version
def get_schema(self, subject: str, version: Optional[str] = None) -> Optional[SchemaVersion]:
"""Get schema by subject and version"""
if subject not in self.schemas:
return None
if version is None:
return self.schemas[subject][-1] # Latest version
for schema_version in self.schemas[subject]:
if schema_version.version == version:
return schema_version
return None
def get_latest_version(self, subject: str) -> Optional[str]:
"""Get latest schema version for subject"""
if subject not in self.schemas or not self.schemas[subject]:
return None
return self.schemas[subject][-1].version
def list_subjects(self) -> List[str]:
"""List all registered subjects"""
return list(self.schemas.keys())
def list_versions(self, subject: str) -> List[str]:
"""List all versions for a subject"""
if subject not in self.schemas:
return []
return [sv.version for sv in self.schemas[subject]]
def _increment_version(self, current_version: str) -> str:
"""Increment version number"""
parts = current_version.split(".")
major, minor, patch = int(parts[0]), int(parts[1]), int(parts[2])
# Simple increment patch version
patch += 1
return f"{major}.{minor}.{patch}"
class SchemaCompatibilityChecker:
"""Check schema compatibility between versions"""
def detect_changes(self, old_schema: Dict[str, Any],
new_schema: Dict[str, Any]) -> List[SchemaChange]:
"""Detect changes between two schemas"""
changes = []
old_fields = old_schema.get("properties", {})
new_fields = new_schema.get("properties", {})
# Check for added fields
for field_name, field_def in new_fields.items():
if field_name not in old_fields:
changes.append(SchemaChange(
change_type=SchemaChangeType.ADD_FIELD,
field_name=field_name,
new_definition=field_def
))
# Check for removed fields
for field_name, field_def in old_fields.items():
if field_name not in new_fields:
changes.append(SchemaChange(
change_type=SchemaChangeType.REMOVE_FIELD,
field_name=field_name,
old_definition=field_def
))
# Check for modified fields
for field_name in old_fields:
if field_name in new_fields:
if old_fields[field_name] != new_fields[field_name]:
change_type = self._determine_change_type(
old_fields[field_name], new_fields[field_name]
)
changes.append(SchemaChange(
change_type=change_type,
field_name=field_name,
old_definition=old_fields[field_name],
new_definition=new_fields[field_name]
))
return changes
def check_compatibility(self, old_schema: Dict[str, Any],
new_schema: Dict[str, Any],
compatibility_type: SchemaCompatibilityType) -> bool:
"""Check if schemas are compatible"""
changes = self.detect_changes(old_schema, new_schema)
if compatibility_type == SchemaCompatibilityType.BACKWARD:
return self._check_backward_compatibility(changes)
elif compatibility_type == SchemaCompatibilityType.FORWARD:
return self._check_forward_compatibility(changes)
elif compatibility_type == SchemaCompatibilityType.FULL:
return (self._check_backward_compatibility(changes) and
self._check_forward_compatibility(changes))
elif compatibility_type == SchemaCompatibilityType.NONE:
return True
return False
def _check_backward_compatibility(self, changes: List[SchemaChange]) -> bool:
"""Check backward compatibility (new schema can read old data)"""
for change in changes:
if change.change_type == SchemaChangeType.REMOVE_FIELD:
# Removing a field breaks backward compatibility
# unless the field was optional
old_def = change.old_definition or {}
if old_def.get("required", True):
return False
elif change.change_type == SchemaChangeType.CHANGE_TYPE:
# Type changes generally break backward compatibility
return False
return True
def _check_forward_compatibility(self, changes: List[SchemaChange]) -> bool:
"""Check forward compatibility (old schema can read new data)"""
for change in changes:
if change.change_type == SchemaChangeType.ADD_FIELD:
# Adding a required field breaks forward compatibility
new_def = change.new_definition or {}
if new_def.get("required", True):
return False
elif change.change_type == SchemaChangeType.CHANGE_TYPE:
# Type changes generally break forward compatibility
return False
return True
def _determine_change_type(self, old_def: Dict[str, Any],
new_def: Dict[str, Any]) -> SchemaChangeType:
"""Determine the type of change between field definitions"""
if old_def.get("type") != new_def.get("type"):
return SchemaChangeType.CHANGE_TYPE
return SchemaChangeType.MODIFY_FIELD
class SchemaEvolutionManager:
"""Manage schema evolution in CDC pipelines"""
def __init__(self, schema_registry: SchemaRegistry):
self.schema_registry = schema_registry
self.converters: Dict[str, SchemaConverter] = {}
def register_converter(self, from_version: str, to_version: str,
converter: 'SchemaConverter'):
"""Register schema converter between versions"""
key = f"{from_version}_to_{to_version}"
self.converters[key] = converter
async def handle_schema_change(self, event: ChangeEvent,
current_schema_version: str) -> ChangeEvent:
"""Handle schema changes in events"""
event_schema_version = event.schema_version
if event_schema_version == current_schema_version:
return event # No conversion needed
# Find converter
converter_key = f"{event_schema_version}_to_{current_schema_version}"
if converter_key not in self.converters:
# Try to find conversion path
conversion_path = self._find_conversion_path(
event_schema_version, current_schema_version
)
if not conversion_path:
raise ValueError(
f"No conversion path from {event_schema_version} to {current_schema_version}"
)
# Apply multiple conversions
converted_event = event
for i in range(len(conversion_path) - 1):
from_ver = conversion_path[i]
to_ver = conversion_path[i + 1]
converter_key = f"{from_ver}_to_{to_ver}"
if converter_key in self.converters:
converted_event = await self.converters[converter_key].convert(converted_event)
return converted_event
# Direct conversion
return await self.converters[converter_key].convert(event)
def _find_conversion_path(self, from_version: str, to_version: str) -> Optional[List[str]]:
"""Find conversion path between schema versions"""
# Simple implementation - in practice, you'd use graph algorithms
# to find the shortest path through available converters
available_conversions = set()
for key in self.converters.keys():
from_ver, to_ver = key.split("_to_")
available_conversions.add((from_ver, to_ver))
# For now, return direct path if available
if (from_version, to_version) in available_conversions:
return [from_version, to_version]
return None
class SchemaConverter(ABC):
"""Abstract base class for schema converters"""
@abstractmethod
async def convert(self, event: ChangeEvent) -> ChangeEvent:
"""Convert event from one schema version to another"""
pass
class CustomerSchemaV1ToV2Converter(SchemaConverter):
"""Convert customer schema from v1.0.0 to v2.0.0"""
async def convert(self, event: ChangeEvent) -> ChangeEvent:
"""Convert customer event from v1 to v2"""
if event.after_image:
converted_data = event.after_image.copy()
# v2 added 'full_name' field combining 'first_name' and 'last_name'
if 'first_name' in converted_data and 'last_name' in converted_data:
converted_data['full_name'] = f"{converted_data['first_name']} {converted_data['last_name']}"
# v2 renamed 'phone' to 'phone_number'
if 'phone' in converted_data:
converted_data['phone_number'] = converted_data.pop('phone')
event.after_image = converted_data
if event.before_image:
converted_data = event.before_image.copy()
if 'first_name' in converted_data and 'last_name' in converted_data:
converted_data['full_name'] = f"{converted_data['first_name']} {converted_data['last_name']}"
if 'phone' in converted_data:
converted_data['phone_number'] = converted_data.pop('phone')
event.before_image = converted_data
# Update schema version
event.schema_version = "2.0.0"
return event
# Advanced data transformation and enrichment
class DataTransformationEngine:
"""Advanced data transformation engine for CDC events"""
def __init__(self):
self.transformations: Dict[str, List[Callable]] = {}
self.enrichment_sources: Dict[str, 'EnrichmentSource'] = {}
def register_transformation(self, table: str, transformation: Callable[[Dict[str, Any]], Dict[str, Any]]):
"""Register transformation for specific table"""
if table not in self.transformations:
self.transformations[table] = []
self.transformations[table].append(transformation)
def register_enrichment_source(self, name: str, source: 'EnrichmentSource'):
"""Register enrichment data source"""
self.enrichment_sources[name] = source
async def transform_event(self, event: ChangeEvent) -> ChangeEvent:
"""Apply transformations to event"""
table_key = f"{event.database}.{event.table}"
# Apply table-specific transformations
if table_key in self.transformations:
for transformation in self.transformations[table_key]:
if event.after_image:
event.after_image = transformation(event.after_image)
if event.before_image:
event.before_image = transformation(event.before_image)
# Apply enrichments
event = await self._enrich_event(event)
return event
async def _enrich_event(self, event: ChangeEvent) -> ChangeEvent:
"""Enrich event with additional data"""
# Customer enrichment example
if event.table == "customers" and event.after_image:
customer_id = event.after_image.get("id")
if customer_id:
# Enrich with customer segment
if "customer_segments" in self.enrichment_sources:
segment_source = self.enrichment_sources["customer_segments"]
segment_data = await segment_source.get_enrichment_data(customer_id)
if segment_data:
event.after_image["customer_segment"] = segment_data.get("segment")
event.after_image["segment_score"] = segment_data.get("score")
return event
class EnrichmentSource(ABC):
"""Abstract base class for enrichment data sources"""
@abstractmethod
async def get_enrichment_data(self, key: Any) -> Optional[Dict[str, Any]]:
"""Get enrichment data for given key"""
pass
class RedisEnrichmentSource(EnrichmentSource):
"""Redis-based enrichment source"""
def __init__(self, config: Dict[str, Any]):
self.config = config
self.redis_client = None # Would initialize Redis client
async def get_enrichment_data(self, key: Any) -> Optional[Dict[str, Any]]:
"""Get enrichment data from Redis"""
# Simulate Redis lookup
enrichment_data = {
"segment": "premium",
"score": 0.85,
"last_updated": datetime.now(timezone.utc).isoformat()
}
return enrichment_data
class DatabaseEnrichmentSource(EnrichmentSource):
"""Database-based enrichment source"""
def __init__(self, config: Dict[str, Any]):
self.config = config
self.connection_pool = None # Would initialize database connection pool
async def get_enrichment_data(self, key: Any) -> Optional[Dict[str, Any]]:
"""Get enrichment data from database"""
# Simulate database lookup
enrichment_data = {
"segment": "gold",
"score": 0.75,
"preferences": {"newsletter": True, "sms": False}
}
return enrichment_data
Production Deployment and Monitoring
Advanced Deployment Strategies
# Production deployment and monitoring for CDC systems
import asyncio
import logging
import time
from typing import Dict, List, Any, Optional
from dataclasses import dataclass
from datetime import datetime, timezone
import json
class CDCDeploymentManager:
"""Manage CDC system deployment and lifecycle"""
def __init__(self, config: Dict[str, Any]):
self.config = config
self.health_checker = CDCHealthChecker()
self.performance_monitor = CDCPerformanceMonitor()
self.backup_manager = CDCBackupManager()
async def deploy_cdc_system(self, deployment_config: Dict[str, Any]) -> bool:
"""Deploy CDC system with zero downtime"""
try:
# Pre-deployment checks
await self._pre_deployment_checks(deployment_config)
# Create deployment snapshot
snapshot_id = await self.backup_manager.create_snapshot()
# Deploy new version
await self._deploy_new_version(deployment_config)
# Perform health checks
health_status = await self.health_checker.comprehensive_health_check()
if not health_status["healthy"]:
# Rollback on health check failure
await self._rollback_deployment(snapshot_id)
return False
# Performance validation
performance_ok = await self.performance_monitor.validate_performance()
if not performance_ok:
await self._rollback_deployment(snapshot_id)
return False
# Cleanup old version
await self._cleanup_old_version()
logging.info("CDC system deployment completed successfully")
return True
except Exception as e:
logging.error(f"CDC deployment failed: {e}")
return False
async def _pre_deployment_checks(self, config: Dict[str, Any]):
"""Run pre-deployment validation checks"""
# Check source connectivity
for source_config in config.get("sources", []):
if not await self._test_source_connectivity(source_config):
raise ValueError(f"Source connectivity check failed: {source_config['name']}")
# Check sink connectivity
for sink_config in config.get("sinks", []):
if not await self._test_sink_connectivity(sink_config):
raise ValueError(f"Sink connectivity check failed: {sink_config['name']}")
# Validate schema registry
if not await self._validate_schema_registry():
raise ValueError("Schema registry validation failed")
logging.info("Pre-deployment checks passed")
async def _test_source_connectivity(self, source_config: Dict[str, Any]) -> bool:
"""Test connectivity to data source"""
# Implementation would test actual connectivity
return True
async def _test_sink_connectivity(self, sink_config: Dict[str, Any]) -> bool:
"""Test connectivity to data sink"""
# Implementation would test actual connectivity
return True
async def _validate_schema_registry(self) -> bool:
"""Validate schema registry connectivity and health"""
# Implementation would validate schema registry
return True
async def _deploy_new_version(self, config: Dict[str, Any]):
"""Deploy new version of CDC system"""
# Blue-green deployment strategy
logging.info("Starting blue-green deployment")
# Deploy to staging environment first
await self._deploy_to_staging(config)
# Run integration tests
test_results = await self._run_integration_tests()
if not test_results["passed"]:
raise ValueError("Integration tests failed")
# Switch traffic to new version
await self._switch_traffic()
logging.info("Blue-green deployment completed")
async def _deploy_to_staging(self, config: Dict[str, Any]):
"""Deploy to staging environment"""
# Implementation would deploy to staging
await asyncio.sleep(1) # Simulate deployment time
async def _run_integration_tests(self) -> Dict[str, Any]:
"""Run integration tests"""
# Implementation would run actual tests
return {"passed": True, "test_count": 25, "duration_seconds": 30}
async def _switch_traffic(self):
"""Switch traffic to new version"""
# Implementation would switch load balancer or service mesh routing
await asyncio.sleep(0.5)
async def _rollback_deployment(self, snapshot_id: str):
"""Rollback to previous version"""
logging.warning(f"Rolling back deployment to snapshot {snapshot_id}")
await self.backup_manager.restore_snapshot(snapshot_id)
async def _cleanup_old_version(self):
"""Cleanup old version resources"""
# Implementation would cleanup old containers, services, etc.
await asyncio.sleep(0.5)
class CDCHealthChecker:
"""Comprehensive health checking for CDC systems"""
def __init__(self):
self.health_checks = {
"source_connectivity": self._check_source_connectivity,
"sink_connectivity": self._check_sink_connectivity,
"processing_pipeline": self._check_processing_pipeline,
"schema_registry": self._check_schema_registry,
"conflict_resolution": self._check_conflict_resolution,
"memory_usage": self._check_memory_usage,
"lag_metrics": self._check_lag_metrics
}
async def comprehensive_health_check(self) -> Dict[str, Any]:
"""Run comprehensive health check"""
health_status = {
"healthy": True,
"timestamp": datetime.now(timezone.utc).isoformat(),
"checks": {}
}
for check_name, check_func in self.health_checks.items():
try:
check_result = await check_func()
health_status["checks"][check_name] = check_result
if not check_result.get("passed", False):
health_status["healthy"] = False
except Exception as e:
health_status["checks"][check_name] = {
"passed": False,
"error": str(e)
}
health_status["healthy"] = False
return health_status
async def _check_source_connectivity(self) -> Dict[str, Any]:
"""Check connectivity to all data sources"""
# Simulate source connectivity check
return {
"passed": True,
"sources_checked": 3,
"all_connected": True,
"details": {
"postgresql": {"connected": True, "latency_ms": 5},
"mysql": {"connected": True, "latency_ms": 8},
"mongodb": {"connected": True, "latency_ms": 12}
}
}
async def _check_sink_connectivity(self) -> Dict[str, Any]:
"""Check connectivity to all data sinks"""
return {
"passed": True,
"sinks_checked": 2,
"all_connected": True,
"details": {
"kafka": {"connected": True, "latency_ms": 3},
"elasticsearch": {"connected": True, "latency_ms": 15}
}
}
async def _check_processing_pipeline(self) -> Dict[str, Any]:
"""Check processing pipeline health"""
return {
"passed": True,
"pipeline_status": "running",
"active_processors": 4,
"queue_depth": 125,
"processing_rate_eps": 1250
}
async def _check_schema_registry(self) -> Dict[str, Any]:
"""Check schema registry health"""
return {
"passed": True,
"registry_status": "healthy",
"schemas_registered": 45,
"compatibility_checks_passed": True
}
async def _check_conflict_resolution(self) -> Dict[str, Any]:
"""Check conflict resolution system"""
return {
"passed": True,
"conflicts_detected": 12,
"conflicts_resolved": 12,
"resolution_rate": 100.0
}
async def _check_memory_usage(self) -> Dict[str, Any]:
"""Check memory usage"""
return {
"passed": True,
"memory_usage_percent": 68,
"memory_threshold_percent": 80,
"gc_frequency": "normal"
}
async def _check_lag_metrics(self) -> Dict[str, Any]:
"""Check processing lag metrics"""
return {
"passed": True,
"avg_lag_seconds": 2.5,
"max_lag_seconds": 8.2,
"lag_threshold_seconds": 30
}
class CDCPerformanceMonitor:
"""Monitor CDC system performance"""
def __init__(self):
self.performance_metrics = {}
self.alert_thresholds = {
"throughput_eps": {"min": 100, "max": 10000},
"latency_p95_ms": {"max": 1000},
"error_rate_percent": {"max": 1.0},
"memory_usage_percent": {"max": 85},
"cpu_usage_percent": {"max": 80}
}
async def collect_performance_metrics(self) -> Dict[str, Any]:
"""Collect comprehensive performance metrics"""
metrics = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"throughput": await self._collect_throughput_metrics(),
"latency": await self._collect_latency_metrics(),
"errors": await self._collect_error_metrics(),
"resources": await self._collect_resource_metrics(),
"business": await self._collect_business_metrics()
}
return metrics
async def _collect_throughput_metrics(self) -> Dict[str, Any]:
"""Collect throughput metrics"""
return {
"events_per_second": 1847,
"bytes_per_second": 2456789,
"transactions_per_second": 312,
"peak_throughput_eps": 2156
}
async def _collect_latency_metrics(self) -> Dict[str, Any]:
"""Collect latency metrics"""
return {
"end_to_end_p50_ms": 45,
"end_to_end_p95_ms": 185,
"end_to_end_p99_ms": 425,
"processing_p95_ms": 12,
"sink_delivery_p95_ms": 8
}
async def _collect_error_metrics(self) -> Dict[str, Any]:
"""Collect error metrics"""
return {
"error_rate_percent": 0.12,
"retries_per_minute": 5,
"failed_events_per_minute": 2,
"dead_letter_queue_size": 8
}
async def _collect_resource_metrics(self) -> Dict[str, Any]:
"""Collect resource utilization metrics"""
return {
"cpu_usage_percent": 65,
"memory_usage_percent": 72,
"disk_usage_percent": 45,
"network_io_mbps": 125,
"open_connections": 234
}
async def _collect_business_metrics(self) -> Dict[str, Any]:
"""Collect business-relevant metrics"""
return {
"data_freshness_minutes": 1.8,
"schema_evolution_events": 0,
"duplicate_events_detected": 3,
"out_of_order_events": 12
}
async def validate_performance(self) -> bool:
"""Validate performance against thresholds"""
metrics = await self.collect_performance_metrics()
# Check throughput
throughput = metrics["throughput"]["events_per_second"]
if (throughput < self.alert_thresholds["throughput_eps"]["min"] or
throughput > self.alert_thresholds["throughput_eps"]["max"]):
logging.warning(f"Throughput outside acceptable range: {throughput} EPS")
return False
# Check latency
latency_p95 = metrics["latency"]["end_to_end_p95_ms"]
if latency_p95 > self.alert_thresholds["latency_p95_ms"]["max"]:
logging.warning(f"High latency detected: {latency_p95}ms")
return False
# Check error rate
error_rate = metrics["errors"]["error_rate_percent"]
if error_rate > self.alert_thresholds["error_rate_percent"]["max"]:
logging.warning(f"High error rate detected: {error_rate}%")
return False
# Check resource usage
memory_usage = metrics["resources"]["memory_usage_percent"]
if memory_usage > self.alert_thresholds["memory_usage_percent"]["max"]:
logging.warning(f"High memory usage detected: {memory_usage}%")
return False
cpu_usage = metrics["resources"]["cpu_usage_percent"]
if cpu_usage > self.alert_thresholds["cpu_usage_percent"]["max"]:
logging.warning(f"High CPU usage detected: {cpu_usage}%")
return False
return True
async def generate_performance_report(self) -> Dict[str, Any]:
"""Generate comprehensive performance report"""
metrics = await self.collect_performance_metrics()
report = {
"report_id": str(uuid.uuid4()),
"generated_at": datetime.now(timezone.utc).isoformat(),
"metrics": metrics,
"alerts": self._generate_alerts(metrics),
"recommendations": self._generate_recommendations(metrics),
"trends": await self._analyze_trends()
}
return report
def _generate_alerts(self, metrics: Dict[str, Any]) -> List[Dict[str, Any]]:
"""Generate alerts based on metrics"""
alerts = []
# Throughput alerts
throughput = metrics["throughput"]["events_per_second"]
if throughput < self.alert_thresholds["throughput_eps"]["min"]:
alerts.append({
"type": "low_throughput",
"severity": "warning",
"message": f"Throughput below minimum: {throughput} EPS",
"threshold": self.alert_thresholds["throughput_eps"]["min"]
})
# Latency alerts
latency_p95 = metrics["latency"]["end_to_end_p95_ms"]
if latency_p95 > self.alert_thresholds["latency_p95_ms"]["max"]:
alerts.append({
"type": "high_latency",
"severity": "critical",
"message": f"High latency detected: {latency_p95}ms",
"threshold": self.alert_thresholds["latency_p95_ms"]["max"]
})
return alerts
def _generate_recommendations(self, metrics: Dict[str, Any]) -> List[str]:
"""Generate performance recommendations"""
recommendations = []
# Resource-based recommendations
memory_usage = metrics["resources"]["memory_usage_percent"]
if memory_usage > 75:
recommendations.append("Consider increasing memory allocation or optimizing memory usage")
cpu_usage = metrics["resources"]["cpu_usage_percent"]
if cpu_usage > 75:
recommendations.append("Consider scaling horizontally or optimizing CPU-intensive operations")
# Throughput recommendations
throughput = metrics["throughput"]["events_per_second"]
if throughput < 500:
recommendations.append("Consider optimizing processing pipeline or increasing parallelism")
return recommendations
async def _analyze_trends(self) -> Dict[str, Any]:
"""Analyze performance trends"""
# Simulate trend analysis
return {
"throughput_trend": "stable",
"latency_trend": "improving",
"error_rate_trend": "stable",
"resource_usage_trend": "increasing"
}
class CDCBackupManager:
"""Manage CDC system backups and recovery"""
def __init__(self):
self.snapshots: Dict[str, Dict[str, Any]] = {}
async def create_snapshot(self) -> str:
"""Create system snapshot"""
snapshot_id = str(uuid.uuid4())
timestamp = datetime.now(timezone.utc)
snapshot = {
"id": snapshot_id,
"timestamp": timestamp.isoformat(),
"configuration": await self._backup_configuration(),
"schema_registry": await self._backup_schema_registry(),
"processing_state": await self._backup_processing_state(),
"metadata": {
"version": "1.0",
"created_by": "cdc_backup_manager"
}
}
self.snapshots[snapshot_id] = snapshot
logging.info(f"Created snapshot {snapshot_id}")
return snapshot_id
async def restore_snapshot(self, snapshot_id: str) -> bool:
"""Restore from snapshot"""
if snapshot_id not in self.snapshots:
logging.error(f"Snapshot {snapshot_id} not found")
return False
snapshot = self.snapshots[snapshot_id]
try:
# Restore configuration
await self._restore_configuration(snapshot["configuration"])
# Restore schema registry
await self._restore_schema_registry(snapshot["schema_registry"])
# Restore processing state
await self._restore_processing_state(snapshot["processing_state"])
logging.info(f"Restored snapshot {snapshot_id}")
return True
except Exception as e:
logging.error(f"Failed to restore snapshot {snapshot_id}: {e}")
return False
async def _backup_configuration(self) -> Dict[str, Any]:
"""Backup system configuration"""
return {
"sources": ["postgresql_config", "mysql_config"],
"sinks": ["kafka_config", "elasticsearch_config"],
"processing": "processing_config"
}
async def _backup_schema_registry(self) -> Dict[str, Any]:
"""Backup schema registry state"""
return {
"schemas": "schema_backup_data",
"versions": "version_backup_data"
}
async def _backup_processing_state(self) -> Dict[str, Any]:
"""Backup processing pipeline state"""
return {
"positions": "position_backup_data",
"offsets": "offset_backup_data",
"checkpoints": "checkpoint_backup_data"
}
async def _restore_configuration(self, config_data: Dict[str, Any]):
"""Restore system configuration"""
# Implementation would restore actual configuration
await asyncio.sleep(0.1)
async def _restore_schema_registry(self, schema_data: Dict[str, Any]):
"""Restore schema registry state"""
# Implementation would restore schema registry
await asyncio.sleep(0.1)
async def _restore_processing_state(self, state_data: Dict[str, Any]):
"""Restore processing pipeline state"""
# Implementation would restore processing state
await asyncio.sleep(0.1)
Conclusion
Implementing high-performance Change Data Capture systems requires sophisticated approaches to handling streaming data, schema evolution, conflict resolution, and production deployment. The advanced patterns and implementations shown in this guide provide a comprehensive foundation for building enterprise-grade CDC systems that can handle millions of events per second with minimal latency.
Key takeaways for successful CDC implementation include:
- Source-Agnostic Design: Build abstractions that can handle multiple database types and CDC mechanisms
- Schema Evolution: Implement robust schema management with backward and forward compatibility
- Conflict Resolution: Design sophisticated conflict resolution strategies for distributed data changes
- Performance Optimization: Optimize for throughput and latency with proper buffering, batching, and parallelization
- Production Readiness: Implement comprehensive monitoring, health checking, and deployment automation
By following these advanced patterns and architectural principles, organizations can build CDC systems that provide reliable, real-time data integration capabilities while maintaining data consistency and operational excellence at scale.