Բոլոր դասերը
ՃարտարապետությունԱռաջադեմ15 րոպե ընթերցումԻմ ստեկում

Multi-tenant SaaS ճարտարապետություն

Tenancy մոդելներ, անթափանց մեկուսացում, պլաններ և սահմանաչափեր, «աղմկոտ հարևաններ» և offline աշխատող կլիենտներ։

  • Tenancy models
  • Isolation
  • Plans & limits
  • Offline sync
Քայլ 1/4
Multi-tenant SaaS. շատ հաճախորդներ (tenant-ներ) ապրում են մեկ պլատֆորմում և մեկ տվյալների բազայում՝ յուրաքանչյուրն իր տվյալներով։

30 վայրկյանում

Multi-tenant SaaS-ը մեկ պլատֆորմից սպասարկում է բազմաթիվ հաճախորդ ընկերությունների (tenant-ների)։ Հիմնական որոշումներն են՝ ինչպես են բաժանվում tenant-ների տվյալները (ընդհանուր սխեմա tenant ID-ով, առանձին սխեմա յուրաքանչյուր tenant-ի համար, թե առանձին տվյալների բազա յուրաքանչյուրի համար) և ինչպես է ապահովվում մեկուսացումը, որպեսզի ոչ մի tenant երբեք չտեսնի ուրիշի տվյալները։ Դրա շուրջ են պլաններն ու սահմանաչափերը, tenant-ը հաշվի առնող cache-ն ու հերթերը, տվյալների արտահանումն ու ջնջումը և tenant-ների միջև արդարությունը։

Ինչու եմ օգտագործում

  • Ես օգտագործում եմ սա, որովհետև tenant-ների միջև տվյալների արտահոսքը այն միակ բագն է, որից SaaS-ը չի կարող վերապրել, ուստի մեկուսացումը պետք է նախագծվի սկզբից, ոչ թե կպցվի հետո։
  • Tenancy մոդելը տարիներով որոշում է ծախսը, onboarding-ի արագությունը, միգրացիաներն ու compliance-ը. հետո այն փոխելը չափազանց թանկ է։
  • Պլանները, սահմանաչափերը և feature switch-երը մեկ codebase-ը վերածում են տարբեր գնային մակարդակների պրոդուկտների՝ առանց fork-երի։
  • Իրական բիզնեսներում ինտերնետը անջատվում է. կլիենտներ նախագծելը, որոնք շարունակում են աշխատել offline և ապահով սինխրոնացվում են, հենց դա է SaaS-ը օգտագործելի դարձնում վաճառասեղանի մոտ։

Ինչպես է աշխատում

01Tenancy-ի երեք մոդել

Պատկերացրեք գրասենյակային շենք։ Ընդհանուր սխեման open-space հարկ է. բոլոր tenant-ների տողերը նույն աղյուսակներում են՝ բաժանված tenant_id սյունակով։ Այն ամենաէժանն ու ամենապարզն է շահագործման համար՝ մեկ միգրացիա բոլորի համար և հեշտ cross-tenant անալիտիկա, բայց մեկուսացումն ամբողջությամբ կախված է նրանից, որ յուրաքանչյուր query ճիշտ scope ունենա։ Schema-per-tenant-ը առանձին սենյակներով հարկ է. յուրաքանչյուր tenant ստանում է իր աղյուսակների հավաքածուն նույն բազայում։ Մեկուսացումն ավելի ուժեղ է, և մեկ tenant-ի վերականգնումն ավելի հեշտ, բայց միգրացիաները պետք է անցնեն հարյուրավոր կամ հազարավոր սխեմաներով, իսկ connection pooling-ը բարդանում է։ Database-per-tenant-ը առանձին շենքեր են. ամենաուժեղ մեկուսացումը, ամեն tenant-ի համար առանձին performance tuning, տեղակայման վայր և գաղտնագրման բանալիներ, հեշտ ջնջում՝ ամենաբարձր շահագործման ծախսով։ Շատ պրոդուկտներ խառնում են դրանք. ընդհանուր սխեմա tenant-ների մեծ մասի համար և առանձին բազաներ խոշոր կամ կարգավորվող (regulated) հաճախորդների համար։

  • Ընդհանուր սխեմա + tenant_id. ամենաէժանն է, մասշտաբավորվում է շատ փոքր tenant-ների համար, մեկուսացումը՝ կոդով և RLS-ով։
  • Սխեմա յուրաքանչյուր tenant-ի համար. միջին տարբերակ. հիմնական ցավը միգրացիաների տարածումն է բոլոր սխեմաներով։
  • Բազա յուրաքանչյուր tenant-ի համար. ամենաուժեղ մեկուսացումը և data residency. ամենաբարձր ծախսն ու ops բեռը։

02Մեկուսացման ապահովումը շերտերով

Մեկուսացումը երբեք չպետք է հենվի այն բանի վրա, որ ծրագրավորողը կհիշի WHERE պայմանը։ Tenant-ը որոշեք մեկ անգամ յուրաքանչյուր request-ի համար վստահելի աղբյուրից (authenticated token, API key կամ subdomain), երբեք body-ի դաշտից, որը կլիենտն է վերահսկում։ Պահեք այն request context-ում և ստիպեք տվյալների շերտին կիրառել այն. repository base class, ORM extension կամ global scope, որը ավելացնում է tenant ֆիլտրը և exception է նետում, եթե այն բացակայում է։ Ավելացրեք երկրորդ, անկախ շերտ բազայում՝ PostgreSQL row-level security-ով, որպեսզի նույնիսկ մոռացված ֆիլտրը ոչինչ չվերադարձնի։ Ուրիշ tenant-ի ID-ի համար լռելյայն պատասխանը թող լինի 404, ոչ թե 403, որպեսզի հարձակվողները չկարողանան պարզել, թե որ ID-ներն են գոյություն ունենում։ Հետո ապացուցեք դա թեստերով. յուրաքանչյուր մոդուլ ստանում է «սխալ tenant» թեստ, որը փորձում է կարդալ և փոփոխել ուրիշ tenant-ի գրառումները և սպասում է ձախողման։

