# Cluster MariaDB master-master en Debian 13: instalación paso a paso desde el repo oficial

*Tiempo de lectura estimado: 16 minutos.*

En la [quinta entrega de la serie MariaDB desde cero](/post/mariadb-desde-cero-v-replicacion-galera-y-produccion) pasé por encima de Galera, replicación asíncrona, backups y producción a vista de pájaro. Esta vez bajo al detalle: montar **un cluster de 3 nodos en Debian 13 trixie** desde cero, usando el **repo oficial de MariaDB Foundation**, y demostrar que funciona con pruebas reales (caídas, recuperación, conflictos de escritura, latencia).

Sobre la terminología: en el mundo MySQL/MariaDB *master-master* significa históricamente "dos primarias asíncronas en círculo". Aquí lo uso en el sentido más amplio y más usado hoy: **multi-master síncrono con Galera**. Es lo que la gente busca cuando dice "cluster master-master" en 2026 y es lo que tiene sentido montar en producción.

## Por qué Galera y por qué tres nodos

Galera resuelve dos problemas de la replicación clásica de un solo plumazo:

- **Cualquier nodo acepta escrituras.** No hay rol "primario" fijo, así que el failover deja de ser un procedimiento manual.
- **El commit es síncrono.** Cuando la transacción se confirma, los tres nodos ya la tienen. No hay ventana de pérdida de datos.

¿Por qué tres y no dos? Por el **quórum**. Galera necesita mayoría (`N/2 + 1`) para seguir aceptando escrituras. Con dos nodos, perder uno te deja con 1 de 2 (sin mayoría) y el cluster se bloquea. Con tres nodos, perder uno te deja con 2 de 3 y el cluster sigue. Es la diferencia entre "alta disponibilidad" y "alta disponibilidad de verdad".

Si solo tienes presupuesto para dos máquinas reales, una solución habitual es añadir un **garbd** (Galera Arbitrator) en una tercera VM minúscula: aporta voto sin almacenar datos. Pero aquí montamos tres nodos completos.

## Topología

Tres VMs Debian 13 trixie en una red privada `/24`. Todo lo que sigue asume estas IPs y nombres; sustituye por los tuyos:

| Hostname    | IP            | Rol           |
|-------------|---------------|---------------|
| `db-01.lan` | `10.10.0.11`  | bootstrap     |
| `db-02.lan` | `10.10.0.12`  | nodo          |
| `db-03.lan` | `10.10.0.13`  | nodo          |

Recursos por VM (mínimo razonable): 2 vCPU, 4 GB RAM, 40 GB de disco. Para producción con carga real, dimensiona según `innodb_buffer_pool_size`.

Puertos que usa Galera:

- **3306/tcp** — cliente SQL.
- **4567/tcp+udp** — replicación entre nodos (gcomm).
- **4568/tcp** — transferencia incremental de estado (IST).
- **4444/tcp** — transferencia completa de estado (SST).

## Preparación del sistema (en los 3 nodos)

Lo siguiente se hace **en cada nodo**, idéntico salvo el hostname y la IP.

### Hostnames y DNS local

Sin DNS interno, el `/etc/hosts` es el mejor amigo. En los tres nodos:

```bash
sudo hostnamectl set-hostname db-01.lan   # ajusta por nodo

sudo tee -a /etc/hosts <<'EOF'
10.10.0.11  db-01.lan db-01
10.10.0.12  db-02.lan db-02
10.10.0.13  db-03.lan db-03
EOF
```

Compruébalo:

```bash
for n in db-01 db-02 db-03; do ping -c1 -W1 $n >/dev/null && echo "$n OK"; done
# db-01 OK
# db-02 OK
# db-03 OK
```

### Reloj sincronizado

Galera detesta el reloj suelto. Debian 13 trae `systemd-timesyncd` activado de serie, pero verifícalo:

```bash
timedatectl status | grep -E "System clock|NTP service"
# System clock synchronized: yes
#               NTP service: active
```

Si por lo que sea estuviera en `no`/`inactive`, arréglalo antes de continuar:

```bash
sudo timedatectl set-ntp true
```

### Firewall

Si tienes `nftables` o `ufw` activo, abre los puertos entre los nodos (no al mundo). Con `nftables` directo:

