DevOps

MySQL Gone Away: HAProxy, Galera, dan Ghost Processes

Asep Alazhari

Jam 2 pagi, 80% koneksi database gagal setelah migrasi ke HAProxy. Root cause-nya ternyata proses yang sudah 83 hari berjalan dan tidak ada yang tahu.

MySQL Gone Away: HAProxy, Galera, dan Ghost Processes

Jam 2 pagi, gue lagi ngeliatin terminal yang nunjukin error paling ditakutin engineer: MySQL server has gone away. Loop test sederhana ke port 3307 ngasih hasil begini:

OK: 4 / FAIL: 16

80% failure rate. Baru aja gue migrasi traffic aplikasi ke HAProxy load balancer di salah satu ISP terbesar di Indonesia. Database-nya Galera cluster 3 node pake MariaDB. Harusnya semua berjalan mulus. Ternyata enggak.

Ini cerita lengkap tentang apa yang terjadi, gimana gue nemuin root cause-nya, dan apa yang gue pelajari setelah tiga jam menggali proses yang harusnya udah mati 83 hari lalu.

Kondisi Sistem Sebelum Gue Sentuh

Environment production dibangun di atas dua Galera cluster yang ada di belakang satu HAProxy server. Satu cluster handle traffic portal di port 3306. Yang satunya handle traffic aplikasi di port 3307.

Masalahnya, aplikasi sama sekali tidak menggunakan HAProxy. Dia connect langsung ke primary database node, bypass load balancer sepenuhnya. Salah satu alasannya adalah db-node-3, member ketiga dari application cluster, udah offline lebih dari 13 bulan setelah crash. Gak ada yang pernah rejoin. Jadi cluster jalan 2-dari-3, dan HAProxy dikonfigurasi cuma ke 2 node aja.

Topologinya kurang lebih begini:

Traffic Aplikasi
    |
    |-- [langsung]  --> db-node-1:3306  (primary, 100% traffic)
    |
    `-- db-lb:3307   (HAProxy, tidak dipakai aplikasi)
          |-- port 3306 --> portal-cluster  (3 node)
          `-- port 3307 --> app-cluster     (db-node-1, db-node-2, db-node-3)

Galera cluster (app-cluster):
  db-node-1  -- Running, production aktif, semua traffic masuk sini
  db-node-2  -- Synced, tersedia sebagai SST donor
  db-node-3  -- OFFLINE, 13+ bulan, survivor dari crash

Goal-nya jelas: rejoin db-node-3 ke cluster, enable Galera-aware health check di HAProxy, dan migrasi aplikasi biar pakai load balancer.

Phase 1: Audit Nemu Dua Masalah Tersembunyi

Sebelum ngapa-ngapain, gue audit dulu semua 4 server secara read-only. Hasil di HAProxy server (db-lb) langsung nunjukin kenapa error “MySQL server has gone away” udah kejadian di beberapa user bahkan sebelum migrasi gue.

Masalah pertama: konfigurasi timeout-nya kacau banget.

timeout client  1m   (60 detik)
timeout server  1m   (60 detik)
MySQL wait_timeout = 10 detik

MySQL nutup koneksi idle setelah 10 detik. HAProxy ekspektasi koneksi tetap hidup 60 detik. Aplikasi yang pakai connection pool bakal hold koneksi, biarin idle lebih dari 10 detik, dan query berikutnya langsung gagal dengan MySQL has gone away.

Masalah kedua: health check tidak Galera-aware.

Konfigurasi yang ada pakai option mysql-check user haproxy. Itu cuma verify apakah MySQL bisa nerima koneksi TCP. Gak cek Galera replication state sama sekali. Waktu sebuah node masuk state DONOR/DESYNCED selama State Snapshot Transfer, HAProxy gak tau. Dia terus kirim traffic ke node itu padahal node tersebut tidak bisa handle writes dengan benar.

Rencananya: deploy clustercheck di port 9200, ganti HAProxy ke option httpchk, dan fix nilai timeout.

Phase 2: Rejoin Node yang Crash

db-node-3 udah offline 13 bulan. Dia jalan di standalone mode dengan wsrep_on=OFF, innodb_force_recovery=1 buat read-only recovery, dan tidak ada file grastate.dat sama sekali.

Tanpa grastate.dat, Incremental State Transfer tidak mungkin dilakukan. Node butuh full SST. Dan gcache dari node lain tidak bisa menyimpan 13 bulan transaksi. Jadi solusinya: full SST bersih dari db-node-2.

