Mirajv1.0
FR

16. Réplication multi-nœud (édition Cluster)

16.1 Présentation#

L'édition Cluster de MIRAJ ajoute la réplication multi-nœud au moteur : un serveur primaire reçoit les écritures, et un ou plusieurs serveurs secondaires reçoivent en continu le journal du primaire, le rejouent et restent accessibles en lecture par les applications clientes.

Principes :

  • Un seul primaire à la fois. Il exécute toutes les instructions qui écrivent (DML, DDL, comptes) et journalise chacune d'elles.
  • Des secondaires en lecture seule, qui rejouent le journal du primaire dans le même ordre et aux mêmes numéros de séquence (LSN). Un client peut s'y connecter comme à n'importe quel serveur MIRAJ, sur le port habituel des clients (7007 par défaut), et y exécuter des lectures.
  • Réplication asynchrone par défaut, semi-synchrone au choix : par défaut le primaire n'attend pas les secondaires pour rendre la main au client qui écrit (voir les limites, §16.7) ; une session, ou tout le serveur, peut demander qu'une écriture ne soit confirmée qu'une fois reçue (ou appliquée) par un ou plusieurs secondaires (§16.8).
  • Promotion manuelle et contrôlée, sans élection automatique ni consensus : c'est l'administrateur qui désigne le primaire, au démarrage du cluster et lors d'une bascule ; le nœud promu vérifie d'abord auprès de ses pairs que la bascule est sûre et récupère ce que le secondaire le plus avancé a reçu de plus que lui (§16.4.1).

Ce que cela apporte :

  • Haute disponibilité en lecture : si le primaire devient indisponible, les secondaires continuent de répondre aux lectures, et l'un d'eux peut être promu primaire pour reprendre les écritures.
  • Répartition de charge en lecture : des rapports, exports ou tableaux de bord peuvent interroger un ou plusieurs secondaires sans peser sur le serveur qui traite les écritures.

Chaque nœud porte une copie complète de chaque base, sauf les partitions qu'une table dédie à un nœud (PARTITION … NODE 'n2') : leurs lignes ne sont que chez ce nœud, et les autres nœuds les lisent et les écrivent à travers lui (§16.10). L'élection automatique du primaire est décrite au §16.9.

16.2 Configuration#

16.2.1 Le fichier cluster.toml#

Un nœud du cluster est configuré par un fichier cluster.toml, lu par un analyseur maison d'un sous-ensemble de TOML (tables [node] et [cluster], clés nues, chaînes entre guillemets, entiers, booléens, tableaux de chaînes sur une seule ligne, commentaires #). Par défaut, MIRAJ cherche cluster.toml à côté de l'exécutable miraj-server ; l'option --cluster-config <fichier> permet d'en indiquer un autre.

Sans fichier cluster.toml, un serveur MIRAJ se comporte exactement comme en dehors du cluster : aucun port entre nœuds n'est ouvert, rien n'est journalisé pour la réplication, le comportement est strictement celui de l'édition sans réplication.

Exemple commenté :

[node]
id = "n1"                    # identifiant stable du nœud (lettres, chiffres, _ et $, 64 caractères au plus)
listen = "0.0.0.0:7107"      # port d'écoute entre nœuds : distinct du port client (7007 par défaut)
advertise = "10.0.0.1:7107"  # adresse annoncée aux autres nœuds (par défaut : listen)

[cluster]
name = "gestium-prod"                            # nom du cluster : un nœud d'un autre cluster est refusé à la connexion
seeds = ["10.0.0.1:7107", "10.0.0.2:7107"]       # adresses des pairs (la propre adresse annoncée du nœud est ignorée dans cette liste)
tls_cert = "node.pem"        # certificat de ce nœud, chemins relatifs à cluster.toml
tls_key = "node-key.pem"     # clé privée de ce nœud
tls_ca = "cluster-ca.pem"    # autorité qui a signé tous les certificats des nœuds du cluster
members = ["n1", "n2", "n3"] # identifiants des nœuds admis (nom commun de leur certificat), celui-ci compris

# Facultatifs (valeurs par défaut indiquées)
tls_crl = "cluster-crl.pem"  # liste de révocation de l'autorité (miraj-cluster-pki revoke) ; absente par défaut
ack = "written"              # "written" (le secondaire a écrit le lot dans son journal) ou "durable"
                              # (le secondaire l'a en plus vidé sur disque) avant d'accuser réception
journal_retention_mb = 1024  # au-delà, un secondaire resté trop longtemps injoignable est réamorcé
                              # par copie complète des bases plutôt que rattrapé par le journal
max_promotion_lag_mb = 0     # 0 : illimité ; sinon, promotion refusée (sauf 'force_primary') si le
                              # candidat, après rattrapage, reste à plus de N Mio de la dernière
                              # position connue du primaire
promotion_catchup_mb = 64    # journal retenu par chaque secondaire pour servir le rattrapage d'un
                              # pair promu (0 : aucune retenue)
sync_commit = "off"          # réplication semi-synchrone (§16.8) : "off", "received", "applied" ou
                              # "majority", valeur initiale de @@GLOBAL.cluster_sync_commit
sync_replicas = 1            # accusés de secondaires requis par écriture
sync_timeout_ms = 10000      # attente maximale d'une écriture, en millisecondes
sync_timeout_action = "fallback"  # au délai : "fallback", "error" ou "wait"
mode = "manual"              # "manual" (primaire désigné par l'administrateur) ou "raft" (élu, §16.9)
election_timeout_ms = 3000   # mode raft : délai sans nouvelle du leader avant candidature (≥ 500)
fragment_timeout_ms = 30000  # partitions dédiées (§16.10) : attente d'une réponse du nœud qui les tient
test_hooks = false           # crochets de test (isolement d'un nœud) : jamais en production

Table [node] :

CléObligatoireDescription
idouiIdentifiant stable du nœud, un identifiant valide MIRAJ (64 caractères au plus). Sert de base à @@server_id et apparaît dans information_schema.MIRAJ_NODES.
listenouiAdresse hôte:port d'écoute pour les connexions des autres nœuds. Doit être un port différent du port client du serveur.
advertisenon (par défaut : listen)Adresse hôte:port que ce nœud annonce aux autres. Doit être une adresse effectivement joignable par les pairs : 0.0.0.0 est refusé au démarrage. Le certificat TLS de ce nœud doit porter cette adresse (nom DNS ou IP) dans son SAN.

Table [cluster] :