```bash
sudo nft add rule inet filter input ip saddr 10.10.0.0/24 \
  tcp dport { 3306, 4567, 4568, 4444 } accept
sudo nft add rule inet filter input ip saddr 10.10.0.0/24 \
  udp dport 4567 accept
```

Con `ufw`:

```bash
sudo ufw allow from 10.10.0.0/24 to any port 3306 proto tcp
sudo ufw allow from 10.10.0.0/24 to any port 4567
sudo ufw allow from 10.10.0.0/24 to any port 4568 proto tcp
sudo ufw allow from 10.10.0.0/24 to any port 4444 proto tcp
```

**No abras 3306 al exterior**. El acceso de aplicaciones debe pasar por una VIP, ProxySQL o MaxScale, y el tráfico entre nodos vive en la red privada.

### AppArmor

Debian 13 mantiene AppArmor activo. El paquete de MariaDB instala su propio perfil, pero ten a mano `aa-status` por si algún directorio custom (datadir alternativo, por ejemplo) requiere ajuste:

```bash
sudo aa-status | grep mariadbd
```

## Repo oficial de MariaDB

Debian 13 trixie incluye MariaDB en sus repos, pero queremos la versión que elijamos nosotros y soporte directo del upstream. La forma limpia es usar el script oficial de MariaDB Foundation, que añade el repo correcto según la distro y la versión.

Vamos a instalar **MariaDB 11.4 LTS** (soporte hasta 2029, estable y muy probada con Galera):

```bash
sudo apt update
sudo apt install -y curl ca-certificates apt-transport-https gnupg

curl -LsS https://r.mariadb.com/downloads/mariadb_repo_setup \
  | sudo bash -s -- --mariadb-server-version="mariadb-11.4" --skip-maxscale
```

Salida esperada:

```
# [info] Repository file successfully written to /etc/apt/sources.list.d/mariadb.list
# [info] Adding trusted package signing keys...
# [info] Successfully added trusted package signing keys
# [info] Cleaning package cache...
```

Comprueba el repo añadido:

```bash
cat /etc/apt/sources.list.d/mariadb.sources
# X-Repolib-Name: MariaDB
# Types: deb
# URIs: https://deb.mariadb.org/11.4/debian
# Suites: trixie
# Components: main
# Signed-By: /etc/apt/keyrings/mariadb-keyring.pgp
```

## Instalación de MariaDB y Galera

El paquete `mariadb-server` ya trae el proveedor wsrep (Galera) integrado. Aún así conviene instalar `galera-4` y `mariadb-backup` (necesario para SST con `mariabackup`):

```bash
sudo apt update
sudo apt install -y mariadb-server mariadb-backup galera-4
```

Verifica versiones:

```bash
mariadb --version
# mariadb from 11.4.5-MariaDB, client 15.2 for debian-linux-gnu (x86_64)

dpkg -l | grep -E "mariadb-server|galera-4|mariadb-backup" | awk '{print $2, $3}'
# galera-4 26.4.21-deb13
# mariadb-backup 1:11.4.5+maria~deb13
# mariadb-server 1:11.4.5+maria~deb13
```

Justo después de instalar, el servicio arranca con la config por defecto en standalone. Lo paramos en los tres nodos antes de configurar Galera:

```bash
sudo systemctl stop mariadb
sudo systemctl status mariadb --no-pager | head -3
# ● mariadb.service - MariaDB 11.4.5 database server
#      Loaded: loaded (/lib/systemd/system/mariadb.service; enabled; preset: enabled)
#      Active: inactive (dead)
```

### mariadb-secure-installation (antes del cluster)

Lo hacemos **una vez por nodo** mientras el servicio está aún en modo standalone (lo levantamos un momento, securizamos, lo paramos):

```bash
sudo systemctl start mariadb
sudo mariadb-secure-installation
```

Responde:

- `Enter current password for root`: vacío (autenticación por socket en Debian).
- `Switch to unix_socket authentication?`: **n** (ya está activo desde 10.4; redundante).
- `Change the root password?`: **n** (root local, sin contraseña, autenticación por socket; más seguro que cualquier contraseña).
- `Remove anonymous users?`: **Y**.
- `Disallow root login remotely?`: **Y**.
- `Remove test database?`: **Y**.
- `Reload privilege tables now?`: **Y**.

