Si vous avez déjà vu un pic de trafic faire exploser vos temps de réponse MySQL, puis un “Error establishing a database connection” dans WordPress 6.9.4, vous avez déjà touché la limite d’un serveur unique. La réplication (un master en écriture, un ou plusieurs replicas en lecture) permet d’absorber la charge, mais aussi de préparer une haute disponibilité réaliste.

Le besoin / Le problème serveur

Le scénario typique : WordPress encaisse des lectures massives (pages, menus, options autoload, requêtes de plugins) et des écritures ponctuelles (commentaires, commandes WooCommerce, sessions, CRON, mises à jour). Sur un seul MySQL, les lectures et écritures se disputent les mêmes verrous, le même buffer pool, les mêmes I/O. À la moindre latence disque, tout ralentit.

Objectif : garder un master pour toutes les écritures, ajouter un ou plusieurs replicas pour les lectures, puis introduire un mécanisme de failover (bascule) qui ne transforme pas votre site en roulette russe.

À la fin, vous saurez :

  • Configurer MySQL 8.x (ou compatible) pour une réplication robuste (GTID, binlog, réplication asynchrone).
  • Initialiser un replica sans incohérence (backup cohérent, restore, positionnement).
  • Router les lectures WordPress vers les replicas sans casser les écritures (et sans “split-brain”).
  • Tester, diagnostiquer, restaurer et migrer sans détruire la chaîne de réplication.

Note honnête : “master-slave” reste l’expression la plus recherchée, mais MySQL parle plutôt de source/replica. Je garde les deux termes pour que vous retrouviez vos repères.

Résumé rapide

  • Activez GTID et le binlog sur le master, et un server_id unique sur chaque nœud.
  • Initialisez le replica via mysqldump –single-transaction (ou MySQL Shell/Clone si disponible), puis CHANGE REPLICATION SOURCE TO.
  • Côté WordPress, évitez les bricolages : utilisez une couche DB-aware (ex: HyperDB en drop-in) ou un proxy (ex: ProxySQL) pour router les lectures.
  • Prévoyez la latence de réplication : pour certaines requêtes, vous devez forcer la lecture sur le master après une écriture.
  • Le vrai gain de HA vient du trio : réplication + proxy + procédure de bascule (testée).

Avant de commencer (prérequis)

Accès requis :

  • SSH root (ou sudo) sur les serveurs MySQL.
  • Accès MySQL admin (ou un compte avec privileges pour réplication).
  • Accès SSH sur le serveur web WordPress.
  • WP-CLI opérationnel (Commandes WP-CLI).

Sauvegarde obligatoire (ne testez pas sur production sans filet) :

  • Dump SQL + fichiers WordPress (wp-content, wp-config.php).
  • Snapshot disque si vous êtes sur VM/Cloud (plus rapide en rollback).

Versions cibles (avril 2026) :

  • WordPress 6.9.4.
  • PHP 8.1+ (8.2/8.3 conseillé selon votre stack) : Versions supportées PHP.
  • MySQL 8.0+ (ou MariaDB compatible, mais les syntaxes diffèrent ; les exemples ci-dessous ciblent MySQL 8.x).
  • Nginx ou Apache ; je fournis des configs de base côté WordPress, mais l’essentiel est MySQL + proxy.

Hypothèses réseau :

  • Chaque serveur MySQL est joignable sur TCP/3306 depuis WordPress (ou depuis ProxySQL si vous l’interposez).
  • Les serveurs MySQL se joignent entre eux (master → replica) sur 3306.
  • Vous avez une résolution DNS stable (ou un VIP) pour pointer WordPress vers le proxy.

Étape 1 : Définir la topologie (master, replicas, VIP/DNS) et les objectifs

Avant de toucher à MySQL, fixez des règles. J’ai souvent vu des setups “réplication OK” devenir inutilisables parce que personne n’avait décidé WordPress se connecte, ni comment on bascule.

Topologie recommandée (pragmatique)

  • db-master-1 : écritures + lectures critiques
  • db-replica-1 : lectures
  • db-replica-2 (optionnel) : lectures + redondance
  • sql-proxy-1 : ProxySQL (point d’entrée unique)
  • sql-proxy-2 (optionnel) : second proxy + VRRP/keepalived ou DNS failover