sql
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE orders FORCE ROW LEVEL SECURITY;
 
CREATE POLICY tenant_isolation ON orders
  USING (tenant_id = current_setting('app.tenant_id', true)::uuid)
  WITH CHECK (tenant_id = current_setting('app.tenant_id', true)::uuid);
 
-- Per request, inside a transaction, using a non-owner app role:
BEGIN;
SET LOCAL app.tenant_id = '00000000-0000-0000-0000-000000000001';
SELECT id, total FROM orders;  -- only this tenant's rows
COMMIT;

03Պլաններ, սահմանաչափեր և feature flag-եր

Tenant-ները վճարում են տարբեր բաների համար, ուստի իրավունքները (entitlements) մոդելավորեք բացահայտ։ Պլանը սահմանում է հնարավորություններ (մոդուլներ, օրինակ՝ loyalty կամ խոհանոցի էկրաններ) և սահմանաչափեր (մասնաճյուղեր, օգտատերեր, պահոց, API կանչեր րոպեում)։ Հնարավորությունները ստուգեք մեկ տեղում՝ policy-ում կամ guard-ում, այլ ոչ թե «if plan === pro» ցրեք ամբողջ կոդով։ Կոշտ սահմանաչափերը կիրառեք գրելու պահին (տասնմեկերորդ մասնաճյուղի ստեղծումը ձախողվում է հստակ հաղորդագրությամբ), իսկ փափուկները՝ զգուշացումներով և օգտագործման dashboard-ներով։ Feature flag-երը առանձին գործիք են. դրանք ֆունկցիոնալը աստիճանաբար միացնում են beta tenant-ների համար, թույլ են տալիս kill switch-եր և tenant-ին հատուկ override-ներ, որոնց շուրջ պայմանավորվել են վաճառքի ժամանակ։ Երկուսն էլ պահեք audit անելու հնարավորությամբ, որովհետև «ինչու՞ է այս tenant-ը կարողանում դա անել» հարցը support-ում անպայման կլսեք։ Օգտագործման ճշգրիտ հաշվառումը օգնում է նաև billing-ին, ուստի գրանցեք այն որպես event-ներ, այլ ոչ թե վերահաշվեք կենդանի աղյուսակներից։

04Tenant-ը հաշվի առնող cache, հերթեր և realtime

Յուրաքանչյուր ընդհանուր կոմպոնենտ մի տեղ է, որտեղ tenant-ները կարող են «արտահոսել» միմյանց մեջ։ Cache key-երը պետք է պարունակեն tenant ID-ն (tenant:42:menu:v3), այլապես մի սրճարանը կտեսնի մյուսի մենյուն։ Background job-երը պետք է իրենց payload-ում կրեն tenant ID-ն, և worker-ը պետք է վերականգնի tenant context-ը նախքան տվյալներին դիպչելը՝ ճիշտ այնպես, ինչպես request-ը։ Realtime ալիքները նույն ուշադրությունն են պահանջում. կլիենտներին բաժանորդագրեք room-ների՝ ըստ tenant-ի և մասնաճյուղի, և յուրաքանչյուր բաժանորդագրություն թույլատրեք սերվերում։ Ֆայլերը պահվում են object storage-ում tenant-ի prefix-ով ճանապարհներով և մատուցվում են կարճատև signed URL-ներով։ Լոգերն ու trace-երը պետք է պարունակեն tenant ID-ն, որպեսզի support-ը կարողանա ուսումնասիրել մեկ հաճախորդի խնդիրը՝ առանց բոլորին զննելու։ Եվ վերջապես, rate limit-երն ու հերթերի concurrency-ն պետք է լինեն ըստ tenant-ի, որպեսզի մի tenant-ի import-ը չկարողանա «սովամահ անել» մյուսներին։

  • Cache-ի, պահոցի և ալիքների անունները միշտ ներառում են tenant-ը։
  • Job-երը վերականգնում են tenant context-ը մինչև աշխատելը։
  • Ըստ tenant-ի rate limit-եր և արդար հերթագրում «աղմկոտ հարևանների» դեմ։

05«Աղմկոտ հարևաններ», տվյալների արտահանում և ջնջում

Ընդհանուր համակարգում մի tenant, որը հսկայական report կամ import է աշխատեցնում, կարող է դանդաղեցնել բոլորին։ Պաշտպանեք մյուսներին ըստ tenant-ի rate limit-երով և քվոտաներով, query-ների statement timeout-ներով, ծանր job-երի համար առանձին հերթերով կամ concurrency սահմանափակումներով, և ծանր աշխատանքը պլանավորեք պիկային ժամերից դուրս։ Հետևեք ռեսուրսների օգտագործմանը ըստ tenant-ի, որպեսզի նկատեք շեղվողին, և անհրաժեշտության դեպքում շատ մեծ tenant-ներին տեղափոխեք առանձին ռեսուրսների վրա. սա պատճառներից մեկն է, որ տվյալների շերտը պետք է կարողանա tenant-ին ուղղել այլ բազա։ Tenant-ներն են նաև իրենց տվյալների տերը։ Առաջարկեք ամբողջական արտահանում փաստաթղթավորված ֆորմատներով (CSV, JSON), իսկ ջնջումն իրականացրեք որպես իրական գործընթաց. soft-delete որոշակի grace period-ով, հետո hard-delete բազայից, ֆայլերից, cache-երից և search index-ներից, իսկ backup-ները ժամկետանց են դառնում հայտնի ժամանակացույցով։ Փաստաթղթավորեք սա GDPR-ի տիպի հարցումների համար։