CléObligatoireDescription
nameouiNom du cluster. Un nœud qui annonce un autre nom est refusé à la poignée de main.
seedsnon (par défaut : vide)Liste des adresses hôte:port des pairs à contacter. L'adresse annoncée de ce nœud lui-même, si elle y figure, est ignorée.
tls_certouiCertificat de ce nœud (chemin relatif au dossier de cluster.toml).
tls_keyouiClé privée correspondante.
tls_caouiCertificat de l'autorité qui a signé les certificats de tous les nœuds du cluster.
membersnon, mais recommandéIdentifiants des nœuds admis dans le cluster (§16.2.3), ce nœud compris (erreur au démarrage sinon). Un nœud dont le certificat porte un autre nom est refusé, qu'il se connecte ou qu'on le joigne. Absente : tout nœud muni d'un certificat de l'autorité est admis, avec un avertissement au démarrage. Ne change pas les membres votants du mode raft (les seeds).
tls_crlnonListe de révocation (PEM X509 CRL) signée par l'autorité (§16.2.4). Un pair dont le certificat y figure est refusé à la poignée de main TLS. Relue d'elle-même quand le fichier change (toutes les deux secondes environ).
acknon (par défaut : written)written ou durable : ce qu'un secondaire doit avoir fait avant d'accuser réception d'un lot du journal.
journal_retention_mbnon (par défaut : 1024)Au-delà de cette taille de journal retenue pour un secondaire absent, celui-ci est réamorcé par copie complète des bases à sa reconnexion plutôt que rattrapé. Un primaire redémarré ne connaît plus la position de ses secondaires : il retient le journal de chaque base à partir de son début au redémarrage pour chacun des autres nœuds de members (sans members : pour autant de nœuds que de pairs dans seeds), jusqu'à leur retour ou jusqu'à cette même limite ; un secondaire à jour qui revient reprend donc le flux sans réamorçage.
max_promotion_lag_mbnon (par défaut : 0, illimité)Retard maximal (Mio, toutes bases cumulées) qu'un secondaire peut garder, après rattrapage, envers la dernière position du journal que le primaire lui a annoncée, pour être promu par 'primary'. Au-delà, la promotion est refusée (erreur 9003) ; 'force_primary' passe outre.
promotion_catchup_mbnon (par défaut : 64)Journal que chaque secondaire garde, par base, au-delà de ce qu'il a appliqué, pour servir le rattrapage d'un pair promu (§16.4.1). 0 : aucune retenue ; un pair plus avancé ne peut alors généralement plus servir ce qui manque au candidat, et la promotion contrôlée est refusée (voir §16.4.4).
sync_commitnon (par défaut : off, majority en mode raft)Valeur initiale de @@GLOBAL.cluster_sync_commit (§16.8) : off, received, applied ou majority.
sync_replicasnon (par défaut : 1)Valeur initiale de @@GLOBAL.cluster_sync_replicas : nombre de secondaires qui doivent accuser réception d'une écriture (entier ≥ 1).
sync_timeout_msnon (par défaut : 10000)Valeur initiale de @@GLOBAL.cluster_sync_timeout : attente maximale des accusés, en millisecondes (≥ 1).
sync_timeout_actionnon (par défaut : fallback, error en mode raft)Valeur initiale de @@GLOBAL.cluster_sync_timeout_action : fallback, error ou wait (§16.8.3).
modenon (par défaut : manual)manual : le primaire est désigné par l'administrateur (§16.3, §16.4). raft : il est élu par une majorité des membres (§16.9) ; les membres votants sont les seeds, qui doivent alors contenir l'adresse annoncée de ce nœud (erreur au démarrage sinon), deux au moins (deux membres : accepté avec un avertissement, aucune panne tolérée).
election_timeout_msnon (par défaut : 3000)Mode raft : délai sans nouvelle du leader au-delà duquel un nœud se présente (tiré au sort entre une et deux fois cette valeur), aussi bail du leader. Entre 500 et 600 000.
fragment_timeout_msnon (par défaut : 30000)Partitions dédiées à un nœud (§16.10) : attente maximale d'une réponse du nœud qui tient une partition (ouverture d'une lecture, lot suivant, résultat d'une écriture transmise), au-delà de laquelle l'instruction échoue (9038). C'est aussi l'inactivité au bout de laquelle une liaison gardée entre deux nœuds est fermée. Entre 100 et 3 600 000.
test_hooksnon (par défaut : false)Crochets réservés aux tests automatisés (isolement d'un nœud par un fichier témoin miraj/cluster-isolate). Ne jamais activer en production.

Au premier démarrage d'un nœud (aucun état de cluster enregistré), tous les nœuds démarrent secondaires : un cluster ne démarre jamais avec deux primaires par défaut. C'est à l'administrateur de désigner le primaire une fois (§16.3).

16.2.2 Certificats TLS mutuels#

Les nœuds se parlent uniquement en TLS mutuel : chaque nœud présente aux autres un certificat signé par l'autorité du cluster (tls_ca), et vérifie de même le certificat de chaque pair auquel il se connecte ou qui se connecte à lui. Le certificat de chaque nœud doit porter son adresse advertise (nom DNS ou adresse IP) dans son SAN (subjectAltName), et un usage étendu couvrant à la fois serverAuth et clientAuth (un nœud est à la fois serveur et client TLS vis-à-vis de ses pairs).

Outil miraj-cluster-pki. MIRAJ fournit un petit outil en ligne de commande, indépendant du serveur, qui crée l'autorité du cluster et signe les certificats des nœuds (clés ECDSA P-256, sans openssl) :

# Une seule fois, sur le poste d'administration
miraj-cluster-pki init-ca --name gestium-prod --out pki

# Pour chaque nœud, avec son adresse annoncée (`advertise`) en --san (répétable : IP ou nom DNS)
miraj-cluster-pki issue-node --ca pki --id n1 --san 10.0.0.1 --san n1.exemple.local --out pki/n1
miraj-cluster-pki issue-node --ca pki --id n2 --san 10.0.0.2 --out pki/n2
miraj-cluster-pki issue-node --ca pki --id n3 --san 10.0.0.3 --out pki/n3

init-ca produit cluster-ca.pem et cluster-ca-key.pem (validité de 10 ans par défaut, --days pour changer) ; issue-node produit node.pem, node-key.pem et une copie de cluster-ca.pem (validité de 825 jours par défaut ; une durée plus courte, --days 90 ou --days 365, limite l'usage d'un certificat perdu), prêts à être copiés dans le dossier de cluster.toml du nœud. Le nom commun (CN) du certificat est l'identifiant --id : c'est l'identité du nœud (§16.2.3). issue-node note chaque certificat signé dans le registre certificats.txt du dossier de l'autorité (numéro de série, nœud, fin de validité), qui sert à la révocation. Gardez cluster-ca-key.pem hors des nœuds : elle signe les certificats, et seul cluster-ca.pem est distribué. --ca <dossier> désigne le dossier de init-ca ; à défaut, --ca-cert et --ca-key donnent le certificat et la clé de l'autorité là où ils se trouvent. Un fichier existant n'est jamais écrasé sans --force.

Vous pouvez aussi utiliser votre propre PKI (autorité d'entreprise existante). À titre indicatif, voici l'équivalent avec openssl (clés EC P-256) — à adapter avec vos propres clés et une durée de validité raisonnable, ces valeurs d'exemple n'étant pas destinées à un usage réel :

# Autorité du cluster
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out cluster-ca-key.pem
openssl req -new -x509 -key cluster-ca-key.pem -out cluster-ca.pem -days 3650 -sha256 \
  -subj "/CN=Autorité du cluster gestium-prod" \
  -addext "basicConstraints=critical,CA:TRUE" \
  -addext "keyUsage=critical,keyCertSign,cRLSign"

# Certificat d'un nœud (répéter pour chaque nœud, avec son adresse annoncée dans le SAN)
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out node-key.pem
openssl req -new -key node-key.pem -subj "/CN=n1" -out node.csr
openssl x509 -req -in node.csr -CA cluster-ca.pem -CAkey cluster-ca-key.pem -CAcreateserial \
  -days 825 -sha256 -out node.pem \
  -extfile <(printf "basicConstraints=critical,CA:FALSE\nkeyUsage=critical,digitalSignature,keyEncipherment\nextendedKeyUsage=serverAuth,clientAuth\nsubjectAltName=IP:10.0.0.1,DNS:n1.exemple.local\n")
rm node.csr

Distribuez ensuite cluster-ca.pem à tous les nœuds, et à chaque nœud son propre node.pem/node-key.pem. Un certificat signé par une autre autorité que celle indiquée dans tls_ca est refusé au moment de la poignée de main entre-nœuds. Avec votre propre PKI, le nom commun (/CN=) de chaque certificat de nœud doit être exactement l'identifiant [node] id du nœud.

16.2.3 Identité des nœuds et liste des membres#

Le certificat d'un nœud ne prouve pas seulement qu'il appartient à l'autorité du cluster : son nom commun (CN) est l'identité du nœud. À chaque connexion entre nœuds, dans les deux sens :

  • l'identité est lue dans le certificat du pair ; si members est renseignée et ne la contient pas, la connexion est fermée aussitôt, avant la lecture de la moindre trame (journal : connexion de … refusée : nœud « x » hors de la liste des membres) ;
  • l'identifiant que le pair déclare ensuite (présentation d'un secondaire, demande de vote, demande de rattrapage, sonde des positions, annonce d'époque, réponse d'un primaire ou d'un votant) doit être celui de son certificat ; sinon le pair est refusé (journal : nœud n1 (…) refusé : se présente comme « n2 » mais son certificat est celui du nœud « n1 »).

Un porteur d'un certificat de l'autorité ne peut donc ni se faire passer pour un autre nœud (voter à sa place, se faire servir un rattrapage ou le flux complet des bases et des comptes sous son nom), ni, s'il n'est pas membre, obtenir quoi que ce soit. Au démarrage, un nœud dont le propre certificat porte un autre nom que son id est signalé dans le journal (sécurité : le certificat … désigne le nœud « x » et non « n1 ») : ses pairs le refuseront. Pour ajouter un nœud, signez son certificat, ajoutez son identifiant à members sur chaque nœud et redémarrez-les un à un (la liste n'est lue qu'au démarrage).

Les identités sont comparées sans tenir compte de la casse, comme les identifiants de nœud.

16.2.4 Révocation d'un certificat#

Un certificat perdu ou un nœud retiré se révoque auprès de l'autorité, puis la liste de révocation est distribuée aux nœuds :

# Tous les certificats signés pour le nœud n3 (registre certificats.txt de l'autorité)
miraj-cluster-pki revoke n3 --ca pki
# Ou un certificat précis (signé par l'autorité)
miraj-cluster-pki revoke --cert ancien/node.pem --ca pki
# Régénérer la liste sans nouvelle révocation (avant sa date de prochaine mise à jour)
miraj-cluster-pki crl --ca pki --days 365

revoke note les certificats dans le registre revocations.txt de l'autorité et régénère la liste signée pki/cluster-crl.pem (--out pour un autre chemin ; --days : date de prochaine mise à jour, 365 jours par défaut). La liste est remplacée d'un bloc (fichier temporaire renommé). Copiez-la sur chaque nœud à l'emplacement de tls_crl : chaque nœud la relit dans les secondes qui suivent sa modification, renoue tous ses liens avec ses pairs (les secondaires se reconnectent aussitôt) et refuse désormais le certificat révoqué, qu'il se connecte (journal : connexion de … refusée (TLS) : certificat du pair révoqué (tls_crl)) ou qu'on le joigne. Une liste illisible est signalée et l'ancienne reste en vigueur. La date de prochaine mise à jour n'est pas imposée : une liste « périmée » reste appliquée, régénérez-la tout de même régulièrement.

Pour remplacer le certificat d'un nœud, signez-en un nouveau (issue-node … --force), installez-le, redémarrez ce nœud, puis révoquez l'ancien par revoke --cert (révoquer par identifiant révoquerait aussi le nouveau).

16.2.5 Garde-fous entre nœuds#

  • Époques : une annonce d'époque (promotion, élection) plus de 1 000 au-delà de la plus haute époque connue du nœud, ou invraisemblable (2⁴⁸ et plus, u64::MAX), est ignorée et consignée (époque … annoncée par le nœud … ignorée (saut anormal …)) : elle n'écarte pas le primaire et n'est pas adoptée. Seul un nœud qui suit un primaire (identité prouvée) accepte de lui un saut plus grand (nœud neuf ou longtemps absent), jamais une époque invraisemblable.
  • Trames : 16 Mio au plus par trame, sauf sur le lien d'un secondaire avec son primaire et pendant un rattrapage engagé par le nœud lui-même (256 Mio : un enregistrement du journal peut être gros).
  • Connexions : au plus quatre connexions entrantes simultanées par membre connu (members ou seeds), seize au moins, sur le port entre nœuds ; les suivantes sont fermées aussitôt (refus consigné au plus toutes les dix secondes). La poignée de main TLS est bornée à dix secondes au total, dans les deux sens.
  • Fichiers reçus (amorçage, magasins de LOB) : un nom de fichier envoyé par un pair n'est accepté que s'il est un simple nom (lettres et chiffres ASCII, _ . $ -, espaces), sans chemin, lecteur (C:), flux de données alternatif (a:b), point ou espace aux extrémités, ni nom de périphérique Windows (CON, NUL, COM1…).

16.3 Mise en place pas à pas#

  1. Préparer les certificats de chaque nœud (§16.2.2) et un fichier cluster.toml par nœud, avec le même [cluster] name, la même tls_ca, la même liste members (identifiants de tous les nœuds, §16.2.3), et une liste seeds couvrant les autres nœuds.
  1. Démarrer chaque serveur avec --cluster-config (ou en plaçant cluster.toml à côté de l'exécutable), et toujours avec --log :

     miraj-server --root data-n1 --port 7007 --cluster-config n1/cluster.toml --log
     miraj-server --root data-n2 --port 7007 --cluster-config n2/cluster.toml --log
     miraj-server --root data-n3 --port 7007 --cluster-config n3/cluster.toml --log

    Au démarrage, chaque nœud sans état antérieur se présente en secondaire et tente de joindre ses seeds en TLS mutuel.

  1. Désigner le primaire, une seule fois, en se connectant à celui des nœuds choisi comme primaire (n'importe quel client SQL) :

     SET GLOBAL cluster_role = 'primary';

    Ce nœud lève sa lecture seule, journalise désormais ses DDL, et se présente aux autres comme primaire d'une nouvelle époque (@@cluster_epoch).

    La commande rend la main avant la fin de la promotion (§16.4.1) : une écriture envoyée aussitôt après reçoit l'erreur 1290. Attendez que SELECT @@cluster_role rende PRIMARY avant de créer des bases ou d'écrire.

  1. Faire rejoindre les secondaires : les autres nœuds, déjà secondaires par défaut, se connectent au primaire dès qu'ils le trouvent parmi leurs seeds. S'ils n'ont aucune base en commun avec lui (nœud neuf) ou un journal trop en retard, ils sont amorcés automatiquement par copie complète des bases du primaire, puis rattrapent le flux du journal.
  1. Vérifier l'état du cluster :

     SHOW CLUSTER STATUS;
     +---------+----------------+-----------+-----------+-------+-----------+-------------+---------------------+------------+------+
     | NODE_ID | ADDRESS        | ROLE      | STATE     | EPOCH | LAG_BYTES | LAG_SECONDS | CONNECTED_SINCE     | LAST_ERROR | SYNC |
     +---------+----------------+-----------+-----------+-------+-----------+-------------+---------------------+------------+------+
     | n1      | 10.0.0.1:7107  | PRIMARY   | SELF      |     1 |         0 | NULL        | NULL                |            | OFF  |
     | n2      | 10.0.0.2:7107  | SECONDARY | CONNECTED |     1 |         0 | 0.021       | 2026-09-23 10:04:12 |            | YES  |
     | n3      | 10.0.0.3:7107  | SECONDARY | CONNECTED |     1 |       512 | 0.048       | 2026-09-23 10:04:15 |            | YES  |
     +---------+----------------+-----------+-----------+-------+-----------+-------------+---------------------+------------+------+

    STATE vaut SELF pour le nœud interrogé lui-même (PROMOTING pendant sa promotion), puis CONNECTING, BOOTSTRAPPING, CONNECTED, DISCONNECTED, REBOOTSTRAP_NEEDED, FENCED, REACHABLE ou UNREACHABLE pour les autres nœuds vus par lui. LAG_BYTES retombe à 0 une fois le secondaire à jour ; cette valeur diffère d'un nœud à l'autre puisque chacun ne connaît que ses propres pairs directs.

16.4 Bascule#

16.4.1 Promotion d'un secondaire#

Sur le secondaire choisi :

SET GLOBAL cluster_role = 'primary';

La promotion est contrôlée : avant de rendre la main, l'instruction interroge tous les pairs de seeds (quelques secondes au plus, en parallèle) et refuse la promotion, avec l'erreur 9003 (Promotion refused: …, motif en clair), dans les cas suivants :

Motif (extrait du message)Situation
node … is still primary at epoch …Le primaire est encore joignable : le rétrograder d'abord (§16.4.3), ou forcer.
node … is already primary at epoch …Un autre nœud a déjà été promu : ce secondaire le rejoindra de lui-même.
node … is being promotedUn autre nœud est en cours de promotion (deux promotions simultanées se refusent mutuellement).
node … is still connected to primary …Un pair voit encore le primaire (partition réseau probable) : ce nœud ne le voit plus, lui.
node … is ahead on database … but no longer retains itUn pair a reçu plus que ce nœud, mais ne garde plus la partie manquante de son journal (promotion_catchup_mb) : promouvoir ce nœud perdrait ces écritures.
node … has database … that this node does not have / database … has another identity on node …Les bases du pair et de ce nœud ne sont pas les mêmes.
database … has writes confirmed to clients up to position … that no reachable node holdsRéplication semi-synchrone (§16.8) : des écritures confirmées aux clients ne sont tenues ni par ce nœud ni par un pair joignable capable de les servir ; promouvoir ce nœud les perdrait.
lag of … bytes … exceeds max_promotion_lag_mbMême après rattrapage, le retard envers la dernière position annoncée par le primaire dépasse le seuil configuré.
cluster_role = FENCED, replication bootstrap in progress, cluster promotion in progressNœud écarté (le rétrograder d'abord), amorçage ou promotion en cours sur ce nœud.

Un pair injoignable n'empêche pas la promotion : c'est le cas normal après la perte du primaire. Il est seulement consigné.

Si la promotion est acceptée, le client reçoit aussitôt OK, et la suite se déroule en arrière-plan (@@cluster_role passe à PRIMARY à la fin ; la ligne du nœud dans SHOW CLUSTER STATUS est à l'état PROMOTING pendant ce temps) :

  1. Le lien avec l'ancien primaire est coupé.
  2. Rattrapage : si un autre secondaire a reçu davantage du journal de l'ancien primaire, le nœud lui demande ce qui lui manque, base par base (journal, valeurs longues, DDL compris). Les secondaires d'un même primaire ont des journaux identiques à la position près : le rattrapage est exact. Il porte sur le secondaire le plus avancé (un seul) ; chaque secondaire garde à cet effet les derniers promotion_catchup_mb Mio de son journal.
  3. L'applicateur termine d'appliquer tout ce qui est reçu.
  4. Une nouvelle époque (@@cluster_epoch + 1) est écrite dans l'état du nœud.
  5. Le nœud lève sa lecture seule, restaure le planificateur d'événements (arrêté tant qu'il était secondaire) et se présente désormais comme primaire de cette nouvelle époque ; il garde son journal à partir de la position des autres secondaires sondés, pour qu'ils le rejoignent par le flux plutôt que par un amorçage.
  6. Les autres secondaires, dès que leur lien avec l'ancien primaire est rompu (ou que leurs PING échouent), rejoignent parmi leurs seeds le primaire de l'époque la plus haute.

Si le rattrapage échoue (pair devenu injoignable, journal local endommagé…), la promotion est abandonnée : le nœud reste secondaire et recommence à suivre un primaire, le motif est consigné dans le journal serveur et affiché dans la colonne LAST_ERROR de sa propre ligne de SHOW CLUSTER STATUS. Il faut donc vérifier @@cluster_role après la commande.

Promotion forcée. Quand la perte est acceptée en connaissance de cause (pair le plus avancé définitivement perdu, retard supérieur au seuil, primaire injoignable pour ce nœud mais pas pour les autres…) :

SET GLOBAL cluster_role = 'force_primary';

Chaque refus devient un avertissement consigné (lignes Cluster : promotion de … (forcée) : …), le rattrapage est tenté quand il est possible et son échec n'arrête pas la promotion. Si l'ancien primaire est encore actif, il est écarté dès qu'il voit l'époque supérieure (§16.4.2) : ses écritures des derniers instants ne sont pas perdues silencieusement, elles sont mises en quarantaine à sa rétrogradation.

Il n'y a pas d'élection automatique : c'est l'administrateur qui choisit le nœud à promouvoir et qui exécute la commande.

16.4.2 Retour d'un ancien primaire#

Un nœud qui se croyait primaire et qui reçoit d'un pair une preuve d'une époque plus récente que la sienne passe à l'état FENCED (« écarté ») :

  • il repasse en lecture seule (toute écriture de client reçoit l'erreur 1290) ;
  • @@cluster_role vaut FENCED sur ce nœud ;
  • l'incident est consigné dans le journal serveur (--log).

Un nœud FENCED ne rejoint pas le nouveau primaire automatiquement. Pour le remettre en service comme secondaire :

SET GLOBAL cluster_role = 'secondary';

À partir de là :

  • s'il a des bases dont le journal ne dépasse pas le point où la bascule a eu lieu, il rattrape simplement le flux du nouveau primaire ;
  • s'il a des écritures locales que personne d'autre n'a reçues (parce qu'il a continué à accepter des écritures après la coupure, avant de redémarrer et de se découvrir écarté), ces bases sont réamorcées : leur journal local est déplacé dans miraj/quarantaine/<horodatage>/<base>/, avec un rapport, puis la base est recopiée depuis le nouveau primaire. Aucune écriture non répliquée n'est donc perdue silencieusement — elle reste consultable dans son dossier de quarantaine, mais elle n'est plus dans la base active.

Procédure complète recommandée après la perte du primaire (pour une bascule planifiée, voir §16.4.3) :

  1. Confirmer que l'ancien primaire est arrêté ou injoignable.
  2. Promouvoir le secondaire choisi (SET GLOBAL cluster_role = 'primary', voir §16.4.4 pour le choisir).
  3. Repointer les applications clientes vers l'adresse du nouveau primaire (MIRAJ ne le fait pas à leur place, voir §16.6).
  4. Relancer l'ancien primaire : il démarre avec l'état qu'il avait, se connecte à ses seeds, se découvre FENCED dès qu'il voit l'époque plus récente.
  5. Vérifier SHOW CLUSTER STATUS : le nœud apparaît en FENCED.
  6. SET GLOBAL cluster_role = 'secondary' sur ce nœud pour le faire rejoindre : il rattrape ou se réamorce selon le cas, et son éventuel dossier de quarantaine peut être examiné puis archivé ou supprimé par l'administrateur.

16.4.3 Bascule planifiée sans perte#

Pour changer de primaire sans rien perdre (maintenance du serveur, migration) :

  1. Arrêter les écritures des applications, ou accepter qu'elles reçoivent l'erreur 1290 pendant la bascule.
  2. Sur le primaire actuel : SET GLOBAL cluster_role = 'secondary'. Il repasse en lecture seule, coupe ses secondaires et garde son journal.
  3. Vérifier SELECT @@cluster_role sur ce nœud (SECONDARY).
  4. Sur le secondaire choisi : SET GLOBAL cluster_role = 'primary'. La sonde trouve l'ancien primaire en secondaire ; si celui-ci a écrit des transactions que le candidat n'avait pas encore reçues, le candidat les rattrape depuis lui avant d'être promu.
  5. Repointer les applications vers le nouveau primaire.

L'ancien primaire et les autres secondaires rejoignent ensuite le nouveau primaire par le flux du journal, sans réamorçage ni quarantaine.

16.4.4 Choisir le secondaire à promouvoir#

Tant qu'un secondaire n'a plus de primaire, il interroge ses pairs toutes les 10 secondes : sur chacun, SHOW CLUSTER STATUS montre les autres nœuds à l'état REACHABLE (avec leur rôle, leur époque et, dans LAG_BYTES, leur retard sur la dernière position annoncée par le primaire) ou UNREACHABLE. Sa propre ligne (SELF) donne son propre retard ; information_schema.MIRAJ_REPLICATION le détaille par base (WRITTEN_LSN et PRIMARY_LSN).

Il n'est pas nécessaire de choisir le secondaire le plus avancé : le nœud promu rattrape lui-même le plus avancé de ses pairs joignables. Mieux vaut choisir le nœud le mieux placé pour recevoir les écritures (réseau, capacité), à condition que les autres secondaires soient joignables au moment de la promotion. Avec promotion_catchup_mb = 0, aucun secondaire ne garde de quoi servir un rattrapage : promouvoir un secondaire en retard sur un pair est alors refusé, et il faut promouvoir le plus avancé (ou forcer en acceptant la perte).

16.5 Suivi et supervision#

16.5.1 Variables système#

VariablePortéeDescription
@@read_onlysession/globale, dynamique1 sur un secondaire ou un nœud FENCED (écritures refusées), 0 sur le primaire. Toujours 0 hors édition Cluster.
@@cluster_roleglobalePRIMARY, SECONDARY ou FENCED ; NONE hors réplication active.
@@cluster_epochglobaleNuméro de l'époque courante du nœud (entier croissant à chaque promotion).
@@cluster_nameglobaleNom du cluster tel que défini dans cluster.toml.
@@cluster_node_idglobaleIdentifiant ([node] id) de ce nœud.
@@cluster_primaryglobaleIdentifiant du primaire connu de ce nœud.
@@server_idglobaleDérivé de l'identifiant du nœud ([node] id) sous réplication active ; 1 hors cluster.
@@cluster_sync_commitsession/globale, dynamiqueRéplication semi-synchrone (§16.8) : OFF, RECEIVED ou APPLIED. Valeur de session modifiable par SET [SESSION] et SET STATEMENT … FOR ; SET GLOBAL fixe celle du serveur (valeur initiale : sync_commit de cluster.toml).
@@cluster_sync_replicasglobale, dynamiqueNombre de secondaires qui doivent accuser réception (≥ 1).
@@cluster_sync_timeoutglobale, dynamiqueAttente maximale des accusés, en millisecondes (≥ 1).
@@cluster_sync_timeout_actionglobale, dynamiqueFALLBACK, ERROR ou WAIT : conduite quand les accusés n'arrivent pas à temps (§16.8.3).

16.5.2 SHOW CLUSTER STATUS et information_schema.MIRAJ_NODES#

SHOW CLUSTER STATUS renvoie exactement les colonnes de information_schema.MIRAJ_NODES, une ligne par nœud connu du serveur interrogé (privilège REPLICATION CLIENT ou PROCESS) :

ColonneDescription
NODE_IDIdentifiant du nœud.
ADDRESSAdresse annoncée entre nœuds.
ROLEPRIMARY ou SECONDARY.
STATESELF (le nœud interrogé ; PROMOTING pendant sa promotion ou sa prise de fonctions après une élection, CANDIDATE pendant une campagne en mode raft), CONNECTING, BOOTSTRAPPING, CONNECTED, DISCONNECTED, REBOOTSTRAP_NEEDED, FENCED, REACHABLE / UNREACHABLE (pair sondé par un secondaire sans primaire, ou lors d'une promotion).
EPOCHÉpoque du nœud.
LAG_BYTESOctets du journal du primaire pas encore appliqués par ce nœud, cumulés sur ses bases. Sur la ligne SELF d'un secondaire, comptés jusqu'à la dernière fin de journal annoncée par le primaire (ce qui n'est pas encore reçu compte aussi) ; pour un pair sondé, son retard annoncé.
LAG_SECONDSAncienneté du dernier lot appliqué (secondes, NULL si sans objet).
CONNECTED_SINCEHorodatage de la connexion en cours (NULL sinon).
LAST_ERRORDernière erreur de réplication rencontrée pour ce nœud, vide sinon ; sur la ligne du nœud lui-même, le motif de l'abandon de sa dernière promotion s'il y en a un (en mode raft, sinon, celui du dernier échec d'une élection : refus d'un votant, absence de majorité joignable).
SYNCRéplication semi-synchrone (§16.8), vue du primaire : sur sa propre ligne, OFF, RECEIVED, APPLIED ou MAJORITY (valeur globale de @@cluster_sync_commit), ou DEGRADED si une base au moins est en état dégradé ; sur la ligne d'un secondaire, YES s'il compte pour les accusés (connecté et en flux), NO sinon (amorçage, déconnexion). NULL sur un secondaire.

16.5.3 information_schema.MIRAJ_REPLICATION#

Détail par nœud et par base :

ColonneDescription
NODE_IDNœud concerné.
DATABASENom de la base.
BASE_IDIdentité interne de la base (utile pour distinguer un DROP suivi d'un CREATE du même nom).
SENT_LSNDernier LSN expédié à ce nœud pour cette base (NULL si sans objet, par exemple côté secondaire).
WRITTEN_LSNDernier LSN écrit dans le journal local de ce nœud pour cette base.
APPLIED_LSNDernier LSN appliqué aux tables.
RETAINED_LSNLSN en dessous duquel le primaire ne garantit plus de pouvoir rattraper ce nœud sans réamorçage (NULL si sans objet).
PRIMARY_LSNFin du journal du primaire pour cette base : sur le primaire, la fin de son journal ; sur un secondaire, la dernière fin annoncée par le primaire (chaque seconde), qui reste connue après la perte du primaire (NULL si inconnue). PRIMARY_LSN − WRITTEN_LSN est ce qu'un secondaire n'a pas encore reçu.
SYNCED_LSNPlus haute position de la base confirmée aux clients par la réplication semi-synchrone (§16.8) : sur le primaire, la sienne ; sur un secondaire, la dernière annoncée par le primaire, qui reste connue après sa perte (NULL si inconnue, 0 si aucune).

Exemple :

SELECT NODE_ID, `DATABASE`, WRITTEN_LSN, APPLIED_LSN
FROM information_schema.MIRAJ_REPLICATION
WHERE `DATABASE` = 'gestium_prod'
ORDER BY NODE_ID;

16.5.4 Journalisation#

Avec --log (toujours recommandé, voir le chapitre sur le démarrage du serveur), le niveau info reçoit la connexion et la déconnexion de chaque pair, le début et la fin d'un amorçage, et chaque promotion ; le journal serveur (fichier) reçoit toute erreur de réplication : refus de poignée de main (nom de cluster différent, autorité TLS étrangère, certificat révoqué, nœud hors de la liste des membres ou qui déclare un autre identifiant que celui de son certificat, édition incompatible), époque annoncée ignorée (saut anormal), connexions entre nœuds en surnombre, rupture de séquence du journal, échec du rejeu d'un DDL, passage d'un secondaire à l'état « à réamorcer », ou table réécrite hors journal par le primaire (réamorçage demandé par le secondaire, §16.7).

Lignes propres à la promotion :

LigneNiveauSens
Cluster : promotion de n3 : rattrapage prévu depuis n2 (… octets sur … base(s)). / … : sans rattrapage.infoPromotion acceptée, plan retenu.
Cluster : promotion de n3 (forcée) : …erreurAvertissement : pair injoignable, ou refus levé par 'force_primary'.
Cluster : promotion de n3 refusée : …erreurPromotion refusée (erreur 9003 rendue au client).
Cluster : rattrapage depuis n2 terminé (… octets).infoRattrapage réussi.
Cluster : promotion de n3 abandonnée : …erreurÉchec en arrière-plan : le nœud reste secondaire.
Cluster : promotion forcée de n3 : base … servie telle qu'avant la réécriture hors journal …erreur'force_primary' sur un secondaire qui attendait le réamorçage de cette base (§16.7) : journal reçu au-delà mis de côté, modifications de l'ancien primaire perdues.
Cluster : rattrapage demandé par n3 (… base(s)). / Cluster : rattrapage de n3 servi (… octets).infoCôté du pair qui sert le rattrapage.

Lignes propres à la réplication semi-synchrone (§16.8), sur le primaire, aux changements d'état seulement (jamais une ligne par écriture) :

LigneNiveauSens
Cluster : réplication synchrone dégradée sur la base shop : aucun accusé de 1 secondaire(s) en 10000 ms ; les écritures suivantes ne patientent plus jusqu'au rattrapage.erreurDélai dépassé : la base passe en état dégradé (§16.8.3).
Cluster : réplication synchrone rétablie sur la base shop.infoLes secondaires ont rattrapé : les écritures attendent de nouveau leurs accusés.
Cluster : 2 session(s) en attente d'accusé interrompue(s) : demoted.erreurLe nœud a cessé d'être primaire (demoted, fenced) pendant que des sessions attendaient : elles reçoivent l'erreur 9004.

16.6 Comportement pour les clients applicatifs#

  • Lecture sur un secondaire : un client se connecte à un secondaire exactement comme à n'importe quel serveur MIRAJ, sur son port client habituel (7007 par défaut) — aucun changement de protocole ni d'outil côté client.
  • Écriture sur un secondaire : toute instruction qui écrit (DML, DDL, gestion des comptes) reçoit l'erreur 1290 (ER_OPTION_PREVENTS_STATEMENT, message « the server is running with the read_only option so it cannot execute this statement »), le code que les pilotes et répartiteurs de charge reconnaissent habituellement pour rediriger une écriture vers le primaire. Restent permis sur un secondaire : toutes les lectures, les tables temporaires de la session, SET, START TRANSACTION/COMMIT/ROLLBACK (sans écriture), KILL, FLUSH, LOCK TABLES … READ, CHECK TABLE et REPAIR TABLE (locaux au secondaire), OPTIMIZE TABLE (sans effet : le secondaire rejoue le compactage fait sur le primaire).
  • Avec miraj-proxy (chapitre 22) : les applications se connectent au proxy, qui mène le port d'écriture au primaire courant (y compris après une bascule) et le port de lecture au secondaire le moins chargé ; rien d'autre à prévoir que la reconnexion après une connexion perdue.
  • Stratégie applicative sans proxy : n'écrire que sur le primaire, connu par sa configuration côté application ; répartir les lectures qui tolèrent un léger retard (LAG_SECONDS) vers un ou plusieurs secondaires ; sur réception d'une erreur 1290, considérer que le nœud contacté n'est plus (ou pas) le primaire et se reconnecter au primaire attendu, en interrogeant au besoin SHOW CLUSTER STATUS ou @@cluster_primary sur un nœud connu du cluster pour le retrouver.
  • Suivre les changements depuis un secondaire : LISTEN TABLE et WAIT FOR CHANGES fonctionnent sur tous les nœuds, qui rendent les mêmes événements que le primaire ; NOTIFY reste local au nœud qui l'exécute (§16.11).

16.7 Limites actuelles connues#

Ces points sont établis dans le code et la documentation interne de conception du moteur, pas des intentions futures :

  • Réplication asynchrone par défaut. Avec @@cluster_sync_commit = OFF (valeur par défaut), le primaire ne consulte pas les secondaires avant de rendre la main au client qui écrit. Une écriture confirmée au client peut donc n'être encore arrivée à aucun secondaire ; si le primaire est perdu à ce moment, cette écriture reste sur l'ancien primaire, mise en quarantaine lors de son retour, jamais rejouée automatiquement sur le nouveau primaire. Le rattrapage à la promotion réduit cette fenêtre de perte aux écritures reçues par aucun secondaire joignable (et non plus à celles que le seul nœud promu n'avait pas reçues). La réplication semi-synchrone (§16.8) la ferme pour les écritures confirmées sans avertissement.
  • Semi-synchrone, visibilité avant accusé. En réplication semi-synchrone, une écriture est validée et visible des autres sessions du primaire avant l'arrivée des accusés ; un délai dépassé, KILL QUERY ou une rétrogradation pendant l'attente laissent l'écriture validée localement (§16.8.4). Les réglages SET GLOBAL cluster_sync_* ne sont pas persistés (ceux de cluster.toml s'appliquent au redémarrage).
  • Mode manuel : un seul primaire, promotion manuelle, sans consensus. Il n'y a pas d'élection automatique du nouveau primaire. La promotion contrôlée refuse de promouvoir un secondaire tant que le primaire est joignable par lui ou par un pair, mais un primaire injoignable de tous les nœuds sondés et pourtant actif (partition réseau complète) peut encore accepter des écritures jusqu'à ce qu'il voie la nouvelle époque ; 'force_primary' lève ces contrôles.
  • Mode raft (incrément 1) : élection par quorum, avec ses limites (§16.9.8) : un leader isolé accepte encore des écritures OFF pendant son bail (elles finissent en quarantaine) ; les lectures ne sont pas linéarisables ; une élection peut rester bloquée (base absente chez les survivants, bases de marques d'époque incomparables) jusqu'au retour d'un nœud ou à 'force_primary' ; une base supprimée pendant l'absence d'un nœud peut réapparaître si ce nœud est élu ; les membres sont fixes (seeds, redémarrage pour en changer).
  • Rattrapage borné. Le rattrapage à la promotion porte sur un seul pair, le plus avancé, et sur ce que ce pair garde encore de son journal (promotion_catchup_mb, 64 Mio par base par défaut) ; il n'y a pas d'amorçage entre secondaires. Deux promotions lancées en même temps sur deux nœuds se refusent l'une l'autre, sans départage automatique.
  • Répartition des données limitée aux partitions dédiées. Chaque nœud porte une copie complète de chaque base, hormis les partitions dédiées à un nœud (§16.10), qui n'ont aucune copie : la perte de leur nœud les rend indisponibles.
  • Pas de renommage de table. Dans un cluster à plusieurs nœuds, RENAME TABLE et ALTER TABLE … RENAME sont refusés sur tous les nœuds (erreur 1290, option cluster), que la table ait ou non des partitions dédiées ; seule une table temporaire reste renommable. Pour changer le nom d'une table, créez-la sous le nouveau nom, recopiez les lignes (INSERT … SELECT), puis supprimez l'ancienne.
  • Tables disque. Une table disque (ENGINE = Aria, MyISAM ou DISK, voir Moteurs de stockage) se réplique comme une table en mémoire : mêmes enregistrements sur le lien, mêmes numéros de ligne sur tous les nœuds. Chaque nœud range les lignes dans son propre fichier .dmrj et fait ses propres points de sauvegarde : la disposition physique des fichiers peut différer d'un nœud à l'autre, seules les lignes comptent (comparez les lignes, jamais les octets des fichiers). À l'amorçage d'un secondaire, les fichiers .dmrj et .dbmrj sont copiés à chaud : le point de sauvegarde de la table copiée est suspendu le temps de la copie (ses écritures continuent, ses pages modifiées restent en mémoire), et une copie de plus de 30 secondes est signalée dans le journal du serveur.
  • Séquences. CREATE SEQUENCE et DROP SEQUENCE s'exécutent sur le primaire et le fichier sequences.mrq de chaque base est copié à l'amorçage d'un secondaire, mais les valeurs servies par NEXTVAL ne passent pas par le journal des lignes (l'état d'une séquence est écrit dans son propre fichier, chapitre 6, « Séquences »). Après une bascule, vérifiez la position des séquences sur le nouveau primaire (SELECT * FROM s) et avancez-la au besoin par SETVAL.
  • Ce qui n'est pas répliqué : la base système des comptes est relayée par un canal séparé (hors journal), non par le mécanisme de réplication du journal lui-même ; les tables temporaires (de session et globales) ; REPAIR TABLE (locale à chaque nœud ; sur le primaire, une réparation qui remplace des valeurs fait réamorcer la base sur les secondaires, voir « Table réécrite hors journal ») ; la dernière date d'exécution d'un événement planifié (les événements planifiés ne s'exécutent que sur le primaire, un secondaire ne les exécute jamais) ; les réglages positionnés par SET GLOBAL ; le contenu des vues en cache ; le journal des requêtes lentes.
  • DDL non déterministe. Un DDL rejoué par SQL sur un secondaire qui évalue une expression sur les lignes existantes au moment de son exécution (par exemple une valeur par défaut calculée) peut produire un résultat différent de celui obtenu sur le primaire, puisqu'il est réexécuté plutôt que rejoué ligne à ligne.
  • Table réécrite hors journal : réamorçage automatique. Quand le primaire réécrit en entier le fichier d'une table en y portant des modifications qu'aucun enregistrement du journal ne décrit (fichier sans position de journal, numéros de ligne au-delà de 2^32, REPAIR TABLE qui remplace des valeurs, conversion d'une colonne vers un type BLOB qui externalise ses valeurs), il écrit d'abord dans le journal un marqueur « table réécrite hors journal ». Chaque secondaire qui l'atteint sans disposer déjà de ce fichier s'arrête juste avant lui, sans rien appliquer de la suite, et se fait réamorcer : la base est recopiée depuis le primaire, puis le flux reprend. Le secondaire est donc en retard le temps de la copie, jamais faux ; le motif est dans la colonne LAST_ERROR de sa propre ligne jusqu'à la fin de la copie, la ligne Cluster : base … : table … réécrite hors journal par le primaire (position …), réamorçage nécessaire : la base sera recopiée depuis le primaire. est consignée dans son journal serveur, et Miraj_cluster_unlogged_rebootstraps (§16.8.5) compte ces réamorçages. Tant que la copie n'est pas faite, 'primary' refuse de promouvoir ce secondaire (erreur 9003, database(s) … waiting to be copied again from the primary). Si le primaire est perdu pendant cette attente, 'force_primary' passe outre : chaque base en attente est servie telle qu'elle est, c'est-à-dire dans son état cohérent d'avant la réécriture ; le journal reçu au-delà (jamais appliqué) est recopié en quarantaine puis retiré, et les modifications de l'ancien primaire à partir du marqueur sont perdues (ligne Cluster : promotion forcée de … : base … servie telle qu'avant la réécriture hors journal …). Comme après toute promotion forcée, l'ancien primaire qui revient est écarté (FENCED) puis réamorcé une fois rétrogradé, et les secondaires qui avaient reçu la suite divergent et sont réamorcés. En mode raft, un nœud qui attend ainsi son réamorçage abandonne l'élection qu'il gagnerait (motif dans LAST_ERROR) : une autre élection suit ; seul 'force_primary' le promeut. Une table de plus de 2^32 lignes réécrit son fichier à chaque écriture : chacune porte un marqueur, et ses secondaires se réamorcent tant que ces écritures continuent. Un nœud seul, hors d'un cluster actif, n'écrit aucun marqueur.
  • Seuil des LOB abaissé. Tous les nœuds doivent avoir le même --lob-threshold (poignée de main). Quand un primaire en mode manuel redémarre avec un seuil plus bas (ou lit le fichier d'un moteur antérieur), les valeurs BLOB restées dans ses tables qui atteignent le seuil rejoignent le magasin à l'ouverture par des UPDATE ordinaires consignés au journal, par tranches de 10 000 lignes ou 4 Mio : les secondaires les appliquent et reçoivent les valeurs par le flux, sans réamorçage : le primaire relancé retient pour eux son journal jusqu'à leur retour (dans la limite de journal_retention_mb, §16.2.1). Un arrêt au milieu est repris au redémarrage suivant. Au-delà d'un plafond par table (256 Mio, ou journal_retention_mb s'il est plus petit), rien n'est déplacé : les valeurs restent dans la table, lisibles et modifiables, seules les valeurs écrites ensuite suivent le nouveau seuil, et le journal serveur reçoit AVERTISSEMENT : base.table : … valeur(s) BLOB … restent dans la table, au-delà du plafond de migration journalisée …. Un secondaire ne déplace jamais rien ; un nœud en mode raft redémarre toujours suiveur et ne migre donc pas ses valeurs.
  • Fenêtre de perte d'un DDL à l'arrêt brutal. Un DDL est journalisé après le succès durable de ses effets sur disque ; un arrêt brutal du primaire survenant exactement entre ces deux étapes laisse le DDL appliqué localement sans avoir été expédié aux secondaires (même classe de risque qu'une politique de sauvegarde relâchée pour une coupure de courant).
  • Retard non borné en cas de secondaire durablement absent. Un secondaire injoignable au-delà de journal_retention_mb de journal accumulé n'est plus rattrapable par le flux normal : il est réamorcé par copie complète des bases à sa reconnexion.
  • Répartition de charge par un proxy séparé. Le serveur lui-même ne redirige pas une écriture reçue par un secondaire (erreur 1290). La redirection automatique vers le primaire et la répartition des lectures passent par miraj-proxy (chapitre 22), qui choisit le nœud à l'ouverture de chaque connexion.
  • Journal en version 3, propre à un nœud de cluster. Une base d'un nœud de l'édition Cluster utilise un journal en version 3 (qui peut porter des enregistrements de DDL et de compactage de valeurs longues) ; une édition antérieure au support du cluster refuse ce journal proprement plutôt que de le lire de travers, tandis qu'une édition Entreprise ou Express le lit et l'ignore sans réplication.

16.8 Réplication semi-synchrone#

Par défaut, une écriture est confirmée au client dès qu'elle est validée sur le primaire (réplication asynchrone). La réplication semi-synchrone fait attendre, avant la confirmation, qu'un ou plusieurs secondaires aient reçu l'écriture (ou l'aient appliquée). Elle se règle par session : une application peut la demander pour ses écritures critiques seulement (une facture, un paiement) et laisser les autres en asynchrone.

16.8.1 Niveaux#

@@cluster_sync_commitL'écriture est confirmée quand…Garantie
OFF (défaut)…elle est validée sur le primaire.Aucune au-delà du primaire.
RECEIVED…@@cluster_sync_replicas secondaires l'ont écrite dans leur journal (vidé sur disque avec ack = "durable").Une écriture confirmée sans avertissement survit à la perte du primaire : le secondaire promu la récupère (§16.8.6).
APPLIED…@@cluster_sync_replicas secondaires l'ont appliquée à leurs tables.Idem, et lecture après écriture : une lecture faite ensuite sur ces secondaires la voit.
MAJORITY…une majorité des membres du cluster (les seeds, primaire compris) l'a écrite dans son journal : le nombre d'accusés est calculé (cluster_sync_replicas est ignoré).En mode raft (où c'est le niveau par défaut), une écriture confirmée sans erreur survit à toute élection (§16.9.5). Juste après une élection, l'attente laisse aux suiveurs le temps de se reconnecter au nouvel élu (dans le délai).

Les secondaires qui comptent sont ceux qui sont connectés au primaire et en flux (pas en cours d'amorçage) : la colonne SYNC de SHOW CLUSTER STATUS les montre à YES sur le primaire.

16.8.2 Portées#

  • SET [SESSION] cluster_sync_commit = 'received' : pour les écritures suivantes de la session ; SET cluster_sync_commit = DEFAULT rend la valeur du serveur.
  • SET STATEMENT cluster_sync_commit = 'applied' FOR INSERT … : pour une seule instruction.
  • SET GLOBAL cluster_sync_commit = 'received' : valeur du serveur, suivie par les sessions qui n'ont pas fixé la leur.
  • cluster_sync_replicas, cluster_sync_timeout (millisecondes) et cluster_sync_timeout_action n'existent qu'au niveau global (SET GLOBAL, erreur 1229 sinon) : ce sont des réglages d'exploitation.

Les valeurs initiales viennent de cluster.toml (sync_commit, sync_replicas, sync_timeout_ms, sync_timeout_action, §16.2.1) ; un SET GLOBAL agit immédiatement mais n'est pas persisté. Ces variables se lisent dans toutes les éditions (OFF, 1, 10000, FALLBACK) ; les modifier hors édition Cluster renvoie l'erreur 9002. Sans cluster.toml, elles sont acceptées sans effet. Réglées sur un secondaire, elles valent à sa promotion.

L'attente porte sur l'instruction entière : une instruction validée seule (auto-validation), un COMMIT, une validation implicite (DDL, SET autocommit = 1) ; un CALL attend une seule fois, à sa fin, pour toutes les écritures de la procédure (de même pour les écritures faites par des déclencheurs ou des fonctions stockées). Un ROLLBACK et une lecture n'attendent jamais. Les événements planifiés n'attendent jamais.

16.8.3 Délai, conduites et état dégradé#

Quand les accusés n'arrivent pas dans @@cluster_sync_timeout millisecondes (10 000 par défaut), ou quand moins de @@cluster_sync_replicas secondaires sont en flux (décision immédiate, sans attendre le délai), @@cluster_sync_timeout_action décide :

ConduiteEffet
FALLBACK (défaut)L'écriture est confirmée avec un avertissement 9004 (SHOW WARNINGS) : Synchronous replication: transaction committed locally, 0 of 1 acknowledgement(s) received within 10000 ms ou …, only 0 secondary(ies) connected, 1 required.
ERRORLe client reçoit l'erreur 9004 avec le même texte. L'écriture reste validée sur le primaire : c'est un « résultat inconnu » pour la réplication, pas une annulation. L'application ne doit pas rejouer aveuglément l'écriture (risque de doublon) : relire, puis décider.
WAITAttente sans limite, jusqu'aux accusés. Seuls KILL QUERY, KILL CONNECTION, une rétrogradation ou un écartement du nœud l'interrompent.

Un délai dépassé met la base en état dégradé (journal serveur : réplication synchrone dégradée sur la base …, colonne SYNC à DEGRADED sur la ligne du primaire) : les écritures suivantes de cette base n'attendent plus et reçoivent directement l'avertissement (ou l'erreur), au lieu de payer le délai chacune. Dès que @@cluster_sync_replicas secondaires ont rattrapé la fin du journal de la base (écrite, ou appliquée si l'attente expirée était en APPLIED), l'état dégradé est levé (réplication synchrone rétablie sur la base …). La conduite WAIT ignore l'état dégradé.

16.8.4 Sémantique précise#

  • Validation locale d'abord. L'écriture est validée sur le primaire (journal écrit, ou vidé selon save_policy), ses verrous sont rendus, puis la session attend les accusés avant de répondre. Pendant cette attente, l'écriture est déjà visible des autres sessions du primaire.
  • KILL QUERY pendant l'attente : l'instruction réussit avec l'avertissement 9004 wait cancelled, transaction committed locally. KILL CONNECTION : la connexion est fermée (1927), l'écriture reste validée.
  • Rétrogradation ou écartement du nœud pendant l'attente : erreur 9004 node is no longer primary (demoted); transaction committed locally, acknowledgement unknown. Si aucun secondaire ne l'avait reçue, cette écriture finira en quarantaine au retour du nœud (§16.4.2).
  • save_policy : en periodic, une écriture en mode semi-synchrone attend au moins l'écriture du journal dans son fichier (seuls les octets écrits sont expédiés aux secondaires). En manual, rien n'est journalisé ni répliqué : le réglage est sans effet.
  • APPLIED et DDL : un DDL appliqué par un secondaire attend qu'aucune transaction de client ne tienne la table sur ce secondaire (lock_wait_timeout) ; une écriture en APPLIED qui le suit peut donc expirer. De même si l'application échoue sur un secondaire (voir LAST_ERROR), ou si une session du secondaire tient la table par LOCK TABLES … READ.
  • Coût : chaque écriture attend un aller-retour réseau et quelques millisecondes (le primaire sollicite un accusé immédiat du secondaire dès qu'une session attend). En OFF, rien ne change par rapport à la réplication asynchrone.
  • Un secondaire arrêté brutalement est vu déconnecté immédiatement dans la plupart des cas (liaison fermée), mais une coupure réseau silencieuse n'est détectée qu'après 15 secondes sans trame : d'ici là, les écritures attendent le délai.
  • @@cluster_sync_replicas supérieur au nombre de secondaires : chaque écriture reçoit l'avertissement (ou l'erreur) immédiatement.

16.8.5 Compteurs#

SHOW STATUS LIKE 'Miraj_cluster_sync%' (réplication active seulement) :

VariableSens
Miraj_cluster_sync_waitsAttentes commencées (une par base et par instruction).
Miraj_cluster_sync_fallbacksÉcritures confirmées sans les accusés, avec l'avertissement 9004.
Miraj_cluster_sync_errorsErreurs 9004 rendues (conduite ERROR, nœud plus primaire).
Miraj_cluster_sync_timeoutsDélais dépassés (y compris les écritures d'une base dégradée).
Miraj_cluster_sync_cancelledAttentes interrompues par KILL.
Miraj_cluster_sync_wait_avg_ms, Miraj_cluster_sync_wait_max_msDurée moyenne et maximale d'une attente.
Miraj_cluster_sync_degraded_basesBases en état dégradé.
Miraj_cluster_sync_secondariesSecondaires en flux, qui comptent pour les accusés.

Hors réplication semi-synchrone, SHOW STATUS LIKE 'Miraj_cluster_unlogged_rebootstraps' compte, sur un secondaire, les réamorçages de bases qu'il a demandés depuis son démarrage après une table réécrite hors journal par le primaire (§16.7).

16.8.6 Garantie à la promotion#

Avec RECEIVED (ou APPLIED) et @@cluster_sync_replicas = N, toute écriture confirmée sans avertissement est dans le journal de N secondaires. À la perte du primaire, la promotion contrôlée (§16.4.1) rattrape le secondaire le plus avancé : l'écriture n'est perdue que si ces N secondaires sont tous perdus ou injoignables. Le primaire annonce en outre à ses secondaires la plus haute position confirmée de chaque base (SYNCED_LSN, §16.5.3) ; une promotion qui ne pourrait pas l'atteindre (aucun nœud joignable ne la tient) est refusée (9003, writes confirmed to clients), sauf 'force_primary'. Cette position n'est gardée qu'en mémoire : un nœud redémarré ne la connaît plus.

Exemple, lecture après écriture entre nœuds :

-- Session de l'application, sur le primaire
SET SESSION cluster_sync_commit = 'applied';
INSERT INTO facture (id, montant) VALUES (1042, 250.00);
-- OK : la facture est appliquée sur le secondaire ; un rapport qui l'y lit maintenant la voit

16.9 Élection automatique (mode raft)#

Avec mode = "raft" dans cluster.toml (tous les nœuds), le primaire n'est plus désigné par l'administrateur : il est élu par une majorité des membres, et un autre l'est automatiquement quand il disparaît. Le mécanisme suit le protocole de consensus Raft (termes, un vote par terme, pré-vote, bail du leader), adapté au journal par base de MIRAJ. C'est un premier incrément : il garantit la sûreté des écritures confirmées ; il ne change ni les membres à chaud, ni la répartition des données.

16.9.1 Membres, quorum, terme#

  • Les membres votants sont les adresses de seeds (dédoublonnées), qui doivent contenir l'adresse annoncée de chaque nœud. Avec N membres, une majorité est N / 2 + 1 membres (2 sur 3, 3 sur 5). Trois membres tolèrent la perte d'un nœud, cinq celle de deux ; deux membres n'en tolèrent aucune.
  • Le terme est l'époque du nœud (@@cluster_epoch) : un candidat l'incrémente, un nœud qui en apprend une supérieure l'adopte. Chaque nœud accorde au plus un vote par terme, écrit dans miraj/cluster.mrs avant de répondre.
  • Un nœud redémarre toujours suiveur (SECONDARY) ; son terme et son vote sont gardés.

16.9.2 Déroulement d'une élection#

  1. Un suiveur sans nouvelle du leader depuis un délai tiré au sort entre election_timeout_ms et le double fait d'abord un pré-vote : il demande aux membres s'ils voteraient pour lui, sans rien changer chez eux. Un nœud qui entend encore son leader répond non (adhérence) : un nœud isolé qui revient ne dépose pas le leader en place.
  2. Avec une majorité de « oui », il incrémente son terme, vote pour lui-même et demande les votes. Un membre accorde son vote si son propre journal n'est pas plus à jour que celui du candidat, base par base : chaque base porte la marque d'époque du dernier leader qui y a écrit ; un candidat de marque inférieure est refusé, à marque égale sa position importe peu (il rattrapera), une base qui manque au candidat entraîne un refus.
  3. Élu, le nœud rattrape d'abord, base par base, le votant le plus avancé de même marque, applique tout, puis écrit sa propre marque d'époque dans chaque base avant d'accepter la moindre écriture : @@cluster_role passe alors à PRIMARY. Un rattrapage impossible abandonne l'élection (motif dans LAST_ERROR).
  4. Les autres nœuds suivent le nouveau leader ; ceux dont le journal n'en est pas un préfixe (ancien leader qui avait des écritures non confirmées) sont réamorcés, leur ancienne copie mise en quarantaine.

Les journaux du serveur retracent l'élection : Cluster : candidat au terme 4., Cluster : vote accordé à n1 (terme 4)., Cluster : nœud n1 élu leader (terme 4, 2 vote(s) sur 3)., Cluster : aucune majorité : 1 vote(s) sur les 2 requis au terme 4.

16.9.3 Bail du leader#

Un leader qui n'a reçu aucune trame d'une majorité des membres (lui compris) pendant election_timeout_ms (après un délai de grâce d'autant à son élection) rend son rôle : il repasse en lecture seule (1290), les sessions qui attendaient un accusé reçoivent l'erreur 9004 (quorum lost), et il le consigne (nœud n1 rétrogradé automatiquement (mode raft) : majorité perdue…). Un leader qui apprend un terme supérieur est rétrogradé de même, sans intervention (au lieu d'être écarté, FENCED, comme en mode manuel).

16.9.4 Niveau MAJORITY#

En mode raft, sync_commit vaut majority et sync_timeout_action error sauf réglage explicite : une écriture n'est confirmée au client qu'une fois écrite dans le journal d'une majorité des membres ; sinon le client reçoit l'erreur 9004 (l'écriture reste validée localement, son sort dépend de l'élection suivante). MAJORITY est aussi utilisable en mode manuel (majorité calculée sur les seeds).

16.9.5 Garanties#

  • Un seul leader par terme. Deux leaders peuvent coexister brièvement à des termes différents (un ancien leader isolé, avant la fin de son bail), jamais au même terme ; seul le plus récent peut confirmer une écriture en MAJORITY.
  • Aucune écriture confirmée en MAJORITY n'est perdue par une élection : elle est dans le journal d'une majorité ; le nouvel élu l'est par une majorité, qui la croise ; le votant commun a refusé un candidat moins à jour, ou le candidat le rattrape avant de servir.
  • Ce qui peut être perdu : les écritures OFF (ou en MAJORITY qui ont reçu l'erreur 9004) faites sur un leader qui perd la majorité ; elles restent en quarantaine sur l'ancien leader.
  • Les lectures ne sont pas linéarisables : un ancien leader lit encore ses données jusqu'à la fin de son bail, un suiveur est en retard.

16.9.6 Commandes manuelles en mode raft#

CommandeEffet en mode raft
SET GLOBAL cluster_role = 'primary' sur un suiveurCampagne immédiate avec transfert : les votants passent outre l'adhérence à leur leader, qui rend son rôle. Refus 9003 avec le motif du premier refus (node n2 refused: not up to date on database shop (epoch 4 < 5)) ou cluster has no quorum: 1 of 3 members reachable.
SET GLOBAL cluster_role = 'secondary' sur le leaderLe leader rend son rôle et ne se présente pas pendant deux délais d'élection : un autre membre est élu.
SET GLOBAL cluster_role = 'force_primary'Levier d'urgence hors quorum (deux nœuds sur trois perdus) : promotion sans vote au terme suivant, sans bail tant qu'une majorité ne l'a pas rejoint, consignée promotion forcée hors quorum : écritures confirmées possiblement perdues. Les écritures MAJORITY échouent (9004) tant qu'aucune majorité n'est revenue : passer au besoin SET GLOBAL cluster_sync_commit = 'off'.

16.9.7 Passer du mode manuel au mode raft#

Faire converger le cluster (aucun retard), arrêter les nœuds, ajouter mode = "raft" (et au besoin election_timeout_ms) dans chaque cluster.toml en vérifiant que seeds contient tous les nœuds, puis les redémarrer : ils redémarrent suiveurs et élisent un leader. Le retour au mode manuel suit le même chemin (le nœud à désigner primaire l'est ensuite par 'primary').

16.9.8 Limites de l'incrément actuel#

  • Une élection peut rester bloquée sans être dangereuse : base absente chez tous les candidats possibles (création pendant l'absence d'un nœud), bases dont les marques d'époque se contredisent entre survivants. Elle se débloque au retour du nœud manquant, ou par 'force_primary' ; le motif est dans LAST_ERROR.
  • Une base supprimée pendant l'absence d'un nœud réapparaît si ce nœud est élu ensuite.
  • Un suiveur dont le journal précède l'élection du leader d'une marque plus ancienne est réamorcé (copie complète) plutôt que rattrapé.
  • Les membres sont fixes : en changer demande un redémarrage de tous les nœuds.
  • Les délais supposent des écritures sur disque rapides : un disque très lent (vidage de plusieurs centaines de millisecondes) impose un election_timeout_ms plus grand.

16.10 Répartir une table entre plusieurs nœuds (partitions dédiées)#

16.10.1 À quoi cela sert#

Par défaut, chaque nœud du cluster garde une copie complète de chaque base. Une table partitionnée (chapitre 6, « Partitionnement ») peut en plus dédier une ou plusieurs de ses partitions à un nœud précis : les lignes de ces partitions ne sont rangées que chez ce nœud. Les autres nœuds connaissent la table et savent où sont ses lignes, mais ne les stockent pas.

Pour l'application, rien ne change : elle interroge la table comme d'habitude, sur n'importe quel nœud. Quand une requête a besoin d'une partition rangée ailleurs, le nœud qui reçoit la requête va la lire chez le nœud qui la tient et assemble le résultat.

Cas d'usage typiques :

  • Une table trop grosse pour une seule machine : les écritures comptables de chaque société, ou de chaque exercice, sur des serveurs différents.
  • Répartir la charge : les requêtes d'une société élaguées à sa partition sont servies par le nœud qui la tient, sans charger les autres ; une requête sur toutes les sociétés fait travailler tous les nœuds en même temps.
  • Garder les données près de leurs utilisateurs : la partition d'un site sur le serveur de ce site.

Important — une partition dédiée n'a aucune copie. Ses lignes ne sont pas répliquées sur les secondaires. Si le nœud qui la tient est perdu (disque détruit, machine perdue), ces lignes sont perdues. Réservez les partitions dédiées à des données que vous pouvez reconstituer ou que vous sauvegardez par ailleurs, et gardez les données critiques dans des partitions ordinaires (répliquées sur tous les nœuds). Voir §16.10.10.

16.10.2 Mise en place#

Prérequis : un cluster en service (§16.3), édition Cluster, et de préférence la liste members renseignée dans cluster.toml (§16.2.3). Les identifiants de nœud utilisés ci-dessous (n1, n2, n3) sont ceux de la clé [node] id de chaque nœud.

1. Créer la table sur le primaire, en ajoutant NODE 'identifiant' aux partitions à dédier :

CREATE TABLE compta.ecritures (
    societe INT NOT NULL,
    id      INT NOT NULL,
    montant DECIMAL(12,2),
    PRIMARY KEY (societe, id)
) PARTITION BY LIST (societe) (
    PARTITION p1 VALUES IN (1),               -- partition ordinaire : copiée sur tous les nœuds
    PARTITION p2 VALUES IN (2) NODE 'n2',     -- lignes de la société 2 : chez n2 seulement
    PARTITION p3 VALUES IN (3) NODE 'n3'      -- lignes de la société 3 : chez n3 seulement
);

Écritures admises : NODE 'n2', NODE = 'n2' ou NODE n2. La clause se place sur une partition (elle vaut alors pour toutes ses sous-partitions) ou sur une sous-partition. Toutes les méthodes de partitionnement sont possibles (RANGE, LIST, HASH, KEY).

La définition de la table est répliquée comme tout DDL : quelques instants plus tard, chaque nœud connaît la table, et n2 et n3 ont créé leur partition.

2. Ajouter une partition dédiée à une table existante :

ALTER TABLE compta.ecritures ADD PARTITION (PARTITION p4 VALUES IN (4) NODE 'n2');

DROP PARTITION et TRUNCATE PARTITION fonctionnent aussi sur une partition dédiée : ses lignes sont supprimées chez son nœud.

Un CREATE TABLE ou un ALTER TABLE d'une table à partitions dédiées ne rend la main qu'une fois appliqué par les nœuds qui les tiennent (au plus fragment_timeout_ms) : la table peut être écrite et lue aussitôt après. Un nœud arrêté (ou coupé depuis plus de quelques secondes) n'est pas attendu, pas plus qu'un nœud en retard au-delà de ce délai : l'instruction réussit avec un avertissement 9038 par nœud, et ses partitions répondent dès qu'il a rattrapé le primaire.

3. Vérifier le placement, depuis n'importe quel nœud :

SELECT PARTITION_NAME, NODE, STATE, TABLE_ROWS
FROM information_schema.MIRAJ_FRAGMENTS
WHERE TABLE_SCHEMA = 'compta' AND TABLE_NAME = 'ecritures';
+----------------+-------+--------+------------+
| PARTITION_NAME | NODE  | STATE  | TABLE_ROWS |
+----------------+-------+--------+------------+
| p1             | local | LOCAL  |      12408 |
| p2             | n2    | REMOTE |      98311 |
| p3             | n3    | REMOTE |      45020 |
+----------------+-------+--------+------------+
ColonneSens
NODENœud qui tient la partition ; local pour une partition ordinaire, copiée partout.
STATELOCAL : les lignes sont sur le nœud interrogé ; REMOTE : elles sont chez un autre nœud, qui a répondu ; UNREACHABLE : ce nœud ne répond pas (dans les deux secondes).
TABLE_ROWSNombre de lignes, demandé au nœud qui tient la partition ; NULL s'il ne répond pas. Cette requête n'échoue jamais, même quand un nœud est arrêté : c'est le bon moyen de vérifier l'état des partitions.

SHOW CREATE TABLE restitue la clause (NODE = 'n2').

Règles de placement :

  • Le nœud cité doit faire partie du cluster en service et, si members est renseignée, y figurer ; sinon l'instruction est refusée (9037). local n'est pas un identifiant de nœud admis. Hors édition Cluster, la clause est refusée (9002).
  • Le nœud cité peut être le primaire lui-même : les lignes de la partition restent alors sur le primaire, sans copie sur les secondaires.
  • Une partition ne change pas de nœud une fois créée (voir §16.10.10).

16.10.3 Lire#

Les lectures se font sur n'importe quel nœud, primaire ou secondaire, exactement comme sur une table ordinaire : SELECT, jointures, sous-requêtes, vues, procédures stockées.

-- Sur n'importe quel nœud
SELECT societe, SUM(montant) FROM compta.ecritures GROUP BY societe;
SELECT * FROM compta.ecritures WHERE societe = 2 AND id = 101;
  • Une requête dont le WHERE désigne des partitions précises (societe = 2) ne lit que celles-ci : si elles sont toutes sur le nœud interrogé, aucun autre nœud n'est contacté. Pour de bonnes performances, filtrez autant que possible sur la colonne de partitionnement.
  • Quand plusieurs partitions sont tenues par d'autres nœuds, elles sont lues en même temps : le temps de réponse est celui du nœud le plus lent, pas la somme des nœuds.
  • Si un nœud qui tient une partition nécessaire ne répond pas, la requête échoue (9038) : MIRAJ ne rend jamais un résultat incomplet sans le dire. Une requête qui n'a pas besoin de ce nœud (filtrée sur d'autres partitions) fonctionne normalement.
  • Chaque partition tenue ailleurs est lue dans son dernier état validé, au moment où elle est lue. Deux partitions tenues par deux nœuds différents ne sont pas lues « au même instant » : une requête qui lit plusieurs nœuds pendant que des écritures s'y font peut voir une écriture sur un nœud et pas encore une autre, faite juste après sur un autre nœud.

16.10.4 Écrire#

Comme pour toute table du cluster, les écritures se font sur le primaire (sur un secondaire : erreur 1290, §16.6), même quand la ligne est rangée chez un autre nœud. Le primaire transmet l'écriture au nœud qui tient la partition, qui l'exécute et renvoie le résultat (nombre de lignes, LAST_INSERT_ID(), avertissements, erreurs) :

-- Sur le primaire
INSERT INTO compta.ecritures VALUES (2, 101, 50.00);                 -- rangée chez n2
UPDATE compta.ecritures SET montant = 60 WHERE societe = 2 AND id = 101;
DELETE FROM compta.ecritures WHERE societe = 3 AND id < 100;         -- exécuté chez n3

INSERT, INSERT … SELECT (la source peut être la table partitionnée elle-même, y compris une partition tenue ailleurs), REPLACE, UPDATE, DELETE, TRUNCATE TABLE sont pris en charge, avec les valeurs par défaut, AUTO_INCREMENT et les colonnes générées habituels.

Une instruction n'écrit que chez un seul nœud. Une instruction dont les lignes iraient à la fois dans des partitions de plusieurs nœuds (par exemple un INSERT de plusieurs sociétés, ou un UPDATE sans filtre sur la colonne de partitionnement) est refusée en entier, sans rien écrire (9039). Découpez-la par nœud :

-- Refusé (9039) : les lignes iraient chez n2 et chez n3
INSERT INTO compta.ecritures VALUES (2, 102, 10.00), (3, 7, 20.00);

-- Accepté : une instruction par nœud
INSERT INTO compta.ecritures VALUES (2, 102, 10.00);
INSERT INTO compta.ecritures VALUES (3, 7, 20.00);

-- Refusé (9039) : sans filtre, l'UPDATE toucherait toutes les partitions
UPDATE compta.ecritures SET montant = montant * 1.1;
-- Accepté
UPDATE compta.ecritures SET montant = montant * 1.1 WHERE societe = 2;

Les partitions ordinaires et celles que tient le primaire comptent ensemble comme « le primaire » : une instruction peut écrire à la fois dans p1 (ordinaire) et dans une partition dédiée au primaire, mais pas dans p1 et p2 (chez n2).

Quelques opérations ne sont pas disponibles sur une table qui a une partition dédiée à un autre nœud (9041) : INSERT … ON DUPLICATE KEY UPDATE et les déclencheurs qui s'appliqueraient à une ligne rangée ailleurs, UPDATE / DELETE sur plusieurs tables à la fois, SELECT … FOR UPDATE et la recherche vectorielle sur une partition tenue ailleurs. La liste complète est au §16.10.9.

16.10.5 Transactions#

Une transaction (START TRANSACTION … COMMIT, ou autocommit = 0) peut écrire dans les partitions d'un seul autre nœud :

-- Sur le primaire
START TRANSACTION;
INSERT INTO compta.ecritures VALUES (2, 201, 50.00), (2, 202, 12.00);  -- chez n2
UPDATE compta.ecritures SET montant = montant * 2 WHERE societe = 2 AND id = 201;
SELECT SUM(montant) FROM compta.ecritures WHERE societe = 2;           -- voit ses propres écritures
COMMIT;                                                                  -- validé chez n2

Ce que vous pouvez attendre :

  • Tout ou rien : COMMIT valide toutes les écritures de la transaction chez le nœud ; ROLLBACK, la fermeture de la connexion ou KILL CONNECTION les annulent toutes.
  • Lecture de ses propres écritures : dans la transaction, les lectures des partitions de ce nœud voient les lignes déjà écrites et non validées. Les autres nœuds ne les voient qu'après le COMMIT.
  • Conflits : si une autre session a modifié les mêmes lignes, le COMMIT (ou l'instruction) rend l'erreur 1213 et la transaction est annulée ; rejouez-la, comme pour toute transaction (chapitre 13).
  • Une erreur ordinaire (doublon 1062…) n'annule que l'instruction fautive ; la transaction continue.

Ce qu'une transaction ne peut pas faire :

  • Écrire chez deux nœuds, ou écrire à la fois sur le primaire (tables ordinaires, partitions ordinaires) et chez un autre nœud : l'instruction qui le tenterait est refusée (9039, le message dit ce que la transaction a déjà écrit) ; elle seule échoue, la transaction reste ouverte et peut être validée ou annulée. Faites deux transactions. Les lectures de toutes les tables restent permises, ainsi que les tables temporaires de la session.
  • Poser un SAVEPOINT après avoir écrit chez un autre nœud (9041).
  • Rester inactive trop longtemps : une transaction qui a écrit chez un autre nœud et n'y fait plus rien pendant plus de fragment_timeout_ms (30 secondes par défaut) est annulée par ce nœud, qui libère ainsi ses verrous. L'instruction suivante reçoit 9040 et la transaction est annulée. Pour de longues transactions interactives, augmentez fragment_timeout_ms dans cluster.toml (§16.2.1).

Panne pendant la transaction : si le nœud devient injoignable avant le COMMIT, l'instruction reçoit 9038 et la transaction entière est annulée (le nœud annule aussi sa part). Si la liaison est coupée pendant le COMMIT, le message de l'erreur 9038 indique que l'issue est inconnue : le nœud a validé s'il a reçu la demande en entier, sinon il a tout annulé. Relisez les lignes concernées avant de rejouer la transaction, pour ne pas les écrire deux fois.

16.10.6 Performances#

MIRAJ fait calculer par le nœud qui tient une partition ce qui réduit le volume échangé sur le réseau :

  • Agrégats : COUNT, SUM, AVG, MIN, MAX (sans DISTINCT), avec ou sans GROUP BY sur des colonnes. Chaque nœud n'envoie qu'une ligne par groupe au lieu de toutes ses lignes.
  • Tri limité : ORDER BY sur des colonnes avec LIMIT n (et OFFSET). Chaque nœud n'envoie que ses meilleures lignes.

Ces deux calculs sont faits à distance quand la requête ne lit que la table partitionnée (pas de jointure ni de sous-requête) et que son WHERE se compose de comparaisons simples de colonnes à des constantes (=, <, >=, BETWEEN…, reliées par AND). Dans les autres cas, le résultat est le même, mais les lignes de la partition sont d'abord rapatriées : sur de grosses partitions, préférez des requêtes simples par table.

EXPLAIN montre ce qui se passe :

EXPLAIN SELECT societe, SUM(montant) FROM compta.ecritures GROUP BY societe;
  • colonne partitions : p1,p2@n2,p3@n3 — @n2 signale une partition lue chez n2 ;
  • colonne Extra : Using remote aggregation (agrégats calculés chez les nœuds) ou Using remote top-n (tri limité calculé chez les nœuds).

EXPLAIN ANALYZE (chapitre 8, 7.21) exécute la requête et mesure aussi l'étape distante : l'opérateur Remote aggregation (partial results merged) apparaît dans l'arbre avec le nombre de lignes qu'il rend (les groupes fusionnés) et son temps, sans détail par nœud.

16.10.7 Réglages#

Un seul réglage, dans cluster.toml (§16.2.1), identique de préférence sur tous les nœuds :

CléDéfautRôle
fragment_timeout_ms30000Attente maximale d'une réponse du nœud qui tient une partition, au-delà de laquelle l'instruction échoue (9038). C'est aussi l'inactivité maximale d'une transaction qui écrit chez un autre nœud (9040). Entre 100 et 3 600 000.

À augmenter pour les requêtes très longues sur des partitions distantes, les clients qui lisent un gros résultat très lentement, ou les transactions interactives ; à réduire pour détecter plus vite un nœud arrêté.

16.10.8 Surveiller#

  • État des partitions : information_schema.MIRAJ_FRAGMENTS (§16.10.2), colonne STATE.
  • Compteurs, depuis le démarrage du nœud qui lance les demandes : SHOW STATUS LIKE 'Miraj_cluster_fragment%'.
VariableSens
Miraj_cluster_fragment_scansPartitions lues chez un autre nœud.
Miraj_cluster_fragment_rowsLignes reçues de ces lectures (une par groupe quand les agrégats sont calculés à distance).
Miraj_cluster_fragment_errorsLectures chez un autre nœud en échec.
Miraj_cluster_fragment_writesÉcritures transmises au nœud qui tient la partition.
Miraj_cluster_fragment_write_errorsÉcritures transmises en échec (nœud injoignable, ou erreur rendue par le nœud, comme un doublon).
  • Journal du serveur (--log) : chaque échec lié à un autre nœud y est consigné avec la table, la partition et le nœud, par exemple :
Cluster : lecture de la partition p2 de compta.ecritures chez le nœud n2 en échec (9038) : …
Cluster : écriture de la partition p3 de ecritures chez le nœud n3 en échec (9038) : …

Le nœud qui annule une transaction inactive le consigne aussi : Cluster : transaction de n1 sur … annulée après … ms d'inactivité.

16.10.9 Erreurs et conduite à tenir#

CodeQuandQue faire
9037NODE cite un nœud absent du cluster ou de members, ou un identifiant invalideVérifier l'identifiant ([node] id) et members dans cluster.toml
9038Le nœud qui tient une partition nécessaire est arrêté, injoignable ou ne répond pas dans fragment_timeout_msRelancer le nœud ; il est de nouveau joint sans autre intervention. En attendant, les requêtes filtrées sur d'autres partitions fonctionnent. Dans une transaction, celle-ci est annulée : la rejouer (après vérification si l'erreur est survenue au COMMIT)
9039Une instruction, ou une transaction, écrirait chez plusieurs nœudsDécouper par nœud : une instruction ou une transaction par nœud
9040Transaction annulée par le nœud distant après une inactivité supérieure à fragment_timeout_msRejouer la transaction ; augmenter fragment_timeout_ms si besoin
9041Opération non disponible sur une table qui a une partition dédiée à un autre nœud : ALTER TABLE qui modifie les colonnes ou les index ou réorganise les partitions (REORGANIZE, COALESCE, REMOVE PARTITIONING, nouveau PARTITION BY), EXCHANGE PARTITION, BACKUP de la base, INSERT … ON DUPLICATE KEY UPDATE, déclencheurs, UPDATE / DELETE multi-tables, SELECT … FOR UPDATE et recherche vectorielle sur une partition tenue ailleurs, SAVEPOINT dans une transaction qui écrit chez un autre nœudVoir les contournements au §16.10.10
9042Le nœud qui tient la partition refuse la demande, en général parce qu'il n'a pas encore reçu la dernière modification de la tableRéessayer après quelques instants ; vérifier dans SHOW CLUSTER STATUS que ce nœud est connecté et à jour

Détail des messages : chapitre 27.

16.10.10 Limites et recommandations#

  • Aucune copie d'une partition dédiée. La perte définitive de son nœud entraîne la perte de ses lignes, et tant qu'il est arrêté, elles sont illisibles (9038). Gardez les données qui ne doivent pas être perdues dans des partitions ordinaires, ou sauvegardez-les par ailleurs.
  • Pas de sauvegarde BACKUP d'une base qui contient une partition dédiée (9041).
  • Pas de déplacement d'une partition vers un autre nœud, et pas de modification de la structure de la table (colonnes, index) tant qu'une partition est dédiée à un autre nœud (9041). Pour y parvenir, créez une nouvelle table avec la structure ou le placement voulu et recopiez-y les lignes par INSERT … SELECT, nœud par nœud (une instruction n'écrit que chez un nœud, par exemple INSERT INTO ecritures_v2 SELECT * FROM ecritures WHERE societe = 2). La nouvelle table garde son nom : un cluster à plusieurs nœuds n'accepte pas RENAME TABLE (§16.7).
  • Une instruction ou une transaction n'écrit que chez un seul nœud (§16.10.4, §16.10.5). Une coupure du réseau pendant un COMMIT chez un autre nœud en laisse l'issue inconnue.
  • Pas d'instant commun entre nœuds pour une lecture qui en couvre plusieurs (§16.10.3).
  • KILL QUERY n'interrompt pas l'attente d'une réponse d'un autre nœud ; cette attente reste bornée par fragment_timeout_ms.
  • Gros résultats lus lentement : pendant qu'il envoie une partition volumineuse (plus de 4 Mo environ) à un client qui la lit lentement, le nœud qui la tient y fait attendre les écritures ; si le client ne lit plus rien pendant fragment_timeout_ms, la lecture échoue (9038).
  • Connexions entre nœuds : chaque nœud accepte un nombre limité de connexions d'un même pair (quatre par membre, seize au moins, §16.2.5). Beaucoup de sessions qui lisent en même temps des partitions tenues ailleurs, ou beaucoup de transactions ouvertes chez un même nœud, peuvent atteindre ce plafond (9038).

16.11 Événements de changement sur un cluster#

Les événements de changement (LISTEN, NOTIFY, WAIT FOR CHANGES, chapitre 20) fonctionnent sur tous les nœuds d'un cluster. Chaque nœud publie les changements de ses propres tables : un abonné peut donc se connecter au primaire ou à n'importe quel secondaire.

PrimaireSecondaire
Moment de la publicationÀ la validation de la transactionÀ l'application du journal reçu du primaire (avec le retard de réplication, LAG_SECONDS)
Événements de ligne (INSERT, UPDATE, DELETE, BULK, TRUNCATE)OuiOui : les mêmes, dans le même ordre, avec un commit_seq commun à chaque transaction
Changements de structure (SCHEMA)OuiOui, quand le DDL reçu est rejoué
Colonne session_idSession qui a écritNULL (la session qui a écrit est sur le primaire)
Filtres COLUMNS, WHERE, WITH ROWOuiOui, de la même façon
NOTIFY, MIRAJ_NOTIFY()Publié aux abonnés du primaireNon reçu : NOTIFY ne passe pas par la réplication
Numérotation (seq)Propre au nœudPropre au nœud

Points à connaître :

  • Tables partitionnées : elles s'écoutent sous leur nom, sur tous les nœuds, quelle que soit la partition écrite. Une ligne qui change de partition donne DELETE puis INSERT. Le seuil change_events_bulk_rows s'entend par partition, et le vidage d'une ou de toutes les partitions donne un BULK par partition vidée (motif TRUNCATE PARTITION).
  • NOTIFY est local au nœud qui l'exécute. Comme les écritures, et donc les NOTIFY émis par des déclencheurs ou des procédures, s'exécutent sur le primaire, un abonné à un canal doit se connecter au primaire.
  • Bascule : les numéros d'un nœud n'ont pas de sens sur un autre. Après une promotion, un client qui se reconnecte à un autre nœud avec LISTEN … FROM n reçoit une ligne RESYNC : il relit ses données, puis se réabonne (§20.3). Un client déjà abonné au secondaire promu n'est pas touché : le nœud garde sa numérotation et continue de publier, désormais à la validation.
  • Redémarrage d'un nœud : les événements sont gardés en mémoire seulement ; après un redémarrage, un client qui reprend avec un ancien numéro reçoit RESYNC.
  • Réglages par nœud : les variables change_events_… et les options --change-events-… (chapitre 15) sont propres à chaque nœud et ne sont pas répliquées. Deux nœuds réglés avec des change_events_bulk_rows différents ne résument pas les mêmes transactions en BULK : garder le même réglage sur tous les nœuds.
  • Modèle pessimiste (--concurrency pessimistic) : pour une écriture hors transaction, un secondaire peut rendre des événements plus précis que le primaire (DELETE + INSERT pour une clé modifiée, colonnes réellement changées seulement, suppressions détaillées ligne à ligne plutôt qu'un BULK). Le modèle multiversion, par défaut, donne les mêmes événements sur tous les nœuds.
  • CREATE TABLE … SELECT : si un abonnement porte déjà sur ce nom de table (par exemple resté après un DROP TABLE), les lignes copiées sont rendues par les secondaires, pas par le primaire.

Répartition recommandée : abonner les écrans et caches applicatifs à un secondaire pour décharger le primaire, et les canaux NOTIFY au primaire ; traiter RESYNC (relecture complète puis nouvel abonnement) comme la réponse normale à une bascule.