IlmHamroh
JavaScript Full-stack/31-qism. Prodakshn backend va AI integratsiya24/34-dars20 daqiqa
Mundarija (29)

Multi-tenancy: umumiy sxema va RLS, tashkilot-boshiga-sxema va alohida baza

Qisqacha: Multi-tenancy — bitta ilova ko'p tashkilotga xizmat qiladi va ularning ma'lumotlari hech qachon aralashmaydi. Uch asosiy model bor: umumiy jadvallar + tashkilot_id ustuni (Postgres'da RLS bilan), har tashkilotga alohida sxema va har tashkilotga alohida baza. Kanon o'lchovida RLS kontekstsiz so'rovga 0 qator berdi. WHERE unutilganda ham faqat o'z 1 000 qatorini ko'rsatdi. 200 ta sxemaga bitta ustun qo'shish esa umumiy jadvalga qaraganda ~15 baravar uzoq davom etdi.

Bu darsda

  • Uch multi-tenancy modelini solishtirasiz va loyihaga mosini tanlaysiz.
  • Umumiy sxemada RLS bilan "yopiq sukut"ni quriysiz va ulanish hovuzidagi kontekst sizishini ko'rasiz.
  • Tashkilot-boshiga-sxema modelining migratsiya narxini va search_path xavfini o'lchaysiz.
  • Tenantni subdomen, sarlavha va sessiyadan xavfsiz aniqlaysiz.
  • "Shovqinli qo'shni" (noisy neighbor) muammosini tashkilot bo'yicha chegara bilan hal qilasiz.

Oldin bilishingiz kerak: Row Level Security, RBAC va resurs egaligi, Prisma Client extensions, So'rov konteksti: nestjs-cls.

Oldingi darsda audit jurnalini qurdik: kim nima qilganini o'zgarmas tarzda yozdik. Bu dars yana tajriba darsi — loyiha o'zgarmaydi. Savol: vazifalar-api ni bitta foydalanuvchidan butun jamoaga — kompaniya yoki maktabga — qanday kengaytiramiz?

1. Nega bu kerak?

«Bahor» oshxonasi vazifalar ilovasini xodimlari uchun sotib olmoqchi: oshpazlar, ofitsiantlar, menejer — hammasi bitta umumiy ro'yxatda. Yonidagi «Lola» kafesi ham. Ikkala tashkilot bitta serverda, bitta ilovada ishlaydi. Lekin Lola xodimi Bahorning bitta vazifasini ham ko'rmasligi kerak.

Bunday ilova multi-tenant deyiladi. Tenant (ijarachi) — ilovadan alohida foydalanadigan mijoz-tashkilot: kompaniya, maktab, do'kon. Atama Prisma Client extensions darsida tilga olingan edi; endi uni to'liq ko'ramiz.

O'xshatish: ko'p qavatli uy. Bino, lift va quvurlar — umumiy. Lekin har oilaning o'z kvartirasi va kaliti bor. Qo'shni sizning eshigingizni ocholmaydi. Multi-tenancy — dasturdagi shunday uy: infratuzilma umumiy, ma'lumot alohida.

Xato narxi juda yuqori. Bir tashkilotning ma'lumoti boshqasiga ko'rinsa — bu ma'lumot oqishi: shartnoma buziladi, mijoz ketadi, qonun talablari buziladi. OWASP API Security Top 10 darsidagi BOLA — aynan shu xatoning bitta foydalanuvchi darajasidagi shakli.

2. Uch model

flowchart TD
  M{"Tenant ma'lumoti qayerda?"} --> A["Umumiy jadvallar + tashkilot_id"]
  M --> B["Har tashkilotga sxema"]
  M --> C["Har tashkilotga baza"]
  A --> A1["RLS yoki har so'rovda WHERE"]
  B --> B1["search_path bilan ajratish"]
  C --> C1["alohida ulanish satri"]

Diagrammani chapdan o'ngga qarab o'qing: izolyatsiya kuchayadi, xarajat ham oshadi.

Umumiy jadval Sxema Baza
Izolyatsiya mantiqiy (RLS) sxema chegarasi to'liq
Migratsiya bitta N marta N marta, N ulanish
Tashkilotlar soni minglab yuzlab o'nlab
Bitta tashkilotni tiklash qiyin o'rtacha oson

Oxirgi qator ko'pincha unutiladi. Bitta mijoz "kecha soat 3 dagi holatga qaytaring" desa, umumiy jadvalda faqat uning qatorlarini qaytarish — murakkab ish. Alohida bazada esa Backup va restore darsidagi oddiy tiklash yetadi.

3. Umumiy sxema va RLS

3.1 Jadval va siyosat

Kanon tajribasida 200 ta tashkilotga 200 000 ta vazifa yaratildi — har biriga 1 000 tadan. Bu bloklarni biz o'z PostgreSQL 17.6 serverimizda ham ishga tushirdik — natija kanon bilan bir xil:

sql
DO $$ BEGIN
  IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'tenant_ilova')
  THEN CREATE ROLE tenant_ilova NOLOGIN;
  END IF;
