Bază de date per tenant sau schemă partajată — cum alegi
Comparația practică între cele două modele de izolare multi-tenant: ce câștigi, ce plătești și ce se schimbă la migrări, backup și onboarding.
Comparația practică între cele două modele de izolare multi-tenant: ce câștigi, ce plătești și ce se schimbă la migrări, backup și onboarding.
Am construit produse în ambele feluri: SaaS-uri cu o singură bază de date și izolare logică, și un produs cu bază de date separată pentru fiecare client. Concluzia la care am ajuns e că întrebarea „care e mai bună“ e greșit pusă. Cele două modele optimizează lucruri diferite, iar alegerea se face pe operare, nu pe eleganță.
Practic, trei — deși în discuții apar de obicei doar două:
TenantId.Restul articolului compară 1 și 3, pentru că astea sunt alegerile pe care le văd cel mai des. Merită totuși să fiu corect cu opțiunea din mijloc: schema per tenant e curentul principal în PostgreSQL, unde search_path face rutarea aproape gratuită și un singur pg_dump -n îți dă backup-ul unui client. Nu e o alegere greșită. Modul ei real de eșec e altul decât se povestește: la câteva mii de tenanți, catalogul de sistem se umflă — pg_class și pg_attribute cresc cu numărul de tabele × numărul de scheme —, iar autovacuum-ul pe catalog, planificarea interogărilor și pg_dump la nivel de cluster încep să sufere. E o opțiune bună până pe la ordinul sutelor de tenanți și devine dureroasă exact când crești.
| Criteriu | Schemă partajată | Bază per tenant |
|---|---|---|
| Izolarea datelor | Logică — depinde de corectitudinea codului | Impusă de motor — separarea nu mai depinde de un predicat din cod |
| Blast radius la un bug de scoping | Toți clienții | Un singur client |
| Restaurare pentru un client | Dificilă — extragi din backup comun | Banală — restaurezi baza lui |
| Migrări de schemă | O singură rulare | N rulări, cu orchestrare și stări parțiale |
| Cost la mulți clienți mici | Foarte bun | Slab — cost fix per bază |
| Conexiuni / pooling | Un singur pool | Pool per bază; devine o constrângere reală |
| Zgomot între vecini | Real — un client greu afectează restul | Redus |
| Raportare cross-tenant | Banală — un GROUP BY |
Costisitoare — agregare din N surse |
| Onboarding client nou | Un INSERT |
Provizionare + migrare + configurare |
| Argument de conformitate | „Avem filtre“ | „Datele sunt în baze separate“ |
Două rânduri au nevoie de nuanțare, ca să nu vinzi mai mult decât livrezi.
„Izolare fizică“ e aproape întotdeauna o exagerare. N baze de date pe aceeași instanță împart procesul, discul, memoria și contul de serviciu. Ce câștigi e altceva, și e real: izolarea nu mai depinde de faptul că fiecare interogare poartă predicatul corect. Devine fizică doar dacă chiar pui instanțe separate — ceea ce schimbă complet costul.
„Blast radius“ se referă doar la bug-urile de scoping. O migrare greșită, o eroare de logică de business sau un cod de calcul greșit ating toți clienții în ambele modele; le rulezi pe toate bazele. Ce izolează baza per tenant e categoria „am uitat WHERE TenantId“.
Ultimul rând din tabel e cel care schimbă adesea decizia — și nu din motive tehnice. Într-o discuție cu un client din zona medicală, „datele dumneavoastră sunt într-o bază separată“ e un argument pe care îl înțelege oricine. „Avem global query filters“ nu e.
Din experiență, când cel puțin două dintre următoarele sunt adevărate:
Pe produsul de programări medicale am mers pe bază per client exact din motivele astea: date medicale, clienți relativ puțini, cerință clară de separare. Am adăugat și izolare de resurse la nivel de sistem de operare, ca un client să nu poată sufoca CPU-ul celorlalți.
E alegerea implicită rezonabilă pentru majoritatea produselor B2B. Cu global query filters aplicate consecvent și cu validare la scriere, izolarea logică e solidă — problema nu e modelul, ci disciplina.
Asta e partea pe care o subestimează toată lumea, inclusiv eu, prima dată.
Cu o bază partajată, o migrare e un eveniment. Cu N baze, e un proces:
Regula practică: dacă mergi pe bază per tenant, scrie orchestratorul de migrări în ziua unu. Dacă îl amâni până ai 30 de clienți, îl vei scrie sub presiune, în producție.
Da, și e adesea răspunsul corect pentru produsele care cresc: schemă partajată ca implicit, bază dedicată pentru clienții care o cer (și o plătesc). Costul e că menții două căi de rutare și de migrare — deci merită doar dacă cererea e reală, nu ipotetică.
Ce funcționează bine ca pregătire, indiferent de alegerea inițială: ține rezolvarea tenant-ului într-un singur loc. Dacă tot codul obține conexiunea printr-un serviciu care știe să rezolve tenant-ul, trecerea de la un model la altul rămâne o schimbare localizată, nu o rescriere.
Dacă ești la începutul unui produs multi-tenant și vrei să iei decizia asta cu ochii deschiși, e exact discuția pe care am purtat-o de câteva ori.
Dacă lucrezi la ceva din zona asta, hai să vorbim 30 de minute.
Programează o discuție