Tools

Membangun UI Sendiri untuk Mengelola Data Replication dengan Debezium

admin
admin

3 Sep 202612 min baca

Membangun UI Sendiri untuk Mengelola Data Replication dengan Debezium

Sebelumnya, kebutuhan data replication di sistem sudah menggunakan Airbyte.

Secara fungsi, Airbyte cukup lengkap. Untuk membuat koneksi source ke destination, melakukan sync, dan memonitor prosesnya, semuanya sudah tersedia melalui UI.

Namun, seiring bertambahnya kebutuhan replication, mulai menemukan satu masalah: resource yang digunakan Airbyte cukup besar.

Untuk kebutuhan yang sebenarnya cukup sederhana, mengambil perubahan data dari database source lalu meneruskannya ke data warehouse, menjalankan platform dengan banyak komponen di dalamnya terasa cukup berat untuk server yang tersedia.

Dari situ muncul pertanyaan:

Apakah benar-benar membutuhkan seluruh fitur Airbyte untuk kebutuhan replication ini?

Saya kemudian mencoba mencari pendekatan yang lebih sederhana.

Karena kebutuhan utamanya adalah Change Data Capture (CDC), saya mulai bereksperimen dengan Debezium + Kafka Connect.

Konsepnya sebenarnya cukup sederhana:

Source Database
      ↓
   Debezium
      ↓
     Kafka
      ↓
Kafka Connect Sink
      ↓
 Data Warehouse

Debezium menangkap perubahan dari source database, Kafka menjadi tempat event tersebut mengalir, kemudian Kafka Connect Sink meneruskannya ke database tujuan.

Dari sisi arsitektur, pendekatan ini terasa lebih ringan dan lebih fleksibel.

Tetapi ada satu konsekuensi.

Airbyte sudah menyediakan abstraction layer dan UI untuk mengelola semuanya.

Sedangkan ketika menggunakan Debezium secara langsung, saya harus berhadapan dengan connector, Kafka topic, offset, schema history, sink configuration, dan berbagai detail lainnya.

Akhirnya muncul ide:

Kalau sebelumnya Airbyte menyediakan UI untuk mengelola replication, kenapa tidak membuat UI sendiri di atas Debezium dan Kafka Connect?

Tujuannya bukan sekadar mengganti Airbyte.

Saya ingin membuat management layer yang lebih sederhana dan sesuai dengan workflow replication yang memang dibutuhkan, tanpa membawa seluruh kompleksitas platform yang tidak digunakan.

Dan ternyata, setelah mulai membangunnya, tantangannya bukan lagi sekadar membuat connector Debezium berjalan.

Tantangannya adalah membuat seluruh lifecycle replication bisa dikelola dengan aman.


Dari Connector menjadi Replication Platform

Pada percobaan pertama, flow-nya terlihat sederhana.

Saya membuat source connector:

Source DB
   ↓
Debezium Source Connector
   ↓
Kafka Topic

Kemudian membuat sink connector:

Kafka Topic
   ↓
Kafka Connect Sink
   ↓
Warehouse

Kalau hanya satu database dan satu tabel, semuanya terasa mudah.

Tetapi ketika replication mulai bertambah, saya membutuhkan lebih dari sekadar connector.

Saya perlu mengetahui:

  • source database apa yang digunakan
  • tabel apa saja yang direplikasi
  • Kafka topic yang terbentuk
  • destination database
  • sink connector
  • status connector
  • status task
  • error terakhir
  • kapan terakhir connector berjalan
  • bagaimana melakukan recovery
  • apa yang harus dibersihkan ketika source dihapus

Dari sinilah UI mulai dibangun.

Kurang lebih arsitekturnya menjadi seperti ini:

                         Replication UI
                               │
              ┌────────────────┼────────────────┐
              │                │                │
           Source             Sink          Monitoring
              │                │                │
              └────────────────┼────────────────┘
                               │
                         Backend API
                               │
                       Kafka Connect
                         /         \
                  Debezium        Kafka
                                    │
                                    ▼
                              Data Warehouse

UI menjadi satu pintu untuk mengelola lifecycle replication.

Bukan hanya create dan delete, tetapi juga monitoring, recovery, dan cleanup.


Tantangan pertama: snapshot Debezium

Salah satu konfigurasi yang ternyata cukup penting adalah snapshot.mode.

Untuk flow normal, saya menggunakan:

snapshot.mode=initial
snapshot.locking.mode=none

initial digunakan ketika replication pertama kali dibuat.

Tujuannya adalah mengambil snapshot data yang sudah ada terlebih dahulu, kemudian Debezium melanjutkan dengan menangkap perubahan baru melalui CDC.

Sedangkan recovery tidak saya jadikan mode normal.

Recovery hanya digunakan ketika terjadi masalah pada schema history, misalnya schema history topic hilang atau mengalami kerusakan.

Masalahnya, pernah ada connector yang tertinggal menggunakan mode recovery.

Dari luar connector terlihat:

RUNNING

Tetapi behaviour-nya tidak sesuai harapan.

Topic data tidak terbentuk seperti flow normal dan snapshot juga tidak berjalan sebagaimana mestinya.

Akhirnya flow di backend saya ubah.

recovery diperlakukan sebagai temporary operational state.

Setelah recovery selesai dan connector kembali RUNNING, konfigurasi dikembalikan ke mode normal.

Normal
  │
  │ schema history problem
  ▼
Recovery
  │
  │ recovery completed
  ▼
RUNNING
  │
  ▼
initial

Jadi recovery bukan konfigurasi permanen.

Untuk create dan update source, backend juga memastikan:

snapshot.mode=initial
snapshot.locking.mode=none

Dengan cara ini, flow utama tetap konsisten meskipun connector sebelumnya pernah mengalami recovery.


Ternyata masalahnya tidak selalu ada di Debezium

Ketika connector gagal berjalan, dugaan pertama biasanya adalah konfigurasi Debezium.

Ternyata tidak selalu.

Kafka Connect sendiri memiliki internal topics yang digunakan untuk menyimpan state connector.

Misalnya:

configs
offsets
status

Pada salah satu kasus, internal topic Kafka Connect menggunakan:

cleanup.policy=delete

Padahal Kafka Connect membutuhkan behaviour compact untuk internal topic tertentu.

Akibatnya worker mengalami masalah ketika startup dan connector tidak berjalan dengan baik.

Setelah policy diperbaiki:

cleanup.policy=compact

Kafka Connect kembali stabil dan source topics mulai terbentuk lagi.

Dari sini saya mulai melihat bahwa debugging replication tidak bisa hanya fokus pada Debezium.

Saya harus melihat seluruh chain:

Database
   ↓
Debezium
   ↓
Kafka Connect
   ↓
Kafka
   ↓
Sink Connector
   ↓
Warehouse

Masalah pada satu layer bisa terlihat seperti masalah pada layer lainnya.


Sink dibuat lebih predictable

Pada sisi sink, saya akhirnya memilih menggunakan:

schema.evolution=none

Artinya sink tidak dibiarkan mengubah struktur tabel destination secara otomatis.

Sebagai gantinya, tabel replication dipersiapkan terlebih dahulu melalui migration.

Flow-nya menjadi:

Migration
    ↓
Prepare Replication Table
    ↓
Create Sink Connector
    ↓
Start Replication

Pendekatan ini memang membuat migration menjadi lebih penting.

Tetapi saya mendapatkan kontrol yang lebih baik terhadap schema warehouse.

Saya tidak ingin sink tiba-tiba mencoba mengubah struktur tabel dan kemudian gagal karena constraint tertentu seperti:

NOT NULL
PRIMARY KEY
INDEX

Dengan schema.evolution=none, schema destination menjadi lebih predictable.

Kalau struktur source berubah, perubahan tersebut harus dipikirkan dan ditangani secara eksplisit.

Menurut saya ini lebih cocok untuk environment yang ingin menjaga schema warehouse tetap terkontrol.


Bagaimana dengan primary key?

Saya juga sempat mempertimbangkan apakah primary key pada replication table sebaiknya mengikuti primary key asli source.

Secara teori ini masuk akal.

Tetapi setelah melihat struktur database yang digunakan, khususnya pada ERPNext, banyak tabel menggunakan:

name

sebagai primary key.

Akhirnya behaviour replication tetap menggunakan kombinasi:

source_table + name

sebagai identifier pada tabel replication.

Ini menjadi salah satu pelajaran bahwa desain CDC tidak selalu bisa dibuat berdasarkan asumsi umum.

Kita harus melihat karakteristik database source yang sebenarnya.


Delete ternyata lebih rumit daripada Create