Objectif de disponibilité

La réplication asynchrone n’offre pas une HA “zéro perte” : si le master tombe, vous pouvez perdre les dernières transactions non répliquées. Pour un blog, c’est souvent acceptable (perte de quelques commentaires). Pour WooCommerce, c’est un autre débat.

Décision clé : où router les lectures ?

  • Option A (simple) : WordPress se connecte uniquement au master. La réplication sert au read-only offload via outils externes (analytics, exports, BI). Pas de gain direct pour le front.
  • Option B (performances) : WordPress route lectures vers replicas, écritures vers master. Nécessite un drop-in DB ou un proxy SQL avec règles.
  • Option C (opérationnelle) : WordPress se connecte au proxy, qui route automatiquement. C’est celle que je privilégie en prod, parce que vous pouvez changer les règles sans redeployer WordPress.

Étape 2 : Configurer MySQL 8.x pour la réplication (GTID + binlog)

Je pars sur une installation MySQL standard (packages distro). Adaptez les chemins si vous êtes sur Docker/Kubernetes.

Configurer le master

Éditez /etc/mysql/mysql.conf.d/mysqld.cnf (Debian/Ubuntu) ou l’équivalent. Voici un bloc complet, avec des choix réalistes pour WordPress.

sudo nano /etc/mysql/mysql.conf.d/mysqld.cnf
sudo systemctl restart mysql
# Vérifiez la version et l'état
mysql --version
sudo systemctl status mysql --no-pager

Exemple de configuration master (à adapter) :

# /etc/mysql/mysql.conf.d/mysqld.cnf
[mysqld]
# --- Réseau ---
bind-address = 0.0.0.0
port = 3306

# --- Identité du serveur (DOIT être unique) ---
server_id = 1

# --- Binlog obligatoire pour la réplication ---
log_bin = /var/log/mysql/mysql-bin.log
binlog_format = ROW
binlog_row_image = FULL
sync_binlog = 1

# --- GTID (facilite la bascule) ---
gtid_mode = ON
enforce_gtid_consistency = ON

# --- Réplication (recommandé) ---
log_replica_updates = ON

# --- Durabilité (compromis perf/sécu) ---
innodb_flush_log_at_trx_commit = 1

# --- WordPress : charset/utf8mb4 ---
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_520_ci

# --- Quelques garde-fous ---
max_connections = 500
sql_mode = STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION

# --- Slow log utile en prod (diagnostic) ---
slow_query_log = 1
slow_query_log_file = /var/log/mysql/mysql-slow.log
long_query_time = 1

Créez un compte de réplication. Ne réutilisez jamais un compte applicatif WordPress pour ça.

mysql -uroot -p -e "
CREATE USER 'repl'@'10.%' IDENTIFIED BY 'CHANGEZ-MOI-EN-PROD';
GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'repl'@'10.%';
FLUSH PRIVILEGES;
"

Référence MySQL officielle (syntaxe et privilèges) : MySQL Replication HowTo.

Configurer le(s) replica(s)

Sur chaque replica, server_id différent, binlog optionnel (mais utile si vous voulez chaîner ou faire un futur failover). J’active aussi read_only + super_read_only : ça évite les écritures “accidentelles” depuis un script ou un admin pressé.

# /etc/mysql/mysql.conf.d/mysqld.cnf
[mysqld]
bind-address = 0.0.0.0
port = 3306

server_id = 2

# Replica : relay logs
relay_log = /var/log/mysql/mysql-relay-bin.log
relay_log_recovery = 1

# GTID
gtid_mode = ON
enforce_gtid_consistency = ON

# Recommandé si vous envisagez la promotion en master
log_bin = /var/log/mysql/mysql-bin.log
log_replica_updates = ON
binlog_format = ROW
sync_binlog = 1

# Lecture seule (sécurité)
read_only = 1
super_read_only = 1

character-set-server = utf8mb4
collation-server = utf8mb4_unicode_520_ci

Redémarrez MySQL sur le replica :

