6.7 KiB
6.7 KiB
id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
| id | title | category | status | source_trust_level | verification_status | created_at | updated_at | tags | tech_stack | applied_in | aliases | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| db-multi-tenant-deep | Multi-Tenant DB — pool / schema / DB / row | Coding | draft | B | conceptual | 2026-05-09 | 2026-05-09 |
|
|
|
Multi-Tenant DB
1 app + N customer (tenant). Pool / schema / DB / row 4 가지 isolation level. Row-level security (RLS) 가 modern Postgres.
📖 핵심 개념
- Pool: 1 schema, 1 DB. Tenant column.
- Schema: 매 tenant 의 schema.
- DB: 매 tenant 의 DB.
- Hybrid: 큰 tenant 가 own DB.
💻 코드 패턴
Pool model (가장 common)
CREATE TABLE users (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
email TEXT
);
CREATE INDEX ON users (tenant_id);
-- 매 query 가 tenant_id filter.
SELECT * FROM users WHERE tenant_id = $1;
→ Cheap. 매 query 가 tenant filter 잊으면 leak.
Row-Level Security (Postgres)
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON users
USING (tenant_id = current_setting('app.tenant_id')::uuid);
-- App 가 connection 별 set.
SET app.tenant_id = '...';
-- 자동 filter.
SELECT * FROM users; -- 만 tenant 의 row.
→ DB-level enforce. App bug 가 leak X.
RLS 의 setup (TS)
async function withTenant<T>(tenantId: string, fn: () => Promise<T>): Promise<T> {
const client = await pool.connect();
try {
await client.query(`SET app.tenant_id = $1`, [tenantId]);
return await fn();
} finally {
await client.query(`RESET app.tenant_id`);
client.release();
}
}
Schema per tenant
CREATE SCHEMA tenant_alice;
CREATE TABLE tenant_alice.users (...);
CREATE SCHEMA tenant_bob;
CREATE TABLE tenant_bob.users (...);
-- App 가 schema 선택
SET search_path = tenant_alice;
SELECT * FROM users;
→ Strong isolation. 1000+ tenant 가 schema 폭발.
DB per tenant
const tenantDBs = new Map<string, Pool>();
function getDB(tenantId: string) {
if (!tenantDBs.has(tenantId)) {
tenantDBs.set(tenantId, new Pool({ database: `tenant_${tenantId}` }));
}
return tenantDBs.get(tenantId)!;
}
→ Strongest. 매우 expensive (1000 DB = 1000 connection pool).
Hybrid (cell-based)
Small tenant: pool.
Big tenant (>10% load): own schema.
Enterprise: own DB.
→ Cost vs isolation balance.
Migration 의 complexity
Pool: 1 migration = 모두.
Schema: 매 schema 가 migrate.
DB: 매 DB 가 migrate (slow).
→ 큰 schema/DB 가 N+1 migration cost.
Connection pool
Pool model: 1 pool, 1000 tenant 공유.
Schema: 1 pool 가 SET search_path.
DB: 1000 pool (PgBouncer 가 도움).
→ DB per tenant 가 connection 폭발.
Cost
Pool: 1 DB instance ($).
Schema: 1 DB ($) — 큰 tenant 만 영향.
DB: N DB ($$$).
→ Pool 가 가장 cheap.
DB 가 가장 expensive.
Backup / restore
Pool: 모두 또는 일부 (matrix backup).
Schema: 매 schema 의 dump.
DB: 매 DB 의 dump.
→ Tenant 별 restore = schema/DB 가 simple.
Pool 가 어려움 (row 별 select + delete).
Query performance
Pool:
- 매 query 가 (tenant_id, ...) index.
- 1 tenant 의 큰 query 가 다른 tenant 영향 (noisy neighbor).
Schema:
- 격리 강.
- Optimizer 가 schema 별 plan.
DB:
- 완전 격리.
- 큰 tenant 가 own resource.
Data export (tenant 가 leave)
Pool: row 별 export. Slow.
Schema: pg_dump schema.
DB: pg_dump database.
→ Schema/DB 가 simple.
Encryption (per-tenant key)
-- pgcrypto + RLS
INSERT INTO sensitive (tenant_id, encrypted)
VALUES ($1, pgp_sym_encrypt($2, $3)); -- $3 = tenant key
SELECT pgp_sym_decrypt(encrypted, $key) FROM sensitive WHERE tenant_id = $1;
→ Tenant 별 key 가 GDPR / HIPAA 친화.
Custom domain
tenant1.app.com → tenant1.
tenant2.app.com → tenant2.
App 의 middleware 가 host → tenant.
app.use((req, res, next) => {
const tenant = subdomain(req.host);
req.tenant = tenant;
next();
});
B2B vs B2C
B2B (Slack, Notion):
- 큰 tenant.
- Schema / DB 가 흔한.
- Cell-based.
B2C (Spotify):
- 매 user 가 자체 = pool.
- Tenant ≈ user.
Citus (Postgres extension)
SELECT create_distributed_table('users', 'tenant_id');
-- → tenant_id hash 가 shard.
→ Multi-tenant Postgres at scale.
Auth + tenant
JWT 의 tenant_id claim.
- Token 가져옴 → tenant 식별.
- RLS 가 query 자동 filter.
GDPR / data residency
EU tenant = EU DB.
US tenant = US DB.
→ DB per tenant + region 별.
Migration from pool → schema
1. Backup pool DB.
2. 매 tenant 의 row → schema.
3. Update app code.
4. Cutover (1 시점).
→ 큰 cost. Plan 신중.
Common 함정
- Pool + tenant_id forget: leak.
- RLS 없이 pool: 누설 가능.
- Schema 1000+: pg_class bloat.
- DB 1000+: connection 폭발.
- Migration 가 매 tenant: slow + drift.
- Backup 가 tenant 별 X: GDPR 위반.
Monitoring
- Per-tenant query count / latency.
- Per-tenant storage.
- Per-tenant cost (chargeback).
- Noisy neighbor 검출.
Noisy neighbor 방지
Pool model:
- Query timeout.
- Per-tenant rate limit.
- 큰 tenant 의 isolated workload (read replica).
Schema/DB:
- 자체 isolated.
Real-world
- Slack: workspace = schema (cell-based).
- Notion: workspace = schema.
- Salesforce: 큰 multi-tenant pool.
- Atlassian: Jira = pool, Bitbucket = pool.
- HubSpot: pool + cell.
Sharding (관련)
Sharding: 큰 tenant = 다른 shard.
Multi-tenant: 작은 tenant = 같은 schema.
→ Sharding + multi-tenant 결합 흔함.
🤔 의사결정 기준
| 상황 | 추천 |
|---|---|
| B2C / 1M user | Pool + RLS |
| B2B / 1k tenant | Schema |
| Enterprise tenant | DB |
| Hybrid | Cell-based |
| GDPR / region | DB per region |
| 큰 schema variation | Schema |
| 작은 / simple | Pool |
❌ 안티패턴
- Pool 가 RLS 없음: leak 가능.
- Schema 1000+: pg_class bloat.
- DB 1000+ + 1 pool 로 connect: pool 폭발.
- Migration 매 tenant manual: drift.
- Noisy neighbor 무시: 1 tenant 가 다른 영향.
- Backup 가 모두 1 file: tenant 별 restore 어려움.
- No per-tenant monitoring: cost / SLA 측정 X.
🤖 LLM 활용 힌트
- Pool + RLS (Postgres) 가 가장 modern.
- Schema 가 sweet spot (B2B).
- DB per tenant 가 enterprise.
- Hybrid (cell-based) 가 큰 system.