Advanced Topics

Migration Strategies

Master zero-downtime migrations - from MySQL to Cassandra, dual writes, data sync, and cutover!

🔄 What is Database Migration?

The Migration Challenge 🚀

Your startup grew from 10K users to 10M users. Your MySQL database is struggling:

  • ⚠️ Write queries taking 5+ seconds
  • 📈 Database at 90% CPU constantly
  • 💰 Vertical scaling costing $50K/month
  • 🔥 Sharding MySQL manually = nightmare
  • 😰 Downtime for schema changes

Solution: Migrate to Cassandra for horizontal scalability. But you have 10M active users and can't afford downtime!

Migration Goals

Database migration = Moving from one database system to another while maintaining service availability and data consistency.

Critical Requirements:
  • ✅ Zero downtime: Users never experience outages
  • ✅ Data consistency: No data loss during migration
  • ✅ Rollback capability: Can revert if issues arise
  • ✅ Validation: Verify data accuracy throughout
  • ✅ Performance: Maintain or improve response times
  • ✅ Incremental: Migrate gradually, not all at once

Common Migration Scenarios

🐘

MySQL → Cassandra

  • Scaling limitations
  • High write throughput
  • Geographic distribution
  • Multi-region active-active
  • Time-series data
🍃

MongoDB → Cassandra

  • True multi-DC
  • Better scalability
  • Lower operational cost
  • Tunable consistency
  • No single point of failure
🔄

Cassandra → Cassandra

  • Data model refactor
  • Version upgrade
  • Cluster consolidation
  • Cloud migration
  • Schema changes
☁️

On-Prem → Cloud

  • Reduce ops overhead
  • Auto-scaling
  • Managed services
  • Multi-region easily
  • Cost optimization

📋 Migration Strategies Overview

Strategy Comparison

Strategy Downtime Complexity Risk Duration
Big Bang Hours ❌ Low ✅ High ❌ Days ✅
Dual Write None ✅ Medium Medium Weeks
Strangler Fig None ✅ High ❌ Low ✅ Months
Blue-Green Minimal ✅ Medium Low ✅ Weeks

Strategy 1: Big Bang (Not Recommended)

Big Bang Migration

Approach: Stop application, migrate all data, switch to Cassandra, restart.

❌ Problems:

  • Hours of downtime (unacceptable for production)
  • No rollback if issues found
  • All-or-nothing risk
  • No validation before cutover

✅ OK for: Dev/test environments, small datasets, scheduled maintenance windows

Strategy 2: Dual Write (Recommended)

Dual Write Pattern

Approach: Write to both databases simultaneously, gradually shift reads to Cassandra.

✅ Benefits:

  • Zero downtime
  • Incremental migration
  • Easy rollback
  • Validation before cutover
  • Risk mitigation

⚠️ Challenges: Application changes, data consistency, temporary complexity

Strategy 3: Strangler Fig

Strangler Fig Pattern

Approach: Gradually replace old system by routing features to new system one by one.

✅ Best for: Large monoliths, microservices migration, long-term refactoring

⚠️ Complexity: Requires routing layer, feature flags, extensive testing

Strategy 4: Blue-Green Deployment

Blue-Green Pattern

Approach: Run parallel environments (Blue=old, Green=new), switch traffic instantly.

✅ Best for: Cloud environments, infrastructure as code, minimal downtime tolerance

⚠️ Cost: Doubles infrastructure temporarily

✍️ Dual Write Pattern (Step-by-Step)

1

Phase 1: Prepare Cassandra

-- 1. Design Cassandra data model -- Map MySQL schema to Cassandra query patterns -- MySQL: users table CREATE TABLE users ( id INT PRIMARY KEY, email VARCHAR(255) UNIQUE, username VARCHAR(100), created_at TIMESTAMP ); -- Cassandra: users_by_id (primary lookup) CREATE TABLE users_by_id ( user_id UUID PRIMARY KEY, email text, username text, created_at timestamp ); -- Cassandra: users_by_email (lookup by email) CREATE TABLE users_by_email ( email text PRIMARY KEY, user_id UUID, username text, created_at timestamp ); -- 2. Set up Cassandra cluster -- 3. Create tables and indexes -- 4. Test connectivity from application
2

Phase 2: Backfill Historical Data