END $$;
CREATE TABLE vazifalar (
  id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  tashkilot_id int NOT NULL,
  matn text NOT NULL
);
CREATE INDEX ON vazifalar (tashkilot_id);
INSERT INTO vazifalar (tashkilot_id, matn)
SELECT 1 + g % 200, 'vazifa ' || g FROM generate_series(1, 200000) g;
ALTER TABLE vazifalar ENABLE ROW LEVEL SECURITY;
CREATE POLICY tashkilot ON vazifalar TO tenant_ilova
  USING (tashkilot_id =
    nullif(current_setting('ilova.tashkilot', true), '')::int);
GRANT SELECT, INSERT ON vazifalar TO tenant_ilova;
ANALYZE vazifalar;
text
DO
CREATE TABLE
CREATE INDEX
INSERT 0 200000
ALTER TABLE
CREATE POLICY
GRANT
ANALYZE

Row Level Security darsidagi naqsh, faqat kalit boshqa. Siyosat har qatorni tekshiradi: uning tashkilot_id si sessiyadagi ilova.tashkilot sozlamasiga tengmi. current_setting(..., true) — ikkinchi argument true: sozlama yo'q bo'lsa xato emas, NULL qaytaradi. nullif(..., '') bo'sh satrni ham NULL ga aylantiradi.

Rolni IF NOT EXISTS bilan yaratdik: rollar butun klasterga tegishli, bloklarni qayta ishga tushirsangiz xato bermaydi.

3.2 Yopiq sukut

Endi ilova roliga o'tamiz va kontekst bermasdan so'raymiz:

sql
SET ROLE tenant_ilova;
SELECT count(*) AS kontekstsiz FROM vazifalar;
text
SET
 kontekstsiz
-------------
           0
(1 row)

0 qator. Ilova set_config ni chaqirishni unutdi — natija xato emas, bo'sh. NULL bilan solishtirish hech qachon rost bo'lmaydi, shuning uchun siyosat hech qaysi qatorni o'tkazmaydi. Bu — yopiq sukut (closed by default): xato bo'lsa, ma'lumot ochilmaydi, yopiladi.

Endi tashkilot 7 ni beramiz va WHERE ni ataylab yozmaymiz:

sql
SET ROLE tenant_ilova;
SELECT set_config('ilova.tashkilot', '7', false);
SELECT count(*) AS faqat_oziniki FROM vazifalar;
SELECT count(*) AS begona FROM vazifalar WHERE tashkilot_id = 8;
text
SET
 set_config
------------
 7
(1 row)

 faqat_oziniki
---------------
          1000
(1 row)

 begona
--------
      0
(1 row)

WHERE yo'q — baribir faqat o'zining 1 000 ta qatori. Begona tashkilotni ataylab so'rasa ham — 0. Dasturchi filtrni unutsa ham, xakerlik qilsa ham baza qo'yib yubormaydi.

3.3 Narxi — so'rov rejasida

Har qatorga qo'shimcha shart — sekinlashtirmaydimi?

sql
SET ROLE tenant_ilova;
SELECT set_config('ilova.tashkilot', '7', false);
EXPLAIN (COSTS OFF) SELECT count(*) FROM vazifalar;
text
SET
 set_config
------------
 7
(1 row)

                                                       QUERY PLAN
------------------------------------------------------------------------------------------------------------------------
 Aggregate
   ->  Bitmap Heap Scan on vazifalar
         Recheck Cond: (tashkilot_id = (NULLIF(current_setting('ilova.tashkilot'::text, true), ''::text))::integer)
         ->  Bitmap Index Scan on vazifalar_tashkilot_id_idx
               Index Cond: (tashkilot_id = (NULLIF(current_setting('ilova.tashkilot'::text, true), ''::text))::integer)
(5 rows)

Siyosat sharti so'rovga qo'shildi va indeksga tushdi (Bitmap Index Scan on vazifalar_tashkilot_id_idx). Ya'ni RLS — faqat himoya emas, oddiy WHERE kabi ishlaydigan filtr. Shart: tashkilot_id bo'yicha indeks bo'lsin. Ko'p ustunli indekslarda ham tashkilot_id birinchi tursin: (tashkilot_id, yaratilgan_vaqt).

3.4 Tuzoq: hovuzdagi ulanish kontekstni eslab qoladi

