L
MySQL • Literally Web ID

Zero-Downtime Database Migration pada MySQL 8.4

⏱️ 22 menit baca • 📅 12 Oct 2026 • 👁️ 1 views
Zero-Downtime Database Migration pada MySQL 8.4

1. Konteks Industri & Analisis Akar Masalah

Dalam lanskap arsitektur web enterprise modern, ketersediaan tinggi (high availability) bukan lagi sebuah fitur tambahan, melainkan sebuah kewajiban fundamental. Klien, regulator, dan pengguna akhir mengharapkan layanan digital dapat diakses 24/7 tanpa ada jeda waktu pemeliharaan (maintenance window). Namun, siklus hidup aplikasi menuntut perubahan skema basis data (schema migration) yang konstan—mulai dari penambahan kolom baru, modifikasi tipe data, penghapusan kolom usang, hingga peningkatan versi sistem manajemen basis data secara keseluruhan, khususnya dari versi legacy menuju MySQL 8.4 LTS (Long Term Support).

Secara historis, perubahan skema pada tabel dengan volume data masif (puluhan hingga ratusan juta baris) memaksa administrator sistem untuk melakukan table locking eksklusif. Perintah seperti ALTER TABLE tradisional akan mengunci tabel secara penuh untuk operasi baca dan tulis, menghentikan seluruh proses backend yang berjalan di atas Node.js, Go, atau PHP-FPM. Dampaknya fatal: lonjakan koneksi tertunda (connection thread pool exhaustion), kegagalan proxy Nginx/Caddy karena gateway timeout, kegagalan pemrosesan antrean queue worker (Supervisor), inkonsistensi sinkronisasi Redis, hingga potensi korupsi data jika transaksi terputus di tengah jalan.

[Sebelum: Traditional ALTER TABLE]
[Web Apps: Go/PHP] ---> (Locks Table) ---> [MySQL: Blocking Read/Write] ---> Downtime / 504 Gateway Timeout

[Sesudah: Zero-Downtime Migration]
[Web Apps: Go/PHP] ---> (Dual-Writes / Online DDL) ---> [MySQL 8.4: Non-Blocking] ---> Seamless Transition

Analisis akar masalah dari kegagalan migrasi biasanya berakar pada tiga faktor utama:

  1. Ketidaktahuan terhadap mekanisme Lock In-Place vs Copy DDL oleh InnoDB.
  2. Ketiadaan strategi sinkronisasi data paralel saat melakukan transisi dari tabel lama ke struktur tabel baru (Shadow Tables).
  3. Kurangnya validasi integritas data pasca-migrasi, yang menyebabkan inkonsistensi antara sistem penyimpanan relasional dengan cache layer (Redis) atau penyimpanan objek eksternal (MinIO/S3).

Melalui panduan ini, kita akan membedah secara mendalam bagaimana merancang, mengeksekusi, dan memverifikasi migrasi basis data tanpa henti (zero-downtime) pada ekosistem MySQL 8.4 dengan menggabungkan strategi Dual-Writes, Shadow Tables, dan Online DDL tingkat lanjut.


2. Konsep Inti & Deep Dive Arsitektur

Zero-downtime database migration pada MySQL 8.4 mengandalkan kombinasi fitur internal mesin penyimpanan InnoDB dan pola arsitektur aplikasi (Application-Level Architecture Pattern). Inti dari pendekatan ini adalah memisahkan proses fisik modifikasi struktur data dari siklus hidup transaksi aplikasi yang sedang aktif.

Alur Arsitektur Dual-Writes & Shadow Tables

+-----------------------------------------------------------------------------------------+
|                                  APPLICATION TIER                                       | 
|  (Node.js / Go / PHP-FPM Service with Dual-Write Logic Enabled)                         | 
+-----------------------------------------------------------------------------------------+
       |                                                             |                    | 
       | (1. Write Primary)                                          | (2. Write Shadow)  | 
       v                                                             v                    v 