Salah satu hal yang cukup banyak mengalami perubahan adalah flow delete.

Pada awalnya saya berpikir delete source cukup dengan menghapus source connector.

Ternyata tidak.

Sebuah source memiliki beberapa resource yang saling berkaitan:

Source Connector
       │
       ├── Kafka Topics
       ├── Schema History Topic
       ├── Sink Subscription
       └── Offset

Kalau hanya connector yang dihapus, resource lainnya bisa tetap tertinggal.

Akhirnya flow delete distandarkan menjadi:

Stop Source Connector
        ↓
Update/Delete Sink Subscription
        ↓
Delete Source Connector
        ↓
Reset Offset
        ↓
Delete Kafka Topics
        ↓
Delete Schema History Topic
        ↓
Cleanup

Ini membuat proses delete menjadi bagian dari lifecycle replication, bukan sekadar operasi CRUD.


Bug kecil pada nama tabel

Salah satu bug yang cukup menarik muncul ketika nama tabel mengandung spasi.

Misalnya source memiliki tabel:

tabBon Merah

Tetapi Kafka topic menggunakan nama yang sudah disanitasi:

tabBon_Merah

Pada saat cleanup, logic awal membandingkan nama tabel secara langsung.

Akibatnya:

tabBon Merah
      ≠
tabBon_Merah

Topic tidak ditemukan dan tidak ikut dihapus.

Masalahnya sebenarnya sederhana.

Tetapi karena terjadi pada proses cleanup, resource Kafka bisa tertinggal tanpa terlihat secara langsung.

Solusinya adalah memastikan proses pembuatan dan penghapusan topic menggunakan aturan sanitasi yang sama.

Saat create:

Source Table
     ↓
Sanitize
     ↓
Kafka Topic

Saat delete:

Source Table
     ↓
Sanitize
     ↓
Find Topic
     ↓
Delete

Dari sini saya belajar bahwa naming convention bukan sekadar masalah kosmetik.

Ketika nama tersebut digunakan sebagai identifier antar sistem, sanitasi harus konsisten di seluruh lifecycle.


Membuat monitoring tanpa refresh manual

Setelah create, update, dan delete berjalan, kebutuhan berikutnya adalah monitoring.

Saya ingin dashboard bisa mengetahui perubahan status connector secara otomatis.

Untuk itu saya menggunakan Server-Sent Events atau SSE.

Arsitekturnya kurang lebih:

Kafka Connect
      ↓
   Stats API
      ↓
      SSE
      ↓
      UI

Tetapi implementasi awal ternyata tidak cukup responsif.

Endpoint statistics cukup berat dan koneksi Kafka Admin dibuka berulang kali.

Akibatnya dashboard terasa lambat dan status connector tidak selalu langsung berubah.

Saya akhirnya memilih untuk tidak mengganti SSE.

Yang diperbaiki justru bagian backend.

Kafka Admin dioptimalkan agar digunakan lebih efisien dalam satu request.

Kemudian ditambahkan timeout dan retry ringan.

Interval update juga diubah dari sekitar:

5 detik

menjadi:

3 detik

Hasilnya dashboard terasa lebih real-time tanpa harus melakukan refresh manual.

Ini cukup menarik karena kadang ketika sebuah fitur terasa lambat, solusi pertama yang terpikir adalah mengganti teknologi.

Padahal masalahnya bisa saja ada pada implementasi di belakang teknologi tersebut.


RUNNING belum tentu berarti sehat

Salah satu pelajaran terbesar dari implementasi ini adalah:

Status RUNNING tidak selalu berarti seluruh dependency di belakangnya sehat.

Contohnya adalah SSH tunnel.

Beberapa source database diakses melalui SSH tunnel.

Ada kondisi di mana object tunnel masih tersimpan di cache, tetapi proses SSH yang sebenarnya sudah mati.

Dari sisi aplikasi:

Tunnel exists

Tetapi dari sisi network:

Tunnel is dead

Connector kemudian mencoba menggunakan tunnel tersebut dan akhirnya gagal mengakses database source.

Solusinya adalah menambahkan health check terhadap tunnel.

Flow-nya:

Get Cached Tunnel
       ↓
   Health Check
      /    \
     /      \
Healthy     Stale
   │          │
 Reuse      Recreate

Dengan begitu cache tidak lagi dianggap sebagai bukti bahwa koneksi masih sehat.


