Multi-Region-Failover: Was in Produktion wirklich bricht
Multi-Region-Failover wirkt im Diagramm simpel — in der Praxis warten Replikationsverzögerung, Split-Brain-Schreibzugriffe und DNS-Caches.

Auf jeder Marketingseite klingt Multi-Region-Failover, als würde man einfach einen Schalter umlegen: Die Primärinstanz fällt aus, die Replik übernimmt, der Traffic wird umgeleitet, niemand merkt etwas. Ich habe inzwischen vier echte Failovers durchgemacht — zwei geplant, zwei ungeplant — und keiner davon entsprach dieser Beschreibung. Die Lücke zwischen „wir haben eine Replik in einer anderen Region" und „wir können tatsächlich failovern, ohne Daten zu verlieren oder veraltete Lesezugriffe auszuliefern" ist der Ort, an dem der eigentliche Ingenieursaufwand steckt.
Dieser Beitrag behandelt die Fehlermodi, die erst sichtbar werden, wenn man bereits mitten in einem Incident steckt, sowie die Designentscheidungen, die darüber entscheiden, ob ein Failover unbemerkt bleibt oder zu einem stundenlangen Ausfall wird.
Replikationsverzögerung ist kein Rundungsfehler
Die Replikationsverzögerung zwischen Regionen wird meist als Metrik behandelt, die man beobachtet, nicht als Design-Randbedingung. Das ist die falsche Reihenfolge. Die Verzögerung bedeutet nicht nur „wie weit hängt die Replik hinterher", sondern „wie viele Daten verlieren wir, wenn wir jetzt sofort promoten".
-- Check replication lag on a PostgreSQL streaming replica
SELECT
client_addr,
state,
pg_wal_lsn_diff(sent_lsn, replay_lsn) AS bytes_behind,
extract(epoch FROM (now() - reply_time)) AS seconds_behind
FROM pg_stat_replication;Eine Replik, die unter normaler Last 200 ms hinterherhängt, kann bei einem Traffic-Spike oder einem großen Batch-Write auf 30 oder mehr Sekunden Rückstand kommen — genau die Bedingungen, unter denen eine Primärinstanz am ehesten ausfällt. Wenn dein Failover-Runbook sagt „Replik promoten", muss er auch sagen „und so viele Sekunden an Schreibzugriffen sind wir bereit, dafür zu verlieren".
// ❌ Blind promotion — no visibility into data loss
async function failover(replicaId: string): Promise<void> {
await promoteReplica(replicaId);
await updateDnsRecord(replicaId);
}
// ✅ Bounded promotion — refuses to fail over past a data-loss threshold
async function failover(
replicaId: string,
maxAcceptableLagSeconds: number,
): Promise<FailoverResult> {
const lag = await getReplicationLag(replicaId);
if (lag.seconds > maxAcceptableLagSeconds) {
return {
status: "blocked",
reason: `Lag ${lag.seconds}s exceeds threshold ${maxAcceptableLagSeconds}s`,
estimatedDataLoss: lag.approximateWriteCount,
};
}
await promoteReplica(replicaId);
await updateDnsRecord(replicaId);
return { status: "completed", lagAtPromotion: lag.seconds };
}Der Schwellenwert selbst ist eine geschäftliche Entscheidung, keine technische. Hol dir von Product und der Führungsebene die Freigabe für ein akzeptables Datenverlustfenster, bevor der Incident eintritt — nicht währenddessen.
Split-Brain: Zwei Primärinstanzen sind schlimmer als keine
Das Szenario, über das niemand sprechen will: Die „ausgefallene" Primärinstanz ist gar nicht wirklich tot, sie ist für deinen Health-Checker nur nicht erreichbar. In der Zwischenzeit hast du die Replik promotet, und jetzt nehmen beide Datenbanken unabhängig voneinander Schreibzugriffe an. Das ist der mit Abstand zerstörerischste Fehlermodus in Multi-Region-Setups, verursacht durch Netzwerkpartitionen, nicht durch Hardwareausfälle.
// ❌ Health check with no fencing — promotes on a single failed probe
async function checkPrimaryHealth(): Promise<boolean> {
try {
await pingDatabase(PRIMARY_HOST, { timeout: 2000 });
return true;
} catch {
return false; // triggers immediate promotion elsewhere
}
}
// ✅ Fencing before promotion — actively prevents the old primary from writing
async function promoteWithFencing(
candidateReplica: string,
oldPrimary: string,
): Promise<void> {
// 1. Confirmed multiple independent probes agree primary is down
const consensus = await getQuorumHealthCheck(oldPrimary, { probes: 3 });
if (consensus.healthy) throw new Error("Primary reachable — abort promotion");
// 2. Fence the old primary at the network or storage layer BEFORE promoting
await revokeWriteAccess(oldPrimary); // e.g. STONITH, IAM policy, VPC rule
// 3. Only now is it safe to promote
await promoteReplica(candidateReplica);
}Fencing — der alten Primärinstanz aktiv die Schreibfähigkeit zu entziehen, statt einfach anzunehmen, dass sie tot ist — ist der Teil, den Teams überspringen, weil er operativ lästig ist. Es ist aber auch der Teil, der das schlimmste Ergebnis verhindert: zwei „Quellen der Wahrheit", die auseinandergelaufen sind und sich nicht mehr automatisch abgleichen lassen.
DNS und Connection Pools reagieren beim Failover langsamer, als man denkt
Selbst eine saubere Promotion mit Fencing bringt nichts, wenn die Anwendungsschicht weiterhin mit der alten Primärinstanz spricht. Zwei Übeltäter tauchen dabei immer wieder auf:
- DNS-TTL-Caching — Clients und zwischengeschaltete Resolver halten weit über die konfigurierte TTL hinaus an der alten IP fest, besonders auf verwalteten Plattformen mit aggressivem Resolver-Caching.
- Connection Pools — Ein Pool, der bereits 50 Verbindungen zur alten Primärinstanz geöffnet hat, nutzt sie munter weiter, bis sie fehlschlagen, nicht bis sich der DNS-Eintrag ändert.
// ❌ Pool holds stale connections indefinitely after failover
const pool = new Pool({
host: "db-primary.internal",
max: 50,
});
// ✅ Pool with bounded connection lifetime forces periodic re-resolution
const pool = new Pool({
host: "db-primary.internal",
max: 50,
maxLifetimeSeconds: 300, // connections recycle every 5 minutes
connectionTimeoutMillis: 3000,
});
pool.on("error", async (err) => {
if (isConnectionRefused(err)) {
await pool.end(); // force full reconnect, re-resolve DNS
}
});Niedrige DNS-TTLs helfen, reichen aber allein nicht aus. Kombiniere sie mit Connection-Recycling auf Anwendungsebene und, wo möglich, mit einer Proxy-Schicht (PgBouncer, ProxySQL oder einem Cloud-Loadbalancer), die sich unabhängig vom clientseitigen Caching neu ausrichten lässt.
Read-Replikate liefern während der Umschaltung veraltete Daten
In dem Zeitfenster zwischen „Primärinstanz ist ausgefallen" und „Promotion ist abgeschlossen" fließt Lese-Traffic oft weiterhin zu Repliken, die inzwischen die aktuellste Datenquelle sind — doch der Anwendungscode geht häufig davon aus, dass Repliken immer etwas hinterherhinken, und routet entsprechend. Das erzeugt einen verwirrenden Fehler: Schreibzugriffe schlagen fehl, aber Lesezugriffe gelingen und liefern Daten, die nur deshalb „falsch" sind, weil die App nicht weiß, dass sich die Topologie gerade geändert hat.
interface DatabaseTopology {
primaryRegion: string;
writableEndpoint: string;
readEndpoints: string[];
lastTopologyChange: Date;
}
// ✅ Centralize topology awareness instead of hardcoding endpoints
class TopologyAwareRouter {
private topology: DatabaseTopology;
async route(query: Query): Promise<string> {
if (query.requiresWrite) {
return this.topology.writableEndpoint;
}
// During transition windows, prefer the writable endpoint for reads too
const secondsSinceChange =
(Date.now() - this.topology.lastTopologyChange.getTime()) / 1000;
if (secondsSinceChange < this.settlingPeriodSeconds) {
return this.topology.writableEndpoint;
}
return this.pickReadEndpoint(this.topology.readEndpoints);
}
private settlingPeriodSeconds = 120;
}Die Kernidee: Topologieänderungen sollten ein erstklassiges Ereignis sein, auf das deine Anwendung reagiert, und nicht etwas, das man erst im Nachhinein aus Verbindungsfehlern ableitet.
Failover testen, wenn gerade nichts brennt
Die unbequeme Wahrheit ist, dass die meisten Failover-Verfahren ausschließlich während echter Incidents getestet werden — dem denkbar schlechtesten Zeitpunkt, um eine falsche Annahme zu entdecken. Game Days, also geplante, bewusst durchgeführte Failover-Übungen, sind die einzige verlässliche Methode, um die gesamte Kette zu validieren: Fencing, Promotion, DNS-Propagation, Pool-Recycling und das Verhalten der Anwendung.
| Failover-Komponente | Wie man sie sicher testet | Häufigkeit |
|---|---|---|
| Alarmierung bei Replikationsverzögerung | Künstliche Schreiblast erzeugen, Auslösen des Alarms prüfen | Monatlich |
| Fencing-Mechanismus | Partition simulieren, bestätigen, dass die alte Primärinstanz Schreibzugriffe ablehnt | Vierteljährlicher Game Day |
| DNS-/Proxy-Umleitung | Messung der Time-to-Convergence über alle Client-Pools hinweg | Vierteljährlicher Game Day |
| Reconnect-Logik der Anwendung | Verbindungen mitten in der Transaktion killen, Retry-Verhalten prüfen | Pro Deployment (CI) |
| Abgleich von Datenverlust | WAL-Position bei Promotion mit dem letzten dauerhaft geschriebenen Stand vergleichen | Vierteljährlicher Game Day |
Wenn man das nach Plan durchführt und die gesamte Bereitschaftsrotation einbezieht, wird Failover von einer theoretischen Fähigkeit zu einem eingeübten Verfahren. Das erste Mal, dass du eine Replik promotest, sollte nicht während eines für Kunden sichtbaren Ausfalls sein.
Die wichtigsten Erkenntnisse
- Verzögerung ist ein Datenverlust-Budget, nicht nur eine Metrik — Definiere einen akzeptablen Schwellenwert, bevor du ihn brauchst, und sorge dafür, dass automatisiertes Failover ihn respektiert.
- Erst fencen, dann promoten — Anzunehmen, dass die alte Primärinstanz tot ist, ohne ihr den Schreibzugriff zu entziehen, ist der Weg in den Split-Brain.
- DNS und Connection Pools hinken Topologieänderungen hinterher — Plane explizit mit veralteten Verbindungen, verlass dich nicht allein auf TTLs.
- Mache Topologieänderungen zu einem erstklassigen Ereignis der Anwendung — Lass nicht zu, dass Annahmen beim Lese-Routing während Übergängen stillschweigend veraltete oder falsche Daten ausliefern.
- Übe Failover nach Plan — Game Days decken die falschen Annahmen auf, die sonst erst Incidents zum ungünstigsten Zeitpunkt offenlegen.
- Lass den Datenverlust-Schwellenwert vom Business absegnen, nicht nur von der Technik — Es ist eine Produktentscheidung, die als technische getarnt ist.


