MySQL Read Replicas: Lastverteilung in Anwendungen
AI generated
InnoDB
SQL
MySQL · Read Replicas · Skalierung · Routing
Read Replicas
Lastverteilung sauber in der Anwendung umsetzen

Read Replicas versprechen mehr Lesekapazität ohne größere Hardware, doch wer Leseanfragen blind auf Replicas verteilt, handelt sich veraltete Daten und schwer reproduzierbare Bugs ein. Erst ein bewusstes Read/Write-Splitting mit klarer Konsistenzstrategie macht MySQL Read Replicas zu einem verlässlichen Baustein der Skalierung.

17 Min. Lesezeit Read/Write-Splitting · Replication Lag · Session-Konsistenz MySQL 8.0 · ProxySQL · Anwendungsseitiges Routing

1. Warum Read Replicas kein Selbstläufer sind

Read Replicas sind schreibgeschützte Kopien einer MySQL-Datenbank, die über asynchrone oder semi-synchrone Replikation aktuell gehalten werden und primär dafür da sind, Leseanfragen von der Primary-Instanz fernzuhalten. Die Grundidee ist einfach: Steigt die Leselast, fügt man weitere Replicas hinzu, statt die Primary-Hardware zu vergrößern. In der Praxis ist die Umsetzung jedoch komplexer, als eine Anwendung einfach mit mehreren Datenbankverbindungen zu konfigurieren.

Der Kernkonflikt bei Read Replicas liegt in der Zeitverzögerung der Replikation. Zwischen einem Schreibvorgang auf der Primary und dessen Sichtbarkeit auf der Replica vergeht immer eine gewisse Zeitspanne, und diese Zeitspanne ist nicht konstant. Anwendungen, die diese Verzögerung ignorieren und Leseanfragen unbedacht auf Replicas verteilen, produzieren Situationen, in denen ein Nutzer eine gerade gespeicherte Änderung nicht sieht, weil die Replica sie noch nicht angewendet hat. Solche Bugs sind besonders unangenehm, weil sie selten reproduzierbar sind und nur unter Last auftreten.

2. Read/Write-Splitting: Muster in der Anwendung

Read/Write-Splitting bezeichnet die bewusste Trennung von Schreiboperationen, die immer auf die Primary gehen, und Leseoperationen, die wahlweise auf Replicas verteilt werden können. Die einfachste Umsetzung erfolgt auf Verbindungsebene: Die Anwendung hält zwei Connection Pools, einen für Schreibzugriffe auf die Primary und einen für Lesezugriffe auf einen Pool von Read Replicas, häufig mit Round-Robin oder gewichteter Lastverteilung zwischen den Replicas.

Diese Trennung sollte konsequent zentral erfolgen, etwa in einer Datenbank-Abstraktionsschicht oder einem Repository-Layer, statt verstreut in Anwendungscode einzelne Queries manuell einer Verbindung zuzuordnen. Eine zentrale Stelle für Read/Write-Splitting erleichtert es, Ausnahmen sauber zu behandeln: Bestimmte Reads, etwa unmittelbar nach einem Schreibvorgang im selben Request, müssen weiterhin auf die Primary gehen, auch wenn sie inhaltlich reine Leseoperationen sind.


-- Application-level connection configuration (conceptual)
-- Write pool: always the primary
WRITE_DSN = "mysql://app:pw@db-primary.internal:3306/shop"

-- Read pool: a set of read replicas behind a load balancer or driver-level list
READ_DSNS = [
  "mysql://app:pw@db-replica-1.internal:3306/shop",
  "mysql://app:pw@db-replica-2.internal:3306/shop",
  "mysql://app:pw@db-replica-3.internal:3306/shop"
]

-- Check replica health and lag before adding it to the read pool
SELECT
  CASE WHEN @@read_only = 1 THEN 'replica' ELSE 'primary' END AS role,
  (SELECT VARIABLE_VALUE FROM performance_schema.global_status
   WHERE VARIABLE_NAME = 'Seconds_Behind_Source') AS lag_seconds;