06Offline աշխատող կլիենտներ և սինխրոնացում

Վաճառքի կետի (POS) տերմինալները, դաշտային հավելվածներն ու kiosk-երը պետք է շարունակեն աշխատել, երբ ինտերնետն անջատվում է։ Պատտերնը հետևյալն է. կլիենտը կամ լոկալ hub-ը պահում է լոկալ բազա (հաճախ SQLite)՝ իրեն անհրաժեշտ տվյալներով, յուրաքանչյուր փոփոխություն գրանցում է որպես հրաման (command) եզակի ID-ով և կապը վերականգնվելիս այդ հրամաններն ուղարկում է սերվեր։ Սերվերը կիրառում է յուրաքանչյուր հրաման idempotent կերպով՝ կրկնօրինակները հեռացնելով ըստ command ID-ի և սարքի, ուստի անկայուն վերամիացումներից հետո կրկնակի փորձերը անվտանգ են։ Կոնֆլիկտների կանոնները որոշեք ըստ entity-ի. append-only գրառումները, օրինակ վաճառքները, հազվադեպ են կոնֆլիկտում. ընդհանուր գրառումները, օրինակ պաշարը կամ գները, պահանջում են հստակ տեր, «սերվերը հաղթում է» կանոններ կամ բացահայտ merge։ Լոկալ սխեման և միգրացիաները զգուշորեն version արեք, որովհետև սարքերը թարմացվում են տարբեր ժամանակներում։ Offline սարքին տվեք միայն այն իրավունքներն ու տվյալները, որոնք անհրաժեշտ են իր մասնաճյուղին, և օգտագործեք սարքին կապված token-ներ, որոնք կարելի է չեղարկել։

Օգտագործեք, երբ

  • Մեկ պրոդուկտ, որը վաճառվում է բազմաթիվ ընկերությունների, որոնք երբեք չպետք է տեսնեն միմյանց տվյալները։
  • Շատ տարբեր չափերի հաճախորդներ, որոնց պետք են պլանից կախված ֆունկցիաներ և սահմանաչափեր։
  • Բիզնեսներ, որոնք շարունակում են աշխատել ցանցի անջատումների ժամանակ (ռիթեյլ, հյուրընկալություն, դաշտային աշխատանք)։
  • Կարգավորվող ոլորտների հաճախորդներ, որոնք կարող են պահանջել առանձին բազաներ կամ data residency։

Մի՛ օգտագործեք, երբ

  • Մեկ հաճախորդի ներքին համակարգ. tenancy-ն բարդություն է ավելացնում առանց օգուտի։
  • Schema-per-tenant տասնյակ հազարավոր փոքր tenant-ների համար. միգրացիաներն ու pooling-ը ցավ կպատճառեն։
  • Offline սինխրոնացում հավելվածների համար, որոնց իսկապես միայն կենդանի տվյալներ են պետք. այն թեստավորման մեծ ծավալ է ավելացնում։

Այլընտրանքներ

  • Single-tenant deployment-ներ

    Մեկ տեղադրում յուրաքանչյուր հաճախորդի համար. ուժեղ մեկուսացում, բայց թարմացումներն ու ծախսերը բազմապատկվում են։

  • Հիբրիդ tenancy

    Ընդհանուր սխեմա tenant-ների մեծ մասի համար, առանձին բազաներ enterprise կամ կարգավորվող հաճախորդների համար։

  • Պլատֆորմային ծառայություններ

    Managed auth՝ կազմակերպությունների աջակցությամբ, և managed բազաներ՝ branching-ով, կրճատում են սեփական աշխատանքը։

  • Միայն online կլիենտներ

    Ավելի պարզ են, քան offline սինխրոնացումը, երբ կապը հուսալի է, և downtime-ը ընդունելի է։

Լավագույն պրակտիկաներ

  1. Tenant-ը ստացեք authentication-ից, երբեք օգտատիրոջ վերահսկվող input-ից։
  2. Մեկուսացումը կիրառեք երկու անգամ՝ տվյալների շերտում և բազայում՝ row-level security-ով։
  3. Ուրիշ tenant-ի ռեսուրսների համար վերադարձրեք 404 և յուրաքանչյուր մոդուլի համար գրեք «սխալ tenant» թեստ։
  4. Tenant ID-ն ներառեք յուրաքանչյուր cache key-ում, job payload-ում, ֆայլի ճանապարհում, ալիքում և լոգի տողում։
  5. Պլանները, սահմանաչափերը և feature flag-երը մոդելավորեք բացահայտ և ստուգեք մեկ տեղում։
  6. Կիրառեք ըստ tenant-ի rate limit-եր, քվոտաներ և query timeout-ներ «աղմկոտ հարևանների» դեմ։
  7. Offline սինխրոնացման հրամանները դարձրեք idempotent՝ յուրաքանչյուր սարքի համար եզակի ID-ներով։

