Vissza a bloghoz

ProCat Solutions

Egy elfelejtett WHERE: így választjuk szét a bérlőket egy SaaS-ban

Bérlőelkülönítési modellek, row-level security, bérlőnkénti konfiguráció adatbázisban, migrációk, zajos szomszéd, számlázási hookok és mentés egy SaaS-ban.

ProCat Solutions saasmulti-tenantpostgresqlarchitektúra
Megosztás
Egy elfelejtett WHERE: így választjuk szét a bérlőket egy SaaS-ban

Egyetlen elfelejtett WHERE tenant_id = ..., és az egyik ügyfél a másik adatait látja. Ez a többbérlős (multi-tenant) SaaS alapkockázata: egy kódbázis és egy infrastruktúra szolgál ki sok, egymástól független ügyfelet. Az előző bejegyzés terhelési problémái itt egy újabb réteget kapnak. A rendszernek gyorsnak és biztonságosnak kell lennie, és garantálnia kell, hogy egyik bérlő se lássa, se lassítsa a másikat. Az alábbi döntéseket az elmúlt év SaaS-projektjeiben hoztuk meg.

Csapat egy kinyomtatott architektúraábrát beszél át

Három modell

A bérlők adatait háromféleképpen lehet elválasztani, és mindháromnak megvan a helye.

Közös séma, tenant_id oszloppal. Minden tábla tartalmaz egy bérlőazonosítót, minden lekérdezés szűr rá. Ez a legolcsóbb üzemeltetni, a migrációk egyszerűek, a mentés egyetlen adatbázist érint. A kockázat: egy elfelejtett WHERE tenant_id = ..., és kész az adatszivárgás.

Séma bérlőnként. Egy PostgreSQL-adatbázison belül minden bérlő saját sémát kap. Erősebb az elkülönítés, és a search_path beállításával a lekérdezések változatlanok maradnak. Cserébe a migrációt bérlőnként kell futtatni, és néhány száz séma felett a katalógusműveletek észrevehetően lassulnak.

Adatbázis bérlőnként. Teljes elkülönítés, bérlőnként külön mentés és visszaállítás, akár külön szerveren. Ez a legdrágább, és csak szerződéses vagy szabályozási kényszer indokolja.

A legtöbb projektünk a közös sémát választja. Az elfelejtett szűrés kockázatát a fejlesztői fegyelemre nem bízzuk, arra adatbázis-szintű eszköz van.

Az adatbázis szűr

A PostgreSQL row-level security (RLS) mellett a szűrést maga az adatbázis kényszeríti ki, akkor is, ha az alkalmazás hibázik. A mintánk:

  • minden bérlőhöz kötött táblán ENABLE ROW LEVEL SECURITY és egy policy, amely a current_setting('app.tenant_id') értékére szűr,
  • az alkalmazás a kérés elején, a tranzakción belül SET LOCAL app.tenant_id = ... utasítással állítja be a bérlőt,
  • az alkalmazás adatbázis-felhasználója nem a tábla tulajdonosa, így a policy rá is vonatkozik.

Ha így hiányzik egy WHERE feltétel, az eredmény egy üres halmaz. Ez sokkal feltűnőbb, mint egy adatszivárgás, és sokkal kevésbé fáj. Az ára a lekérdezéstervek minimális bonyolódása, valamint az, hogy PgBouncer tranzakciós módjában a SET LOCAL az egyetlen biztonságos forma.

Admin felület és ügyféllista egy irodai asztalon

A beállítások táblába kerülnek

Ezt a leckét többször is megfizettük. A bérlőspecifikus beállítások (limitek, bekapcsolt funkciók, integrációs kulcsok, arculati elemek) helye az adatbázis, egy tenant_settings jellegű, verziózott tábla. Környezeti változóban és konfigurációs fájlban nem tartjuk őket. Az okok:

  • új bérlő felvételéhez nem kell deploy,
  • a beállítások módosítása naplózható és visszavonható,
  • az adminisztrációs felület ugyanazt az adatot látja, mint az alkalmazás.

A titkos értékeket (API-kulcsok, jelszavak) titkosítva tároljuk, a kulcsot az alkalmazás környezetéből olvassuk. A beállításokat rövid TTL-lel Redisben cache-eljük, mert minden kérésnél kellenek.

Migrációk és a zajos szomszéd

A közös séma nagy előnye, hogy a migráció egyszer fut. Egy nagy tábla módosítása viszont minden bérlőt egyszerre érint. Ezért néhány szabályt tartunk: éles idő alatt csak additív változtatás, oszlop törlése két lépésben (előbb a kód, aztán a séma), és minden indexre CREATE INDEX CONCURRENTLY.

A „zajos szomszéd” ellen, amikor egy bérlő terhelése lassítja a többit, több szinten védekezünk:

  • bérlőnkénti rate limit Redisben, a globális mellett,
  • a nehéz műveletek (export, tömeges import, riport) háttérsorba kerülnek, bérlőnkénti párhuzamossági korláttal,
  • a lekérdezéseken statement_timeout van, hogy egyetlen rossz lekérdezés ne foglaljon le egy kapcsolatot percekig,
  • a bérlőazonosító minden metrikán címkeként szerepel, így a problémás bérlő azonnal látszik.

Számlázás és mentés

Egy SaaS-ban a számlázás architekturális kérdés, utólag nehéz hozzáépíteni. Két dolgot az első naptól beépítünk:

Használati események. Minden számlázható művelet (API-hívás, elküldött üzenet, tárolt rekord) eseményt ír egy append-only táblába, bérlőazonosítóval és időbélyeggel. A számlázási időszak végén ebből készül az összesítés, vita esetén ebből lehet visszakeresni.

Állapotátmenet-hookok. A bérlő életciklusa (próbaidőszak, aktív, fizetési késedelem, felfüggesztett, törölt) explicit állapotgép. Minden átmenetnél lefutnak a hozzá tartozó hookok: értesítés, funkciók korlátozása, az adatmegőrzési időzítő indítása. Arra a kérdésre, hogy „mi történik, ha nem fizet”, így dokumentált válasz van.

A közös séma gyenge pontja a mentés: egyetlen bérlő visszaállítása nem triviális. Ezért a napi teljes mentés mellett bérlőnkénti logikai exportot is készítünk (pg_dump szűrt adatokkal vagy alkalmazásszintű export), és a visszaállítást rendszeresen, a gyakorlatban is kipróbáljuk.

A multi-tenant architektúra egy tucat kisebb, egymásba kapaszkodó döntésből áll. A közös séma RLS-sel, az adatbázisban tárolt bérlőkonfiguráció és a bérlőnkénti korlátok a legtöbb esetben jó egyensúlyt adnak az üzemeltetési egyszerűség és a biztonság között. Ha egy bérlő szerződéses okból teljes elkülönítést kér, külön adatbázist kap. Ez nálunk kivétel marad.

Ilyen architektúrát építünk és üzemeltetünk ügyfeleinknek is (SaaS fejlesztés és üzemeltetés). A saját termékünkön, az Abigel.ai-on esettanulmányban mutatjuk be, hogyan működik élesben.

Megosztás
Széchenyi Terv Plusz kedvezményezetti infoblokk
QR Code