3. Konsistenzprobleme durch Replication Lag

Replication Lag ist bei Read Replicas keine theoretische Randbedingung, sondern ein Faktor, der bei jedem Design von Lesezugriffen aktiv berücksichtigt werden muss. Ein typisches Szenario: Ein Nutzer aktualisiert sein Profil, die Anwendung bestätigt den Schreibvorgang, und derselbe Nutzer wird direkt danach auf die Profilseite weitergeleitet, die eine Leseanfrage an eine Replica schickt. Liegt der Lag dieser Replica bei auch nur wenigen hundert Millisekunden, sieht der Nutzer seine eigene Änderung nicht und hält die Anwendung fälschlicherweise für fehlerhaft.

Solche Inkonsistenzen betreffen nicht nur einzelne Nutzer, sondern auch interne Konsistenz zwischen mehreren Tabellen. Wird eine Bestellung in Tabelle A geschrieben und ein zugehöriger Log-Eintrag in Tabelle B, kann eine Leseanfrage, die beide Tabellen von unterschiedlich stark verzögerten Replicas liest, inkonsistente Zwischenzustände sehen, selbst wenn beide Schreibvorgänge auf der Primary in derselben Transaktion liefen. Deshalb sollten Leseanfragen, die mehrere zusammengehörige Tabellen betreffen, innerhalb derselben Anfrage immer dieselbe Replica verwenden, niemals über mehrere Replicas hinweg gemischt werden.

Für Read Replicas, die stark hinter der Primary zurückliegen, empfiehlt sich ein aktiver Ausschluss aus dem Lesepool, bis der Lag wieder unter einen definierten Schwellenwert fällt. Diese Health-Check-Logik verhindert, dass Nutzer systematisch auf einer überlasteten, stark verzögerten Replica landen, während andere Replicas nahezu aktuell sind.

4. Read-after-Write und Session-Konsistenz

Das Read-after-Write-Problem ist der häufigste praktische Stolperstein beim Einsatz von Read Replicas. Die robusteste Lösung ist sogenanntes Sticky Routing innerhalb einer Session: Nach jedem Schreibvorgang eines Nutzers werden alle folgenden Leseanfragen derselben Session für ein definiertes Zeitfenster, etwa einige Sekunden, zwingend auf die Primary geleitet, statt auf eine Replica. Erst nach Ablauf dieses Fensters wird wieder auf den Replica-Pool zurückgegriffen.

Eine präzisere, aber aufwendigere Alternative nutzt GTID-basiertes Waiting: Nach einem Schreibvorgang merkt sich die Anwendung die GTID der Transaktion und führt vor der nächsten Leseanfrage auf einer Replica SELECT WAIT_FOR_EXECUTED_GTID_SET(gtid, timeout) aus. Dieser Befehl blockiert, bis die Replica die betreffende Transaktion angewendet hat, oder gibt nach dem Timeout einen Fehler zurück, den die Anwendung dann als Fallback auf die Primary behandeln kann. Diese Methode garantiert Konsistenz, erhöht aber die Latenz der betroffenen Leseanfrage um die verbleibende Replikationsverzögerung.


-- After a write on the primary, capture the resulting GTID
SELECT @last_gtid := @@session.last_gtid;

-- Before the next read on a replica, wait until it applied that GTID
-- Returns 0 once caught up, 1 on timeout (fall back to primary in that case)
SELECT WAIT_FOR_EXECUTED_GTID_SET(@last_gtid, 2) AS caught_up;

-- Application pseudocode for the fallback decision
-- if caught_up == 1: route this read to the primary instead of the replica
-- if caught_up == 0: safe to read from the replica connection

5. Routing-Strategien: wann Reads auf die Replica gehören