Yuqoridagi bloklarda set_config(..., false) ishlatdik — uchinchi argument false degani "butun sessiya uchun". Ilovada bu xavfli. Ulanishlar hovuzda qayta ishlatiladi: bitta ulanish ketma-ket turli foydalanuvchilarning so'rovlarini bajaradi.

Ikki so'rovni bitta ulanishda modellaymiz. Birinchisi — Bahor xodimi (tashkilot 7). Ikkinchisi — boshqa xodim, uning kodi set_config ni chaqirishni unutgan:

sql
SET ROLE tenant_ilova;
-- 1-so'rov: tashkilot 7, sessiya darajasida
SELECT set_config('ilova.tashkilot', '7', false);
-- 2-so'rov: o'sha ulanish, kontekst berilmadi
SELECT count(*) AS ikkinchi_sorov FROM vazifalar;
text
SET
 set_config
------------
 7
(1 row)

 ikkinchi_sorov
----------------
           1000
(1 row)

Ikkinchi so'rov 1 000 qator ko'rdi — tashkilot 7 ning qatorlarini. Yopiq sukut ishlamadi: kontekst oldingi so'rovdan qolgan. To'g'ri yo'l — sozlamani faqat tranzaksiya ichida berish (true = SET LOCAL):

sql
SET ROLE tenant_ilova;
BEGIN;
SELECT set_config('ilova.tashkilot', '9', true);
SELECT count(*) AS ichida FROM vazifalar;
COMMIT;
SELECT count(*) AS commitdan_keyin FROM vazifalar;
text
SET
BEGIN
 set_config
------------
 9
(1 row)

 ichida
--------
   1000
(1 row)

COMMIT
 commitdan_keyin
-----------------
               0
(1 row)

Tranzaksiya tugashi bilan kontekst o'chdi — keyingi so'rov yana yopiq sukutga tushdi. vazifalar-api 28-qismdan beri aynan shunday ishlaydi: har so'rov tranzaksiya ichida SET LOCAL bilan egasini beradi.

Tekshirib ko'ring: Ilova postgres (superuser) roli bilan ulansa, RLS ishlaydimi?

Javob