Տարածված սխալներ

  • Վստահել request-ի body-ում կամ query string-ում ուղարկված tenant ID-ին։
  • Background job-եր կամ admin script-եր, որոնք շրջանցում են tenant scope-ը։
  • Cache key-եր առանց tenant-ի, որոնք տվյալներ են արտահոսեցնում հաճախորդների միջև։
  • Հավելվածը աշխատեցնել աղյուսակի owner-ի կամ superuser-ի անունից, ինչը շրջանցում է row-level security-ն։
  • Tenant-ին ջնջել հիմնական բազայից, բայց թողնել ֆայլերը, search index-ներն ու cache-երը։

Խորը վերլուծություն

07Row-level security պրոդակշնում․ pooling, արտադրողականություն և դերեր

SET LOCAL-ը չի կարող ընդունել bind պարամետրեր, ուստի շատ հավելվածներ հրամանը կառուցում են որպես տող։ Ավելի անվտանգ տարբերակ է set_config('app.tenant_id', $1, true)-ը, որտեղ true-ն նշանակում է «լոկալ այս տրանզակցիայի համար»․ այն աշխատում է պարամետրերի և transaction-mode pooler-ների հետ։ Policy-ներում օգտագործիր current_setting-ի missing_ok տարբերակը, որպեսզի չսահմանված արժեքը տա NULL, որը ոչ մի տողի չի համապատասխանում, սխալի փոխարեն։ Արտադրողականություն․ policy-ները ամեն հարցմանը ավելացնում են պայման, ուստի tenant_id-ն դիր առաջինը կոմպոզիտ ինդեքսներում (tenant_id, created_at) և unique constraint-ներում (tenant_id, slug)։ Policy-ների ներսում կանչվող ֆունկցիաները պետք է լինեն պարզ և STABLE․ ամեն տողի համար բարդ subquery-ները դանդաղ են։ Օգտագործիր երեք դեր․ միգրացիաների դեր, որին պատկանում են աղյուսակները, հավելվածի դեր՝ առանց BYPASSRLS-ի, և առանձին, աուդիտվող սպասարկման դեր՝ իրական cross-tenant job-երի համար։

sql
-- Inside each request transaction (works with bind parameters and poolers)
SELECT set_config('app.tenant_id', $1, true);
 
-- Indexes and constraints lead with the tenant
CREATE INDEX orders_tenant_created_idx ON orders (tenant_id, created_at DESC);
ALTER TABLE products ADD CONSTRAINT products_tenant_sku_key UNIQUE (tenant_id, sku);
 
-- App role cannot bypass policies
CREATE ROLE app_rw LOGIN NOBYPASSRLS;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_rw;

08Tenant-ների routing․ tenant directory, cell-եր և տեղաբաշխում

Պահիր tenant directory՝ փոքր, բարձր հասանելիությամբ աղյուսակ կամ ծառայություն, որը յուրաքանչյուր tenant քարտեզագրում է այնտեղ, որտեղ ապրում են նրա տվյալները․ ընդհանուր A կլաստեր, առանձին B տվյալների բազա, EU տարածաշրջան։ Ամեն հարցում որոշում է tenant-ին, հետո նրա տեղաբաշխումը և միանում ճիշտ pool-ին։ Այս մեկ միջնորդ շերտը հետագա տեղափոխումները դարձնում է հնարավոր․ մեծ tenant-ին միգրացիա արա իր սեփական տվյալների բազա՝ տվյալները պատճենելով, փոփոխությունները վերարտադրելով, գրառումները կարճ ժամանակով սառեցնելով և directory-ի գրառումը փոխելով։ Ավելի առաջ գնալով՝ cell-based ճարտարապետությունը գործարկում է մի քանի նույնական stack (հավելված, տվյալների բազա, քեշ, հերթեր), որոնցից յուրաքանչյուրը սպասարկում է tenant-ների ենթաբազմություն։ Այդ դեպքում վատ դեպլոյը կամ գերբեռնվածությունը ազդում է մեկ cell-ի, ոչ թե բոլոր հաճախորդների վրա, և փոփոխությունները թողարկում ես cell առ cell։ Գինը ավելի շատ ինֆրաստրուկտուրան և գործիքներն են cross-cell գործողությունների համար, օրինակ՝ գլոբալ ադմին որոնում և billing, ուստի պրոդուկտների մեծ մասը սկսում է մեկ cell-ով և directory-ով, որը պատրաստ է ավելիին։

09Միգրացիաներ և սխեմայի փոփոխություններ շատ tenant-ների համար

Ընդհանուր սխեմայում մեկ միգրացիան միանգամից դիպչում է բոլոր tenant-ների տվյալներին, ուստի այն պետք է լինի online․ սյունակներն ավելացրու nullable կամ հաստատուն default-ներով, ինդեքսները ստեղծիր CONCURRENTLY, backfill արա փոքր batch-երով, հետո constraint-ներն ավելացրու NOT VALID և վալիդացրու առանձին։ Երբեք րոպեներով lock մի արա զբաղված աղյուսակը։ Schema-per-tenant կամ database-per-tenant մոդելների դեպքում խնդիրը դառնում է fan-out․ հազարավոր միգրացիաներ, որոնք կարող են մասնակի ձախողվել։ Գործարկիր դրանք job runner-ով՝ յուրաքանչյուր tenant-ի կարգավիճակով, retry-ներով և զուգահեռության սահմաններով, թողարկման ընթացքում հավելվածը պահիր համատեղելի և՛ հին, և՛ նոր սխեմայի հետ (expand, migrate, contract), և դարձրու այնպես, որ կեսից կանգնեցնելն անվտանգ լինի։ Հետևիր սխեմայի տարբերակին յուրաքանչյուր tenant-ի համար և հրաժարվիր սպասարկել այն tenant-ին, որի տարբերակը գործող կոդը չի աջակցում։

  • Նախ expand, դեպլոյ արա կոդ, որն աշխատում է երկուսի հետ էլ, contract՝ հետո։
  • CREATE INDEX CONCURRENTLY և batch-երով backfill մեծ աղյուսակների համար։
  • Միգրացիայի կարգավիճակ ըստ tenant-ի schema- կամ database-per-tenant կարգավորումներում։