Para el servicio antes de configurar Galera:

```bash
sudo systemctl stop mariadb
```

## Configuración Galera

Crea el fichero `/etc/mysql/mariadb.conf.d/60-galera.cnf` en **los tres nodos**. La mayor parte es idéntica; las dos últimas líneas (`wsrep_node_address` y `wsrep_node_name`) son específicas de cada nodo.

```ini
[galera]
# Activación
wsrep_on                 = ON
wsrep_provider           = /usr/lib/galera/libgalera_smm.so

# Identidad del cluster
wsrep_cluster_name       = "blog_cluster"
wsrep_cluster_address    = "gcomm://10.10.0.11,10.10.0.12,10.10.0.13"

# Identidad del nodo (ajusta en cada máquina)
wsrep_node_address       = "10.10.0.11"
wsrep_node_name          = "db-01"

# SST (State Snapshot Transfer)
wsrep_sst_method         = mariabackup
wsrep_sst_auth           = "sstuser:CAMBIA_ESTO"

# Requisitos de Galera
binlog_format            = ROW
default_storage_engine   = InnoDB
innodb_autoinc_lock_mode = 2

# Aceptar conexiones de otros nodos
bind_address             = 0.0.0.0

# Tuning razonable de partida
wsrep_slave_threads      = 4
wsrep_provider_options   = "gcache.size=512M; gcs.fc_limit=128"
```

Notas:

- `gcomm://...` con todas las IPs es lo correcto en arranque normal. En el bootstrap se usa una variante distinta (la veremos en un momento).
- `innodb_autoinc_lock_mode = 2` es obligatorio en Galera. Con valor 1 las escrituras concurrentes en tablas con `AUTO_INCREMENT` se serializan de forma que rompe el cluster.
- `gcache.size=512M` permite que un nodo que estuvo caído pueda recuperarse con IST (transferencia incremental) si vuelve dentro de la ventana. Si tarda más, hará SST completa.

En `db-02.lan`:

```ini
wsrep_node_address       = "10.10.0.12"
wsrep_node_name          = "db-02"
```

En `db-03.lan`:

```ini
wsrep_node_address       = "10.10.0.13"
wsrep_node_name          = "db-03"
```

### Usuario SST

`mariabackup` necesita un usuario en MariaDB para hacer las transferencias de estado entre nodos. Lo creamos **solo en el primer nodo** (después se replica solo); arrancamos brevemente standalone, lo creamos, y paramos:

```bash
sudo systemctl start mariadb
sudo mariadb <<'SQL'
CREATE USER 'sstuser'@'localhost' IDENTIFIED BY 'CAMBIA_ESTO';
GRANT PROCESS, RELOAD, LOCK TABLES, BINLOG MONITOR, REPLICA MONITOR
  ON *.* TO 'sstuser'@'localhost';
GRANT SELECT ON mysql.* TO 'sstuser'@'localhost';
FLUSH PRIVILEGES;
SQL
sudo systemctl stop mariadb
```

La contraseña tiene que coincidir con la de `wsrep_sst_auth` del fichero `60-galera.cnf`.

## Bootstrap del cluster

El bootstrap es **solo la primera vez en la vida del cluster**, y solo en **uno** de los nodos. El elegido inicia el cluster en estado `Primary` con `wsrep_cluster_address = gcomm://` (sin IPs, indicando que es la semilla).

En `db-01.lan`:

```bash
sudo galera_new_cluster
```

Este wrapper de Debian lanza `mariadbd` con `--wsrep-new-cluster`. Verifica:

```bash
sudo systemctl status mariadb --no-pager | head -5
# ● mariadb.service - MariaDB 11.4.5 database server
#      Loaded: loaded (/lib/systemd/system/mariadb.service; enabled; preset: enabled)
#      Active: active (running) since Fri 2026-05-15 09:42:11 CEST; 4s ago

sudo mariadb -e "SHOW STATUS LIKE 'wsrep_cluster_size';"
# +--------------------+-------+
# | Variable_name      | Value |
# +--------------------+-------+
# | wsrep_cluster_size | 1     |
# +--------------------+-------+
```