sudo systemctl restart mysql

Étape 3 : Initialiser le replica proprement (backup cohérent + restore)

Le piège classique : faire un dump “à chaud” sans transaction cohérente, restaurer, puis démarrer la réplication. Ça marche… jusqu’au moment où une table InnoDB a bougé pendant le dump. Résultat : réplication cassée, données incohérentes, et vous ne le voyez pas tout de suite.

Option 1 : mysqldump cohérent (InnoDB) + GTID

Sur le master :

# Remplacez wp_db, wp_user, etc.
DB_NAME="wp_db"
DB_USER="wp_admin"
DB_PASS="CHANGEZ-MOI"

mysqldump -u"${DB_USER}" -p"${DB_PASS}" 
  --single-transaction 
  --routines --triggers --events 
  --set-gtid-purged=ON 
  --hex-blob 
  --default-character-set=utf8mb4 
  "${DB_NAME}" | gzip > /root/wp_db.sql.gz

Copiez le dump vers le replica :

scp /root/wp_db.sql.gz root@db-replica-1:/root/

Sur le replica : créez la base (si besoin), restaurez, puis configurez la réplication GTID.

mysql -uroot -p -e "CREATE DATABASE IF NOT EXISTS wp_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci;"
gunzip -c /root/wp_db.sql.gz | mysql -uroot -p wp_db

Ensuite, attachez le replica au master. Avec MySQL 8, la commande moderne est CHANGE REPLICATION SOURCE TO.

mysql -uroot -p -e "
STOP REPLICA;
RESET REPLICA ALL;

CHANGE REPLICATION SOURCE TO
  SOURCE_HOST='db-master-1',
  SOURCE_PORT=3306,
  SOURCE_USER='repl',
  SOURCE_PASSWORD='CHANGEZ-MOI-EN-PROD',
  SOURCE_AUTO_POSITION=1;

START REPLICA;
"

Vérifiez l’état :

mysql -uroot -p -e "SHOW REPLICA STATUSG" | egrep "Replica_IO_Running|Replica_SQL_Running|Seconds_Behind_Source|Last_Error"

Option 2 : MySQL Clone (si supporté) / MySQL Shell

Si vous avez la main sur les plugins MySQL et un réseau rapide, le clone peut être plus simple et moins risqué. Je ne le détaille pas ici parce que les contraintes varient fortement (permissions, plugin clone, volumes). Gardez en tête : c’est souvent ce que les équipes infra préfèrent à mysqldump dès qu’on dépasse quelques dizaines de Go.


Étape 4 : Séparer lectures/écritures côté WordPress (HyperDB ou plugin moderne)

WordPress 6.9.4 utilise toujours wpdb comme abstraction SQL. Nativement, WordPress ne sait pas router “SELECT vers replica, INSERT/UPDATE vers master”. Il faut une couche.

Deux approches réalistes :

  • Drop-in db.php : vous remplacez le moteur DB WordPress via wp-content/db.php. HyperDB reste une base solide, mais il faut savoir ce que vous faites.
  • Proxy SQL : WordPress ne voit qu’un hôte MySQL. Le proxy route.

Pour un blogueur avancé, je conseille la voie proxy (étape suivante). Mais je vous montre quand même la logique côté WordPress, parce que vous devrez comprendre les effets de bord (réplication lag, lectures après écritures, etc.).

Point critique : “read-your-writes”

Après une écriture (ex: publication, commentaire), WordPress relit souvent immédiatement (ex: recalcul de caches, lecture de métadonnées). Si vous routez ces lectures vers un replica en retard de 200 ms, vous obtenez des bugs fantômes : commentaire “disparu” puis “réapparaît”, options non à jour, sessions incohérentes.

La règle de terrain : après une écriture, forcez quelques secondes de lectures sur le master pour la même requête HTTP.

Exemple de drop-in minimaliste (didactique, pas suffisant pour prod)

Ce snippet montre le pattern. Ne copiez pas en prod tel quel : il manque la gestion complète des erreurs, du multi-DB, et des requêtes non-SELECT qui commencent par des commentaires SQL.

