RO EN

Cosmos DB patterns (3) — multi-region writes

Cosmos DB patterns (3) — multi-region writes ✨ Imagine generată cu AI
Doru Bulubașa
29 iulie 2026
37 vizualizări

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.