Masalah sink PostgreSQL karena idle connection

Di sisi PostgreSQL sink, pernah muncul error:

Failed to check for existence of table

Awalnya terlihat seperti masalah sink connector atau permission.

Setelah diperiksa lebih jauh, ternyata root cause-nya adalah idle-session timeout pada PostgreSQL.

Connection JDBC dibiarkan idle cukup lama, kemudian server memutus koneksi tersebut.

Ketika connector mencoba menggunakan connection yang sudah mati, muncul error.

Flow-nya:

Sink Connector
      ↓
JDBC Connection
      ↓
Idle terlalu lama
      ↓
PostgreSQL disconnect
      ↓
Connector reuse connection
      ↓
Error

Solusinya adalah menambahkan parameter pada JDBC connection seperti:

tcpKeepAlive=true
socketTimeout=0
connectTimeout=10000

Setelah itu sink kembali RUNNING dan lebih tahan terhadap idle connection timeout.

Konfigurasi tersebut kemudian dijadikan default dalam flow create dan update sink melalui UI.


Ketika jumlah data source dan destination berbeda

Masalah lain yang cukup menarik adalah ketika jumlah data antara source dan replication table tidak sama.

Misalnya:

Source       : 100.000 rows
Replication  : 99.800 rows

Dugaan pertama biasanya:

Ada data yang gagal direplikasi.

Tetapi ternyata belum tentu.

Pada salah satu kasus, name sebagai primary key memang unique sehingga bukan masalah duplicate atau primary key conflict.

Setelah diperiksa lebih jauh, ternyata perbandingan dilakukan terhadap source dan topic dari database yang berbeda.

Artinya masalahnya bukan:

CDC kehilangan data

melainkan:

Source A
   vs
Replication dari Source B

Ini membuat proses debugging menjadi lebih hati-hati.

Sebelum menyimpulkan ada data yang hilang, seluruh mapping harus diperiksa:

Source Database
      ↓
Database Include
      ↓
Table Include
      ↓
Debezium Connector
      ↓
Topic Prefix
      ↓
Kafka Topic
      ↓
Sink Connector
      ↓
Destination Table

Semua harus mengarah ke sumber data yang sama.


Dari UI menjadi control plane

Setelah semua masalah tersebut ditangani, saya mulai melihat bahwa UI yang dibuat sebenarnya sudah bukan sekadar dashboard.

Ia sudah menjadi control plane kecil untuk data replication.

Sebelumnya workflow operasional bisa terlihat seperti:

SSH Server
   ↓
Kafka Connect API
   ↓
Kafka CLI
   ↓
Database
   ↓
Check Logs
   ↓
Manual Cleanup

Sekarang workflow tersebut bisa diabstraksikan menjadi:

                Replication UI
                      │
       ┌──────────────┼──────────────┐
       │              │              │
     Source          Sink         Monitoring
       │              │              │
       └──────────────┼──────────────┘
                      │
                Kafka Connect
                      │
                  ┌───┴───┐
                  │       │
               Debezium  Kafka
                          │
                          ▼
                     Warehouse

User tidak perlu mengetahui detail bagaimana Kafka topic dibuat, bagaimana connector di-restart, bagaimana offset di-reset, atau bagaimana resource dibersihkan.

Mereka cukup melihat konsep yang lebih sederhana:

Create
Update
Monitor
Recover
Delete

Detail operasionalnya ditangani oleh backend.


Apa yang berbeda dengan Airbyte?

Tujuan awal project ini memang berangkat dari penggunaan Airbyte.

Bukan berarti Airbyte tidak bagus.

Justru Airbyte sangat membantu ketika kita membutuhkan platform dengan banyak connector, banyak destination, dan workflow data integration yang kompleks.

Tetapi kebutuhan saya lebih spesifik.

Saya terutama membutuhkan:

CDC
+
Replication
+
Monitoring
+
Recovery

Karena itu, menggunakan Debezium + Kafka Connect memberi saya kontrol yang lebih granular terhadap pipeline.

Konsekuensinya, banyak hal yang sebelumnya sudah disediakan oleh Airbyte harus saya bangun sendiri.

Mulai dari UI sampai lifecycle management.

Jadi trade-off-nya cukup jelas:

Airbyte
────────────────────────
Banyak fitur
Abstraction tinggi
UI sudah tersedia
Setup relatif mudah
Resource lebih besar