<?php
/**
 * wp-content/db.php
 * Exemple DIDACTIQUE : route SELECT vers un replica, le reste vers master.
 * Ne pas utiliser tel quel en production sans durcir (regex, erreurs, failover).
 */

defined('ABSPATH') || exit;

require_once ABSPATH . WPINC . '/wpdb.php';

class WPDB_Router extends wpdb {
	/** @var wpdb */
	private $master;

	/** @var wpdb */
	private $replica;

	/** @var float */
	private $force_master_until = 0.0;

	public function __construct($dbuser, $dbpassword, $dbname, $dbhost) {
		// Connexion master
		$this->master = new wpdb(DB_USER, DB_PASSWORD, DB_NAME, DB_HOST);

		// Connexion replica (variables d'env recommandées)
		$replica_host = getenv('WP_DB_REPLICA_HOST') ?: DB_HOST;
		$this->replica = new wpdb(DB_USER, DB_PASSWORD, DB_NAME, $replica_host);

		// Initialiser l'objet parent avec master pour compatibilité
		parent::__construct(DB_USER, DB_PASSWORD, DB_NAME, DB_HOST);
	}

	private function should_use_replica($query) {
		// Si on force le master (après écriture), on ne route rien vers le replica
		if (microtime(true) < $this->force_master_until) {
			return false;
		}

		$q = ltrim($query);

		// Pattern simpliste : SELECT uniquement
		if (stripos($q, 'SELECT') === 0) {
			return true;
		}

		return false;
	}

	private function mark_write_happened() {
		// Force master pendant 2 secondes pour éviter des lectures incohérentes
		$this->force_master_until = microtime(true) + 2.0;
	}

	public function query($query) {
		// Détecter une écriture (simplifié)
		$q = ltrim($query);
		$is_write = (
			stripos($q, 'INSERT') === 0 ||
			stripos($q, 'UPDATE') === 0 ||
			stripos($q, 'DELETE') === 0 ||
			stripos($q, 'REPLACE') === 0 ||
			stripos($q, 'CREATE') === 0 ||
			stripos($q, 'ALTER') === 0 ||
			stripos($q, 'DROP') === 0
		);

		if ($is_write) {
			$this->mark_write_happened();
			return $this->master->query($query);
		}

		if ($this->should_use_replica($query)) {
			return $this->replica->query($query);
		}

		return $this->master->query($query);
	}
}

// Remplacer l'instance globale $wpdb
global $wpdb;
$wpdb = new WPDB_Router(DB_USER, DB_PASSWORD, DB_NAME, DB_HOST);

Ce type de drop-in peut casser des plugins qui manipulent directement les propriétés internes de wpdb. C’est fréquent avec des plugins de cache objet ou des outils de reporting. Testez avec votre stack (Elementor, Divi 5, Avada) : ces builders ajoutent des requêtes, mais le risque vient surtout des plugins “performance” et WooCommerce.

Référence officielle sur wpdb : Classe wpdb.


Étape 5 : Ajouter un proxy SQL (ProxySQL) pour le failover et le routage

Le proxy vous donne un point d’entrée unique (ex: sql-proxy.service.local:6033) et vous permet :

  • Routage SELECT → replicas, writes → master, via règles.
  • Health checks et mise hors rotation d’un replica malade.
  • Changement de topologie sans toucher WordPress.

Je prends ProxySQL parce qu’il est largement utilisé et scriptable. Alternatives : HAProxy (plus brut), MySQL Router (surtout pour InnoDB Cluster), solutions cloud managées.

Installer ProxySQL (exemple Debian/Ubuntu)

sudo apt-get update
sudo apt-get install -y proxysql

ProxySQL écoute souvent sur :

  • 6032 (admin)
  • 6033 (MySQL traffic)

Configurer les serveurs backends

Connectez-vous à l’interface admin :

mysql -uadmin -padmin -h127.0.0.1 -P6032

Exemple de configuration : hostgroup 10 = master, hostgroup 20 = replicas.

# Dans le shell mysql connecté à ProxySQL admin
INSERT INTO mysql_servers(hostgroup_id,hostname,port,comment) VALUES
(10,'db-master-1',3306,'Master'),
(20,'db-replica-1',3306,'Replica 1'),
(20,'db-replica-2',3306,'Replica 2');

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

