A treia parte din seria despre pattern-uri avansate în Cosmos DB. După partition keys și Change Feed, urcăm la nivel global: scrieri în mai multe regiuni.
Single-region writes cu read replicas — punctul de plecare
Prima nuanță importantă: nu ai nevoie de multi-region writes ca să servești citiri global. Configurația clasică — o regiune de scriere + read replicas în alte regiuni — acoperă majoritatea scenariilor: utilizatorii citesc din regiunea apropiată, scrierile merg toate în regiunea principală.
# Adauga o regiune de citire
az cosmosdb update \
--name my-cosmos --resource-group my-rg \
--locations regionName=westeurope failoverPriority=0 isZoneRedundant=true \
--locations regionName=eastus failoverPriority=1 isZoneRedundant=true
Multi-region writes intră în discuție doar când:
- Latența de scriere contează global — utilizatori activi pe mai multe continente care scriu frecvent (un chat, un editor colaborativ), iar round-trip-ul la regiunea unică de scriere e inacceptabil
- Disponibilitate la scriere peste tot — vrei ca o regiune picată să nu oprească scrierile nicăieri (RTO aproape zero, failover automat)
Prețul: complexitatea conflictelor. Dacă publicul tău e concentrat regional (ex. clienți din România și UE), o singură regiune de scriere în West Europe e mai simplă și suficientă — concluzie care se aplică direct unui SaaS regional.
Activarea multi-region writes
az cosmosdb update \
--name my-cosmos --resource-group my-rg \
--enable-multiple-write-locations true
// SDK .NET: spune-i clientului ce regiuni prefera, in ordine
builder.Services.AddSingleton(_ => new CosmosClient(
accountEndpoint: endpoint,
tokenCredential: new DefaultAzureCredential(),
clientOptions: new CosmosClientOptions
{
// Instanta din Europa scrie/citeste in West Europe,
// cea din US in East US -- fiecare local
ApplicationPreferredRegions = new List<string>
{
"West Europe",
"East US"
}
}));
Cu ApplicationPreferredRegions, fiecare instanță a aplicației (deployată regional) scrie în regiunea ei — latență de scriere locală. SDK-ul face failover automat pe lista de preferințe dacă prima regiune pică.
Consistency levels în context multi-region
Cu scrieri în mai multe locuri, consistency level-ul devine o decizie vizibilă:
| Level | Garanție | Notă multi-region |
|---|---|---|
| Strong | Liniarizabilitate globală | Incompatibil cu multi-region writes pe conturi cu regiuni distante — latența ar fi RTT-ul global |
| Bounded Staleness | Întârziere maximă K versiuni / T secunde | Compromis bun când vrei predictibilitate |
| Session | Consistență în cadrul sesiunii proprii | Default-ul și alegerea corectă pentru majoritatea aplicațiilor — citești propriile scrieri |
| Consistent Prefix | Ordinea scrierilor păstrată | Rar alegerea explicită |
| Eventual | Convergență, fără ordine | Doar pentru date unde ordinea nu contează |
Session consistency merită subliniat: un utilizator care scrie un mesaj îl vede imediat în propria sesiune, chiar dacă replicarea globală durează. Pentru un chat sau un dashboard per client, exact asta vrei.
Conflictele: inevitabile, deci gestionate
Două regiuni scriu același document aproape simultan. Replicarea le aduce față în față: conflict. Cosmos DB oferă două politici de rezolvare, setate per container, la creare:
Last-Writer-Wins (default)
Câștigă documentul cu valoarea mai mare pe o proprietate numerică (default _ts, timestamp-ul). Simplu și suficient când pierderea unei scrieri concurente e acceptabilă:
var containerProperties = new ContainerProperties("sessions", "/tenantId")
{
ConflictResolutionPolicy = new ConflictResolutionPolicy
{
Mode = ConflictResolutionMode.LastWriterWins,
// Poti folosi propria proprietate in loc de _ts
ResolutionPath = "/updatedEpoch"
}
};
await database.CreateContainerIfNotExistsAsync(containerProperties);
Custom cu conflict feed
Când pierderea silențioasă nu e acceptabilă (ex. două update-uri concurente pe configurația unui tenant care trebuie combinate, nu suprascrise), alegi Custom: conflictele nerezolvate merg într-un conflict feed pe care îl procesezi tu:
// Politica custom fara stored procedure -- conflictele merg in feed
var props = new ContainerProperties("tenant-config", "/tenantId")
{
ConflictResolutionPolicy = new ConflictResolutionPolicy
{
Mode = ConflictResolutionMode.Custom
}
};
// Procesarea conflictelor -- job periodic sau BackgroundService
var conflicts = _container.Conflicts;
using var iterator = conflicts.GetConflictQueryIterator<ConflictProperties>();
while (iterator.HasMoreResults)
{
foreach (var conflict in await iterator.ReadNextAsync())
{
var current = conflicts.ReadCurrentConflictContent<TenantConfig>(conflict);
// Logica ta de merge: combina, alege, sau ridica alerta
var merged = MergeConfigs(current /*, ... */);
await _container.UpsertItemAsync(merged, new PartitionKey(merged.TenantId));
await conflicts.DeleteAsync(conflict, new PartitionKey(merged.TenantId));
}
}
Regula de design mai valoroasă decât orice politică: modelează datele să evite conflictele. Documente per-utilizator sau per-sesiune (nu partajate), operații append-only (mesaje noi, nu update pe același document) fac conflictele aproape imposibile prin construcție.
Ce urmează
Conflictele dintre regiuni au un frate mai mic și mult mai frecvent: scrieri concurente pe același document în aceeași regiune. În partea a patra: optimistic concurrency cu ETags — pattern-ul care previne lost updates.
Întrebări? Scrie-mi la contact@ludoprogramming.com.