Taryana
Kembali ke Semua Artikel Teknis/Arsitektur Multi-Tenant Database PostgreSQL: Schema-per-Tenant, PgBouncer Multiplexing, dan NestJS
PostgreSQLPgBouncerMulti-TenancyNestJSArchitectureDatabase

Arsitektur Multi-Tenant Database PostgreSQL: Schema-per-Tenant, PgBouncer Multiplexing, dan NestJS

Belajar dari pengalaman nyata menangani insiden connection starvation dan kebocoran data antar-organisasi: bagaimana isolasi Schema-per-Tenant, PgBouncer Transaction Pooling, dan SET LOCAL menyelamatkan platform SaaS B2B.

1. Mengapa Multi-Tenancy Tradisional Selalu Berujung Bencana?

Pernahkah kamu membayangkan skenario mimpi buruk di mana data faktur pajak, daftar pesanan rahasia, atau nomor kontak pelanggan milik satu perusahaan klien tiba-tiba muncul di dasbor perusahaan kompetitor mereka? Atau saat server database PostgreSQL mendadak mati suri karena kehabisan koneksi tepat saat bisnismu mulai mendapatkan ratusan klien baru?

Pengalaman mengerikan itulah yang pernah saya dan tim hadapi ketika pertama kali merancang platform Software-as-a-Service (SaaS) logistik B2B. Di awal perancangan, kami dihadapkan pada dilema klasik:

  • Jika menggunakan pendekatan Shared Schema (Row-Level ) dengan menyelipkan kolom tenant_id di setiap tabel, satu kelalaian kecil pengembang yang lupa menuliskan klausa WHERE tenant_id = :currentTenant dalam kueri SQL kompleks langsung memicu kebocoran data massal lintas-organisasi (cross-tenant data leakage).
  • Jika menggunakan pendekatan Database-per-Tenant, menjalankan ratusan instance database terpisah membutuhkan biaya server yang membengkak gila-gilaan, di mana ribuan gigabyte RAM terbuang percuma hanya untuk menahan koneksi idle.

Ketika kami mencoba menjembatani keduanya dengan model Schema-per-Tenant, kami justru tersandung dua masalah produksi fatal:

  • : Setiap NestJS membuka puluhan koneksi langsung ke PostgreSQL. Karena model internal PostgreSQL melahirkan proses Linux baru untuk setiap koneksi (fork()), ratusan koneksi idle menguras habis RAM dan CPU database hingga server tumbang total.
  • Tragedi Schema Bleeding: Saat kami buru-buru memasang connection pooler tanpa pemahaman mendalam, penggunaan perintah naif SET search_path mencemari state sesi koneksi. Data sensitif organisasi Tenant Acme bocor ke layar dasbor Tenant Beta!

Dari rangkaian insiden berdarah tersebut, kami menyadari satu hukum arsitektural: keamanan isolasi data dan efisiensi memori hanya bisa tercapai jika kamu mengombinasikan Schema-per-Tenant dengan Transaction Pooling dan isolasi transaksional SET LOCAL.

Executive Summary & Core Insight

Blueprint Solusi: Arsitektur Produksi Skala Tinggi Untuk mewujudkan isolasi mutlak dengan biaya infrastruktur yang minimal, kami merancang arsitektur terpadu berbasis 3 pilar:

  1. 1PostgreSQL Schema-per-Tenant: Setiap tenant memperoleh namespace skema logis mandiri (tenant_acme, tenant_globex). Struktur tabel bersih tanpa polusi kolom tenant_id, memangkas risiko human error hingga 0%.
  2. 2 Transaction Pooling: Mengelompokkan dan memultipleks ribuan koneksi soket NestJS ke sekumpulan kecil koneksi fisik backend PostgreSQL (cukup 20-40 koneksi fisik).
  3. 3Zero-Bleed Boundary dengan SET LOCAL search_path: Membungkus setiap request di dalam transaksi ACID dengan perintah BEGIN; SET LOCAL search_path = <schema>, public; ... COMMIT;. Konfigurasi skema terisolasi sempurna dan otomatis ter-revert saat transaksi selesai tanpa menyisakan jejak state.