Déclarer l’utilisateur applicatif WordPress

Créez un user MySQL côté serveurs DB (master + replicas) avec droits adaptés. Pour WordPress, il faut au minimum SELECT/INSERT/UPDATE/DELETE, et selon vos plugins : CREATE/ALTER (à éviter en runtime, mais certaines extensions le font en update).

# Sur le master (puis répliqué si vous gérez les users de façon cohérente)
mysql -uroot -p -e "
CREATE USER 'wp_app'@'10.%' IDENTIFIED BY 'CHANGEZ-MOI';
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER, CREATE TEMPORARY TABLES, LOCK TABLES
ON wp_db.* TO 'wp_app'@'10.%';
FLUSH PRIVILEGES;
"

Dans ProxySQL :

INSERT INTO mysql_users(username,password,default_hostgroup,transaction_persistent)
VALUES('wp_app','CHANGEZ-MOI',10,1);

LOAD MYSQL USERS TO RUNTIME;
SAVE MYSQL USERS TO DISK;

Règles de routage (Query Rules)

Règle de base :

  • SELECT → hostgroup 20 (replicas)
  • tout le reste → hostgroup 10 (master)

Mais vous devez gérer les edge cases :

  • SELECT ... FOR UPDATE doit aller au master.
  • Transactions : si une transaction commence, tout doit rester sur le master (sinon incohérences).
  • Requêtes avec commentaires en tête (/* ... */ SELECT) : votre regex doit les tolérer.
# Routage des transactions vers master
INSERT INTO mysql_query_rules(rule_id,active,match_pattern,destination_hostgroup,apply)
VALUES
(10,1,'^\s*BEGIN',10,1),
(11,1,'^\s*START\s+TRANSACTION',10,1),
(12,1,'^\s*COMMIT',10,1),
(13,1,'^\s*ROLLBACK',10,1);

# SELECT FOR UPDATE vers master
INSERT INTO mysql_query_rules(rule_id,active,match_pattern,destination_hostgroup,apply)
VALUES
(20,1,'^\s*(/\*.*\*/\s*)?SELECT.*FOR\s+UPDATE',10,1);

# SELECT classique vers replicas
INSERT INTO mysql_query_rules(rule_id,active,match_pattern,destination_hostgroup,apply)
VALUES
(30,1,'^\s*(/\*.*\*/\s*)?SELECT',20,1);

LOAD MYSQL QUERY RULES TO RUNTIME;
SAVE MYSQL QUERY RULES TO DISK;

Attention : ces regex sont volontairement conservatrices. J’ai déjà vu des règles trop agressives envoyer des SELECT “internes” (dans une transaction) vers un replica, ce qui produit des bugs impossibles à reproduire.

Configurer WordPress pour utiliser ProxySQL

Dans wp-config.php, pointez DB_HOST vers le proxy (port 6033) :

define('DB_NAME', 'wp_db');
define('DB_USER', 'wp_app');
define('DB_PASSWORD', 'CHANGEZ-MOI');
define('DB_HOST', 'sql-proxy-1:6033');

WordPress ne gère pas nativement un paramètre “read host / write host”. Avec un proxy, vous n’en avez pas besoin.

Référence WordPress officielle sur wp-config.php : Editing wp-config.php.


Étape 6 : Staging, restauration et migrations sans casser la réplication

La réplication change vos procédures. Un staging qui pointe par erreur sur le master de prod peut écrire des options, déclencher WP-Cron, et polluer la DB. Je l’ai vu sur des sites Avada avec des tâches planifiées qui régénèrent des assets.

Créer un staging à partir d’un replica (recommandé)

Le staging doit être isolé réseau et idéalement cloné depuis un replica (moins de pression sur le master).

# Sur db-replica-1 : dump
mysqldump -uroot -p --single-transaction --default-character-set=utf8mb4 wp_db | gzip > /root/wp_db_staging.sql.gz

Sur le staging, après import, neutralisez les écritures “externes” :