Nicht jede Leseoperation eignet sich für Read Replicas. Als Faustregel gilt: Reads, die für Reporting, Analytics, Suchfunktionen oder Listen-Ansichten ohne unmittelbaren Bezug zu einer vorherigen Schreiboperation des aktuellen Nutzers verwendet werden, sind gute Kandidaten für Replicas. Reads, die direkt nach einem Schreibvorgang im selben Request oder derselben Session stattfinden, insbesondere solche, bei denen der Nutzer die soeben gespeicherte Änderung sofort sehen soll, sollten auf der Primary bleiben.

Eine weitere Kategorie sind Transaktionen, die aus mehreren Statements bestehen: Sobald eine Datenbanktransaktion mit START TRANSACTION beginnt, sollten alle darin enthaltenen Reads und Writes konsequent auf derselben Verbindung zur Primary bleiben, um die Transaktionssemantik nicht durch das Aufsplitten über mehrere Server zu verletzen. Read/Write-Splitting funktioniert nur zuverlässig auf der Ebene einzelner, unabhängiger Anfragen außerhalb expliziter Transaktionen.

6. Transparentes Routing mit ProxySQL

ProxySQL ist ein spezialisierter Datenbank-Proxy, der Read/Write-Splitting auf Basis regelbasierter Query-Analyse übernimmt, ohne dass die Anwendung selbst zwischen mehreren Verbindungen unterscheiden muss. Die Anwendung verbindet sich mit einem einzigen ProxySQL-Endpunkt, und ProxySQL entscheidet anhand konfigurierbarer Regeln, ob ein Query an die Primary oder an eine Replica aus dem konfigurierten Hostgroup weitergeleitet wird.


-- ProxySQL: define host groups for primary and replicas
INSERT INTO mysql_servers (hostgroup_id, hostname, port, weight)
VALUES
  (10, 'db-primary.internal', 3306, 1000),
  (20, 'db-replica-1.internal', 3306, 900),
  (20, 'db-replica-2.internal', 3306, 900);

-- Route SELECT statements to the replica hostgroup, everything else to primary
INSERT INTO mysql_query_rules (rule_id, active, match_pattern, destination_hostgroup, apply)
VALUES
  (1, 1, '^SELECT.*FOR UPDATE$', 10, 1),   -- locking reads must hit the primary
  (2, 1, '^SELECT', 20, 1);                -- plain reads go to replicas

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

Der Vorteil dieses Ansatzes: Anwendungscode bleibt frei von Routing-Logik, und ProxySQL erkennt automatisch SELECT ... FOR UPDATE oder Statements innerhalb einer aktiven Transaktion und leitet diese korrekt an die Primary weiter. Der Nachteil: ProxySQL kennt den fachlichen Kontext einer Anfrage nicht und kann Read-after-Write-Probleme nicht lösen, die auf Anwendungsebene explizit behandelt werden müssen, etwa über Sticky Sessions oder GTID-Waiting.

7. Read/Write-Splitting auf ORM- und Framework-Ebene

Viele moderne ORMs und Frameworks bieten native Unterstützung für Read Replicas, häufig über eine Konfiguration, die eine Liste von Replica-Verbindungen neben der primären Verbindung definiert. Der ORM entscheidet dann anhand des Statement-Typs, SELECT versus INSERT, UPDATE, DELETE, automatisch, welche Verbindung genutzt wird, ähnlich wie ProxySQL, aber innerhalb der Anwendung statt in der Infrastrukturschicht.

Der Vorteil der ORM-Ebene liegt in der Möglichkeit, fachliche Ausnahmen explizit zu markieren: Ein Entwickler kann einen bestimmten Read innerhalb eines Read-after-Write-Szenarios gezielt mit einem Hinweis wie forcePrimary() oder einer entsprechenden Annotation von der automatischen Replica-Zuweisung ausnehmen. Diese Explizitheit macht Konsistenzentscheidungen im Code sichtbar und nachvollziehbar, statt sie implizit in einer Infrastrukturkomponente zu verstecken, die der Entwickler beim Schreiben der Anfrage nicht vor Augen hat.