Cara paling aman supaya tidak ganggu traffic production:

  1. Paksa db-node-2 jadi SST donor dengan set wsrep_sst_donor=db-node-2 di sisi joiner
  2. Sementara comment out db-node-2 di HAProxy biar tidak ada traffic selama SST
  3. Bersihkan data directory di db-node-3 untuk SST fresh

Langkah pertama: isolasi db-node-2 dari HAProxy:

sudo sed -i 's/^  server db-node-2/# server db-node-2/' /etc/haproxy/haproxy.cfg
sudo systemctl reload haproxy

Langkah kedua: fix konfigurasi db-node-3:

# Hapus recovery mode
sudo sed -i '/innodb_force_recovery/d' /etc/my.cnf.d/server.cnf

# Enable Galera replication
sudo sed -i 's/wsrep_on=OFF/wsrep_on=ON/' /etc/my.cnf.d/server.cnf

# Paksa donor selection — ini yang proteksi db-node-1 supaya tidak dipilih
sudo sed -i '/^wsrep_on=ON/a wsrep_sst_donor=db-node-2' /etc/my.cnf.d/server.cnf

Langkah ketiga: verifikasi isi direktori dulu, baru bersihkan:

# Selalu verifikasi sebelum jalanin perintah destruktif
ls -la /var/lib/mysql/