# Python script: Migrate existing MySQL data to Cassandra import mysql.connector from cassandra.cluster import Cluster import uuid # Connect to both databases mysql_conn = mysql.connector.connect( host='localhost', user='root', database='myapp' ) cassandra_cluster = Cluster(['127.0.0.1']) cassandra_session = cassandra_cluster.connect('myapp') # Prepared statements for Cassandra insert_by_id = cassandra_session.prepare(""" INSERT INTO users_by_id (user_id, email, username, created_at) VALUES (?, ?, ?, ?) """) insert_by_email = cassandra_session.prepare(""" INSERT INTO users_by_email (email, user_id, username, created_at) VALUES (?, ?, ?, ?) """) # Batch migrate users (1000 at a time) mysql_cursor = mysql_conn.cursor() offset = 0 batch_size = 1000 while True: mysql_cursor.execute(""" SELECT id, email, username, created_at FROM users LIMIT %s OFFSET %s """, (batch_size, offset)) rows = mysql_cursor.fetchall() if not rows: break for row in rows: user_id = uuid.uuid4() # Generate new UUID # Write to both Cassandra tables cassandra_session.execute(insert_by_id, (user_id, row[1], row[2], row[3])) cassandra_session.execute(insert_by_email, (row[1], user_id, row[2], row[3])) offset += batch_size print(f"Migrated {offset} users...") print("✅ Historical data migration complete!") # ⚠️ Run during off-peak hours! # ⚠️ Monitor MySQL and Cassandra load
3

Phase 3: Implement Dual Write

# Application code: Write to both databases class UserRepository: def __init__(self, mysql_conn, cassandra_session): self.mysql = mysql_conn self.cassandra = cassandra_session self.dual_write_enabled = True # Feature flag def create_user(self, email, username): user_id = uuid.uuid4() created_at = datetime.now() try: # 1. Write to MySQL (source of truth during migration) mysql_cursor = self.mysql.cursor() mysql_cursor.execute(""" INSERT INTO users (email, username, created_at) VALUES (%s, %s, %s) """, (email, username, created_at)) mysql_id = mysql_cursor.lastrowid self.mysql.commit() # 2. Write to Cassandra (if dual write enabled) if self.dual_write_enabled: try: self.cassandra.execute(""" INSERT INTO users_by_id (user_id, email, username, created_at) VALUES (%s, %s, %s, %s) """, (user_id, email, username, created_at)) self.cassandra.execute(""" INSERT INTO users_by_email (email, user_id, username, created_at) VALUES (%s, %s, %s, %s) """, (email, user_id, username, created_at)) except Exception as e: # Log error but DON'T fail request # MySQL write succeeded, so user is created logger.error(f"Cassandra write failed: {e}") # Alert ops team for investigation return {'id': mysql_id, 'email': email} except Exception as e: self.mysql.rollback() raise e # ✅ Key points: # - MySQL = source of truth (fails request if write fails) # - Cassandra = best effort (log errors, don't fail) # - Feature flag allows quick disable if issues
4

Phase 4: Shadow Reads (Validation)

# Validate data consistency by reading from both def get_user_by_email(self, email): # 1. Read from MySQL (still serving production traffic) mysql_cursor = self.mysql.cursor() mysql_cursor.execute(""" SELECT id, email, username, created_at FROM users WHERE email = %s """, (email,)) mysql_result = mysql_cursor.fetchone() # 2. Shadow read from Cassandra (for validation) if self.shadow_read_enabled: try: cassandra_result = self.cassandra.execute(""" SELECT user_id, email, username, created_at FROM users_by_email WHERE email = %s """, (email,)).one() # Compare results (asynchronously) self.compare_results(mysql_result, cassandra_result) except Exception as e: logger.error(f"Shadow read failed: {e}") # Return MySQL result (still source of truth) return mysql_result def compare_results(self, mysql_data, cassandra_data): # Log any discrepancies if mysql_data[1] != cassandra_data.email: logger.error(f"Data mismatch for {mysql_data[1]}") # Alert ops team # ✅ Shadow reads validate data without affecting users # ✅ Monitor error rates and data discrepancies # ✅ Fix any issues before cutover
5

Phase 5: Gradual Read Cutover

# Use feature flags to gradually shift reads to Cassandra class UserRepository: def __init__(self, mysql, cassandra, config): self.mysql = mysql self.cassandra = cassandra self.cassandra_read_percentage = config.get('cassandra_read_pct', 0) def get_user_by_email(self, email): # Randomly decide which database to read from if random.random() < (self.cassandra_read_percentage / 100): # Read from Cassandra try: result = self.cassandra.execute(""" SELECT * FROM users_by_email WHERE email = %s """, (email,)).one() return result except Exception as e: # Fallback to MySQL if Cassandra fails logger.error(f"Cassandra read failed, falling back: {e}") return self.get_from_mysql(email) else: # Read from MySQL return self.get_from_mysql(email) # Rollout schedule: # Week 1: 5% reads from Cassandra # Week 2: 25% reads from Cassandra # Week 3: 50% reads from Cassandra # Week 4: 100% reads from Cassandra # ✅ Monitor latency, error rates, throughput at each stage # ✅ Roll back percentage if issues detected
6

Phase 6: Final Cutover

# Once 100% reads from Cassandra with no issues: # 1. Make Cassandra the source of truth for writes def create_user(self, email, username): # Now write to Cassandra FIRST user_id = uuid.uuid4() try: # Primary write (Cassandra) self.cassandra.execute("""INSERT INTO users_by_id ...""") self.cassandra.execute("""INSERT INTO users_by_email ...""") # Secondary write (MySQL, for safety) if self.mysql_write_enabled: try: self.mysql.execute("""INSERT INTO users ...""") except: logger.error("MySQL write failed (non-critical)") return user_id except Exception as e: raise e # 2. Run for 1-2 weeks monitoring everything # 3. Disable MySQL writes (keep as backup) # 4. After 30 days of stability, decommission MySQL # ✅ Migration complete!