; database.ini (conceptual ORM-level read/write split configuration)
[database.write]
host = db-primary.internal
port = 3306

[database.read]
; multiple entries form the round-robin read pool
hosts[] = db-replica-1.internal
hosts[] = db-replica-2.internal
hosts[] = db-replica-3.internal
strategy = round_robin
sticky_after_write_seconds = 3

; Usage in application code (pseudocode)
; $products = DB::read()->select('SELECT * FROM products');
; $order = DB::write()->insert('INSERT INTO orders ...');
; $order = DB::forcePrimary()->select('SELECT * FROM orders WHERE id = ?', $id);

8. Grenzen der Leseskalierung mit Replicas

Read Replicas skalieren Lesekapazität nicht unbegrenzt. Jede zusätzliche Replica erhöht die Netzwerk- und IO-Last auf der Primary, weil der Binlog an jede Replica separat gestreamt werden muss. Ab einer gewissen Anzahl von Replicas, in der Praxis häufig irgendwo zwischen fünf und zehn, wird die Primary selbst durch den Replikations-Overhead spürbar belastet, unabhängig von der eigentlichen Schreiblast der Anwendung.

Für Workloads, die über die Kapazität einzelner Replicas hinauswachsen, sind horizontale Sharding-Strategien oder Caching-Schichten wie Redis vor der Datenbank oft der nachhaltigere Weg, statt die Anzahl der Read Replicas immer weiter zu erhöhen. Replicas lösen das Problem der Leseverteilung, aber nicht das Problem eines fundamental zu großen Datenvolumens oder einer zu hohen Query-Komplexität, die sich besser durch Indexierung, Denormalisierung oder Caching adressieren lässt.


#!/usr/bin/env bash
# health-check-replica-pool.sh: remove lagging replicas from the active pool
set -euo pipefail

MAX_LAG_SECONDS=5
REPLICAS=("db-replica-1.internal" "db-replica-2.internal" "db-replica-3.internal")

for host in "${REPLICAS[@]}"; do
  lag=$(mysql -h "$host" -N -e \
    "SHOW STATUS LIKE 'Seconds_Behind_Source'" | awk '{print $2}')

  if [[ -z "$lag" || "$lag" -gt "$MAX_LAG_SECONDS" ]]; then
    echo "[WARN] $host lag=${lag:-unknown}s, removing from read pool"
    # call your load balancer or service discovery API here
  else
    echo "[OK] $host lag=${lag}s"
  fi
done

9. Routing-Strategien im Vergleich

Die Wahl der Routing-Strategie für Read Replicas hat direkten Einfluss auf Implementierungsaufwand, Konsistenzgarantien und Betriebskomplexität. Die folgende Tabelle vergleicht die drei gängigsten Ansätze.

Ansatz Implementierungsaufwand Konsistenzkontrolle Typischer Einsatz
Manuelles Connection-Pool-Splitting Mittel, zentrale Abstraktionsschicht nötig Vollständig in der Hand der Anwendung Kleine bis mittlere Codebasen
ProxySQL / Datenbank-Proxy Gering für die Anwendung, Proxy-Setup extra Kein Read-after-Write-Schutz eingebaut Polyglotte oder heterogene Anwendungslandschaften
ORM-natives Splitting mit GTID-Wait Höher, explizite Markierung nötig Präzise, pro Query steuerbar Konsistenzkritische Anwendungen

In der Praxis kombinieren viele Setups mehrere Ebenen: ProxySQL übernimmt die grobe Verteilung, während die Anwendung für bekannte Read-after-Write-Szenarien gezielt forcePrimary() oder GTID-Waiting einsetzt. Diese Kombination reduziert den Implementierungsaufwand gegenüber reinem Anwendungs-Routing, ohne die Konsistenzkontrolle vollständig aus der Hand zu geben.

Mironsoft

MySQL Read Replicas, Lastverteilung und Skalierungsarchitektur

Lesekapazität skalieren, ohne Konsistenz zu opfern?