# Désactiver WP-Cron côté staging (à mettre aussi dans wp-config.php)
wp config set DISABLE_WP_CRON true --type=constant --path=/var/www/html

Et changez l’URL (sinon redirections/cookies incohérents) :

wp search-replace 'https://www.example.com' 'https://staging.example.com' --skip-columns=guid --all-tables --path=/var/www/html

Référence WP-CLI search-replace : wp search-replace.

Restauration : choisissez le bon point

En incident, la question n’est pas “restaurer où”, mais “restaurer quoi” :

  • Si le master est corrompu, restaurer un replica et le promouvoir peut être plus rapide.
  • Si vous restaurez un dump ancien sur un replica sans remettre les GTID correctement, vous cassez l’auto-positionnement.

Procédure simple (cas : master HS, promotion db-replica-1) :

# Sur le replica promu :
mysql -uroot -p -e "
STOP REPLICA;
RESET REPLICA ALL;
SET GLOBAL read_only=0;
SET GLOBAL super_read_only=0;
"

Puis mettez à jour ProxySQL (hostgroup 10 doit pointer vers le nouveau master). C’est exactement le moment où un proxy vous évite d’éditer 6 frontaux WordPress.


Fichiers de configuration complets

wp-config.php (extrait complet orienté DB + durcissement)

<?php
/**
 * wp-config.php (extrait)
 * Cible WordPress 6.9.4+ et PHP 8.1+
 */

define('DB_NAME', 'wp_db');
define('DB_USER', 'wp_app');
define('DB_PASSWORD', 'CHANGEZ-MOI');
define('DB_HOST', 'sql-proxy-1:6033');
define('DB_CHARSET', 'utf8mb4');
define('DB_COLLATE', 'utf8mb4_unicode_520_ci');

/**
 * Désactive WP-Cron si vous le gérez via cron système (recommandé en prod).
 * Évite des écritures surprises et l'effet "trafic = cron storm".
 */
define('DISABLE_WP_CRON', true);

/**
 * Limite les révisions (réduit les écritures).
 */
define('WP_POST_REVISIONS', 20);

/**
 * Sécurité : clés/salts générées via l’API officielle.
 * https://api.wordpress.org/secret-key/1.1/salt/
 */

// ... le reste de votre wp-config.php

Cron système (remplace WP-Cron)

# /etc/cron.d/wordpress-cron
*/5 * * * * www-data php -d detect_unicode=0 /var/www/html/wp-cron.php >/dev/null 2>&1

Nginx (extrait) : headers + cache safe (sans casser l’admin)

Ce n’est pas le cœur du sujet, mais une réplication DB ne compense pas un front mal configuré.

# /etc/nginx/sites-available/wordpress.conf (extrait)
server {
  listen 80;
  server_name example.com;

  root /var/www/html;
  index index.php;

  add_header X-Content-Type-Options nosniff always;
  add_header X-Frame-Options SAMEORIGIN always;
  add_header Referrer-Policy strict-origin-when-cross-origin always;

  location / {
    try_files $uri $uri/ /index.php?$args;
  }

  location ~ .php$ {
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
  }

  location ~* /(wp-admin|wp-login.php) {
    # Pas de cache ici, sinon comportements bizarres (sessions, nonce)
    add_header Cache-Control "no-store, no-cache, must-revalidate, max-age=0";
    try_files $uri $uri/ /index.php?$args;
  }
}

php.ini (extrait) : limites réalistes

; /etc/php/8.3/fpm/conf.d/99-wordpress.ini
memory_limit = 256M
max_execution_time = 120
upload_max_filesize = 64M
post_max_size = 64M
opcache.enable=1
opcache.memory_consumption=256
opcache.max_accelerated_files=50000

Vérification

Vérifier la réplication MySQL

# Sur le replica
mysql -uroot -p -e "SHOW REPLICA STATUSG" | egrep "Replica_IO_Running|Replica_SQL_Running|Seconds_Behind_Source|Last_Error"

Attendu :

  • Replica_IO_Running: Yes
  • Replica_SQL_Running: Yes
  • Seconds_Behind_Source faible (0 à quelques secondes selon charge)
  • Last_Error vide