Debezium + Kafka Connect
────────────────────────
Lebih low-level
Kontrol lebih besar
Resource lebih terkontrol
UI harus dibuat sendiri
Operational logic harus dibangun sendiri

Saya tidak menganggap pendekatan kedua selalu lebih baik.

Tetapi untuk kebutuhan replication yang cukup spesifik, kontrol tersebut menjadi alasan kenapa pendekatan ini menarik untuk dicoba.


Beberapa hal yang akhirnya saya pelajari

1. Membuat CDC berjalan itu bukan bagian tersulit

Membuat:

Database → Debezium → Kafka → Sink

adalah bagian awal.

Tantangan sebenarnya muncul ketika pipeline harus digunakan terus-menerus.


2. Happy path hanya sebagian kecil dari sistem

Create connector adalah happy path.

Production justru akan bertemu dengan:

Connector FAILED
Topic hilang
Schema history rusak
Database timeout
SSH tunnel mati
Schema berubah
Sink disconnect
Offset bermasalah
Source dihapus

Karena itu saya mulai mendesain UI bukan hanya berdasarkan:

Apa yang bisa dilakukan ketika semuanya normal?

Tetapi juga:

Apa yang harus dilakukan ketika sesuatu gagal?


3. Recovery harus menjadi bagian dari lifecycle

Recovery tidak seharusnya selalu menjadi proses manual melalui SSH.

Kalau recovery memang bagian dari operasional replication, maka ia sebaiknya menjadi bagian dari workflow aplikasi.

Misalnya:

RUNNING
   ↓
FAILED
   ↓
RECOVERING
   ↓
RUNNING

Dengan state yang jelas, user tidak perlu memahami seluruh detail internal Debezium.


4. Cleanup sama pentingnya dengan Create

Semakin banyak resource yang dibuat secara otomatis, semakin penting proses cleanup.

Create:

Create Connector
Create Topic
Create Subscription

Delete harus menjadi kebalikannya:

Delete Subscription
Delete Connector
Delete Topic
Delete Offset
Cleanup

Kalau tidak, infrastructure akan perlahan dipenuhi resource yang sudah tidak digunakan.


5. Monitoring harus melihat dependency, bukan hanya status connector

RUNNING bukan berarti semuanya sehat.

Connector bergantung pada:

Database
Network
SSH Tunnel
Kafka
Kafka Connect
JDBC
Warehouse

Karena itu monitoring yang baik seharusnya tidak hanya melihat status connector, tetapi juga kondisi dependency yang relevan.


Penutup

Awalnya saya hanya ingin mencari alternatif dari Airbyte karena kebutuhan replication yang ada terasa terlalu berat dari sisi resource server.

Saya kemudian mencoba pendekatan yang lebih sederhana menggunakan:

Debezium
+
Kafka Connect
+
Kafka

Secara teknis, pendekatan tersebut memberikan kontrol yang lebih besar.

Tetapi semakin jauh saya menggunakannya, semakin jelas bahwa mengganti Airbyte dengan Debezium bukan sekadar mengganti satu tool dengan tool lainnya.

Airbyte sebelumnya sudah menyembunyikan banyak kompleksitas di balik UI dan abstraction layer.

Ketika menggunakan Debezium secara langsung, saya harus membangun abstraction tersebut sendiri.

Dari situlah akhirnya saya membuat UI untuk mengelola replication.

Dan proses tersebut mengajarkan satu hal yang cukup penting:

Menjalankan Debezium itu mudah. Membuat seluruh lifecycle replication menjadi reliable dan mudah dioperasikan adalah bagian yang sebenarnya sulit.

Karena pada akhirnya, data replication bukan hanya tentang bagaimana data berpindah dari satu database ke database lain.

Tetapi juga tentang bagaimana sistem tersebut bisa:

Create
   ↓
Run
   ↓
Monitor
   ↓
Recover
   ↓
Update
   ↓
Delete
   ↓
Cleanup

tanpa membutuhkan intervensi manual setiap kali sesuatu terjadi.

Dan mungkin, justru di situlah alasan kenapa sebuah management UI menjadi penting ketika Debezium mulai digunakan bukan lagi sebagai eksperimen, tetapi sebagai bagian dari sistem yang berjalan setiap hari.

Artikel Terkait

(3)

Komentar

(0)

Komentar Anda akan dimoderasi.

Memuat komentar...