Vissza a bloghoz

ProCat Solutions

Multi-tenant SaaS architektúra a gyakorlatban

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
Multi-tenant SaaS architektúra a gyakorlatban

A többbérlős (multi-tenant) SaaS az a rendszertípus, ahol egyetlen kódbázis és egyetlen infrastruktúra szolgál ki sok, egymástól független ügyfelet. Az előző bejegyzésben említett terhelési problémák itt egy újabb réteggel bővülnek: nem elég, hogy a rendszer gyors és biztonságos legyen, azt is garantálni kell, hogy egyik bérlő se lássa, se lassítsa a másikat. Az alábbiakban azokat a döntéseket szedjük össze, amelyeket az elmúlt év SaaS-projektjeiben meghoztunk.

Három elkülönítési modell

A bérlők adatainak elválasztására három alapvető modell létezik, és mindegyiknek 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á. A legolcsóbb üzemeltetni, a migrációk egyszerűek, a mentés egyetlen adatbázist érint. A kockázat: egyetlen elfelejtett WHERE tenant_id = ... és adatszivárgás van.

Séma bérlőnként. Egy PostgreSQL-adatbázison belül minden bérlő saját sémát kap. Az elkülönítés erősebb, a search_path beállításával a lekérdezések nem változnak. Cserébe a migrációt bérlőnként kell futtatni, és néhány száz séma felett a katalógus mű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 akkor indokolt, ha szerződéses vagy szabályozási okból van rá szükség.

A legtöbb projektünk a közös sémás modellt választja, és az elfelejtett szűrés kockázatát nem fegyelemmel, hanem adatbázis-szintű eszközzel kezeli.

Row-level security mint biztonsági háló

A PostgreSQL row-level security (RLS) lehetővé teszi, hogy a szűrést ne az alkalmazás, hanem az adatbázis kényszerítse ki. A minta, amit használunk:

  • 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 érvényes.

Ezzel a hiányzó WHERE feltétel nem adatszivárgást, hanem üres eredményhalmazt okoz, ami sokkal jobban észrevehető és sokkal kevésbé fájdalmas. Az ára a lekérdezéstervek minimális bonyolódása és az, hogy PgBouncer tranzakciós módban a SET LOCAL az egyetlen biztonságos forma.

Bérlőnkénti konfiguráció az adatbázisban

Egy tanulság, amit többször megfizettünk: a bérlőspecifikus beállításokat (limitek, bekapcsolt funkciók, integrációs kulcsok, arculati elemek) nem környezeti változóban és nem konfigurációs fájlban tartjuk, hanem az adatbázisban, egy tenant_settings jellegű, verziózott táblában. Ennek több oka van:

  • új bérlő felvétele nem igényel deployt,
  • 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 pedig az alkalmazás környezetéből olvassuk. A beállításokat Redisben cache-eljük rövid TTL-lel, mert minden kérésnél előkerülnek.

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

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

A “zajos szomszéd” jelenség (egy bérlő terhelése lassítja a többit) ellen több szinten védekezünk:

  • bérlőnkénti rate limit Redisben, nem csak globális,
  • 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ésekre statement_timeout van beállítva, hogy egyetlen rossz lekérdezés ne foglaljon el egy kapcsolatot percekig,
  • a bérlőazonosító minden metrikán címkeként szerepel, így a problémás bérlő azonnal látható.

Számlázási hookok és mentés

A számlázás egy SaaS-ban nem utólagos kiegészítés, hanem architekturális kérdés. Két dolgot építünk be az első naptól:

Használati események. Minden számlázható művelet (API-hívás, elküldött üzenet, tárolt rekord) egy 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, é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 futnak a hozzá tartozó hookok: értesítés, funkciók korlátozása, adatmegőrzési időzítő indítása. Így a “mi történik, ha nem fizet” kérdésre van dokumentált válasz.

A mentésnél a közös sémás modell hátránya, hogy egyetlen bérlő visszaállítása nem triviális. Ezt úgy hidaljuk át, hogy 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ási folyamatot rendszeresen, nem csak elméletben próbáljuk ki.

Összefoglalva: a multi-tenant architektúra nem egyetlen döntés, hanem egy tucat kisebb, egymással összefüggő döntés. A közös séma RLS-sel, adatbázisban tárolt bérlőkonfigurációval és bérlőnkénti korlátokkal a legtöbb esetben jó egyensúlyt ad 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, azt külön adatbázissal oldjuk meg, de ezt kivételként kezeljük, nem szabályként.

QR Code