+----------------------------+                               +----------------------------+
|     PRIMARY TABLE          |                               |       SHADOW TABLE         | 
|     (`users_old`)          |                               |     (`users_new`)          | 
+----------------------------+                               +----------------------------+
       |                                                             |                    | 
       +-------------------------> [MySQL 8.4] <---------------------+                    |
                                        |                                                 |
                                        +---> [Backfill Worker: Batch Migration] ---------+
  1. Shadow Table Creation: Administrator membuat tabel baru dengan skema yang diinginkan (users_new) tanpa mengganggu tabel lama (users_old).
  2. Dual-Writes Implementation: Lapisan aplikasi (backend Go/Node.js/PHP) dimodifikasi untuk menulis data baru ke kedua tabel secara simultan. Operasi baca (read) masih sepenuhnya diarahkan ke tabel lama.
  3. Data Backfill (Historical Sync): Proses latar belakang (background worker) secara asinkron menyalin data historis dari tabel lama ke tabel baru menggunakan potongan batch berbasis kunci utama (primary key chunking) untuk menghindari penguncian baris yang berlebihan.
  4. Atomic Cutover: Setelah data sinkron 100%, dilakukan pengalihan penamaan tabel secara atomik menggunakan perintah RENAME TABLE di dalam satu transaksi penutup.

3. Perbandingan Komparatif / Analisis Pendekatan

Untuk memilih strategi migrasi yang tepat, kita harus memahami trade-off sumber daya antara berbagai metode yang didukung oleh MySQL 8.4.

Parameter Perbandingan Traditional ALTER TABLE Online DDL (INPLACE / ALGORITHM=INSTANT) Shadow Tables & Dual-Writes
Downtime Layanan Total (100% Downtime) Nol hingga Sangat Minimal Nol (Zero Downtime)
Konsumsi CPU & RAM Rendah hingga Sedang Tinggi (Bergantung pada operasi) Sedang (Didistribusikan ke aplikasi)
I/O Disk Latency Sangat Tinggi (Disk I/O Spike) Sedang hingga Tinggi Rendah (Dilakukan bertahap / batched)
Kompleksitas Kode Rendah (Perintah SQL tunggal) Rendah (Perintah SQL tunggal) Tinggi (Modifikasi logika backend)
Risiko Rollback Sulit & Berisiko Tinggi Sulit jika melibatkan perubahan tipe data Sangat Mudah (Cukup arahkan kembali baca ke tabel lama)
Cocok Untuk Sistem non-produksi / data kecil Perubahan kolom non-destruktif di MySQL 8.4 Perubahan skema masif, restrukturisasi relasi, & upgrade besar

[!IMPORTANT] MySQL 8.4 membawa peningkatan signifikan pada ALGORITHM=INSTANT untuk operasi seperti menambahkan kolom di posisi mana pun (bukan hanya di akhir tabel) dan menambahkan indeks sekunder tertentu. Namun, untuk restrukturisasi tabel relasional yang kompleks, pendekatan Shadow Tables tetap menjadi standar emas di lingkungan high-traffic.


4. Prasyarat Sistem & Persiapan Lingkungan

Sebelum memulai proses migrasi, pastikan infrastruktur memenuhi standar spesifikasi dan konfigurasi berikut:

1. Spesifikasi Minimum Lingkungan

  • Database Engine: MySQL 8.4.x LTS (InnoDB buffer pool size minimal 70% dari total RAM tersedia).
  • Web Server / Reverse Proxy: Nginx 1.24+ / Caddy 2.7+ dengan konfigurasi keepalive dan upstream timeout yang dioptimalkan.
  • Backend Service: Go 1.22+ atau Node.js 20+ dengan implementasi connection pooling (max_open_conns dan max_idle_conns terdefinisi ketat).
  • Monitoring: Prometheus & Grafana untuk memantau metrik Innodb_rows_read, Innodb_rows_inserted, dan Threads_running secara real-time.

2. Konfigurasi MySQL 8.4 (/etc/mysql/mysql.conf.d/mysqld.cnf)

[mysqld]
server-id                   = 1
log_bin                     = mysql-bin
binlog_format               = ROW
binlog_row_image            = FULL
innodb_buffer_pool_size     = 12G
innodb_log_file_size        = 2G
innodb_flush_log_at_trx_commit = 1
innodb_file_per_table       = 1
max_connections             = 1000