10Offline sync-ի ներքին մեխանիզմները․ հերթականություն, ժամացույցներ և կոնֆլիկտներ

Սարքերի ժամացույցները շեղվում են, ուստի միայն wall-clock timestamp-ները չեն կարող դասավորել մի քանի սարքից եկող փոփոխությունները։ Տարածված մոտեցումներ․ թող սերվերը նշանակի մոնոտոն աճող հաջորդականության համար, երբ ընդունում է հրամանը, իսկ կլիենտները քաշեն «ամեն ինչ N համարից հետո», կամ օգտագործիր hybrid logical clock-եր, որոնք ֆիզիկական ժամանակը համադրում են counter-ի հետ՝ մնալով իրական ժամանակին մոտ և պահպանելով պատճառականությունը։ Կոնֆլիկտների համար յուրաքանչյուր դաշտի համար ընտրիր կանոն․ միայն ավելացվող իրադարձությունները երբեք չեն բախվում, last-writer-wins-ը պարզ է, բայց լուռ կորցնում է խմբագրումները, դաշտի մակարդակի merge-ը պահում է չհամընկնող փոփոխությունները, իսկ CRDT-ները ավտոմատ merge են անում counter-ների, բազմությունների և տեքստի համար՝ բարդության գնով։ Հրամանները պահիր փոքր և մտադրության վրա հիմնված («ավելացրու 2 միավոր 15 պատվերին»)՝ ամբողջ վիճակը վերագրելու փոխարեն, քանի որ դրանք շատ ավելի լավ են merge լինում։ Սահմանափակիր, թե սարքը որքան կարող է offline մնալ, մինչև պարտադիր ամբողջական resync։

Իրական իրավիճակներ

01

Աջակցության ինժեները տեսնում է մեկ այլ tenant-ի տվյալներ

Խնդիր

Ներքին dashboard-ի համար կառուցված report endpoint-ը սովորական tenant token-ով կանչվելիս վերադարձրեց մի քանի ընկերությունների տողեր։ Review-ի ժամանակ ոչ ոք դա չնկատեց։

Ինչպես լուծել

Նախ զսպիր․ անջատիր endpoint-ը, փոխիր export արված բոլոր ֆայլերը և լոգերով պարզիր, թե ով է այն կանչել և ինչ է վերադարձվել, որովհետև սա կարող է լինել ինցիդենտ, որի մասին պարտադիր է ծանուցել։ Հետո պարզիր, թե ինչու շերտերը չաշխատեցին․ հավանաբար raw query, որը շրջանցել է scoped repository-ն, և տվյալների բազայի դեր՝ BYPASSRLS-ով։ Հավելվածը տեղափոխիր ոչ տեր դերի՝ առանց BYPASSRLS-ի, ստիպողաբար միացրու RLS-ը tenant-ի բոլոր աղյուսակների վրա և ավելացրու CI ստուգում, որը թվարկում է առանց policy-ների աղյուսակները։ Endpoint-ի համար գրիր «սխալ tenant» թեստ, իսկ ներքին cross-tenant ռեպորտինգը անցկացրու առանձին, աուդիտվող ծառայության և դերի միջով։

02

Մեկ tenant-ի import-ը դանդաղեցնում է բոլորին

Խնդիր

Նոր մեծ հաճախորդը import է անում երկու միլիոն ապրանք։ Մեկ ժամվա ընթացքում API-ի latency-ն եռապատկվում է բոլոր tenant-ների համար, իսկ մյուս tenant-ների background job-երը սպասում են հերթում։

Ինչպես լուծել

Զանգվածային import-ները տար առանձին հերթ՝ յուրաքանչյուր tenant-ի զուգահեռության սահմանով, և մշակիր դրանք batch-երով՝ կարճ տրանզակցիաներով։ Import-ի դերի համար դիր statement timeout և մեծ import-ները պլանավորիր պիկից դուրս։ API-ով արվող import-ների վրա կիրառիր rate limit-ներ ըստ tenant-ի։ Ավելացրու մետրիկաներ ըստ tenant-ի՝ հարցումների և job-երի ժամանակի համար, որպեսզի շեղվողը տեսանելի լինի րոպեների ընթացքում։ Եթե այս tenant-ի կայուն բեռը մնում է մյուսներից շատ բարձր, tenant directory-ի միջոցով տեղափոխիր նրան առանձին տվյալների բազայի ռեսուրսներ և դա արտացոլիր նրա պլանում։

03

Enterprise հաճախորդը պահանջում է սեփական տվյալների բազա EU-ում

Խնդիր

Պոտենցիալ հաճախորդը կստորագրի միայն այն դեպքում, եթե իր տվյալները ապրեն առանձին տվյալների բազայում կոնկրետ տարածաշրջանում, մինչդեռ պրոդուկտն աշխատում է մեկ ընդհանուր կլաստերի վրա։

Ինչպես լուծել

