- 6-node MariaDB cluster with GTID replication - MaxScale 24.02 proxy with automatic failover - Flask dashboard with SSE transaction monitor - Per-server toggle controls + mode selector - Systemd services for auto-start on boot - One-command deploy.sh
397 lines
12 KiB
Bash
Executable file
397 lines
12 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# ── MariaDB HA Cluster Control ───────────────────────────
|
|
# Manages 6 MariaDB nodes (DC1+DC2) + 2 MaxScale proxies.
|
|
# Idempotent: create if missing, start if stopped, skip if running.
|
|
#
|
|
# Usage: cluster.sh {start|stop|status}
|
|
#
|
|
# Persists across reboots — all volumes under $HOME, not /tmp.
|
|
set -euo pipefail
|
|
|
|
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
readonly CONFIG_DIR="${SCRIPT_DIR}/../cluster-config"
|
|
|
|
# ── Container definitions ────────────────────────────────
|
|
# Format: name|image|host_port|container_port|config_file|admin_port(optional)
|
|
readonly NODES_DC1=(
|
|
"mariadb1|mariadb:latest|3307|3306|${CONFIG_DIR}/server1.cnf"
|
|
"mariadb2|mariadb:latest|3308|3306|${CONFIG_DIR}/server2.cnf"
|
|
"mariadb3|mariadb:latest|3309|3306|${CONFIG_DIR}/server3.cnf"
|
|
)
|
|
readonly NODES_DC2=(
|
|
"mariadb4|mariadb:latest|3310|3306|${CONFIG_DIR}/server4.cnf"
|
|
"mariadb5|mariadb:latest|3311|3306|${CONFIG_DIR}/server5.cnf"
|
|
"mariadb6|mariadb:latest|3312|3306|${CONFIG_DIR}/server6.cnf"
|
|
)
|
|
|
|
readonly PROXIES=(
|
|
"maxscale1|mariadb/maxscale:latest|4000|4000|${CONFIG_DIR}/maxscale.cnf|8989"
|
|
)
|
|
|
|
readonly MARIADB_USER="root"
|
|
readonly MARIADB_PASS="monitor123"
|
|
readonly MAXSCALE_PORT=4000
|
|
readonly MARIADB_TEST_PORT=3307
|
|
readonly MAX_POLL_SECONDS=60
|
|
readonly POLL_INTERVAL=2
|
|
|
|
# ── Pure helpers ─────────────────────────────────────────
|
|
|
|
_container_exists() {
|
|
local name="$1"
|
|
docker ps -a --format '{{.Names}}' --filter "name=^${name}$" | grep -q .
|
|
}
|
|
|
|
_container_running() {
|
|
local name="$1"
|
|
docker ps --format '{{.Names}}' --filter "name=^${name}$" | grep -q .
|
|
}
|
|
|
|
_mariadb_healthy() {
|
|
mariadb -h 127.0.0.1 -P "${MARIADB_TEST_PORT}" \
|
|
-u "${MARIADB_USER}" -p"${MARIADB_PASS}" \
|
|
--skip-ssl -e "SELECT 1" &>/dev/null
|
|
}
|
|
|
|
_maxscale_healthy() {
|
|
mariadb -h 127.0.0.1 -P "${MAXSCALE_PORT}" \
|
|
-u "${MARIADB_USER}" -p"${MARIADB_PASS}" \
|
|
--skip-ssl -e "SELECT 1" &>/dev/null
|
|
}
|
|
|
|
_poll_health() {
|
|
local label="$1"
|
|
local check_fn="$2"
|
|
local elapsed=0
|
|
|
|
while (( elapsed < MAX_POLL_SECONDS )); do
|
|
if ${check_fn}; then
|
|
echo " [OK] ${label} ready (${elapsed}s)"
|
|
return 0
|
|
fi
|
|
sleep "${POLL_INTERVAL}"
|
|
elapsed=$(( elapsed + POLL_INTERVAL ))
|
|
done
|
|
echo " [FAIL] ${label} not ready after ${MAX_POLL_SECONDS}s" >&2
|
|
return 1
|
|
}
|
|
|
|
_create_node() {
|
|
local name="$1" image="$2" host_port="$3" cont_port="$4" config="$5"
|
|
|
|
if _container_exists "${name}"; then
|
|
if _container_running "${name}"; then
|
|
echo " [SKIP] ${name} already running"
|
|
return 0
|
|
fi
|
|
echo " [START] ${name} (existing container)"
|
|
docker start "${name}" >/dev/null
|
|
return 0
|
|
fi
|
|
|
|
echo " [CREATE] ${name} (port ${host_port})"
|
|
docker run -d --name "${name}" \
|
|
-e MYSQL_ROOT_PASSWORD="${MARIADB_PASS}" \
|
|
-p "${host_port}:${cont_port}" \
|
|
-v "${config}:/etc/mysql/conf.d/seb.cnf:ro" \
|
|
"${image}" >/dev/null
|
|
}
|
|
|
|
_create_proxy() {
|
|
local name="$1" image="$2" host_port="$3" cont_port="$4" config="$5" admin_port="$6"
|
|
|
|
if _container_exists "${name}"; then
|
|
if _container_running "${name}"; then
|
|
echo " [SKIP] ${name} already running"
|
|
return 0
|
|
fi
|
|
echo " [START] ${name} (existing container)"
|
|
docker start "${name}" >/dev/null
|
|
return 0
|
|
fi
|
|
|
|
echo " [CREATE] ${name} (port ${host_port}, admin :${admin_port})"
|
|
docker run -d --name "${name}" \
|
|
-p "${host_port}:${cont_port}" \
|
|
-p "${admin_port}:8989" \
|
|
-v "${config}:/etc/maxscale.cnf:ro" \
|
|
"${image}" >/dev/null
|
|
|
|
# Create REST API user after container is ready (only for first container)
|
|
if [[ "${name}" == "maxscale1" ]]; then
|
|
sleep 3
|
|
docker exec "${name}" maxctrl create user monitor monitor123 --type=admin 2>/dev/null || true
|
|
fi
|
|
}
|
|
|
|
# ── Commands ─────────────────────────────────────────────
|
|
|
|
cmd_start() {
|
|
echo "=== MariaDB Cluster: Starting ==="
|
|
|
|
# 1. Start all MariaDB nodes (DC1 + DC2)
|
|
for entry in "${NODES_DC1[@]}" "${NODES_DC2[@]}"; do
|
|
IFS='|' read -r name image host_port cont_port config <<< "${entry}"
|
|
_create_node "${name}" "${image}" "${host_port}" "${cont_port}" "${config}"
|
|
done
|
|
|
|
# 2. Wait for at least one MariaDB node
|
|
echo " [WAIT] Polling MariaDB on :${MARIADB_TEST_PORT} ..."
|
|
_poll_health "MariaDB" _mariadb_healthy || exit 1
|
|
|
|
# 3. Generate MaxScale config (after all node IPs are known)
|
|
local mx_config="${CONFIG_DIR}/maxscale.cnf"
|
|
if ! _container_exists "maxscale1"; then
|
|
echo " [CONFIG] Generating ${mx_config}"
|
|
_generate_maxscale_config "${mx_config}"
|
|
fi
|
|
|
|
# 4. Start MaxScale proxies
|
|
for entry in "${PROXIES[@]}"; do
|
|
IFS='|' read -r name image host_port cont_port config admin_port <<< "${entry}"
|
|
_create_proxy "${name}" "${image}" "${host_port}" "${cont_port}" "${config}" "${admin_port}"
|
|
done
|
|
|
|
# 5. Wait for MaxScale
|
|
echo " [WAIT] Polling MaxScale on :${MAXSCALE_PORT} ..."
|
|
_poll_health "MaxScale" _maxscale_healthy || exit 1
|
|
|
|
# 6. Setup replication if needed (idempotent)
|
|
_setup_replication
|
|
|
|
echo "=== Cluster ready ==="
|
|
}
|
|
|
|
cmd_stop() {
|
|
echo "=== MariaDB Cluster: Stopping ==="
|
|
|
|
# Stop proxies first, then nodes
|
|
local containers=()
|
|
for entry in "${PROXIES[@]}"; do
|
|
IFS='|' read -r name _ <<< "${entry}"
|
|
containers+=("${name}")
|
|
done
|
|
for entry in "${NODES_DC1[@]}" "${NODES_DC2[@]}"; do
|
|
IFS='|' read -r name _ <<< "${entry}"
|
|
containers+=("${name}")
|
|
done
|
|
|
|
for name in "${containers[@]}"; do
|
|
if _container_running "${name}"; then
|
|
echo " [STOP] ${name}"
|
|
docker stop "${name}" >/dev/null
|
|
else
|
|
echo " [SKIP] ${name} not running"
|
|
fi
|
|
done
|
|
|
|
echo "=== Cluster stopped ==="
|
|
}
|
|
|
|
cmd_status() {
|
|
echo "=== MariaDB Cluster: Status ==="
|
|
echo ""
|
|
|
|
echo "Cluster 1 (ports 3307-3309):"
|
|
for entry in "${NODES_DC1[@]}"; do
|
|
IFS='|' read -r name _ host_port _ _ <<< "${entry}"
|
|
if _container_running "${name}"; then
|
|
echo " ${name} :${host_port} [RUNNING]"
|
|
elif _container_exists "${name}"; then
|
|
echo " ${name} :${host_port} [STOPPED]"
|
|
else
|
|
echo " ${name} :${host_port} [MISSING]"
|
|
fi
|
|
done
|
|
|
|
echo ""
|
|
echo "Cluster 2 (ports 3310-3312):"
|
|
for entry in "${NODES_DC2[@]}"; do
|
|
IFS='|' read -r name _ host_port _ _ <<< "${entry}"
|
|
if _container_running "${name}"; then
|
|
echo " ${name} :${host_port} [RUNNING]"
|
|
elif _container_exists "${name}"; then
|
|
echo " ${name} :${host_port} [STOPPED]"
|
|
else
|
|
echo " ${name} :${host_port} [MISSING]"
|
|
fi
|
|
done
|
|
|
|
echo ""
|
|
echo "MaxScale:"
|
|
for entry in "${PROXIES[@]}"; do
|
|
IFS='|' read -r name _ host_port _ _ admin_port <<< "${entry}"
|
|
if _container_running "${name}"; then
|
|
echo " ${name} :${host_port} (admin :${admin_port}) [RUNNING]"
|
|
elif _container_exists "${name}"; then
|
|
echo " ${name} :${host_port} (admin :${admin_port}) [STOPPED]"
|
|
else
|
|
echo " ${name} :${host_port} (admin :${admin_port}) [MISSING]"
|
|
fi
|
|
done
|
|
|
|
echo ""
|
|
if _maxscale_healthy; then
|
|
echo "Health: ALL HEALTHY"
|
|
elif _mariadb_healthy; then
|
|
echo "Health: MariaDB UP, MaxScale DOWN"
|
|
else
|
|
echo "Health: DOWN"
|
|
fi
|
|
}
|
|
|
|
# ── MaxScale config generator ─────────────────────────────
|
|
# Unified config with DC1 (servers 1-3) + DC2 (servers 4-6)
|
|
|
|
_generate_maxscale_config() {
|
|
local out="$1"
|
|
local ip1 ip2 ip3 ip4 ip5 ip6
|
|
|
|
ip1=$(docker inspect -f '{{range $k,$v := .NetworkSettings.Networks}}{{$v.IPAddress}}{{end}}' mariadb1 2>/dev/null || echo "172.17.0.2")
|
|
ip2=$(docker inspect -f '{{range $k,$v := .NetworkSettings.Networks}}{{$v.IPAddress}}{{end}}' mariadb2 2>/dev/null || echo "172.17.0.3")
|
|
ip3=$(docker inspect -f '{{range $k,$v := .NetworkSettings.Networks}}{{$v.IPAddress}}{{end}}' mariadb3 2>/dev/null || echo "172.17.0.4")
|
|
ip4=$(docker inspect -f '{{range $k,$v := .NetworkSettings.Networks}}{{$v.IPAddress}}{{end}}' mariadb4 2>/dev/null || echo "172.17.0.5")
|
|
ip5=$(docker inspect -f '{{range $k,$v := .NetworkSettings.Networks}}{{$v.IPAddress}}{{end}}' mariadb5 2>/dev/null || echo "172.17.0.6")
|
|
ip6=$(docker inspect -f '{{range $k,$v := .NetworkSettings.Networks}}{{$v.IPAddress}}{{end}}' mariadb6 2>/dev/null || echo "172.17.0.7")
|
|
|
|
cat > "${out}" << EOF
|
|
[maxscale]
|
|
threads=auto
|
|
admin_host=0.0.0.0
|
|
admin_port=8989
|
|
admin_gui=true
|
|
admin_secure_gui=false
|
|
|
|
[server1]
|
|
type=server
|
|
address=${ip1}
|
|
port=3306
|
|
protocol=MariaDBBackend
|
|
|
|
[server2]
|
|
type=server
|
|
address=${ip2}
|
|
port=3306
|
|
protocol=MariaDBBackend
|
|
|
|
[server3]
|
|
type=server
|
|
address=${ip3}
|
|
port=3306
|
|
protocol=MariaDBBackend
|
|
|
|
[server4]
|
|
type=server
|
|
address=${ip4}
|
|
port=3306
|
|
protocol=MariaDBBackend
|
|
|
|
[server5]
|
|
type=server
|
|
address=${ip5}
|
|
port=3306
|
|
protocol=MariaDBBackend
|
|
|
|
[server6]
|
|
type=server
|
|
address=${ip6}
|
|
port=3306
|
|
protocol=MariaDBBackend
|
|
|
|
[HA-monitor]
|
|
type=monitor
|
|
module=mariadbmon
|
|
servers=server1,server2,server3,server4,server5,server6
|
|
user=${MARIADB_USER}
|
|
password=${MARIADB_PASS}
|
|
replication_user=repl
|
|
replication_password=repl123
|
|
monitor_interval=500ms
|
|
backend_connect_timeout=1s
|
|
backend_read_timeout=1s
|
|
failcount=1
|
|
verify_master_failure=false
|
|
master_failure_timeout=3000ms
|
|
auto_failover=true
|
|
auto_rejoin=true
|
|
|
|
[HA-RWS]
|
|
type=service
|
|
router=readwritesplit
|
|
servers=server1,server2,server3,server4,server5,server6
|
|
user=${MARIADB_USER}
|
|
password=${MARIADB_PASS}
|
|
enable_root_user=true
|
|
|
|
[HA-port]
|
|
type=listener
|
|
service=HA-RWS
|
|
protocol=MariaDBClient
|
|
port=4000
|
|
ssl=false
|
|
EOF
|
|
}
|
|
|
|
# ── Replication setup ─────────────────────────────────────
|
|
# Idempotent: skips if replication already running
|
|
|
|
_setup_replication() {
|
|
echo " [REPL] Setting up GTID replication..."
|
|
|
|
local ip1 ip2 ip3 ip4 ip5 ip6
|
|
ip1=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' mariadb1 2>/dev/null || echo "")
|
|
ip2=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' mariadb2 2>/dev/null || echo "")
|
|
ip3=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' mariadb3 2>/dev/null || echo "")
|
|
ip4=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' mariadb4 2>/dev/null || echo "")
|
|
ip5=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' mariadb5 2>/dev/null || echo "")
|
|
ip6=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' mariadb6 2>/dev/null || echo "")
|
|
|
|
_mariadb_exec() {
|
|
local port="$1" sql="$2"
|
|
mariadb -h 127.0.0.1 -P "${port}" -u root -p"${MARIADB_PASS}" --skip-ssl -e "${sql}" 2>/dev/null
|
|
}
|
|
|
|
_replication_running() {
|
|
local port="$1"
|
|
local status
|
|
status=$(_mariadb_exec "${port}" "SHOW SLAVE STATUS\G" 2>/dev/null)
|
|
echo "${status}" | grep -q "Slave_IO_Running: Yes" && \
|
|
echo "${status}" | grep -q "Slave_SQL_Running: Yes"
|
|
}
|
|
|
|
# DC1: mariadb1 → mariadb2, mariadb3
|
|
_mariadb_exec 3307 "CREATE USER IF NOT EXISTS 'repl'@'%' IDENTIFIED BY 'repl123'; GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%'; FLUSH PRIVILEGES;"
|
|
for port in 3308 3309; do
|
|
if _replication_running "${port}"; then
|
|
echo " [SKIP] Replication already running on :${port}"
|
|
continue
|
|
fi
|
|
_mariadb_exec "${port}" "STOP SLAVE;"
|
|
_mariadb_exec "${port}" "CHANGE MASTER TO MASTER_HOST='${ip1}', MASTER_PORT=3306, MASTER_USER='repl', MASTER_PASSWORD='repl123', MASTER_USE_GTID=slave_pos; START SLAVE;"
|
|
echo " [OK] Replication set up on :${port} → mariadb1"
|
|
done
|
|
|
|
# DC2: mariadb1 → mariadb4, mariadb5, mariadb6 (cross-DC DR replicas)
|
|
# Replication user already created on mariadb1 above
|
|
for port in 3310 3311 3312; do
|
|
if _replication_running "${port}"; then
|
|
echo " [SKIP] Replication already running on :${port}"
|
|
continue
|
|
fi
|
|
_mariadb_exec "${port}" "STOP SLAVE;"
|
|
_mariadb_exec "${port}" "CHANGE MASTER TO MASTER_HOST='${ip1}', MASTER_PORT=3306, MASTER_USER='repl', MASTER_PASSWORD='repl123', MASTER_USE_GTID=slave_pos; START SLAVE;"
|
|
echo " [OK] Replication set up on :${port} → mariadb1 (DC1 master)"
|
|
done
|
|
}
|
|
|
|
# ── Dispatch ──────────────────────────────────────────────
|
|
|
|
case "${1:-}" in
|
|
start) cmd_start ;;
|
|
stop) cmd_stop ;;
|
|
status) cmd_status ;;
|
|
*)
|
|
echo "Usage: $0 {start|stop|status}" >&2
|
|
exit 1
|
|
;;
|
|
esac
|