Yo'q. PostgreSQL hujjatiga ko'ra superuser va BYPASSRLS atributli rollar RLS'ni har doim chetlab o'tadi. Jadval egasi ham odatda chetlab o'tadi — faqat ALTER TABLE … FORCE ROW LEVEL SECURITY qilinsa, egaga ham siyosat qo'llanadi (Row Level Security darsida ko'rgansiz). Bizning bloklarda jadvalni postgres yaratgan — u ham superuser, ham ega. Shuning uchun avval SET ROLE tenant_ilova qildik. Prodakshnda ilova alohida, huquqi cheklangan rol bilan ulanadi — Ma'lumotlar bazasi xavfsizligi darsidagi "eng kam huquq" qoidasi. Kanon vazifalar_api roli bypassrls = false ekani tekshirilgan.

3.5 Ilova tomoni

Postgres'da RLS bor ekan, ilova uchta ishni qiladi:

  1. Tenantni aniqlaydi — sessiya yoki tokendan («Tenantni aniqlash» bo'limi).
  2. Kontekstga qo'yadi — Nest'da nestjs-cls bilan: so'rov boshida cls.set("tashkilot", id), chuqur qatlamlar uni argumentsiz oladi.
  3. Har tranzaksiyada SET LOCAL — ombor tranzaksiya boshida set_config('ilova.tashkilot', $1, true) ni chaqiradi.

ORM'da qo'shimcha qatlam — Prisma Client extensions darsidagi tenant filtri. Lekin Soft delete ORM'da darsida o'lchaganimizdek, bunday extension'da teshik qoladi. findUnique ni tuzatsa bo'ladi, include ning "bitta" tomoni va xom SQL esa filtrsiz qoladi. Tenant filtri uchun bu teshik soft delete'dagidan ham xavfli: u yerda Savatdagi yozuv chiqardi, bu yerda — begona tashkilotning ma'lumoti. Shuning uchun asosiy himoya — bazadagi RLS, extension — faqat qulaylik.

4. Tashkilot-boshiga-sxema

4.1 200 ta sxema

Ikkinchi model: har tashkilotga Postgres sxemasi (t1, t2, …), ichida bir xil jadvallar. Kanon 200 ta sxemada 1 000 tadan vazifa yaratdi:

sql
DO $$ BEGIN FOR i IN 1..200 LOOP
  EXECUTE format('CREATE SCHEMA t%s', i);
  EXECUTE format('CREATE TABLE t%s.vazifalar (id bigint GENERATED '
    || 'ALWAYS AS IDENTITY PRIMARY KEY, matn text NOT NULL)', i);
  EXECUTE format('INSERT INTO t%s.vazifalar (matn) SELECT '
    || '''vazifa '' || g FROM generate_series(1, 1000) g', i);
END LOOP; END $$;
SELECT count(*) AS jadvallar
FROM pg_class
WHERE relkind = 'r'
  AND relnamespace::regnamespace::text ~ '^t[0-9]+$';
text
DO
 jadvallar
-----------
       200
(1 row)

Katalogda endi 200 ta vazifalar jadvali bor. Ilova qaysi tashkilot bilan ishlashini search_path bilan tanlaydi:

sql
SET search_path = t7;
SELECT count(*) AS t7_qatorlar FROM vazifalar;
text
SET
 t7_qatorlar
-------------
        1000
(1 row)

Sxemasiz vazifalar nomi t7.vazifalar ga aylandi. RLS kerak emas — boshqa tashkilotning jadvali umuman boshqa joyda.

4.2 Xavf: search_path — yagona qulf

Bu modelning zaif joyi o'sha qulayligida. Izolyatsiyani bitta sozlama ushlab turadi. Ilova xato search_path qo'ysa (keshdan eski qiymat, hovuzdan qolgan sessiya sozlamasi, "7" o'rniga "17") — u boshqa tashkilotning to'liq ma'lumotini ko'radi. Baza buni sezmaydi: RLS yo'q, har sxemadagi jadval ilova roli uchun ochiq.

Umumiy sxemada xato kontekst odatda 0 qator beradi (yopiq sukut). Sxema modelida xato kontekst begona qatorlar beradi. «Hovuzdagi ulanish kontekstni eslab qoladi» tuzog'i bu yerda ham bor: SET search_path ham sessiya darajasida qoladi, SET LOCAL search_path esa tranzaksiya oxirida o'chadi.

4.3 Migratsiya narxi

Endi ikkala modelga bir xil migratsiya: bajarildi ustunini qo'shish. Kanon chiqishi (\timing bilan, vaqtlar har yugurishda farq qiladi):

sql
DO $$ BEGIN FOR i IN 1..200 LOOP
  EXECUTE format('ALTER TABLE t%s.vazifalar ADD COLUMN bajarildi '
    || 'boolean NOT NULL DEFAULT false', i);
END LOOP; END $$;
ALTER TABLE vazifalar
  ADD COLUMN bajarildi boolean NOT NULL DEFAULT false;
text
DO
Time: 79.678 ms
ALTER TABLE
Time: 5.171 ms
Bitta ustun qo'shish migratsiyasi: nisbiy vaqt (umumiy jadval = 100 %)
  • Umumiy jadval (200 000 qator)100 %
  • 200 ta sxema (200 ALTER)1 541 %

Manba: IlmHamroh kanon o'lchovi, 2026-11-03: PostgreSQL 18.6 (Docker), bitta mashina, bitta yugurish — 5.171 ms va 79.678 ms

Grafikni shunday o'qing: bir xil o'zgarish 200 ta sxemada 15 baravar ko'p vaqt oldi. Mutlaq raqamlar kichik — chunki bu ustun qo'shish juda arzon amal. Haqiqiy loyihada 50 ta jadval, o'nlab migratsiya va minglab tashkilot bo'ladi. Bitta migratsiya yuzlab sxemada yarim yo'lda yiqilsa, sxemalarning bir qismi yangi, bir qismi eski holatda qoladi.

4.4 Nega umumiy jadval 5 ms da tugadi

200 000 qatorli jadvalga yangi ustun 5 ms da qo'shildi. Postgres qatorlarni qayta yozmadimi? Tekshiramiz: jadvalning disk fayli raqami (pg_relation_filenode) o'zgarsa — jadval qayta yozilgan.

sql
CREATE TEMP TABLE f AS SELECT pg_relation_filenode('vazifalar') AS n;
ALTER TABLE vazifalar ADD COLUMN muhim boolean NOT NULL DEFAULT false;
SELECT pg_relation_filenode('vazifalar') = n AS fayl_osha FROM f;
ALTER TABLE vazifalar
  ADD COLUMN qoshilgan timestamptz NOT NULL DEFAULT clock_timestamp();
SELECT pg_relation_filenode('vazifalar') = n AS fayl_osha FROM f;
text
SELECT 1
ALTER TABLE
 fayl_osha
-----------
 t
(1 row)

ALTER TABLE
 fayl_osha
-----------
 f
(1 row)

Birinchi ustun — o'zgarmas sukut qiymat (false): Postgres uni faqat katalogga yozdi, fayl o'sha. Ikkinchisi — clock_timestamp(), har qatorga boshqa qiymat: jadval butunlay qayta yozildi, fayl yangi. Demak migratsiya narxi uning turiga bog'liq — sxemalar soniga ko'paytirilgan qayta yozish esa soatlarga cho'zilishi mumkin.

4.5 Tuzoq: RLS siyosati ustun turini "qulflaydi"

Umumiy modelning ham o'z migratsiya tuzog'i bor. Biz tashkilot_id ni bigint ga o'zgartirmoqchi bo'ldik:

sql
ALTER TABLE vazifalar ALTER COLUMN tashkilot_id TYPE bigint;
text
ERROR:  cannot alter type of a column used in a policy definition
DETAIL:  policy tashkilot on table vazifalar depends on column "tashkilot_id"

Tarjimasi: "siyosat ta'rifida ishlatilgan ustunning turini o'zgartirib bo'lmaydi; tashkilot siyosati tashkilot_id ustuniga bog'liq". Yechim — bitta tranzaksiyada siyosatni o'chirish, turni o'zgartirish va siyosatni qayta yaratish. Siyosatsiz oraliq tashqariga ko'rinmasligi uchun aynan bitta tranzaksiyada. Tenant ustunining turini boshidan to'g'ri tanlang (bigint).

5. Har tashkilotga alohida baza

Uchinchi model — eng kuchli izolyatsiya: har tashkilot o'z bazasida, hatto o'z serverida. Kanon bu modelni sinamadi. Afzallik va narxlari hujjat va tajribadan:

  • Afzallik: bitta tashkilotni alohida zaxiralash va tiklash; katta mijozga alohida, kuchliroq server; ma'lumotni ma'lum mamlakatda saqlash talabi bo'lsa — bazani o'sha yerda ochish (Shaxsiy ma'lumotlar qonunchiligi).
  • Narx: har baza — alohida ulanish hovuzi. 100 tashkilot × 10 ulanish = 1 000 ulanish, Postgres esa har ulanishga alohida jarayon ochadi. Migratsiyalar N marta, monitoring N marta.
  • Ilova tomoni: so'rov kelganda tenant bo'yicha ulanish satrini tanlash (Map<tashkilot, Pool>), kam ishlatiladigan hovuzlarni yopish.

Amalda ko'p kompaniyalar aralash yo'lni tanlaydi: kichik mijozlar umumiy jadvalda, bir nechta katta mijoz — alohida bazada.

6. Tenantni aniqlash

6.1 Uch manba

So'rov qaysi tashkilotga tegishli? Odatda uch joydan biri:

Manba Misol Izoh
Subdomen bahor.vazifalar.example brauzer uchun qulay; cookie domenini o'ylang
Sarlavha X-Tashkilot: bahor API mijozlari uchun
Token / sessiya sessiyadagi tanlangan tashkilot eng ishonchli

Muhim qoida: subdomen va sarlavha — foydalanuvchi bergan ma'lumot. Ular tenantni faqat taklif qiladi. Yakuniy qaror — sessiyadagi a'zolik: bu foydalanuvchi shu tashkilot a'zosimi?

6.2 Hujumchi nigohi

Hujumchi Bahor xodimi sifatida tizimga kirgan. U so'rovga X-Tashkilot: lola sarlavhasini qo'shadi. Agar backend tenantni sarlavhadan "ishonib" olsa — Lola'ning ma'lumoti ochiladi. Bu — BOLA'ning tashkilot darajasidagi shakli. Himoya — a'zolikni tekshirish va ziddiyatni rad etish. Brauzerda modellaymiz:

js
const ASOSIY = "vazifalar.example";

function tenantAniqla(sorov) {
  const host = sorov.host.toLowerCase();
  const sub = host.endsWith(`.${ASOSIY}`)
    ? host.slice(0, -(ASOSIY.length + 1))
    : null;
  const sarlavha = sorov.sarlavhalar["x-tashkilot"] ?? null;
  if (sub !== null && sarlavha !== null && sub !== sarlavha) {
    return "400: subdomen va sarlavha zid";
  }
  const kerak = sub ?? sarlavha;
  if (kerak === null) return "400: tashkilot ko'rsatilmagan";
  const azo = sorov.sessiya?.azoliklar ?? [];
  if (!azo.includes(kerak)) return `403: ${kerak} a'zosi emas`;
  return `ok: ${kerak}`;
}

const aziz = { azoliklar: ["bahor"] };
const sorovlar = [
  { host: "bahor.vazifalar.example", sarlavhalar: {}, sessiya: aziz },
  { host: "api.vazifalar.example", sarlavhalar: {}, sessiya: aziz },
  { host: "vazifalar.example",
    sarlavhalar: { "x-tashkilot": "lola" }, sessiya: aziz },
  { host: "bahor.vazifalar.example",
    sarlavhalar: { "x-tashkilot": "lola" }, sessiya: aziz },
  { host: "vazifalar.example", sarlavhalar: {}, sessiya: aziz },
];
for (const s of sorovlar) console.log(tenantAniqla(s));

Konsolda:

text
ok: bahor
403: api a'zosi emas
403: lola a'zosi emas
400: subdomen va sarlavha zid
400: tashkilot ko'rsatilmagan

Qatorma-qator:

  1. Aziz o'z tashkilotining subdomenidan kirdi — ruxsat.
  2. api — xizmat subdomeni, lekin model uni tashkilot deb oldi va rad etdi. Haqiqiy ilovada band nomlar ro'yxati kerak (api, www, admin) — ular tashkilot nomi bo'la olmaydi.
  3. Sarlavha orqali Lola'ga urinish — a'zolik yo'q, 403.
  4. Subdomen bitta, sarlavha boshqa — ziddiyat, 400. Qaysi biriga ishonishni taxmin qilmaymiz.
  5. Hech narsa ko'rsatilmagan — 400. Tenantsiz so'rov "hamma tashkilot"ga aylanmasligi kerak.

Model soddalashtirilgan: haqiqiy ilovada a'zolik bazadan olinadi va keshlanadi, rol ham tashkilot bo'yicha bo'ladi (Bahorda admin, Lola'da oddiy a'zo). Kesh kaliti ham tenantni o'z ichiga olishi shart — aks holda kesh zaharlash bo'ladi.

7. Shovqinli qo'shni

Noisy neighbor (shovqinli qo'shni) — bitta tashkilotning og'ir yuklamasi boshqalarni sekinlashtirishi. Lola kafesi import skripti bilan soniyasiga minglab so'rov yuborsa, Bahor xodimlari ilovani ochib bo'lmaydigan holga tushadi.

Faqat umumiy tezlik chegarasi bu yerda yordam bermaydi — uni bitta tenant butunlay yeb qo'yadi. Ikki chegarani solishtiramiz:

js
// har soniyada umumiy chegara 8, tashkilotga 4 (oyna — 1 soniya)
function cheklovchi(umumiy, tashkilotga) {
  let jami = 0;
  const sanoq = new Map();
  return (tashkilot) => {
    const n = sanoq.get(tashkilot) ?? 0;
    if (jami >= umumiy) return "429 (umumiy)";
    if (n >= tashkilotga) return "429 (tashkilot)";
    jami++;
    sanoq.set(tashkilot, n + 1);
    return "200";
  };
}

// "lola" bir soniyada 10 ta so'rov yubordi, keyin "bahor" 2 ta
const navbat = [...Array(10).fill("lola"), "bahor", "bahor"];
for (const [nom, cheklov] of [
  ["faqat umumiy", cheklovchi(8, Infinity)],
  ["umumiy + tashkilot", cheklovchi(8, 4)],
]) {
  const natija = navbat.map((t) => `${t}:${cheklov(t).slice(0, 3)}`);
  console.log(nom);
  console.log("  " + natija.slice(-4).join(" "));
}

Konsolda:

text
faqat umumiy
  lola:429 lola:429 bahor:429 bahor:429
umumiy + tashkilot
  lola:429 lola:429 bahor:200 bahor:200

Oxirgi to'rtta so'rovga qarang. Faqat umumiy chegarada Lola 8 ta joyni egalladi — Bahor sababsiz 429 oldi. Tashkilot chegarasi qo'shilganda Lola 4 tadan oshmadi va Bahor uchun joy qoldi.

Tezlik chegarasidan tashqari, boshqa resurslar ham tashkilot bo'yicha bo'linadi:

  • AI kvotasi — AI xususiyatlarini prodakshnda ishlatish darsidagi kunlik kvota foydalanuvchiga emas, tashkilotga ham.
  • Uzoq so'rovlar — statement_timeout: bitta tashkilotning og'ir hisoboti bazani band qilmasin.
  • Fon vazifalari — navbatda adolatli taqsimot: bitta tashkilotning 10 000 ta ishi boshqalarning ishini to'sib qo'ymasin.

8. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
Bir tashkilot boshqasining ma'lumotini ko'rdi sessiya darajasidagi kontekst hovuzda qoldi faqat SET LOCAL / set_config(..., true)
RLS ishlamayapti ilova superuser yoki jadval egasi bilan ulangan cheklangan rol, bypassrls = false; egaga ham kerak bo'lsa FORCE ROW LEVEL SECURITY
Tashkilot sarlavhasi bilan begona ma'lumot ochildi sarlavhaga ishonilgan a'zolikni tekshirish
Migratsiya yarim sxemalarda qoldi N ta sxema ketma-ket, birida xato har sxema uchun versiya jadvali, qayta urinish
cannot alter type of a column used in a policy definition RLS siyosati ustunga bog'liq bitta tranzaksiyada siyosatni qayta yaratish
Bitta mijoz hammani sekinlashtirdi faqat umumiy chegara tashkilot bo'yicha chegara va kvota

9. Mashqlar

Mashqlar kurs/mashqlar/31/24-multi-tenancy/ da. 2- va 3-mashq yechimlari Node 24.21.0 da tekshirilgan (biz ishga tushirdik).

1-mashq (oson): bo'shliqlarni to'ldiring

  • Ilovadan alohida foydalanadigan mijoz-tashkilotning inglizcha nomi —
  • Kontekstsiz so'rovga RLS nechta qator qaytardi? —
  • Faqat tranzaksiya ichida amal qiladigan set_config uchinchi argumenti —
  • Kanonda 200 ta sxemaga ustun qo'shish taxminan necha baravar uzoq bo'ldi? —
  • Bitta tenant boshqalarni sekinlashtirishi — "shovqinli "
Yechim
  • tenant — ijarachi; tashkilot, kompaniya, maktab.
  • 0 — NULL bilan solishtirish hech qachon rost emas: yopiq sukut.
  • true — SET LOCAL ga teng; false — butun sessiya, hovuzda xavfli.
  • 15 — 79.7 ms ga 5.2 ms (bitta yugurish, nisbat).
  • qo'shni — noisy neighbor; tashkilot bo'yicha chegara bilan hal qilinadi.

2-mashq (o'rta): tenantli kesh kaliti

Kesh kalitida tashkilot bo'lmasa, Bahorniki Lola'ga berilib yuborishi mumkin. keshKaliti(tashkilot, yol, parametrlar) funksiyasini yozing: natija t:<tashkilot>:<yol>?<parametrlar> ko'rinishida, parametrlar kalit bo'yicha alfavit tartibida (a=1&b=2 va b=2&a=1 — bir xil kalit). tashkilot bo'sh yoki undefined bo'lsa — xato tashlansin ("tashkilotsiz kesh kaliti"). Ishora: URLSearchParams va sort() (URL va URLSearchParams).

Yechim
js
function keshKaliti(tashkilot, yol, parametrlar) {
  if (!tashkilot) throw new Error("tashkilotsiz kesh kaliti");
  const p = new URLSearchParams(parametrlar);
  p.sort();
  return `t:${tashkilot}:${yol}?${p}`;
}

const YOL = "/v1/vazifalar";
console.log(keshKaliti("bahor", YOL, "limit=20&bajarildi=0"));
console.log(keshKaliti("bahor", YOL, "bajarildi=0&limit=20"));
console.log(keshKaliti("lola", YOL, "limit=20&bajarildi=0"));
try {
  keshKaliti(undefined, "/v1/vazifalar", "");
} catch (e) {
  console.log(e.message);
}

Konsolda:

text
t:bahor:/v1/vazifalar?bajarildi=0&limit=20
t:bahor:/v1/vazifalar?bajarildi=0&limit=20
t:lola:/v1/vazifalar?bajarildi=0&limit=20
tashkilotsiz kesh kaliti

Birinchi ikki qator bir xil — parametrlar tartibi kalitga ta'sir qilmaydi. Uchinchisi — boshqa tashkilot, boshqa kalit. Oxirgisi — tashkilotsiz kalit umuman yasalmaydi: "unutib qo'yish" xatosi jim o'tmaydi, darhol ko'rinadi.

3-mashq (qiyin): sxemalarga migratsiya

Tashkilot-boshiga-sxema modelida migratsiyani modellang. migratsiyaQil(sxemalar, migratsiya): sxemalar — { nom, versiya } massivi, migratsiya — { versiya, qol(nom) } (qo'llash funksiyasi, xato tashlashi mumkin). Faqat versiya dan past sxemalarga qo'llang, muvaffaqiyatli bo'lsa sxema versiyasini yangilang. Bitta sxemada xato bo'lsa — to'xtamang, keyingilarini davom ettiring va oxirida { yangilandi, otkazildi, xato } hisobotini qaytaring (xato — sxema nomlari). Ishora: try/catch (xatolarni ushlash).

Yechim
js
function migratsiyaQil(sxemalar, migratsiya) {
  const hisobot = { yangilandi: [], otkazildi: [], xato: [] };
  for (const s of sxemalar) {
    if (s.versiya >= migratsiya.versiya) {
      hisobot.otkazildi.push(s.nom);
      continue;
    }
    try {
      migratsiya.qol(s.nom);
      s.versiya = migratsiya.versiya;
      hisobot.yangilandi.push(s.nom);
    } catch {
      hisobot.xato.push(s.nom);
    }
  }
  return hisobot;
}

const sxemalar = [
  { nom: "t1", versiya: 4 }, { nom: "t2", versiya: 5 },
  { nom: "t3", versiya: 4 }, { nom: "t4", versiya: 4 },
];
const m5 = {
  versiya: 5,
  qol(nom) {
    if (nom === "t3") throw new Error("lock timeout");
  },
};
console.log(migratsiyaQil(sxemalar, m5));
console.log(sxemalar.map((s) => `${s.nom}=${s.versiya}`).join(" "));

Konsolda:

text
{ yangilandi: [ 't1', 't4' ], otkazildi: [ 't2' ], xato: [ 't3' ] }
t1=5 t2=5 t3=4 t4=5

Ikkinchi qator — tizim holati: t3 hali eski versiyada. Ilova kodi bir vaqtda ikkala versiya bilan ishlay olishi kerak (Zero-downtime: expand-contract g'oyasi), aks holda t3 tashkiloti xato oladi. Migratsiyani qayta ishga tushirsangiz, faqat t3 ga qo'llanadi — har sxemada versiya saqlangani shuning uchun.

10. Loyiha qadami: kanon tajribasi

Bu darsda vazifalar-api o'zgarmaydi va commit yo'q.

Kanon tajribasi alohida bazada ikki modelni qurdi. (A) — umumiy jadval, tashkilot_id va RLS: 200 tashkilot × 1 000 vazifa. (B) — 200 ta sxema, har birida 1 000 vazifa. Keyin ikkalasiga bir xil migratsiya qo'llandi. Tajriba oxirida baza va rol o'chirildi. Darsdagi SQL bloklarini biz PostgreSQL 17.6 da qayta yurgizdik: sonlar va so'rov rejasi kanon bilan bir xil. Hovuzdagi kontekst sizishi, pg_relation_filenode va siyosat tuzog'i — bizning qo'shimcha sinovlarimiz. Kanon skriptidagi bitta kichik nuqson: (B) qismi sarlavhasini chiqaradigan \echo qatorida apostrof bor edi, psql uni "tugallanmagan qo'shtirnoq" deb kesdi. Natijalarga ta'sir qilmagan.

Nega loyihaga kirmadi: vazifalar — shaxsiy ilova. Undagi tenant — foydalanuvchining o'zi, va bu himoya 28-qismdan beri bor: egasi_id bo'yicha RLS, vazifalar_api roli, har so'rovda SET LOCAL. Bu darsdagi (A) modelining aynan o'zi, faqat kalit egasi_id. Tashkilotlar, a'zolik va tashkilot bo'yicha rollar — 38-qismdagi jamoa yakuniy loyihasi uchun.

Docker'siz takrorlaydigan qism: darsdagi SQL bloklarini o'z Postgres'ingizda ishga tushiring (alohida bazada). Keyin siyosatni TO tenant_ilova siz yarating va postgres roli bilan so'rang — nechta qator ko'rasiz va nega? Javobni «Yopiq sukut» bo'limidagi savoldan topasiz: siyosat endi hamma rolga tegishli, lekin superuser uni baribir chetlab o'tadi.

11. Real ishda

  • Qayerda uchraydi: har B2B SaaS — CRM, buxgalteriya, LMS (maktablar), loyiha boshqaruvi. Slack'dagi "workspace", GitHub'dagi "organization" — tenant.
  • Intervyu savollari: "Multi-tenancy'ning uch modelini solishtiring", "Tenant ma'lumoti aralashib ketmasligini qanday kafolatlaysiz?", "Noisy neighbor muammosi nima?"
  • Arxitektura qarori: ko'pchilik umumiy jadval + RLS bilan boshlaydi, katta mijozlar uchun keyinroq alohida baza qo'shadi.
  • Kod review'da: "bu so'rovda tenant qayerdan keldi — sarlavhadanmi?", "set_config uchinchi argumenti true mi?", "kesh kalitida tashkilot bormi?"

Xulosa

  • Multi-tenancy — bitta ilova, ko'p tashkilot, aralashmaydigan ma'lumot; uch model: umumiy jadval, sxema, baza.
  • Umumiy jadval + RLS: kontekstsiz — 0 qator, WHERE unutilsa ham faqat o'ziniki; shart indeksga tushadi.
  • Kontekst faqat tranzaksiya ichida (set_config(..., true)): sessiya darajasidagisi hovuzda keyingi so'rovga sizadi.
  • Sxema modelida izolyatsiyani search_path ushlaydi; migratsiya har sxemada — kanonda 15 baravar uzoq.
  • Tenantni sarlavhadan emas, a'zolikdan aniqlang; chegara, kvota va kesh kaliti — tashkilot bo'yicha.

Keyingi dars: Feature flag'lar backend'da: foizli rollout va segmentlar — kodni chiqarish va xususiyatni yoqishni ajratamiz; audit jurnali bilan birga commit qilamiz.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Multi-tenancy: umumiy sxema va RLS, tashkilot-boshiga-sxema va alohida baza — IlmHamroh