Եթե տվյալների շերտն արդեն միացումները որոշում է tenant directory-ի միջոցով, սա provisioning-ի խնդիր է։ Պահանջվող տարածաշրջանում ստեղծիր առանձին տվյալների բազա՝ նույն սխեմայով և միգրացիաների pipeline-ով, գրանցիր այն directory-ում և tenant-ին onboard արա այնտեղ։ Համոզվիր, որ մնացած բոլոր պահոցները հետևում են տեղաբաշխմանը․ object storage bucket, backup-ներ, որոնման ինդեքս, լոգեր և քեշեր՝ տվյալ տարածաշրջանում։ Տվյալների բազան ավելացրու միգրացիաների fan-out-ին և մոնիթորինգին։ Գնահատիր այն որպես premium պլան, որովհետև այն ավելացնում է ինֆրաստրուկտուրայի և օպերացիաների ֆիքսված ծախս։

Արագ ուղեցույց

Միացնել և ստիպողաբար կիրառել RLS
ALTER TABLE t ENABLE ROW LEVEL SECURITY; ALTER TABLE t FORCE ROW LEVEL SECURITY;
Սահմանել tenant-ը ամեն տրանզակցիայի համար
SELECT set_config('app.tenant_id', $1, true);
Առանց RLS-ի աղյուսակներ
SELECT relname FROM pg_class WHERE relkind = 'r' AND relnamespace = 'public'::regnamespace AND NOT relrowsecurity;
Հավելվածի դեր՝ առանց bypass-ի
CREATE ROLE app_rw LOGIN NOBYPASSRLS;
Tenant-ով սկսվող ինդեքս
CREATE INDEX CONCURRENTLY ON orders (tenant_id, created_at);
Tenant-ով սահմանափակված քեշի key
tenant:{tenantId}:menu:v{version}
Tenant-ի prefix-ով օբյեկտի ուղի
tenants/{tenantId}/media/{uuid}.webp
Statement timeout ծանր դերերի համար
ALTER ROLE importer SET statement_timeout = '30s';

Իմ իրական նախագծերում

  • POS SaaS-ում, որը ես համատեղ ղեկավարում եմ, tenancy-ն կառուցված է այսպես՝ Organization, հետո Company (tenant-ը), հետո Branch, պլանից կախված հնարավորություններով և feature switch-երով։
  • Այնտեղ մեկուսացումը կիրառվում է երկու անգամ. բացահայտ companyId յուրաքանչյուր query-ում, գումարած Prisma extension, որը exception է նետում, եթե scope-ը բացակայում է. բացառությունները պետք է allow-list արվեն պատճառաբանությամբ, cross-tenant տողերը վերադարձնում են 404, և յուրաքանչյուր մոդուլի թեստ ներառում է «սխալ tenant» դեպք։
  • Դրա realtime շերտը օգտագործում է socket.io room-եր ըստ ընկերության, մասնաճյուղի, տերմինալի և օգտատիրոջ, իսկ մեդիա upload-ներն ունեն պահոցի քվոտաներ և signed private հղումներ։
  • Մասնաճյուղի offline hub-ը լոկալ SQLite բազայով թույլ է տալիս վաճառքները շարունակել առանց ինտերնետի. սինխրոնացման հրամանների կրկնօրինակները հեռացվում են ըստ տերմինալի, իսկ hub-ի միգրացիաները version են արված և ամրագրված checksum-ով։

Հարցեր և պատասխաններ

Q1Tenancy-ի ո՞ր մոդելը կընտրեիք նոր SaaS-ի համար։

Նոր պրոդուկտների մեծ մասի համար՝ ընդհանուր սխեմա, որտեղ tenant-ին պատկանող յուրաքանչյուր աղյուսակ ունի tenant ID։ Այն ամենաէժանն է շահագործման համար, tenant-ի onboarding-ը պարզապես մեկ տողի ավելացում է, և մեկ միգրացիան թարմացնում է բոլորին։ Ավելի թույլ ֆիզիկական մեկուսացումը փոխհատուցում եմ խիստ կիրառմամբ. scope արված տվյալների հասանելիություն կոդում, PostgreSQL row-level security որպես երկրորդ շերտ և «սխալ tenant» թեստեր։ Տվյալների շերտը պահում եմ այնպես, որ հետագայում կարողանա tenant-ին ուղղել իր սեփական բազա, որովհետև խոշոր կամ կարգավորվող հաճախորդները կարող են դա պահանջել։ Schema-per-tenant-ը միջին տարբերակ է, որը կընտրեի միայն tenant-ների համեստ քանակի դեպքում։

Q2Ինչպե՞ս եք երաշխավորում, որ մի tenant երբեք չտեսնի մյուսի տվյալները։

Պաշտպանության շերտեր դնելով, որպեսզի ոչ մի առանձին սխալ արտահոսքի չհանգեցնի։ Tenant-ը որոշվում է մեկ անգամ authentication-ից և պահվում request context-ում։ Տվյալների շերտը ավտոմատ ավելացնում է tenant ֆիլտրը և exception է նետում, եթե query-ն scope չունի, իսկ ցանկացած միտումնավոր բացառություն allow-list է արվում և հիմնավորվում։ Բազան կիրառում է row-level security որպես անկախ ստուգում։ Cache-երը, հերթերը, ֆայլերը և realtime ալիքները բոլորը կրում են tenant ID-ն։ Ուրիշ tenant-ների գրառումները վերադարձնում են 404։ Եվ վերջապես, թեստերն են դա ապացուցում. յուրաքանչյուր մոդուլ ներառում է դեպք, որը փորձում է կարդալ և փոփոխել ուրիշ tenant-ի տվյալները, գումարած լոգավորում, որը թույլ է տալիս audit անել հասանելիությունը։

Q3Ի՞նչ է «աղմկոտ հարևանը» (noisy neighbour), և ինչպե՞ս եք դրա դեմ պայքարում։

