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.
Inhaltsverzeichnis
- 1. Warum Read Replicas kein Selbstläufer sind
- 2. Read/Write-Splitting: Muster in der Anwendung
- 3. Konsistenzprobleme durch Replication Lag
- 4. Read-after-Write und Session-Konsistenz
- 5. Routing-Strategien: wann Reads auf die Replica gehören
- 6. Transparentes Routing mit ProxySQL
- 7. Read/Write-Splitting auf ORM- und Framework-Ebene
- 8. Grenzen der Leseskalierung mit Replicas
- 9. Routing-Strategien im Vergleich
- 10. Zusammenfassung
- 11. FAQ
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.