Arsitektur: Schema-per-Tenant + PgBouncer Multiplexing + NestJSMulti-Tenancy
Skenario Alur:
Pinch touchpad / scroll untuk zoom • Klik-drag untuk geser (kiri, kanan, atas, bawah)
INGRESS REQUESTtenant-a.apiSubdomain RoutingX-Tenant-ID: alphatenant-b.apiSubdomain RoutingX-Tenant-ID: betatenant-c.apiSubdomain RoutingX-Tenant-ID: gammaNESTJS API PODSTenantMiddlewareParse Header / SubdomainHeader: 'X-Tenant-ID'AsyncLocalStorageContext Injection:tenantId: "alpha"Thread-safe Node.js storeTransactional ScopeSET LOCAL search_path ="tenant_alpha", publicAuto-reverts on COMMITPGBOUNCER CONNECTION POOLERMultiplexing Ratio:1,000 Client Conns↓ 10 Server Conns100:1 Dense Concurrencypool_mode:"transaction"Released immediately on COMMITMemory ProtectionRAM: ~104 MB (10 PIDs)Direct: ~10.4 GB (1,000 PIDs)Zero Worker StarvationPOSTGRESQL SINGLE INSTANCEschema "tenant_alpha"orders | users | productsIsolated namespace • 0 tenant_idpg_dump -n tenant_alphaschema "tenant_beta"orders | users | productsIsolated namespace • 0 tenant_idpg_dump -n tenant_betaschema "tenant_gamma"orders | users | productsIsolated namespace • 0 tenant_idpg_dump -n tenant_gammaMICROSERVICE EXTRACTIONDedicated DBalpha_dedicated_dbPort 5433 / RDSpg_dump -n tenant_alphaExtraction Guarantee:• 0 Table DDL Change• 0 Query Rewrites• Clean Slice Out• Zero DowntimeOther tenants (beta, gamma)completely unaffected!Ready to slice
Rute Aktif: Ingress 'tenant-a.api' (X-Tenant-ID: alpha) → NestJS ALS → SET LOCAL search_path = tenant_alpha.
PgBouncer: 1,000 → 10 ConnsPostgres: Isolated Schemas

2. Peta Topologi dan Matriks Komparasi 3 Model Multi-Tenancy

Sebelum kita membedah insiden lapangan dan implementasi kode, mari kita bandingkan trade-off mendasar antara ketiga paradigma arsitektur di industri:

Dimensi EvaluasiRow-Level Isolation (Shared Schema)Schema-per-Tenant (Isolated Schema, Shared DB)Database-per-Tenant (Isolated DB Instance)
Batas Isolasi DataLemah (Tergantung filter kolom tenant_id)Tinggi (Namespace skema fisik terpisah)Maksimal (Instance database mandiri)
Risiko Human Error Kebocoran DataSangat Tinggi (Lupa klausa WHERE)Nol / Sangat Rendah (Tabel fisik terisolasi)Nol (Fisik terpisah total)
Overhead Memori & KoneksiSangat RendahRendah - Sedang (Optimal via )Sangat Boros (Ratusan pool koneksi mandiri)
Pencadangan & Pemulihan MandiriSangat Sulit (Ekstraksi manual per baris)Sangat Cepat & Mudah (pg_dump -n schema)Sangat Mudah (Backup database standar)
Blast Radius Kerusakan DataMenimpa seluruh tenant dalam 1 tabelTerisolasi pada skema tenant terkaitTerisolasi Sempurna
Kesiapan Dekoupling ke MicroservicesRumit (Wajib hapus kolom & rombak DDL)Sangat Mulus (Ekspor skema tanpa ubah kode)Instan (Sudah mandiri)
Skalabilitas Jumlah TenantJutaan mikro-user B2CRatusan hingga Ribuan Tenant B2B EnterpriseTerbatas biaya server puluhan tenant

3. Krisis Saturasi Koneksi PostgreSQL & Solusi PgBouncer

Masalah 1: Model Proses Fork() PostgreSQL yang Membakar RAM Server

Berbeda dengan MySQL atau Node.js yang berbasis multi-threaded, arsitektur PostgreSQL beroperasi dengan model multi-process:

  • Setiap koneksi TCP klien yang tiba diterima oleh proses daemon utama (postmaster).
  • postmaster kemudian memanggil syscall sistem operasi Linux fork() untuk melahirkan satu proses worker Linux mandiri (postgres: worker process).
  • Setiap proses worker mengalokasikan sekitar 10 MB RAM murni hanya untuk menahan soket TCP dalam kondisi idle tanpa kueri.
  • Jika kamu memiliki 15 NestJS di Kubernetes di mana masing-masing membuka pool 40 koneksi langsung, ada 600 koneksi fisik yang menuntut 6 GB RAM hanya untuk standby! Begitu traffic melonjak, CPU habis terkuras untuk context switching di tingkat kernel daripada mengeksekusi kueri SQL.
TEXT Reference10 baris kode
> +------------------------------------------------------------------------+ | | Linux Operating System Kernel | ...Klik untuk melihat implementasi lengkap →

Solusi 1: PgBouncer Transaction Pooling Multiplexing