Un nodo solo, en estado primary. Ahora levantamos los otros dos como un servicio normal.

En `db-02.lan` y `db-03.lan` (uno tras otro, no en paralelo):

```bash
sudo systemctl start mariadb
```

La primera vez harán SST desde `db-01` usando `mariabackup`. En el log lo ves claro:

```bash
sudo journalctl -u mariadb -n 30 --no-pager | grep -E "WSREP|SST"
# ... WSREP: Member 0.0 (db-02) requested state transfer from '*any*'
# ... WSREP: Running: 'wsrep_sst_mariabackup --role 'joiner' ...'
# ... WSREP: SST received: ...
# ... WSREP: Member 0.0 (db-02) synced with group.
```

Cuando los tres están arriba:

```bash
sudo mariadb -e "SHOW STATUS LIKE 'wsrep_cluster_size';"
# +--------------------+-------+
# | Variable_name      | Value |
# +--------------------+-------+
# | wsrep_cluster_size | 3     |
# +--------------------+-------+
```

## Verificación del cluster

Las cuatro variables que miro siempre, en cada nodo:

```bash
sudo mariadb -e "
  SHOW STATUS WHERE Variable_name IN (
    'wsrep_cluster_size',
    'wsrep_cluster_status',
    'wsrep_ready',
    'wsrep_local_state_comment'
  );"
# +---------------------------+----------+
# | Variable_name             | Value    |
# +---------------------------+----------+
# | wsrep_cluster_size        | 3        |
# | wsrep_cluster_status      | Primary  |
# | wsrep_local_state_comment | Synced   |
# | wsrep_ready               | ON       |
# +---------------------------+----------+
```

Los cuatro valores tienen que ser los de arriba en los tres nodos. Si uno se queda en `Donor/Desynced` un rato es normal mientras envía SST a otro; debe volver a `Synced` en segundos.

También útil:

```bash
sudo mariadb -e "SHOW STATUS LIKE 'wsrep_incoming_addresses';"
# +--------------------------+-----------------------------------------------------+
# | Variable_name            | Value                                               |
# +--------------------------+-----------------------------------------------------+
# | wsrep_incoming_addresses | 10.10.0.11:3306,10.10.0.12:3306,10.10.0.13:3306     |
# +--------------------------+-----------------------------------------------------+
```

## Pruebas reales de funcionamiento

Aquí viene lo interesante: demostrar que el cluster hace lo que dice. Todas las pruebas son reproducibles con la instalación de arriba.

### Prueba 1: escritura en un nodo, lectura en los otros dos

En `db-01`:

```sql
CREATE DATABASE demo;
USE demo;

CREATE TABLE notas (
  id        BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  contenido VARCHAR(200) NOT NULL,
  origen    VARCHAR(32)  NOT NULL,
  ts        TIMESTAMP    DEFAULT CURRENT_TIMESTAMP
);

INSERT INTO notas (contenido, origen) VALUES ('hola desde db-01', @@hostname);
```

Inmediatamente en `db-02`:

```sql
SELECT * FROM demo.notas;
-- +----+------------------+--------+---------------------+
-- | id | contenido        | origen | ts                  |
-- +----+------------------+--------+---------------------+
-- |  1 | hola desde db-01 | db-01  | 2026-05-15 09:51:02 |
-- +----+------------------+--------+---------------------+
```

Y en `db-03`:

```sql
SELECT * FROM demo.notas;
-- (mismo resultado)
```

### Prueba 2: escritura en cada nodo

Para que sea master-master de verdad, todos los nodos tienen que aceptar escrituras:

```bash
# Desde el host de control, lanzando contra cada nodo
for n in db-01 db-02 db-03; do
  mariadb -h $n -uroot -e \
    "INSERT INTO demo.notas (contenido, origen) VALUES ('eco', @@hostname);"
done
```

Lectura en cualquier nodo:

```sql
SELECT id, origen, ts FROM demo.notas ORDER BY id;
-- +----+--------+---------------------+
-- | id | origen | ts                  |
-- +----+--------+---------------------+
-- |  1 | db-01  | 2026-05-15 09:51:02 |
-- |  4 | db-01  | 2026-05-15 09:52:31 |
-- |  7 | db-02  | 2026-05-15 09:52:31 |
-- | 10 | db-03  | 2026-05-15 09:52:31 |
-- +----+--------+---------------------+
```

Fíjate en los `id`: saltan de 3 en 3 (1, 4, 7, 10). Es **lo esperado**: Galera asigna `auto_increment_increment = 3` (número de nodos) y un `auto_increment_offset` distinto por nodo. Así dos nodos nunca generan el mismo `id` sin coordinarse. Verifica:

```sql
SHOW VARIABLES LIKE 'auto_increment_%';
-- +--------------------------+-------+
-- | Variable_name            | Value |
-- +--------------------------+-------+
-- | auto_increment_increment | 3     |
-- | auto_increment_offset    | 1     |   -- en db-01 (2 en db-02, 3 en db-03)
-- +--------------------------+-------+
```

### Prueba 3: conflicto de escritura

Galera resuelve los conflictos con **first commit wins** (certificación optimista). Si dos nodos actualizan la misma fila a la vez, uno gana y el otro recibe error `1213` (deadlock) en el `COMMIT`.

En dos terminales en paralelo, una contra `db-01` y otra contra `db-02`:

```sql
-- Terminal A (db-01)                   -- Terminal B (db-02)
START TRANSACTION;                       START TRANSACTION;
UPDATE demo.notas                        UPDATE demo.notas
  SET contenido='gana A' WHERE id=1;       SET contenido='gana B' WHERE id=1;
COMMIT;                                  COMMIT;
-- Query OK, 1 row affected              -- ERROR 1213 (40001): Deadlock found
--                                       --   when trying to get lock; try
--                                       --   restarting transaction
```

La fila queda con el valor de A. La aplicación tiene que **reintentar** las transacciones que reciben 1213. Es la misma disciplina que ya aplicarías con `SERIALIZABLE` en PostgreSQL.

### Prueba 4: caída de un nodo (2/3 sigue con quórum)

Tirar `db-02` por las bravas:

```bash
ssh db-02 sudo systemctl stop mariadb
```

Desde `db-01`:

```sql
SHOW STATUS LIKE 'wsrep_cluster_size';
-- +--------------------+-------+
-- | Variable_name      | Value |
-- +--------------------+-------+
-- | wsrep_cluster_size | 2     |
-- +--------------------+-------+

SHOW STATUS LIKE 'wsrep_cluster_status';
-- | wsrep_cluster_status | Primary |

INSERT INTO demo.notas (contenido, origen) VALUES ('aún vivo', @@hostname);
-- Query OK, 1 row affected (0.003 sec)
```

Sigue aceptando escrituras. Quórum 2/3 = mayoría.

### Prueba 5: recuperación con IST

Vuelve a levantar `db-02`:

```bash
ssh db-02 sudo systemctl start mariadb
ssh db-02 sudo journalctl -u mariadb -n 20 --no-pager | grep WSREP | tail -5
# ... WSREP: Member 1.0 (db-02) requested state transfer from '*any*'
# ... WSREP: IST receiver addr using tcp://10.10.0.12:4568
# ... WSREP: IST received: 8023-8047
# ... WSREP: 0.0 (db-01): State transfer to 1.0 (db-02) complete.
# ... WSREP: Member 1.0 (db-02) synced with group.
```

IST: solo se transfirieron las transacciones que se perdió mientras estaba caído. Rapidísimo. Si el nodo hubiera estado caído más tiempo del que cubre `gcache.size`, Galera caería automáticamente a SST completa (más lenta pero igual de automática).

```sql
-- Desde db-02, ya sincronizado:
SELECT contenido FROM demo.notas WHERE contenido='aún vivo';
-- +----------+
-- | contenido|
-- +----------+
-- | aún vivo |
-- +----------+
```

Recibió la fila que se insertó mientras estaba abajo. Sin intervención manual.

### Prueba 6: caída de dos nodos (sin quórum)

Tira `db-02` y `db-03`:

```bash
ssh db-02 sudo systemctl stop mariadb
ssh db-03 sudo systemctl stop mariadb
```