Դա tenant է, որի ծանրաբեռնվածությունը վատացնում է ծառայությունը մյուսների համար ընդհանուր ենթակառուցվածքում, օրինակ՝ մեծ import, թանկ report կամ API կանչերի պոռթկում։ Լուծում եմ մի քանի մակարդակում. ըստ tenant-ի rate limit-եր և քվոտաներ API-ում, statement timeout-ներ և query-ների սահմանափակումներ բազայում, առանձին հերթեր կամ concurrency սահմանափակումներ ծանր job-երի համար, և զանգվածային աշխատանքի պլանավորում ոչ պիկային ժամերին։ Ըստ tenant-ի մետրիկաները ցույց են տալիս, թե ով ինչ է սպառում։ Եթե մի tenant-ին մշտապես շատ ավելին է պետք, քան մյուսներին, տեղափոխում եմ նրան առանձին ռեսուրսների վրա, իդեալում՝ առանձին բազա, և համապատասխանաբար գնահատում դա։

Q4Ինչպե՞ս է աշխատում row-level security-ն PostgreSQL-ում, և որո՞նք են դրա թակարդները։

Աղյուսակի վրա միացնում եք RLS և ստեղծում policy-ներ, որոնք սովորաբար համեմատում են տողի tenant_id-ն session setting-ի հետ, որը հավելվածը սահմանում է յուրաքանչյուր transaction-ի համար։ Այդ դեպքում յուրաքանչյուր query, ներառյալ նրանք, որոնցում ծրագրավորողները մոռանում են ֆիլտրել, տեսնում է միայն թույլատրված տողերը. WITH CHECK-ը թույլ չի տալիս տողեր գրել ուրիշ tenant-ի համար։ Թակարդները. superuser-ները և BYPASSRLS ունեցող դերերն անտեսում են այն, աղյուսակի owner-ները նույնպես, եթե չեք կիրառում FORCE row-level security, ուստի հավելվածը պետք է օգտագործի առանձին, ոչ owner դեր։ Connection pooling-ի դեպքում օգտագործեք SET LOCAL transaction-ի ներսում, որպեսզի setting-ը չարտահոսի հաջորդ request-ին։ Նաև index արեք tenant_id-ն, որովհետև policy-ները ֆիլտրեր են ավելացնում յուրաքանչյուր query-ին։

Q5Ինչպե՞ս կնախագծեիք offline ռեժիմը POS կլիենտի համար։

Տերմինալը կամ լոկալ hub-ը պահում է լոկալ բազա՝ իրեն անհրաժեշտ մենյուով, գներով և բաց պատվերներով, և լիովին աշխատում է դրա հետ։ Յուրաքանչյուր փոփոխություն գրանցվում է որպես հրաման՝ եզակի ID-ով, սարքի ID-ով և timestamp-ով, և դրվում է հերթի մեջ։ Երբ կապը վերականգնվում է, հրամաններն ուղարկվում են հերթականությամբ, և սերվերը կիրառում է յուրաքանչյուրը idempotent կերպով, այնպես որ կրկնակի փորձը երբեք չի կրկնօրինակում վաճառքը։ Վաճառքները append-only են և հազվադեպ են կոնֆլիկտում. ընդհանուր տվյալների, օրինակ գների համար, սերվերն է տերը և ինքն է ներքև ուղարկում թարմացումները։ Ֆիսկալ և վճարման քայլերին պետք են բացահայտ offline տարբերակներ և հետագա համադրում (reconciliation), իսկ սարքերը օգտագործում են չեղարկելի token-ներ։

Q6Ինչպե՞ս եք վարվում, երբ tenant-ը խնդրում է արտահանել կամ ջնջել իր բոլոր տվյալները։

Արտահանումը պետք է լինի աջակցվող ֆունկցիա, ոչ թե ad hoc script. background job, որը tenant-ին պատկանող բոլոր entity-ները հավաքում է փաստաթղթավորված ֆորմատներով, փաթեթավորում է ֆայլերը և տրամադրում ժամանակով սահմանափակ signed հղում։ Ջնջումը փուլերով գործընթաց է։ Նախ soft delete՝ grace period-ով, սխալի դեպքի համար, հետո hard delete բազայից, object storage-ից, search index-ներից, cache-երից և երրորդ կողմի processor-ներից։ Backup-ները սովորաբար հնարավոր չէ խմբագրել, ուստի փաստաթղթավորում եմ, որ դրանք ժամկետանց են դառնում ֆիքսված ժամանակացույցով։ Յուրաքանչյուր քայլ լոգավորվում է, որպեսզի կարողանանք ապացուցել, որ հարցումը կատարվել է, ինչը կարևոր է գաղտնիության կարգավորումների համար։

Սենիոր մակարդակի հարցեր

S1Ինչպե՞ս կմիգրացնեիր shared-schema SaaS-ը, որպեսզի այն աջակցի առանձին տվյալների բազաներ որոշ tenant-ների համար։