3. Verifikasi Hak Akses Basis Data

Pastikan akun pengguna yang digunakan memiliki hak istimewa yang memadai untuk melakukan operasi DDL dan manajemen tabel:

CREATE USER 'migrator'@'%' IDENTIFIED BY 'StrongSecurePassword_8.4!';
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, INDEX, REFERENCES ON production_db.* TO 'migrator'@'%';
FLUSH PRIVILEGES:

5. Panduan Implementasi Langkah demi Langkah

Berikut adalah tahapan implementasi nyata menggunakan strategi Shadow Tables dan Dual-Writes pada aplikasi backend berbasis Node.js/TypeScript serta database MySQL 8.4.

Langkah 1: Membuat Shadow Table

Misalkan kita memiliki tabel users yang ingin dimodifikasi strukturnya (misalnya, memecah kolom full_name menjadi first_name dan last_name).

USE production_db;

-- Buat shadow table dengan struktur baru
CREATE TABLE users_new (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    uuid CHAR(36) NOT NULL UNIQUE,
    email VARCHAR(255) NOT NULL,
    first_name VARCHAR(100) NOT NULL,
    last_name VARCHAR(100) NOT NULL,
    status ENUM('active', 'suspended', 'deleted') DEFAULT 'active',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_email (email),
    INDEX idx_status_created (status, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

Langkah 2: Implementasi Dual-Writes pada Backend (Node.js / TypeScript)

Buat modul repositori yang menangani penulisan data secara paralel ke kedua tabel (Dual-Writes):

import mysql2, { Pool, ResultSetHeader } from 'mysql2/promise';

export class UserRepository {
    private pool: Pool;

    constructor(pool: Pool) {
        this.pool = pool;
    }

    public async createUser(userData: {
        uuid: string;
        email: string;
        firstName: string;
        lastName: string;
    }): Promise<void> {
        const connection = await this.pool.getConnection();
        try {
            await connection.beginTransaction();

            // 1. Tulis ke tabel utama (Primary Table)
            const primaryQuery = `
                INSERT INTO users (uuid, email, full_name, created_at)
                VALUES (?, ?, ?, NOW())
            `;
            const fullName = `${userData.firstName} ${userData.lastName}`;
            await connection.execute<ResultSetHeader>(primaryQuery, [
                userData.uuid,
                userData.email,
                fullName,
            ]);

            // 2. Tulis ke shadow table (Dual-Write)
            const shadowQuery = `
                INSERT INTO users_new (uuid, email, first_name, last_name, created_at)
                VALUES (?, ?, ?, ?, NOW())
            `;
            await connection.execute<ResultSetHeader>(shadowQuery, [
                userData.uuid,
                userData.email,
                userData.firstName,
                userData.lastName,
            ]);

            await connection.commit();
        } catch (error) {
            await connection.rollback();
            console.error('[CRITICAL] Dual-write operation failed:', error);
            throw error;
        } finally {
            connection.release();
        }
    }
}

Langkah 3: Skrip Backfill Data Historis (Python / Go / Node)

Jalankan skrip backfill secara asinkron untuk menyalin data lama yang belum masuk ke users_new berdasarkan rentang ID (id).

async function backfillHistoricalData(pool: Pool, batchSize: number = 1000) {
    let lastId = 0;
    let hasMore = true;

    console.log('[INFO] Starting historical data backfill...');

    while (hasMore) {
        const [rows]: any = await pool.execute(
            `SELECT id, uuid, email, full_name, created_at 
             FROM users 
             WHERE id > ? 
             ORDER BY id ASC 
             LIMIT ?`,
            [lastId, batchSize]
        );

        if (rows.length === 0) {
            hasMore = false;
            break;
        }

        const connection = await pool.getConnection();
        try {
            await connection.beginTransaction();

            for (const row of rows) {
                const nameParts = row.full_name.split(' ');
                const firstName = nameParts[0] || '';
                const lastName = nameParts.slice(1).join(' ') || '';

                await connection.execute(
                    `INSERT IGNORE INTO users_new (id, uuid, email, first_name, last_name, created_at)
                     VALUES (?, ?, ?, ?, ?, ?)`,
                    [row.id, row.uuid, row.email, firstName, lastName, row.created_at]
                );
                lastId = row.id;
            }

            await connection.commit();
            console.log(`[INFO] Backfilled up to ID: ${lastId}`);
        } catch (err) {
            await connection.rollback();
            console.error(`[ERROR] Failed at batch starting ID ${lastId}:`, err);
            throw err;
        } finally {
            connection.release();
        }

        // Beri jeda singkat untuk mencegah saturasi CPU/I/O
        await new Promise((resolve) => setTimeout(resolve, 100));
    }

    console.log('[INFO] Historical data backfill completed successfully.');
}

Langkah 4: Atomic Cutover (Pengalihan Tabel)

Setelah backfill selesai dan log sinkronisasi terverifikasi konsisten, lakukan atomic swap di dalam satu instruksi SQL:

SET SESSION lock_wait_timeout = 5;
RENAME TABLE users TO users_legacy, users_new TO users;

6. Bedah Kode & Penjelasan Detail Baris Kunci

Mari kita bedah beberapa komponen kode paling krusial di atas untuk memahami implikasi arsitekturalnya:

  1. connection.beginTransaction() & connection.rollback():

    • Pada fungsi createUser, transaksi atomik memastikan bahwa jika penulisan ke users_new gagal (misalnya karena pelanggaran unique constraint pada kolom uuid), penulisan ke tabel utama users juga dibatalkan. Ini menjaga konsistensi transaksi tingkat aplikasi (application-level consistency).
  2. INSERT IGNORE INTO users_new ... pada Skrip Backfill:

    • Penggunaan INSERT IGNORE mencegah terjadinya kesalahan fatal Duplicate Entry apabila proses backfill mengalami gangguan (restart) dan mengulangi pembacaan blok ID yang sama. Ini membuat skrip backfill bersifat idempotent.
  3. SET SESSION lock_wait_timeout = 5:

    • Sebelum menjalankan RENAME TABLE, parameter ini memastikan bahwa jika ada transaksi lama yang masih menahan kunci tabel users, perintah rename tidak akan menggantung (hang) tanpa batas waktu yang dapat memicu pemadaman layanan, melainkan akan dibatalkan dengan aman setelah 5 detik.

7. Skenario Produksi & Studi Kasus Dunia Nyata

[!TIP] Studi Kasus: Platform e-commerce skala enterprise dengan 15.000 transaksi per detik (TPS) mengalami kendala pada tabel orders yang membengkak hingga 450 juta baris. Penambahan kolom indeks baru secara langsung menyebabkan table lock selama 42 menit, mengakibatkan kerugian finansial masif.

Solusi Penerapan di Produksi:

  1. Pemisahan Beban Kerja: Proses backfill historis dibatasi hanya berjalan pada rentang pukul 02.00 - 04.00 pagi menggunakan penjadwal berbasis sistem operasi (Cron) yang terintegrasi dengan pengawas proses (Supervisor).
  2. Optimasi Redis Caching: Selama fase Dual-Writes, lapisan pembacaan (reads) dialihkan sementara ke Redis Cache Layer untuk mengurangi beban pembacaan langsung ke database relasional utama.
  3. Circuit Breaker Pattern: Pada kode backend, jika kegagalan dual-write ke tabel bayangan mendeteksi tingkat error di atas 1%, sistem akan mengaktifkan circuit breaker yang secara otomatis mencatat log anomali ke sistem pemantauan (Prometheus/Grafana) tanpa menghentikan alur transaksi pengguna utama di tabel lama.

8. Common Pitfalls, Anti-Patterns & Solusi

Berikut adalah kesalahan fatal yang sering dilakukan oleh pengembang maupun administrator basis data saat melakukan migrasi:

  • Anti-Pattern 1: Mengabaikan Kolom Auto-Increment Offset pada Shadow Table

    • Masalah: Saat RENAME TABLE dilakukan, jika nilai maksimum AUTO_INCREMENT pada users_new lebih kecil dari users (karena ada data baru yang masuk lewat dual-write), operasi penulisan pasca-cutover akan gagal karena pelanggaran Primary Key Duplication.
    • Solusi: Selalu sinkronkan nilai auto-increment sebelum cutover:
      SELECT MAX(id) INTO @max_id FROM users;
      SET @alter_sql = CONCAT('ALTER TABLE users_new AUTO_INCREMENT = ', @max_id + 1);
      PREPARE stmt FROM @alter_sql;
      EXECUTE stmt;
      DEALLOCATE PREPARE stmt;
      
  • Anti-Pattern 2: Menjalankan Backfill Tanpa Pembatasan Kecepatan (Rate Limiting)

    • Masalah: Skrip backfill menyedot seluruh sumber daya CPU dan disk I/O, menyebabkan lonjakan latensi pada kueri API pengguna (latency spike).
    • Solusi: Terapkan jeda waktu (sleep/delay) antar-iterasi batch dan gunakan replika baca (read replica) sebagai sumber data untuk proses backfill jika memungkinkan.

9. Benchmark, Metrik Kinerja & Tuning Performa

Untuk memastikan migrasi berjalan tanpa degradasi performa, lakukan pengujian beban (load testing) menggunakan alat seperti k6 atau wrk sebelum dan sesudah cutover.

Skrip Pengujian Load Testing (k6):

import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
    stages: [
        { duration: '2m', target: 500 }, // Ramp-up hingga 500 virtual users
        { duration: '5m', target: 500 }, // Sustained load
        { duration: '1m', target: 0 },   // Ramp-down
    ],
    thresholds: {
        http_req_duration: ['p(99)<350'], // 99% request harus di bawah 350ms
    },
};

export default function () {
    const payload = JSON.stringify({
        uuid: '123e4567-e89b-12d3-a456-426614174000',
        email: `user_${Math.random()}@example.com`,
        firstName: 'Enterprise',
        lastName: 'Architect',
    });

    const params = {
        headers: { 'Content-Type': 'application/json' },
    };

    const res = http.post('https://api.domain.com/v1/users', payload, params);
    check(res, {
        'status is 201': (r) => r.status === 201,
    });
    sleep(1);
}

Metrik Kinerja Utama yang Harus Dipantau (Prometheus / MySQL Metrics):

  • Innodb_buffer_pool_read_requests vs Innodb_buffer_pool_reads: Pastikan rasio hit buffer pool berada di atas 99%.
  • Threads_running: Pastikan jumlah thread aktif tidak mengalami lonjakan ekstrem yang menandakan adanya lock contention.
  • Handler_read_rnd_next: Pantau untuk mendeteksi potensi kueri tanpa indeks (full table scan) selama proses migrasi.

10. Checklist Penerapan Produksi & Kesimpulan

Sebelum merilis migrasi zero-downtime ke lingkungan produksi penuh, pastikan seluruh item dalam daftar periksa berikut telah diverifikasi:

  • Cadangan basis data penuh (Full Database Backup) menggunakan mysqldump atau utilitas physical backup (Percona XtraBackup) telah berhasil dibuat dan diuji pada lingkungan staging.
  • Mekanisme Dual-Writes pada lapisan backend telah diuji melalui unit test dan integration test dengan tingkat cakupan kode minimal 90%.
  • Skrip backfill historis telah diverifikasi bebas dari kebocoran memori (memory leak) dan mendukung fungsi resume otomatis.
  • Nilai AUTO_INCREMENT pada shadow table telah disesuaikan dengan posisi maksimum tabel utama.
  • Pemantauan metrik real-time (Grafana/Prometheus) dan aturan peringatan (alerting rules) via Slack/PagerDuty telah aktif.
  • Rencana rollback darurat (Emergency Rollback Plan) telah disiapkan secara tertulis dan dipahami oleh seluruh anggota tim DevOps/DBA.

Dengan mengikuti panduan komprehensif ini, Anda dapat mengeksekusi perubahan skema database yang paling kompleks sekalipun pada MySQL 8.4 tanpa mengorbankan ketersediaan layanan, integritas data, dan pengalaman pengguna akhir.

🏷️ Tags: #High Availability #Backend Architecture #Devops #Database Migration #Mysql 8.4