Desde `db-01`:

```sql
SHOW STATUS LIKE 'wsrep_cluster_status';
-- | wsrep_cluster_status | non-Primary |

INSERT INTO demo.notas (contenido, origen) VALUES ('???', @@hostname);
-- ERROR 1047 (08S01): WSREP has not yet prepared node for application use
```

Galera **bloquea las escrituras** al perder mayoría. Es lo que quieres: prefiere parar a permitir un split-brain. Las lecturas también se bloquean por defecto; si quieres permitirlas se puede ajustar `wsrep_dirty_reads = ON`, pero piensatelo dos veces.

Para recuperar, lo correcto es **levantar primero los nodos que apagaste** (no rebootstrappear `db-01` a lo loco — eso fuerza una primaria nueva y puede provocar pérdida si las otras tienen datos más recientes):

```bash
ssh db-02 sudo systemctl start mariadb
ssh db-03 sudo systemctl start mariadb
```

En cuanto cualquiera de los dos vuelve y forma quórum con `db-01`, el cluster vuelve a estado `Primary` y `db-01` recupera escrituras. Verifica:

```sql
SHOW STATUS LIKE 'wsrep_cluster_size';
-- | wsrep_cluster_size | 3 |

SHOW STATUS LIKE 'wsrep_cluster_status';
-- | wsrep_cluster_status | Primary |
```

### Prueba 7: latencia de escritura con sysbench

Lo síncrono cuesta. Vamos a medir cuánto. Instala `sysbench`:

```bash
sudo apt install -y sysbench
```

Prepara la carga (10 tablas, 100 000 filas cada una):

```bash
sysbench oltp_write_only \
  --mysql-host=db-01 --mysql-user=root \
  --tables=10 --table-size=100000 prepare
```

Lanza 60 segundos de escrituras puras con 16 hilos:

```bash
sysbench oltp_write_only \
  --mysql-host=db-01 --mysql-user=root \
  --tables=10 --table-size=100000 \
  --threads=16 --time=60 --report-interval=10 run
```

Salida típica en 3 nodos en la misma LAN (1 Gb/s, sin saturar nada):

```
[ 10s ] thds: 16 tps: 412.3 qps: 2473.8 (r/w/o: 0.0/2061.5/412.3)
[ 20s ] thds: 16 tps: 418.9 qps: 2513.4 (r/w/o: 0.0/2094.5/418.9)
...
SQL statistics:
    transactions:                        25018  (416.85 per sec.)
    queries:                             150108 (2501.10 per sec.)

Latency (ms):
         min:                                    8.42
         avg:                                   38.34
         95th percentile:                       58.99
         max:                                  148.27
```

Compáralo con la misma prueba contra un nodo standalone:

```
[ 10s ] thds: 16 tps: 1827.4 qps: 10964.3
...
Latency (ms):
         avg:                                    8.74
         95th percentile:                       13.46
```

Cuatro o cinco veces más latencia y un quinto del throughput. Es **el coste de la durabilidad multi-DC síncrona**. Si esto te asusta, replanteate si necesitas Galera de verdad o si te basta con replicación asíncrona + failover automático. Y si lo necesitas, dimensiona en consecuencia: red baja latencia, discos NVMe y `gcache` grande.

Limpia:

```bash
sysbench oltp_write_only --mysql-host=db-01 --mysql-user=root \
  --tables=10 cleanup
```

## Operaciones del día a día

### Rolling restart (sin downtime)

Reinicia un nodo cada vez, esperando a que vuelva a `Synced` antes del siguiente:

```bash
for n in db-01 db-02 db-03; do
  ssh $n sudo systemctl restart mariadb
  until ssh $n "sudo mariadb -BNe \"SHOW STATUS LIKE 'wsrep_local_state_comment';\" \
    | grep -q Synced"; do sleep 2; done
  echo "$n OK"
done
```

### Añadir un cuarto nodo

1. Instala MariaDB y `galera-4` igual que en los primeros tres.
2. Pon su IP en `wsrep_cluster_address` **de todos los nodos** (y añade el nuevo `wsrep_node_address`/`wsrep_node_name` en el nuevo).
3. Arranca con `systemctl start mariadb`. Hará SST desde uno de los nodos existentes.