Տեղաբաշխումը ներմուծիր որևէ մեկին տեղափոխելուց առաջ։ Նախ տվյալներին ամեն դիմում թող որոշի tenant-ի միացումը tenant directory-ի միջոցով՝ լռելյայն ընդհանուր կլաստերով․ դեպլոյ արա սա՝ առանց վարքագծի փոփոխության։ Համոզվիր, որ ոչ մի հարցում join չի անում tenant-ների միջև, և որ background job-երը, ռեպորտները և ադմին գործիքներն էլ օգտագործում են directory-ն։ Հետո կառուցիր տեղափոխումը․ ստեղծիր թիրախային տվյալների բազան, պատճենիր tenant-ի տողերը ըստ tenant_id-ի, հետագա փոփոխությունները stream արա logical replication-ով կամ dual-write պատուհանով, ստուգիր քանակներն ու checksum-ները, մի քանի վայրկյանով սառեցրու tenant-ի գրառումները, փոխիր directory-ի գրառումը և ապասառեցրու։ Հին տողերը պահիր read-only՝ rollback-ի ժամանակահատվածի համար։ Փոխզիջումը օպերացիոն է․ միգրացիաները, մոնիթորինգը և backup-ները հիմա բաշխվում են մի քանի տվյալների բազայի վրա, ուստի ավտոմատացրու դրանք, մինչև առանձին տեղաբաշխումը լայնորեն առաջարկելը։

S2Ինչպե՞ս ես նախագծում գաղտնագրման key-եր յուրաքանչյուր tenant-ի համար, և ե՞րբ է դա արժե։

Օգտագործիր envelope encryption։ Յուրաքանչյուր tenant ստանում է data encryption key, որը գաղտնագրում է զգայուն դաշտերը կամ ֆայլերը․ այդ key-ն ինքն էլ գաղտնագրված է key-encryption key-ով, որը պահվում է key management service-ում և երբեք չի պահվում բաց տեսքով։ Վերծանված key-երը կարճ ժամանակով քեշավորվում են հիշողության մեջ։ Օգուտներ․ crypto-shredding, երբ tenant-ի key-ը ոչնչացնելը նրա բոլոր տվյալները, ներառյալ backup-ներում եղած պատճենները, դարձնում է անընթեռնելի, customer-managed key-եր enterprise պայմանագրերի համար և blast radius-ի նվազում, եթե մեկ key արտահոսի։ Ծախսեր․ գաղտնագրված դաշտերը հեշտությամբ չես կարող ինդեքսավորել կամ որոնել, ամեն ընթերցում վճարում է վերծանման փոքր գին, key-երի rotation-ը պահանջում է re-wrapping, և KMS-ի հասանելիությունը դառնում է կախվածություն։ Սա արժե կարգավորվող տվյալների և enterprise գործարքների համար․ դաշտերի մեծ մասի համար դիսկի մակարդակի գաղտնագրումը գումարած խիստ access control-ը բավարար է։

S3Ինչպե՞ս պահել պլանի սահմանափակումները ճշգրիտ զուգահեռության պայմաններում։

Տողերը հաշվելը և հետո insert անելը race է․ երկու հարցում էլ տեսնում են տասից ինը մասնաճյուղ, և երկուսն էլ ստեղծում են մեկական։ Սահմանափակումները կիրառիր այնտեղ, որտեղ գրառումները հաջորդականացված են։ Տարբերակներ․ usage counter տող յուրաքանչյուր tenant-ի և ռեսուրսի համար, որը թարմացվում է պայմանական հրամանով (մեծացնել միայն եթե սահմանից ցածր է) նույն տրանզակցիայում, ինչ insert-ը, կամ lock tenant-ի տողի վրա ստուգման և insert-ի ժամանակ։ Մեծ ծավալի չափվող սահմանափակումների համար, օրինակ՝ API կանչերի, ճշգրիտ կիրառումը թանկ է, ուստի օգտագործիր Redis counter՝ sliding window-ով և ընդունիր փոքր գերազանցում, հետո billing արա հեղինակավոր usage իրադարձություններից։ Յուրաքանչյուր սահմանափակման համար որոշիր՝ այն կոշտ է (պայմանագրային, կիրառիր խիստ), թե մեղմ (զգուշացրու, թույլ տուր կարճ գերազանցում), և tenant-ներին ցույց տուր նրանց ընթացիկ օգտագործումը, որպեսզի սահմանափակումները երբեք անակնկալ չլինեն։

Բառարան

Tenant
Հաճախորդ կազմակերպություն, որի օգտատերերն ու տվյալները մեկուսացված են մյուս բոլոր հաճախորդներից։
Row-level security
Տվյալների բազայի policy-ներ, որոնք զտում են, թե որ տողերը կարող է սեսիան կարդալ կամ գրել։
Noisy neighbour
Tenant, որի ծանր օգտագործումը վատացնում է արտադրողականությունը ընդհանուր ռեսուրսների վրա գտնվող մյուսների համար։
Tenant directory
Յուրաքանչյուր tenant-ի քարտեզագրում այն տվյալների բազային, տարածաշրջանին կամ cell-ին, որտեղ գտնվում են նրա տվյալները։
Cell
Ամբողջ stack-ի անկախ պատճեն, որը սպասարկում է tenant-ների ենթաբազմություն՝ սահմանափակելով blast radius-ը։
Entitlement
Կարողություն կամ սահմանափակում, որը տալիս է tenant-ի պլանը և ստուգում է հավելվածը։
Data residency
Պահանջ, որ տվյալները պահվեն և մշակվեն կոնկրետ երկրում կամ տարածաշրջանում։
Hybrid logical clock
Timestamp, որը համադրում է ֆիզիկական ժամանակը և counter-ը՝ սարքերի միջև իրադարձությունները դասավորելու համար։
CRDT
Տվյալների կառուցվածք, որի զուգահեռ խմբագրումները միշտ merge են լինում նույն արդյունքին՝ առանց համակարգման։

Պրոդուկտի գաղափար ունե՞ք։Եկեք ստեղծենք այն։

Պատմեք ձեր գաղափարի, պրոդուկտի կամ թիմի մասին։ Սովորաբար պատասխանում եմ մեկ աշխատանքային օրվա ընթացքում։