Vérifier ProxySQL (routing)

# Admin ProxySQL : voir stats basiques
mysql -uadmin -padmin -h127.0.0.1 -P6032 -e "SELECT hostgroup, srv_host, status, ConnUsed, Queries FROM stats_mysql_connection_pool;"

Test rapide depuis le serveur WordPress :

# Connexion via ProxySQL
mysql -uwp_app -p'CHANGEZ-MOI' -h sql-proxy-1 -P6033 -e "SELECT NOW();"

Vérifier WordPress (WP-CLI)

cd /var/www/html
wp core version
wp db check
wp option get siteurl
wp cron event list --due-now

Référence WP-CLI db check : wp db check.

Vérifier HTTP côté client (latence et cache)

curl -I https://example.com/ | egrep "HTTP/|cache-control|x-cache|server"

Si ça ne marche pas

Diagnostic MySQL (master et replica)

# Logs MySQL (systemd)
sudo journalctl -u mysql --since "30 min ago" --no-pager

# Erreurs MySQL (selon distro)
sudo tail -n 200 /var/log/mysql/error.log

Erreurs fréquentes :

  • Access denied for user ‘repl’ : host non autorisé, mot de passe faux, firewall.
  • Replica_SQL_Running: No avec Duplicate entry : vous avez écrit sur le replica (ou dump incohérent).
  • Seconds_Behind_Source qui grimpe : I/O saturées, requêtes lourdes, index manquants.

Diagnostic ProxySQL

sudo journalctl -u proxysql --since "30 min ago" --no-pager
sudo tail -n 200 /var/lib/proxysql/proxysql.log

Vérifiez vos règles :

mysql -uadmin -padmin -h127.0.0.1 -P6032 -e "SELECT rule_id, match_pattern, destination_hostgroup, apply FROM mysql_query_rules ORDER BY rule_id;"

Diagnostic WordPress (erreurs DB)

Activez temporairement le debug (sur un environnement de test ou en fenêtre courte) :

define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
define('WP_DEBUG_DISPLAY', false);

Puis :

tail -n 200 /var/www/html/wp-content/debug.log

Tableau de diagnostic

Symptôme Cause probable Vérification Solution
Commentaires “disparaissent” puis reviennent Lectures routées vers replica avec lag Seconds_Behind_Source > 0, et requêtes SELECT routées vers hostgroup replicas Forcer master après écriture (proxy rules, transaction stickiness), réduire lag
Replica_SQL_Running: No (Duplicate entry) Écritures accidentelles sur replica ou dump incohérent SHOW REPLICA STATUSG (Last_Error) Remettre read_only/super_read_only, réinitialiser replica depuis backup cohérent
WordPress “Error establishing a database connection” ProxySQL down / mauvais DB_HOST / firewall telnet sql-proxy-1 6033, logs proxysql Redémarrer proxy, corriger DB_HOST, ouvrir ports, health checks
Pages lentes malgré replicas Règles ProxySQL ne matchent pas (SELECT pas routés), ou cache objet absent stats_mysql_connection_pool (Queries par hostgroup) Ajuster regex, activer cache objet (Redis/Memcached), indexer
Réplication en retard constante Replica sous-dimensionné / I/O / requêtes non indexées slow log, performance_schema, iostat Augmenter ressources, optimiser requêtes, ajouter indexes, réduire autoload

Pièges et erreurs courantes

Erreur Cause Solution
Modifier DB_HOST dans le mauvais fichier Vous éditez un wp-config.php d’un autre vhost / container Vérifiez wp --path=... et le chemin du docroot, puis wp config get DB_HOST
Oublier un point-virgule dans wp-config.php Copier-coller rapide en prod Testez d’abord sur staging, surveillez php-fpm logs, utilisez un diff
Routage SELECT trop agressif Regex ProxySQL envoie des SELECT en transaction vers replica Ajoutez des règles “BEGIN/START TRANSACTION” et stickiness vers master
Tester la bascule sur production sans sauvegarde Procédure non répétée Simulez une panne sur staging, documentez chaque commande, chronométrez
Conflit avec un plugin de cache Cache objet/persistant masque des incohérences Videz cache (Redis/OPcache), testez sans plugin, comparez requêtes
Réplication cassée après migration Dump importé sans GTID cohérent / reset mal fait Recréez le replica depuis une base cohérente, vérifiez gtid_mode
Permaliens “cassés” après staging Rewrite non régénéré, ou base URL incohérente wp rewrite flush --hard et wp option get home
Code d’un ancien tutoriel MySQL Utilise CHANGE MASTER TO sans GTID, ou paramètres obsolètes Utilisez MySQL 8+ : CHANGE REPLICATION SOURCE TO + GTID

