Mundarija (29)
- Bu darsda
- 1. Nega bu kerak?
- 2. Uch model
- 3. Umumiy sxema va RLS
- 3.1 Jadval va siyosat
- 3.2 Yopiq sukut
- 3.3 Narxi — so'rov rejasida
- 3.4 Tuzoq: hovuzdagi ulanish kontekstni eslab qoladi
- 3.5 Ilova tomoni
- 4. Tashkilot-boshiga-sxema
- 4.1 200 ta sxema
- 4.2 Xavf: search_path — yagona qulf
- 4.3 Migratsiya narxi
- 4.4 Nega umumiy jadval 5 ms da tugadi
- 4.5 Tuzoq: RLS siyosati ustun turini "qulflaydi"
- 5. Har tashkilotga alohida baza
- 6. Tenantni aniqlash
- 6.1 Uch manba
- 6.2 Hujumchi nigohi
- 7. Shovqinli qo'shni
- 8. Ko'p uchraydigan xatolar
- 9. Mashqlar
- 1-mashq (oson): bo'shliqlarni to'ldiring
- 2-mashq (o'rta): tenantli kesh kaliti
- 3-mashq (qiyin): sxemalarga migratsiya
- 10. Loyiha qadami: kanon tajribasi
- 11. Real ishda
- Xulosa
- Manbalar
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_idustuni (Postgres'da RLS bilan), har tashkilotga alohida sxema va har tashkilotga alohida baza. Kanon o'lchovida RLS kontekstsiz so'rovga 0 qator berdi.WHEREunutilganda 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_pathxavfini 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:
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;DO
CREATE TABLE
CREATE INDEX
INSERT 0 200000
ALTER TABLE
CREATE POLICY
GRANT
ANALYZERow 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:
SET ROLE tenant_ilova;
SELECT count(*) AS kontekstsiz FROM vazifalar;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:
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;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?
SET ROLE tenant_ilova;
SELECT set_config('ilova.tashkilot', '7', false);
EXPLAIN (COSTS OFF) SELECT count(*) FROM vazifalar;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:
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;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):
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;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:
- Tenantni aniqlaydi — sessiya yoki tokendan («Tenantni aniqlash» bo'limi).
- Kontekstga qo'yadi — Nest'da nestjs-cls bilan: so'rov boshida
cls.set("tashkilot", id), chuqur qatlamlar uni argumentsiz oladi. - Har tranzaksiyada
SET LOCAL— ombor tranzaksiya boshidaset_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:
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]+$';DO
jadvallar
-----------
200
(1 row)Katalogda endi 200 ta vazifalar jadvali bor. Ilova qaysi tashkilot bilan ishlashini search_path bilan tanlaydi:
SET search_path = t7;
SELECT count(*) AS t7_qatorlar FROM vazifalar;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):
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;DO
Time: 79.678 ms
ALTER TABLE
Time: 5.171 ms- 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.
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;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:
ALTER TABLE vazifalar ALTER COLUMN tashkilot_id TYPE bigint;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:
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:
ok: bahor
403: api a'zosi emas
403: lola a'zosi emas
400: subdomen va sarlavha zid
400: tashkilot ko'rsatilmaganQatorma-qator:
- Aziz o'z tashkilotining subdomenidan kirdi — ruxsat.
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.- Sarlavha orqali Lola'ga urinish — a'zolik yo'q, 403.
- Subdomen bitta, sarlavha boshqa — ziddiyat, 400. Qaysi biriga ishonishni taxmin qilmaymiz.
- 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:
// 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:
faqat umumiy
lola:429 lola:429 bahor:429 bahor:429
umumiy + tashkilot
lola:429 lola:429 bahor:200 bahor:200Oxirgi 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_configuchinchi 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 —
NULLbilan solishtirish hech qachon rost emas: yopiq sukut. - true —
SET LOCALga 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
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:
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 kalitiBirinchi 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
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:
{ yangilandi: [ 't1', 't4' ], otkazildi: [ 't2' ], xato: [ 't3' ] }
t1=5 t2=5 t3=4 t4=5Ikkinchi 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_configuchinchi argumentitruemi?", "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,
WHEREunutilsa 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_pathushlaydi; 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
- PostgreSQL 18 hujjati — Row Security Policies: https://www.postgresql.org/docs/18/ddl-rowsecurity.html
- PostgreSQL 18 hujjati —
set_config,current_setting: https://www.postgresql.org/docs/18/functions-admin.html#FUNCTIONS-ADMIN-SET - PostgreSQL 18 hujjati — Schemas va
search_path: https://www.postgresql.org/docs/18/ddl-schemas.html - PostgreSQL 18 hujjati — ALTER TABLE (ustun qo'shish va qayta yozish): https://www.postgresql.org/docs/18/sql-altertable.html
- AWS — SaaS Tenant Isolation Strategies (whitepaper): https://docs.aws.amazon.com/whitepapers/latest/saas-tenant-isolation-strategies/saas-tenant-isolation-strategies.html
- (Havolalar 2026-11-03 da ochildi; superuser,
BYPASSRLSvaFORCEqoidasi — Row Security Policies sahifasidan.) - Kanon o'lchovi (IlmHamroh, 2026-11-03): PostgreSQL 18.6 (
pgvector/pgvector:pg18, Docker), bitta mashina. SQL bloklari, JS modellari va mashq yechimlari — biz ishga tushirdik (2026-11-03, PostgreSQL 17.6, Node 24.21.0).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!