sudo systemctl stop mariadb
sudo rm -rf /var/lib/mysql/*
sudo chown mysql:mysql /var/lib/mysql
sudo chmod 750 /var/lib/mysql
sudo systemctl start mariadb

Gue estimasi SST bakal makan waktu 30 sampai 90 menit karena disk usage di server itu sekitar 227GB. Ternyata selesai dalam hitungan menit. Cuma sekitar 28GB data MySQL aktual yang ditransfer. Sisa ruang disk adalah file OS, log, dan konten non-MySQL lainnya. rsync cuma mindahin file database yang beneran.

Galera log konfirmasi berhasil:

[Note] WSREP: Shifting JOINER -> JOINED
[Note] WSREP: Shifting JOINED -> SYNCED

Semua tiga node setelah SST:

wsrep_cluster_size:         3
wsrep_cluster_status:       Primary
wsrep_local_state_comment:  Synced

Phase 3: Bikin HAProxy Galera-Aware

Solusi standar untuk health check HAProxy yang Galera-aware adalah clustercheck. Ini script shell yang jalan sebagai xinetd service di port 9200. Dia query local MySQL node untuk nilai wsrep_local_state dan return HTTP 200 hanya kalau nilainya 4, yang artinya Synced. Kalau node di state lain termasuk Donor atau Desynced, dia return HTTP 503.

Ada satu komplikasi nih. Server-server ini jalan di CentOS 7.6 yang udah end of life, dengan MariaDB 10.1 yang juga udah end of life. Sebagian besar package repository udah mati. Di db-node-1, masih bisa akses mirror dengan package xinetd. Di db-node-2 dan db-node-3, DNS bahkan tidak bisa resolve mirror list-nya.

Workaround-nya: download RPM di satu node yang masih punya akses, terus transfer ke yang lain:

# Di db-node-1: download tanpa install
yumdownloader xinetd --disablerepo=mariadb --destdir=/tmp/

# Transfer ke node lain
scp /tmp/xinetd-2.3.15-14.el7.x86_64.rpm user@db-node-2:/tmp/
scp /tmp/xinetd-2.3.15-14.el7.x86_64.rpm user@db-node-3:/tmp/

# Di db-node-2 dan db-node-3: install dari file lokal
sudo rpm -ivh /tmp/xinetd-2.3.15-14.el7.x86_64.rpm

Setelah deploy clustercheck script dan xinetd config di semua 3 node, gue bikin MySQL user yang dibutuhin (cukup dibuat sekali di db-node-1, langsung tereplikasi otomatis ke db-node-2 dan db-node-3 via Galera):

CREATE USER IF NOT EXISTS 'clustercheck'@'localhost' IDENTIFIED BY 'clustercheckpassword';
GRANT USAGE ON *.* TO 'clustercheck'@'localhost';
FLUSH PRIVILEGES;

Verifikasi dari HAProxy server:

curl -s -o /dev/null -w "%{http_code}" http://db-node-1:9200  # 200
curl -s -o /dev/null -w "%{http_code}" http://db-node-2:9200  # 200
curl -s -o /dev/null -w "%{http_code}" http://db-node-3:9200  # 200

Semua return 200. Terus gue update konfigurasi HAProxy:

defaults
  mode tcp
  timeout client  28800s
  timeout server  28800s
  timeout connect 5s
  timeout check   10s

backend app-cluster
  balance leastconn
  option tcpka
  option httpchk GET /
  server db-node-1 db-node-1:3306 check port 9200 inter 5s rise 2 fall 3 weight 1
  server db-node-2 db-node-2:3306 check port 9200 inter 5s rise 2 fall 3 weight 1
  server db-node-3 db-node-3:3306 check port 9200 inter 5s rise 2 fall 3 weight 1

Setelah reload, stats page HAProxy nunjukin L7OK/200 buat semua 3 node. Phase 2 dan 3 selesai. Gue seneng banget waktu itu.

Also Read: Kubernetes Cluster Mati Setelah Node Reboot: Postmortem Lengkap

Phase 4: Kegagalan yang Bikin Bingung

Keesokan harinya, gue migrasi satu aplikasi test buat pakai db-lb:3307 gantiin koneksi langsung. Dalam hitungan menit, error mulai muncul. Gue jalanin loop test koneksi:

ok=0; fail=0
for i in $(seq 1 20); do
  result=$(mysql -hdb-lb -P3307 --connect-timeout=3 2>&1 | head -1)
  if echo "$result" | grep -q '1045'; then
    ok=$((ok+1))
  else
    fail=$((fail+1))
  fi
done
echo "OK: $ok / FAIL: $fail"

Hasilnya: OK: 4 / FAIL: 16

Koneksi langsung ke masing-masing MySQL node jalan baik-baik saja. HAProxy yang reject koneksi. Tapi gue baru aja fix HAProxy kemarin. Kenapa masih broken?

Nemu Ghost Processes

Jawabannya tersembunyi di satu perintah sederhana:

ps aux | grep haproxy | grep -v grep

Output:

haproxy  10787  Feb11  437:57  haproxy -f haproxy.cfg
haproxy  10788  Feb11   93:13  haproxy -f haproxy.cfg
haproxy  10789  Feb11  155:41  haproxy -f haproxy.cfg
haproxy  10790  Feb11  266:49  haproxy -f haproxy.cfg
haproxy   3094  May06  00:08   /usr/sbin/haproxy -Ds -f /etc/haproxy/haproxy.cfg -sf 592
haproxy   3095  May06  00:08   /usr/sbin/haproxy -Ds -f /etc/haproxy/haproxy.cfg -sf 592

Empat proses yang start tanggal 11 Februari. Delapan puluh tiga hari lalu. Masih hidup.

Ini kronologinya: HAProxy awalnya dijalankan secara manual pakai haproxy -f haproxy.cfg, bukan via systemd. Proses-proses itu tidak pernah didaftarkan ke systemd. Setiap kali ada yang jalankan systemctl reload haproxy atau systemctl restart haproxy, systemd cuma ngurusin proses yang dia sendiri track. Proses yang dijalankan manual (PID 10787 sampai 10790) gak pernah dapat signal apapun dan tetap jalan dengan konfigurasi lama.

Konfigurasi lama itu pakai option mysql-check bukan option httpchk. db-node-3 masih di-comment out. Tidak ada clustercheck. Proses-proses lama ini tidak bisa nemuin backend yang UP karena metode health check-nya sudah tidak kompatibel dengan setup yang baru.

Dengan nbproc 4, HAProxy jalan dengan 4 worker process. Setiap generasi nambahin 4 listener lagi di port yang sama. Port 3307 sekarang didengarkan oleh 8 proses: 4 dengan konfigurasi lama yang broken, dan 4 dengan konfigurasi baru yang benar. Kernel mendistribusikan koneksi masuk ke semua 8 proses secara round-robin. 4 dari 8 proses langsung reject koneksi tanpa backend valid. Makanya failure rate-nya sekitar 80%.

Fix-nya cuma satu perintah:

sudo kill 10787 10788 10789 10790

Hasil test setelah itu:

OK: 20 / FAIL: 0

Nol failure. Bersih total.

Root Cause ke-2: Masalah wait_timeout

Setelah kill ghost processes, error intermittent masih sesekali muncul. Investigasi nunjukin kalau db-node-2 dan db-node-3 punya wait_timeout = 10, sementara db-node-1 punya wait_timeout = 28800.

MySQL nutup koneksi idle setelah wait_timeout detik. 10 detik itu sangat agresif buat aplikasi yang pakai connection pool. PHP-FPM worker yang hold koneksi antar request bakal nemuin koneksi mati setelah cuma 10 detik idle. Query berikutnya langsung kena “MySQL server has gone away”.

Dengan balance leastconn, HAProxy distribusikan koneksi ke semua 3 node. Koneksi yang landing di db-node-2 atau db-node-3 bakal expired setelah 10 detik tidak aktif.

Fix di kedua node:

SET GLOBAL wait_timeout = 28800;
SET GLOBAL interactive_timeout = 28800;

Dan supaya persistent setelah MariaDB restart:

# /etc/my.cnf.d/server.cnf
wait_timeout         = 28800
interactive_timeout  = 28800

Setelah kedua fix ini, error rate turun ke nol dan tetap di sana.

Also Read: Fixing RabbitMQ 4.x Connection: amqplib ke CloudAMQP

Yang Gue Pelajari

Jangan pernah start HAProxy secara manual di server yang dikelola systemd. Kalau lo lakuin itu, systemctl restart tidak akan kill proses-proses tersebut. Mereka bakal terus jalan selamanya dengan konfigurasi apapun yang mereka punya saat startup. Setelah setiap reload atau restart, selalu verifikasi dulu:

# Cek semua process start time — tidak boleh ada yang beda berhari-hari
ps aux | grep haproxy | grep -v grep | grep -v wrapper

Pakai option httpchk dengan clustercheck untuk Galera cluster, bukan option mysql-check. TCP-level check tidak bisa detect node yang lagi di state DONOR atau DESYNCED. Node masih bisa nerima koneksi TCP tapi tetap tidak bisa handle writes dengan benar.

Sinkronkan timeout lo. MySQL wait_timeout dan HAProxy timeout client/timeout server harus aligned. Pendekatan umum adalah set HAProxy timeout sesuai nilai MySQL wait_timeout.

Durasi SST tidak proporsional dengan total disk usage. rsync saat SST cuma transfer file MySQL yang beneran. Server dengan 227GB total disk usage mungkin hanya punya 28GB file database untuk ditransfer.

Selalu verifikasi isi direktori sebelum jalanin rm -rf di database data directory. Satu path yang salah di server production bisa berakhir sangat buruk. Tanya diri sendiri apa isi direktori itu sebelum dihapus.

Also Read: Kubernetes Logging yang Bener: Fluent Bit ke Elasticsearch

Konfigurasi Final yang Berhasil

defaults
  mode tcp
  timeout client  28800s
  timeout server  28800s
  timeout connect 5s
  timeout check   10s

backend app-cluster
  balance leastconn
  option tcpka
  option httpchk GET /
  server db-node-1 db-node-1:3306 check port 9200 inter 5s rise 2 fall 3 weight 1
  server db-node-2 db-node-2:3306 check port 9200 inter 5s rise 2 fall 3 weight 1
  server db-node-3 db-node-3:3306 check port 9200 inter 5s rise 2 fall 3 weight 1
# MySQL di semua node
wait_timeout         = 28800
interactive_timeout  = 28800

Dan satu checklist preventif setelah setiap perubahan konfigurasi HAProxy:

# Pastikan tidak ada ghost process
ps aux | grep haproxy | grep -v grep | grep -v wrapper

# Test 20 koneksi dan ukur failure rate
ok=0; fail=0
for i in $(seq 1 20); do
  result=$(mysql -hdb-lb -P3307 --connect-timeout=3 2>&1 | head -1)
  if echo "$result" | grep -q '1045'; then ok=$((ok+1)); else fail=$((fail+1)); fi
done
echo "OK: $ok / FAIL: $fail"

# Cek HAProxy stats, pastikan L7OK/200 di semua backend
curl -s 'http://db-lb:9000/;csv' | grep app-cluster | awk -F',' '{print $2, $18, $19}'

Satu perintah kill menyelesaikan 80% dari failure. Sinkronisasi timeout yang benar menyelesaikan sisanya. Root cause asli dari keluhan “MySQL has gone away” yang sudah ada berbulan-bulan ternyata cuma dua hal: health check HAProxy yang tidak ngerti Galera, dan timeout yang beda sampai 2.880 kali lipat. Audit yang teliti sebelum bertindak, dan postmortem yang serius waktu ada yang gagal, itu yang bikin perbedaan antara incident yang selesai dalam satu jam versus yang dikejar selama berminggu-minggu.

Back to Blog

Related Posts

View All Posts »