Recuerda mantener el cluster con **número impar** de nodos (3, 5, 7) si quieres preservar quórum claro. Con 4 nodos, perder 2 te deja 2/4 (sin mayoría).

### Retirar un nodo

Para retirar `db-03` definitivamente:

```bash
ssh db-03 sudo systemctl stop mariadb
ssh db-03 sudo systemctl disable mariadb
```

Quita su IP de `wsrep_cluster_address` en los nodos restantes. No requiere reinicio del cluster; el cambio se aplica en el próximo restart de cada nodo.

### Backups

`mariabackup` funciona perfectamente con Galera. Hazlo desde **un solo nodo** (idealmente uno no productivo o uno marcado como *backup donor*):

```bash
sudo mariabackup --backup \
  --target-dir=/var/backups/mariadb/$(date +%F-%H%M) \
  --user=sstuser --password=CAMBIA_ESTO

# Después, "preparar" el backup
sudo mariabackup --prepare \
  --target-dir=/var/backups/mariadb/2026-05-15-1042
```

Sube los `.xb` a almacenamiento externo (S3, B2, lo que tengas). Restore probado mensualmente, como siempre.

## Frontal: ProxySQL o MaxScale

Galera te da el cluster, pero la app necesita un punto único de entrada. Las dos opciones razonables:

- **ProxySQL**: muy ligero, configuración en runtime vía SQL, lo que prefiero para la mayoría.
- **MaxScale**: producto de MariaDB Corporation, más integrado pero con licencia BSL para producción a partir de cierto tamaño.

Una config mínima de ProxySQL para repartir lecturas entre los tres y mandar escrituras al "writer principal" (un nodo elegido para reducir conflictos de certificación):

```sql
INSERT INTO mysql_servers (hostgroup_id, hostname, port) VALUES
  (10, '10.10.0.11', 3306),
  (20, '10.10.0.12', 3306),
  (20, '10.10.0.13', 3306);

INSERT INTO mysql_galera_hostgroups
  (writer_hostgroup, reader_hostgroup, max_writers, writer_is_also_reader, active)
VALUES (10, 20, 1, 1, 1);

LOAD MYSQL SERVERS TO RUNTIME;
SAVE  MYSQL SERVERS TO DISK;
```

ProxySQL monitoriza Galera con `mysql_galera_hostgroups` y reasigna escrituras automáticamente si el writer cae.

## Lo que no te he dicho (y tienes que mirar)

- **TLS entre nodos** (`wsrep_provider_options="socket.ssl_..."`). Imprescindible si los nodos van entre data centers por enlace público o compartido.
- **Backups encriptados** con `--encrypt`.
- **Audit log** (plugin `server_audit`) para cumplimiento.
- **Monitorización** con Prometheus + `mysqld_exporter` (el exporter tiene métricas específicas de wsrep).
- **Particionado** de tablas grandes para que el SST sea más manejable.
- **Selección de versión LTS** según tu ventana de soporte. 11.4 LTS cubre hasta 2029; 11.8 LTS llegará más lejos cuando salga.

Para el panorama amplio (replicación asíncrona, GTID, semi-sync, checklist de producción) consulta la [quinta entrega de la serie MariaDB desde cero](/post/mariadb-desde-cero-v-replicacion-galera-y-produccion). Para el resto de la serie:

- [I: instalación y primeros pasos](/post/mariadb-desde-cero-i-instalacion-y-primeros-pasos)
- [II: storage engines, tipos y restricciones](/post/mariadb-desde-cero-ii-storage-engines-tipos-y-restricciones)
- [III: consultas, CTEs y window functions](/post/mariadb-desde-cero-iii-consultas-ctes-y-window-functions)
- [IV: índices, EXPLAIN y tuning](/post/mariadb-desde-cero-iv-indices-explain-y-tuning)
- [V: replicación, Galera y producción](/post/mariadb-desde-cero-v-replicacion-galera-y-produccion)

Si te montas el cluster siguiendo esto y se atasca en algo (típicamente el SST inicial entre nodos por temas de firewall o de versión), escríbeme. Es de esas cosas donde un par de pares de ojos ahorran horas.