🔁 Data Synchronization Tools

Option 1: Custom CDC Pipeline

# Use MySQL binlog to stream changes to Cassandra from pymysqlreplication import BinLogStreamReader from pymysqlreplication.row_event import WriteRowsEvent, UpdateRowsEvent # Connect to MySQL binlog stream = BinLogStreamReader( connection_settings = { 'host': 'localhost', 'port': 3306, 'user': 'repl_user', 'passwd': 'password' }, server_id=100, only_events=[WriteRowsEvent, UpdateRowsEvent] ) for binlogevent in stream: for row in binlogevent.rows: vals = row['values'] # Sync to Cassandra cassandra_session.execute(""" INSERT INTO users_by_id (user_id, email, username) VALUES (%s, %s, %s) """, (vals['id'], vals['email'], vals['username'])) # ✅ Real-time sync from MySQL to Cassandra # ✅ Keeps data fresh during migration

Option 2: Debezium + Kafka

# Production-grade CDC pipeline MySQL → Debezium (CDC) → Kafka → Kafka Connect → Cassandra # Debezium connector config (JSON) { "name": "mysql-source", "config": { "connector.class": "io.debezium.connector.mysql.MySqlConnector", "database.hostname": "localhost", "database.port": "3306", "database.user": "debezium", "database.password": "password", "database.server.id": "184054", "database.include.list": "myapp", "table.include.list": "myapp.users" } } # ✅ Proven at scale (Netflix, Uber) # ✅ Handles schema changes # ✅ Exactly-once semantics

Option 3: Spark Streaming

# Batch sync every 5 minutes from pyspark.sql import SparkSession spark = SparkSession.builder.appName("MysqlToCassandra").getOrCreate() # Read from MySQL df = spark.read \ .format("jdbc") \ .option("url", "jdbc:mysql://localhost:3306/myapp") \ .option("dbtable", "users") \ .option("user", "root") \ .load() # Write to Cassandra df.write \ .format("org.apache.spark.sql.cassandra") \ .options(table="users_by_id", keyspace="myapp") \ .mode("append") \ .save() # Schedule every 5 minutes with Airflow/cron

🎯 Cutover Checklist

Pre-Cutover Validation

  • ✅ Historical data migrated and validated
  • ✅ Dual writes working for 2+ weeks
  • ✅ Shadow reads show < 0.1% discrepancy
  • ✅ Cassandra performance meets SLAs
  • ✅ Backup and restore tested
  • ✅ Monitoring and alerts configured
  • ✅ Rollback plan documented and tested
  • ✅ Team trained on Cassandra operations
  • ✅ Load testing passed at 2x traffic

Cutover Day Timeline

T-1 hour: Team briefing, final checks
T-30 min: Increase Cassandra read % to 100%
T-0: Monitor all metrics closely
T+15 min: Check error rates, latency
T+30 min: Verify data consistency
T+1 hour: Switch writes to Cassandra primary
T+2 hours: Disable MySQL writes (keep as backup)
T+24 hours: Final validation, success party! 🎉

Rollback Plan

If issues detected during cutover:

  1. Immediately reduce Cassandra read % to 0%
  2. Route all traffic back to MySQL
  3. Investigate root cause
  4. Fix issues in Cassandra
  5. Re-validate before retrying cutover

Criteria for rollback:

  • Error rate > 0.5%
  • Latency > 2x baseline
  • Data inconsistency detected
  • Cassandra cluster instability

✅ Migration Best Practices

✅ DO These

  • Plan migration in phases
  • Use feature flags for control
  • Monitor everything continuously
  • Validate data at every step
  • Test rollback procedures
  • Migrate during off-peak hours
  • Keep MySQL as backup for 30+ days
  • Document all decisions
  • Communicate with stakeholders
  • Celebrate milestones! 🎉

❌ DON'T Do These

  • Rush the migration
  • Skip validation steps
  • Migrate all at once (big bang)
  • Ignore performance testing
  • Forget about rollback plan
  • Delete old data immediately
  • Underestimate complexity
  • Skip shadow reads
  • Migrate during peak hours
  • Assume everything will work

🎯 Migration Summary

You now know how to migrate to Cassandra safely!

📚 Key Takeaways:

  • 🔄 Dual write pattern = zero downtime
  • 📊 Backfill historical data first
  • ✍️ Write to both databases during migration
  • 👀 Shadow reads validate consistency
  • 📈 Gradual read cutover (5% → 100%)
  • 🎯 Feature flags enable quick rollback
  • 📡 CDC tools keep data synchronized
  • ✅ Validate, validate, validate!

Plan carefully, execute gradually, validate continuously! 🔄✅

Advertisement

Responsive Ad