Dalam artikel kali ini kita akan coba membuat contoh atau implementasi Database Cluster arsitektur Master Slave dengan PHP MySqli: termasuk metode round robin, failover dan health check otomatis berkala.
Konsep Master-Slave MySQL
Master → server utama untuk INSERT, UPDATE, DELETE (write operation).
Slave → server yang mereplikasi data dari master secara otomatis, biasanya untuk SELECT (read operation).
Tujuannya:
- Membagi beban kerja (write → master, read → slave).
- Failover (kalau master down, slave bisa di-promote).
- Performa lebih tinggi pada aplikasi skala besar.
Skema Sederhana
- Master DB: 192.168.1.10
- Slave DB: 192.168.1.11
- Database: shopdb
Contoh Implementasi PHP MySQLi
Kita bisa buat class Database Connection yang otomatis memilih master untuk INSERT/UPDATE/DELETE dan slave untuk SELECT.
<?php
class DatabaseCluster {
private $master;
private $slave;
public function __construct() {
// Koneksi ke MASTER (write)
$this->master = new mysqli("192.168.1.10", "user_master", "password_master", "shopdb");
if ($this->master->connect_error) {
die("Koneksi ke Master gagal: " . $this->master->connect_error);
}
// Koneksi ke SLAVE (read)
$this->slave = new mysqli("192.168.1.11", "user_slave", "password_slave", "shopdb");
if ($this->slave->connect_error) {
die("Koneksi ke Slave gagal: " . $this->slave->connect_error);
}
}
// Query SELECT → ke SLAVE
public function queryRead($sql) {
$result = $this->slave->query($sql);
if (!$result) {
die("Error SELECT di Slave: " . $this->slave->error);
}
return $result;
}
// Query INSERT/UPDATE/DELETE → ke MASTER
public function queryWrite($sql) {
$result = $this->master->query($sql);
if (!$result) {
die("Error Write di Master: " . $this->master->error);
}
return $result;
}
public function __destruct() {
$this->master->close();
$this->slave->close();
}
}
?>
Catatan Penting
- Sinkronisasi Master-Slave harus sudah di-setup di server MySQL (CHANGE MASTER TO …, START SLAVE;).
- Biasanya untuk production digunakan load balancer (misalnya ProxySQL, HAProxy, atau MariaDB MaxScale) agar aplikasi tidak perlu tahu detail master/slave.
- Bisa juga pakai random slave selection kalau lebih dari 1 slave untuk load balancing query baca.
Implementasi Lebih dari 1 Slave
Jika terdapat lebih dari 1 slave (misalnya 3 server untuk baca), kita bisa pakai metode round robin supaya query SELECT tidak hanya ke 1 slave saja tapi dibagi rata. Berikut ini contoh implementasi dengan lebih dari 1 slave (misalnya ada 3 slave, query SELECT diarahkan secara bergantian / round robin).
Skema
Master (Write): 192.168.1.10
Slaves (Read):
- 192.168.1.11
- 192.168.1.12
- 192.168.1.13
Contoh Implementasi Round Robin dengan PHP MySQLi
<?php
class DatabaseCluster {
private $master;
private $slaves = [];
private $slaveCount;
private $slaveIndex = 0;
public function __construct() {
// MASTER (untuk INSERT/UPDATE/DELETE)
$this->master = new mysqli("192.168.1.10", "user_master", "password_master", "shopdb");
if ($this->master->connect_error) {
die("Koneksi ke Master gagal: " . $this->master->connect_error);
}
// SLAVES (untuk SELECT)
$slaveConfigs = [
["192.168.1.11", "user_slave", "password_slave", "shopdb"],
["192.168.1.12", "user_slave", "password_slave", "shopdb"],
["192.168.1.13", "user_slave", "password_slave", "shopdb"],
];
foreach ($slaveConfigs as $cfg) {
$conn = new mysqli($cfg[0], $cfg[1], $cfg[2], $cfg[3]);
if ($conn->connect_error) {
die("Koneksi ke Slave {$cfg[0]} gagal: " . $conn->connect_error);
}
$this->slaves[] = $conn;
}
$this->slaveCount = count($this->slaves);
}
// ROUND ROBIN untuk SELECT
private function getSlaveConnection() {
$conn = $this->slaves[$this->slaveIndex];
$this->slaveIndex = ($this->slaveIndex + 1) % $this->slaveCount;
return $conn;
}
// Query SELECT → ke SLAVE (bergantian)
public function queryRead($sql) {
$conn = $this->getSlaveConnection();
$result = $conn->query($sql);
if (!$result) {
die("Error SELECT di Slave: " . $conn->error);
}
return $result;
}
// Query INSERT/UPDATE/DELETE → ke MASTER
public function queryWrite($sql) {
$result = $this->master->query($sql);
if (!$result) {
die("Error Write di Master: " . $this->master->error);
}
return $result;
}
public function __destruct() {
$this->master->close();
foreach ($this->slaves as $conn) {
$conn->close();
}
}
}
?>
Contoh Penggunaan
<?php
require_once "DatabaseCluster.php";
$db = new DatabaseCluster();
// INSERT ke MASTER
$db->queryWrite("INSERT INTO products (name, price) VALUES ('Produk X', 50000)");
// SELECT ke SLAVE (bergantian setiap query)
for ($i = 1; $i <= 5; $i++) {
$result = $db->queryRead("SELECT * FROM products LIMIT 3");
echo "<b>Query ke-$i:</b><br>";
while ($row = $result->fetch_assoc()) {
echo $row['name'] . " - Rp " . $row['price'] . "<br>";
}
echo "<hr>";
}
?>
Jika script dijalankan query SELECT akan diarahkan ke slave1, slave2, slave3, lalu balik lagi ke slave1 (round robin).
Bagaimana Jika Terjadi Failover?
Failover artinya:
- Kalau master down, sistem otomatis pilih salah satu slave untuk jadi pengganti master sementara.
- Biasanya di production dipakai orchestrator (MHA, Orchestrator, ProxySQL), tapi kita bisa buat logic sederhana di PHP.
Skema
Master (utama untuk write): 192.168.1.10
Slaves (read & backup):
- 192.168.1.11
- 192.168.1.12
Contoh Implementasi PHP MySQLi (Failover)
<?php
class DatabaseCluster {
private $master;
private $slaves = [];
private $slaveCount;
private $slaveIndex = 0;
private $currentMasterConfig;
public function __construct() {
// MASTER (untuk INSERT/UPDATE/DELETE)
$this->currentMasterConfig = ["192.168.1.10", "user_master", "password_master", "shopdb"];
$this->connectMaster();
// SLAVES (untuk SELECT)
$slaveConfigs = [
["192.168.1.11", "user_slave", "password_slave", "shopdb"],
["192.168.1.12", "user_slave", "password_slave", "shopdb"],
];
foreach ($slaveConfigs as $cfg) {
$conn = new mysqli($cfg[0], $cfg[1], $cfg[2], $cfg[3]);
if ($conn->connect_error) {
echo "⚠️ Koneksi ke Slave {$cfg[0]} gagal: " . $conn->connect_error . "<br>";
} else {
$this->slaves[] = $conn;
}
}
$this->slaveCount = count($this->slaves);
}
// Fungsi untuk koneksi ke MASTER
private function connectMaster() {
$this->master = @new mysqli(
$this->currentMasterConfig[0],
$this->currentMasterConfig[1],
$this->currentMasterConfig[2],
$this->currentMasterConfig[3]
);
if ($this->master->connect_error) {
echo "⚠️ Master DB down ({$this->currentMasterConfig[0]}). Ganti ke slave sebagai master.<br>";
$this->promoteSlaveAsMaster();
}
}
// Jika master down, pilih salah satu slave jadi master sementara
private function promoteSlaveAsMaster() {
if ($this->slaveCount > 0) {
$this->master = $this->slaves[0]; // Slave pertama jadi master sementara
echo "✅ Slave dipromosikan jadi master sementara.<br>";
} else {
die("❌ Tidak ada slave tersedia untuk failover!");
}
}
// ROUND ROBIN untuk SELECT
private function getSlaveConnection() {
if ($this->slaveCount == 0) {
return $this->master; // fallback ke master kalau tidak ada slave
}
$conn = $this->slaves[$this->slaveIndex];
$this->slaveIndex = ($this->slaveIndex + 1) % $this->slaveCount;
return $conn;
}
// Query SELECT → ke SLAVE
public function queryRead($sql) {
$conn = $this->getSlaveConnection();
$result = $conn->query($sql);
if (!$result) {
die("Error SELECT: " . $conn->error);
}
return $result;
}
// Query INSERT/UPDATE/DELETE → ke MASTER (atau slave jika failover)
public function queryWrite($sql) {
$result = $this->master->query($sql);
if (!$result) {
die("Error Write: " . $this->master->error);
}
return $result;
}
public function __destruct() {
if ($this->master && $this->master->ping()) {
$this->master->close();
}
foreach ($this->slaves as $conn) {
if ($conn && $conn->ping()) {
$conn->close();
}
}
}
}
?>
Contoh Penggunaan:
<?php
require_once "DatabaseCluster.php";
$db = new DatabaseCluster();
// Coba INSERT (jika master down, otomatis dialihkan ke slave yang dipromosikan)
$db->queryWrite("INSERT INTO products (name, price) VALUES ('Produk Failover', 75000)");
// SELECT (round robin ke slave)
$result = $db->queryRead("SELECT * FROM products LIMIT 5");
while ($row = $result->fetch_assoc()) {
echo $row['name'] . " - Rp " . $row['price'] . "<br>";
}
?>
Penjelasan
- Saat master mati → koneksi gagal → otomatis slave pertama dipromosikan jadi master sementara.
- Query write (INSERT/UPDATE/DELETE) otomatis diarahkan ke master baru (slave).
- Query read tetap memakai round robin antar slave.
- Bisa ditambah log error/alert (misalnya kirim email ke admin kalau master down).
Implementasi dengan Load Balancing dan Failover Otomatis
Load balancing untuk query SELECT → pilih slave yang sehat (ping ok).
Failover otomatis untuk WRITE → kalau master down, cari slave terbaik lalu jadikan master sementara.
Kalau ada banyak slave, sistem bisa skip yang error.
Skema
Master (utama untuk write) → 192.168.1.10
Slaves (read + backup):
- 192.168.1.11
- 192.168.1.12
- 192.168.1.13
Class Database Cluster (Load Balancing + Failover)
<?php
class DatabaseCluster {
private $master;
private $masterConfig;
private $slaves = [];
private $slaveConfigs;
private $slaveCount;
private $slaveIndex = 0;
public function __construct() {
// MASTER (utama untuk write)
$this->masterConfig = ["192.168.1.10", "user_master", "password_master", "shopdb"];
$this->connectMaster();
// SLAVES (untuk read + backup)
$this->slaveConfigs = [
["192.168.1.11", "user_slave", "password_slave", "shopdb"],
["192.168.1.12", "user_slave", "password_slave", "shopdb"],
["192.168.1.13", "user_slave", "password_slave", "shopdb"],
];
$this->connectSlaves();
}
/** ===============================
* MASTER HANDLER
* =============================== */
private function connectMaster() {
$this->master = @new mysqli(
$this->masterConfig[0],
$this->masterConfig[1],
$this->masterConfig[2],
$this->masterConfig[3]
);
if ($this->master->connect_error) {
echo "⚠️ Master {$this->masterConfig[0]} gagal, cari slave sebagai master baru...<br>";
$this->promoteSlaveAsMaster();
}
}
private function promoteSlaveAsMaster() {
foreach ($this->slaveConfigs as $cfg) {
$conn = @new mysqli($cfg[0], $cfg[1], $cfg[2], $cfg[3]);
if (!$conn->connect_error) {
$this->master = $conn;
echo "✅ Slave {$cfg[0]} dipromosikan jadi master sementara.<br>";
return;
}
}
die("❌ Tidak ada slave sehat untuk dijadikan master!");
}
/** ===============================
* SLAVE HANDLER
* =============================== */
private function connectSlaves() {
foreach ($this->slaveConfigs as $cfg) {
$conn = @new mysqli($cfg[0], $cfg[1], $cfg[2], $cfg[3]);
if (!$conn->connect_error) {
$this->slaves[] = $conn;
} else {
echo "⚠️ Slave {$cfg[0]} tidak bisa dihubungi, dilewati.<br>";
}
}
$this->slaveCount = count($this->slaves);
}
private function getHealthySlave() {
if ($this->slaveCount == 0) {
// fallback ke master
return $this->master;
}
// Round robin untuk sebar beban
for ($i = 0; $i < $this->slaveCount; $i++) {
$conn = $this->slaves[$this->slaveIndex];
$this->slaveIndex = ($this->slaveIndex + 1) % $this->slaveCount;
if ($conn->ping()) {
return $conn;
}
}
// Kalau semua slave mati → fallback ke master
echo "⚠️ Semua slave mati, fallback ke master.<br>";
return $this->master;
}
/** ===============================
* QUERY HANDLER
* =============================== */
public function queryRead($sql) {
$conn = $this->getHealthySlave();
$result = $conn->query($sql);
if (!$result) {
die("Error SELECT: " . $conn->error);
}
return $result;
}
public function queryWrite($sql) {
if (!$this->master->ping()) {
echo "⚠️ Master mati saat query, coba failover...<br>";
$this->promoteSlaveAsMaster();
}
$result = $this->master->query($sql);
if (!$result) {
die("Error WRITE: " . $this->master->error);
}
return $result;
}
/** ===============================
* CLEANUP
* =============================== */
public function __destruct() {
if ($this->master && $this->master->ping()) {
$this->master->close();
}
foreach ($this->slaves as $conn) {
if ($conn && $conn->ping()) {
$conn->close();
}
}
}
}
?>
Contoh Penggunaan:
<?php
require_once "DatabaseCluster.php";
$db = new DatabaseCluster();
// WRITE → ke master, atau slave dipromosikan kalau master mati
$db->queryWrite("INSERT INTO products (name, price) VALUES ('Produk LoadBalance', 99000)");
// READ → ke slave (round robin, skip yang mati)
for ($i = 1; $i <= 5; $i++) {
$result = $db->queryRead("SELECT * FROM products LIMIT 3");
echo "<b>Query ke-$i:</b><br>";
while ($row = $result->fetch_assoc()) {
echo $row['name'] . " - Rp " . $row['price'] . "<br>";
}
echo "<hr>";
}
?>
Penjelasan
- Load Balancing SELECT → round robin antar slave, skip yang mati.
- Failover WRITE → kalau master down, cari slave sehat → langsung dipromosikan jadi master sementara.
- Fallback → kalau semua slave mati, SELECT diarahkan ke master.
- Bisa dikembangkan dengan health check lebih advance (misalnya test query SELECT 1 tiap 5 detik).
- Bisa ditambahkan notifikasi (misalnya kirim email/Telegram kalau master down).
Implementasi health check otomatis berkala
Tujuannya:
- Secara berkala (misalnya setiap 10 detik via cron job), sistem akan cek apakah master/slave sehat.
- Kalau ada node mati → ditandai statusnya down, tidak dipakai untuk query.
- Kalau master mati → otomatis pilih slave sehat jadi master baru.
Struktur Tabel (opsional, untuk monitor)
Bisa simpan status server di tabel db_nodes misalnya:
CREATE TABLE db_nodes (
id INT AUTO_INCREMENT PRIMARY KEY,
role ENUM('master','slave') NOT NULL,
host VARCHAR(100) NOT NULL,
user VARCHAR(50) NOT NULL,
pass VARCHAR(50) NOT NULL,
dbname VARCHAR(50) NOT NULL,
status ENUM('up','down') DEFAULT 'up',
last_check TIMESTAMP NULL
);
Script Health Check (cron job, misalnya healthcheck.php)
<?php
// Jalankan via cron setiap 10 detik
// * * * * * php /var/www/html/healthcheck.php
$mysqli = new mysqli("127.0.0.1", "root", "password", "clusterdb");
// Ambil semua node
$res = $mysqli->query("SELECT * FROM db_nodes");
while ($node = $res->fetch_assoc()) {
$conn = @new mysqli($node['host'], $node['user'], $node['pass'], $node['dbname']);
if ($conn->connect_error || !$conn->ping()) {
// Tandai DOWN
$mysqli->query("UPDATE db_nodes SET status='down', last_check=NOW() WHERE id={$node['id']}");
echo "❌ {$node['role']} {$node['host']} DOWN\n";
} else {
// Tandai UP
$mysqli->query("UPDATE db_nodes SET status='up', last_check=NOW() WHERE id={$node['id']}");
echo "✅ {$node['role']} {$node['host']} UP\n";
}
$conn->close();
}
?>
Cluster Manager (gunakan hasil health check)
<?php
class DatabaseCluster {
private $master;
private $slaves = [];
private $slaveIndex = 0;
public function __construct() {
$this->loadNodes();
}
// Ambil node dari tabel db_nodes yang sehat
private function loadNodes() {
$db = new mysqli("127.0.0.1", "root", "password", "clusterdb");
// Master
$res = $db->query("SELECT * FROM db_nodes WHERE role='master' AND status='up' LIMIT 1");
if ($res->num_rows > 0) {
$node = $res->fetch_assoc();
$this->master = new mysqli($node['host'], $node['user'], $node['pass'], $node['dbname']);
} else {
// Failover: pilih slave sehat sebagai master
$res2 = $db->query("SELECT * FROM db_nodes WHERE role='slave' AND status='up' LIMIT 1");
if ($res2->num_rows > 0) {
$node = $res2->fetch_assoc();
$this->master = new mysqli($node['host'], $node['user'], $node['pass'], $node['dbname']);
echo "⚠️ Master mati, slave {$node['host']} dipromosikan sementara.\n";
} else {
die("❌ Tidak ada node sehat untuk dijadikan master.");
}
}
// Slaves
$res = $db->query("SELECT * FROM db_nodes WHERE role='slave' AND status='up'");
while ($node = $res->fetch_assoc()) {
$this->slaves[] = new mysqli($node['host'], $node['user'], $node['pass'], $node['dbname']);
}
}
// Round robin slave
private function getSlave() {
if (count($this->slaves) == 0) return $this->master;
$conn = $this->slaves[$this->slaveIndex];
$this->slaveIndex = ($this->slaveIndex + 1) % count($this->slaves);
return $conn;
}
// Query SELECT → slave (atau master fallback)
public function queryRead($sql) {
$conn = $this->getSlave();
$res = $conn->query($sql);
if (!$res) die("Error SELECT: " . $conn->error);
return $res;
}
// Query WRITE → master
public function queryWrite($sql) {
$res = $this->master->query($sql);
if (!$res) die("Error WRITE: " . $this->master->error);
return $res;
}
}
?>
Contoh Penggunaan
<?php
require_once "DatabaseCluster.php";
$db = new DatabaseCluster();
// WRITE → ke master
$db->queryWrite("INSERT INTO products (name, price) VALUES ('Produk HealthCheck', 123456)");
// READ → ke slave yang sehat (round robin)
$res = $db->queryRead("SELECT * FROM products LIMIT 5");
while ($row = $res->fetch_assoc()) {
echo $row['name'] . " - Rp " . $row['price'] . "<br>";
}
?>
Penjelasan
- Cron job health check: cek koneksi tiap node (ping) → update status up/down di DB.
- Cluster Manager: hanya pakai node yang statusnya up.
- Failover otomatis: kalau master down, pilih salah satu slave sehat jadi master sementara.
- Load balancing: query SELECT didistribusikan antar slave yang sehat.
- Bisa diperluas dengan: Logging ke file (untuk audit), kirim notifikasi (email/Telegram) kalau master down, auto reconfig db_nodes kalau slave dipromosikan jadi master permanen.
Pingback: Kombinasi Database Cluster Master Slave dan Sharding Database untuk Solusi Jutaan Data - Tutorial