Wir bauen Read/Write-Splitting in eurer Anwendung oder über ProxySQL auf, identifizieren kritische Read-after-Write-Pfade und richten Monitoring für Replication Lag ein.

Splitting-Konzept

Analyse eurer Query-Patterns und Design der Routing-Strategie

ProxySQL-Setup

Transparentes Routing ohne Anwendungsänderungen implementieren

Konsistenz-Audit

Read-after-Write-Risiken im Code identifizieren und beheben

10. Zusammenfassung

Read Replicas sind ein wirksames Mittel zur Lastverteilung, aber kein Automatismus. Ohne bewusstes Read/Write-Splitting, ohne Berücksichtigung von Replication Lag und ohne eine klare Strategie für Read-after-Write-Szenarien entstehen schwer nachvollziehbare Inkonsistenzen, die Nutzer als Bugs wahrnehmen. Zentrale Routing-Logik, sei es in der Anwendung, im ORM oder über einen Proxy wie ProxySQL, ist die Voraussetzung dafür, dass Read Replicas verlässlich funktionieren.

Die richtige Strategie hängt vom Konsistenzbedarf der jeweiligen Leseoperation ab: Reporting- und Such-Queries vertragen Replicas problemlos, während Reads unmittelbar nach eigenen Schreibvorgängen entweder auf der Primary bleiben oder über GTID-Waiting explizit synchronisiert werden müssen. Wer diese Unterscheidung sauber im Code abbildet, gewinnt echte Lesekapazität, ohne die Datenkonsistenz der Anwendung zu gefährden.

MySQL Read Replicas: Das Wichtigste auf einen Blick

Read/Write-Splitting

Schreibzugriffe immer auf die Primary, Lesezugriffe zentral gesteuert auf Replicas verteilen.

Read-after-Write

Sticky Sessions oder GTID-Waiting mit WAIT_FOR_EXECUTED_GTID_SET verhindern veraltete Reads.

Transaktionen

Innerhalb einer Transaktion konsequent auf der Primary bleiben, niemals über mehrere Server splitten.

Grenzen

Zu viele Replicas belasten die Primary selbst, Sharding oder Caching sind die nächste Skalierungsstufe.

11. FAQ: MySQL Read Replicas

1Was ist Read/Write-Splitting?
Trennung von Schreibzugriffen auf die Primary und Lesezugriffen auf einen Replica-Pool, zentral gesteuert in einer Abstraktionsschicht.
2Warum sieht ein Nutzer eigene Änderungen nicht?
Read-after-Write-Problem: Schreibvorgang lief auf der Primary, Read ging an eine Replica mit noch nicht angewendeter Änderung.
3Wie löse ich das zuverlässig?
Sticky Sessions kurz nach Writes, oder präziser WAIT_FOR_EXECUTED_GTID_SET vor dem nächsten Read.
4Reads in Transaktionen auf Replica?
Nein, innerhalb einer Transaktion immer auf derselben Primary-Verbindung bleiben.
5Was macht ProxySQL?
Analysiert Queries regelbasiert und leitet sie automatisch an Primary oder Replica weiter, transparent für die Anwendung.
6Löst ProxySQL Read-after-Write?
Nicht automatisch, dafür braucht es zusätzliche Anwendungslogik wie Sticky Sessions oder GTID-Waiting.
7Wie viele Replicas sind sinnvoll?
Ab fünf bis zehn wird die Primary durch Replikations-Overhead belastet, dann Sharding oder Caching erwägen.
8Reporting-Queries auf Replicas?
Ja, ideale Kandidaten, weil geringe Aktualität meist unkritisch ist.
9Replica aus dem Pool entfernen, wann?
Bei Überschreitung eines Lag-Schwellenwerts über Seconds_Behind_Source Monitoring.
10Read/Write-Splitting mit jedem ORM?
Viele ORMs unterstützen es nativ, sonst manuell über zwei getrennte Connection Pools umsetzbar.