Sécurité serveur

La réplication ouvre des flux réseau supplémentaires. Sans durcissement, vous venez d’élargir la surface d’attaque.

  • Réseau : limitez 3306 aux IP nécessaires (master ↔ replicas, proxy ↔ DB, WordPress ↔ proxy). Firewall strict (ufw/nftables/security groups).
  • Comptes : un user réplication dédié, mot de passe fort, scope IP limité. Un user WordPress dédié, pas de droits globaux.
  • Chiffrement : activez TLS MySQL si vous traversez des réseaux non fiables. (Beaucoup de gens l’oublient en multi-DC.)
  • Read-only : laissez super_read_only=1 sur replicas. Retirez-le uniquement lors d’une promotion.
  • Backups : testez vos restores. Un backup non restaurable est un placebo.
  • WordPress : gardez le core et plugins à jour. Référence : Updating WordPress.

Pour les headers HTTP et bonnes pratiques côté WordPress, la base officielle reste utile : Sécurité (Advanced Administration).


Ressources


FAQ

Est-ce que la réplication master/replica suffit pour la haute disponibilité ?

Non. Elle améliore la résilience et la scalabilité, mais sans mécanisme de bascule (proxy, procédure de promotion, DNS/VIP), WordPress continuera à pointer vers un master mort.

Quel est le risque principal quand WordPress lit sur des replicas ?

Le replication lag. Les incohérences “juste après une écriture” sont les plus coûteuses à diagnostiquer. Prévoyez une stratégie “read-your-writes” (stickiness master temporaire).

ProxySQL est-il obligatoire ?

Non, mais c’est la solution la plus flexible pour router sans modifier WordPress. Sans proxy, vous partez sur un drop-in db.php (plus intrusif), ou vous restez master-only côté WordPress.

Puis-je utiliser cette architecture avec Divi 5, Elementor ou Avada ?

Oui. Ces builders ne changent pas le modèle DB de WordPress, mais ils augmentent parfois le volume de lectures (options, meta, templates). Le point d’attention reste votre cache objet et vos plugins “performance”.

Comment éviter que le staging écrive sur la prod ?

Isolation réseau + credentials différents + désactivation de WP-Cron + vérification systématique de DB_HOST et siteurl/home via WP-CLI avant de lancer le site.

Que faire si un replica prend des écritures ?

Remettez read_only et super_read_only, puis re-synchronisez depuis une source saine. Si des écritures ont eu lieu, vous ne pouvez pas “réparer proprement” sans analyse transactionnelle.

Dois-je répliquer les utilisateurs et privilèges MySQL ?

Ça dépend. Beaucoup d’équipes gèrent les users via IaC (Ansible/Terraform) plutôt que via réplication des tables système. Le plus simple : créer les mêmes users sur chaque nœud (ou via un outil).

Est-ce compatible avec un cache objet (Redis) ?

Oui, et c’est même recommandé. Mais attention : un cache objet peut masquer des bugs de réplication (vous “ne voyez plus” le lag parce que tout sort du cache). Testez avec cache vidé.

Comment mesurer si les SELECT vont bien sur les replicas ?

Regardez les compteurs ProxySQL (stats_mysql_connection_pool) et comparez les Queries par hostgroup. Vous pouvez aussi activer temporairement un log de requêtes côté proxy.

Quel est le minimum viable pour un blog à fort trafic ?

Un master, un replica, un ProxySQL, et une procédure de promotion documentée. Sans tests de bascule, vous n’avez pas de HA, vous avez juste “deux serveurs”.