Kami memasang di depan PostgreSQL dengan mode pool_mode = transaction. memutus relasi 1:1 antara dan proses worker PostgreSQL:

  • Ribuan koneksi klien dari NestJS ditampung di dengan memori sangat ringan (<50 MB).
  • hanya meminjamkan koneksi fisik backend ke PostgreSQL selama transaksi berlangsung (dari BEGIN hingga COMMIT).
  • Begitu kueri selesai, koneksi fisik langsung dikembalikan ke pool untuk melayani request berikutnya. Cukup 20-30 koneksi fisik backend untuk melayani ribuan request per detik!

Masalah 2: Bencana Schema Bleeding Akibat Perintah Naif SET search_path

Saat tim kami pertama kali mengaktifkan Transaction Pooling, sebuah bencana baru muncul. Pengembang menulis kueri seperti ini:

PostgreSQL Schema Reference3 baris kode
> -- ANTI-PATTERN SANGAT BERBAHAYA PADA TRANSACTION POOLING: | SET search_path = "tenant_acme", public; ...Klik untuk melihat implementasi lengkap →

Perintah SET search_path reguler mengubah konfigurasi pada level sesi koneksi fisik PostgreSQL backend. Karena meminjamkan koneksi tersebut ke klien lain setelah kueri usai, request berikutnya dari Tenant Globex mendapatkan koneksi yang masih membawa variabel search_path = tenant_acme! Data faktur Tenant Acme bocor ke layar Tenant Globex!

Solusi 2: Penegakan Deterministiik dengan SET LOCAL search_path

Solusi mutlak dan aman untuk mengeliminasi pencemaran state koneksi adalah menggunakan klausa SET LOCAL di dalam transaksi ACID:

PostgreSQL Schema Reference10 baris kode
> -- POLA EMAS: TRANSAKSI STATELESS & ZERO-BLEED | BEGIN; ...Klik untuk melihat implementasi lengkap →

4. Kasus Nyata di Medan Produksi: Dua Insiden Berantai yang Mengguncang Platform

Pada kuartal pertama peluncuran sistem logistik B2B kami, tim menghadapi insiden ganda yang menjadi pelajaran paling berharga dalam karir rekayasa kami:

Production Incident & Bottleneck Case

Kronologi Insiden Produksi: Bencana Flash Sale & Kebocoran Skema Antar-Tenant

  • Waktu Kejadian: Malam Promo Nasional (Pukul 00:05 - 01:40 UTC+7).
  • Dampak: Sistem lumpuh selama 45 menit akibat kehabisan koneksi database, disusul insiden kebocoran faktur organisasi Tenant Acme ke dasbor Tenant Beta.
  • Akar Masalah 1: fisik langsung (600 koneksi menabrak batas max_connections = 500).
  • Akar Masalah 2: Solusi cepat menggunakan SET search_path tanpa klausa LOCAL, memicu pencemaran state koneksi (Schema Bleeding).

Episode 1: Bencana Connection Starvation

Tepat pukul 00:05 saat promo dimulai, 15 NestJS mencoba membuka koneksi maksimal ke PostgreSQL secara bersamaan. CPU database terkunci di 100%. PostgreSQL kehabisan alokasi worker dan melempar error fatal: FATAL: remaining connection slots are reserved for non-replication superuser connections. Seluruh aplikasi mengalami cascading timeout HTTP 503 selama 45 menit.

Episode 2: Tindakan Cepat yang Berbuah Petaka (Schema Bleeding)

Untuk meredakan krisis koneksi, tim segera menyalakan dengan mode transaction pooling. Namun, karena kueri aplikasi masih menggunakan SET search_path = "tenant_acme", koneksi yang dikembalikan ke pool masih membawa state skema lama. Staf akuntansi Tenant Beta tiba-tiba melihat faktur dan nomor kontak pelanggan milik Tenant Acme!

Tim segera melakukan perbaikan definitif:

  1. 1Melakukan rolling restart untuk membersihkan seluruh koneksi yang tercemar.
  2. 2Mengubah modul database NestJS agar selalu menggunakan blok transaksi BEGIN; SET LOCAL search_path ... COMMIT;.
  3. 3Memasang middleware sanitasi identifier skema berbasis regex untuk mencegah SQL Injection.

5. Implementasi Kode NestJS Skala Produksi (TypeScript)

Berikut adalah arsitektur implementasi produksi di NestJS yang aman dari kebocoran data dan ramah performa:

A. Context Propagation via AsyncLocalStorage

Menjaga identitas tenant di sepanjang rantai pemanggilan asynchronous tanpa perlu mengoper objek tenantId secara manual di setiap parameter fungsi:

TypeScript CRDT Reference27 baris kode
> // src/common/tenant/tenant-context.ts | import { AsyncLocalStorage } from "node:async_hooks"; ...Klik untuk melihat implementasi lengkap →

B. Tenant Context Middleware dengan Sanitasi Regex Anti-Injection

TypeScript CRDT Reference41 baris kode
> // src/common/tenant/tenant.middleware.ts | import { Injectable, NestMiddleware, BadRequestException } from "@nestjs/common"; ...Klik untuk melihat implementasi lengkap →

C. Konfigurasi pgbouncer.ini (Transaction Pooling)

INI Reference28 baris kode
> ;; /etc/pgbouncer/pgbouncer.ini | [databases] ...Klik untuk melihat implementasi lengkap →

D. Tenant Database Service dengan SET LOCAL

TypeScript CRDT Reference47 baris kode
> // src/database/tenant-database.service.ts | import { Injectable, Logger } from "@nestjs/common"; ...Klik untuk melihat implementasi lengkap →

6. Hasil Riil di Produksi: Metrik Sebelum vs Sesudah

Pengujian beban komparatif dilakukan menggunakan k6 pada server 8 vCPU / 16 GB RAM dengan 1.000 Virtual Users (VU) yang mengirim kueri simultan ke 100 skema tenant:

Parameter PengujianDirect PostgreSQL (Unpooled) Transaction Pooling (Pool = 25)Efisiensi & Dampak Nyata
Throughput Transaksi410 (Terguncang antrean worker)14.850 Kapasitas Melesat 36x Lipat
Latensi P50185 ms2,4 ms77x Lebih Cepat
Latensi P998.420 ms (Koneksi antre parah)18,6 ms452x Lebih Rendah & Stabil
Konsumsi RAM Database Server6.840 MB (6,8 GB)380 MBPenghematan Memori 94,4%
Utilisasi CPU Database98% (Dominasi context-switch)24% (Fokus eksekusi SQL)Beban CPU Berkurang 75%
Tingkat Error (Error Rate)14,8% (Connection Refused)0,00% (Nol Kegagalan)Reliabilitas 100% Sempurna

7. 5 Kaidah Emas Rekayasa Multi-Tenant Produksi

  1. 1Wajib Transaksional untuk SET LOCAL: Jangan pernah menjalankan SET search_path tanpa klausa LOCAL saat menggunakan mode transaction pooling. Selalu bungkus eksekusi di dalam batas BEGIN ... COMMIT.
  2. 2Ukuran Pool Backend Kecil dan Padat: Hindari menyetel pool backend ke angka ratusan. Formula optimal untuk koneksi backend PostgreSQL adalah (2 * CPU Core) + Jumlah Disk Spindle (15 hingga 30 koneksi fisik sudah optimal melayani ribuan request klien).
  3. 3Sanitasi Ketat Identifier Skema: Lindungi eksekusi DDL nama skema dinamis dari ancaman SQL Injection dengan memvalidasi string identifier hanya berupa alfanumerik aman (/^[a-z0-9_]{3,32}$/).
  4. 4Isolasi Konteks Bebas via : Hindari menyimpan context tenant di level variabel modul global atau objek request yang rentan terhadap asinkron di Node.js.
  5. 5Pantau Metrik Internal Secara Proaktif: Monitor metrik cl_waiting dan sv_active pada output perintah SHOW POOLS; secara berkala untuk mendeteksi potensi antrean koneksi sebelum berdampak pada pengguna akhir.

8. Simulator Interaktif & Laboratorium Multi-Tenant

Kamu dapat menguji dan membuktikan secara langsung seluruh konsep arsitektur di atas melalui simulator interaktif di artikel ini:

  • Uji Mode Aman (SET LOCAL): Kirim kueri untuk Acme Corp, Globex, atau Wayne Enterprise dan perhatikan bagaimana koneksi backend dikembalikan ke pool dalam kondisi higienis.
  • Uji Kerentanan State Bleed pada Mode Unsafe: Saksikan langsung peringatan bahaya saat request tenant berikutnya mengeksekusi data di bawah skema milik penyewa sebelumnya.
  • Uji Langsung (Direct PostgreSQL): Amati lonjakan konsumsi memori akibat syscall fork() Linux per koneksi.
  • Simulasi Dekoupling Microservices: Saksikan betapa mudahnya mencopot skema tenant besar menjadi database mandiri terdedikasi tanpa perlu merombak struktur DDL tabel.
Multi-Tenant Database Simulator
Memuat lab interaktif...

Ingin bereksplorasi lebih dalam?

Buka seluruh koleksi simulator sistem terdistribusi di Central Sandbox Playground.

Buka Central Sandbox →