multi-tenancy

Kiến trúc · nhiều khách hàng, một appArchitecture · many customers, one app

App luôn dùng chung. Câu hỏi là đặt bức tường ở đâu.The app is always shared. The question is where to put the wall.

Multi-tenancy là nhiều công ty (tenant) dùng chung một ứng dụng đang chạy, nhưng không ai thấy được dữ liệu của ai. Ba mô hình chỉ khác nhau ở chỗ bức tường giữa các tenant nằm ở từng dòng, từng database, hay từng bản triển khai.Multi-tenancy means many companies (tenants) share one running application, yet none can see another's data. The three models differ only in where the wall between tenants sits: on each row, each database, or each deployment.

Bức tường nằm ở đâuWhere the wall sits

Cùng hai tenant: Acme và Beta. Nét đen đậm là bức tường thật; nét đứt là bức tường chỉ tồn tại trong code của app.Same two tenants: Acme and Beta. A thick black line is a real wall; a dashed line is a wall that exists only in the app's code.

Model 1: one app, one database, rows from both tenants in the same table, separated only by tenant_id in code App 1 DB · students
01 · DB dùng chung01 · Shared DBDòng của hai tenant nằm xen kẽ trong cùng bảng. Tường là WHERE tenant_id = ?.Both tenants' rows interleave in the same table. The wall is WHERE tenant_id = ?.
Model 2: one shared app, a small control-plane directory, and a separate database per tenant App school_acme school_beta control plane
02 · Database-per-tenantApp dùng chung, mỗi tenant một database. Tường là ranh giới database.Shared app, one database per tenant. The wall is the database boundary.
Model 3: each tenant has its own app copy and its own database App · Acme App · Beta DB · Acme DB · Beta
03 · Instance-per-tenantMỗi tenant một bản app và một database. Tường chạy suốt từ trên xuống dưới.Each tenant gets its own app copy and database. The wall runs top to bottom.

Thử làm rò rỉ dữ liệuTry to leak some data

Chọn mô hình, đăng nhập bằng một tenant, rồi bật một bug. Mỗi mô hình có đúng một kiểu lỗi chết người — và mức độ thiết kế chặn được lỗi đó cũng khác nhau.Pick a model, sign in as a tenant, then switch on a bug. Each model has one deadly mistake — and a different amount of protection against it by design.

Mô hìnhModel
Đăng nhập làSigned in as
Bug


        
studentclassthuộc tenantowner

Chọn mô hình nàoWhich model to pick

Nguyên tắc kinh nghiệm: dữ liệu càng nhạy cảm và số tenant càng ít, càng dịch về phía tách riêng (01 → 02 → 03).Rule of thumb: the more sensitive the data and the fewer the tenants, the further you move toward separation (01 → 02 → 03).

Q1

Luật hoặc hợp đồng bắt buộc hạ tầng tách biệt vật lý, và khách hàng chịu trả tiền?Do law or contract require physically separate infrastructure, and will the customer pay for it?

→ 03 Instance
Q2

Dữ liệu nhạy cảm (sức khoẻ, tài chính) và số tenant ở mức hàng chục–hàng trăm?Sensitive data (health, finance) and tens to hundreds of tenants?

→ 02 DB-per-tenant
Q3

Hàng nghìn tenant nhỏ, chi phí là ưu tiên số một, dữ liệu rủi ro thấp?Thousands of small tenants, cost comes first, low-risk data?

→ 01 DB chung (+ RLS)→ 01 Shared DB (+ RLS)

Không bắt buộc chọn một: nhiều nền tảng để phần lớn khách ở mô hình rẻ, gói cao cấp có DB hoặc instance riêng.You don't have to pick just one: many platforms keep most customers on the cheap model and sell a dedicated DB or instance as a premium tier.

Tra cứu từng mô hìnhEach model in detail

Ví dụ xuyên suốt: một app quản lý trường học phục vụ nhiều trường, mỗi trường là một tenant.Running example: a school-management app serving many schools, each school a tenant.

01

Database dùng chung (theo dòng)Shared database (row-level)

rẻ nhấtcheapest

Dữ liệu của mọi tenant nằm trong cùng các bảng. Cột tenant_id đánh dấu ai sở hữu từng dòng, và app lọc theo nó ở mọi truy vấn.Every tenant's data sits in the same tables. A tenant_id column marks each row's owner, and the app filters on it in every query.

CREATE TABLE students (
  id        BIGSERIAL PRIMARY KEY,
  tenant_id BIGINT NOT NULL
            REFERENCES tenants(id),
  full_name TEXT NOT NULL
);
CREATE INDEX idx_students_tenant
  ON students(tenant_id);

-- Safety net: the DB enforces the filter
ALTER TABLE students
  ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON students
  USING (tenant_id =
    current_setting('app.current_tenant')::BIGINT);

Hình dung: một phòng lớn chung, đồ đạc dán nhãn. Quản lý quên dán nhãn một lần là đồ của bạn lẫn vào đống của người khác.Analogy: one big shared room, everything labelled. Forget one label and your things end up in someone else's pile.

ĐượcGains

  • Rẻ nhất: một database cho tất cảCheapest: one database for everyone
  • Một schema, một lần migration, một bản sao lưuOne schema, one migration, one backup
  • Mở rộng tới hàng nghìn tenant trở lênScales to thousands of tenants and more

MấtCosts

  • Cô lập yếu nhất: một bug = một vụ rò rỉ dữ liệuWeakest isolation: one bug = a data breach
  • Noisy neighbour: một tenant nặng làm chậm tất cảNoisy neighbour: one heavy tenant slows everyone
  • Sao lưu/khôi phục riêng một tenant khá rắc rốiPer-tenant backup/restore is awkward

Đánh đổiTrade-offChọn DB dùng chung là đổi mức cô lập lấy chi phí thấp nhất và quy mô lớn nhất.Choosing a shared DB trades isolation for the lowest cost and the largest scale.

Phù hợp choGood forNền tảng blog miễn phí, SaaS freemiumFree blogging platforms, freemium SaaS
Rủi ro chínhMain riskQuên điều kiện lọcForgetting the filter
02

Database-per-tenant

app trường học dùng mô hình nàythe school app's choice

App dùng chung, nhưng mỗi tenant có database riêng. Một database danh bạ nhỏ (control plane) ánh xạ địa chỉ web → database; mỗi request, app tra danh bạ rồi chỉ kết nối tới database của tenant đó.The app is shared, but each tenant gets its own database. A small directory database (the control plane) maps web address → database; per request, the app looks it up and connects only to that tenant's database.

acme.schoolapp.example.com → school_acme
beta.schoolapp.example.com → school_beta

-- schoolapp_control (the only shared data)
CREATE TABLE tenant_hostnames (
  hostname  TEXT PRIMARY KEY,
  tenant_id UUID NOT NULL REFERENCES tenants(id)
);

-- school_acme: no tenant_id anywhere
CREATE TABLE students (
  id        BIGSERIAL PRIMARY KEY,
  full_name TEXT NOT NULL
);
SELECT * FROM students;  -- nothing to forget

Hình dung: các căn hộ khoá riêng trong một toà nhà chung. Đi nhầm phòng là không thể về mặt vật lý.Analogy: separate locked apartments in one shared building. Wandering into the wrong room is physically impossible.

ĐượcGains

  • Cô lập chặt từ thiết kế: quên lọc không thể làm lộ dữ liệu tenant khácStrong isolation by design: a forgotten filter cannot expose another tenant
  • Sao lưu, khôi phục, chuyển server từng tenant dễEasy per-tenant backup, restore, or move to a bigger server
  • Tải nặng của một tenant được khoanh vùng tốt hơnOne tenant's heavy load is better contained

MấtCosts

  • Migration chạy một lần cho mỗi tenant — cần tự động hoá bằng CLIMigrations run once per tenant — automate with a CLI
  • Mỗi DB tốn kết nối/bộ nhớ: một server chứa có hạn, phải tính dung lượng (sizing)Each DB costs connections/memory: a server holds a limited number, so do the sizing
  • Nhiều thành phần phải vận hành hơn mô hình 01More moving parts than model 01

Đánh đổiTrade-offChọn database-per-tenant là đổi công vận hành nhiều database lấy việc rò rỉ không thể xảy ra từ thiết kế, chứ không chỉ ngăn được nhờ code cẩn thận.Choosing database-per-tenant trades the work of running many databases for leaks being impossible by design, not merely prevented by careful code.

Phù hợp choGood forDữ liệu nhạy cảm, hàng chục–hàng trăm tenantSensitive data, tens to hundreds of tenants
Rủi ro chínhMain riskKết nối nhầm databaseConnecting to the wrong database
03

Instance-per-tenant (silo)

đắt nhấtmost expensive

Mỗi tenant có một bản sao hoàn toàn riêng của cả app lẫn database — đôi khi cả server riêng. Gần như không còn "multi". Không cần schema đặc biệt: mỗi tenant chạy schema bình thường cho một khách hàng.Each tenant gets a completely separate copy of the app and the database — sometimes separate servers too. Barely "multi" at all. No special schema: each tenant runs an ordinary single-customer schema.

Hình dung: mỗi tenant một ngôi nhà riêng. Riêng tư tối đa, chi phí tối đa.Analogy: each tenant gets their own house. Maximum privacy, maximum cost.

ĐượcGains

  • Cô lập tuyệt đối, tách biệt vật lýTotal, physical isolation
  • Tuỳ biến sâu; đáp ứng quy định chính phủ/đấu thầuHeavy customisation; meets strict government/procurement rules

MấtCosts

  • Đắt nhất: chi phí nhân lên theo số tenantMost expensive: cost multiplies per tenant
  • Mỗi bản cập nhật phải triển khai lên mọi bản saoEvery update must be deployed to every copy

Đánh đổiTrade-offChọn instance-per-tenant là đổi chi phí và công bảo trì nhân theo số tenant lấy cô lập vật lý tuyệt đối.Choosing instance-per-tenant trades cost and maintenance that multiply per tenant for total physical isolation.

Phù hợp choGood forVài khách hàng doanh nghiệp lớn/chính phủ chịu trả tiềnA few large enterprise/government customers who will pay
Rủi ro chínhMain riskCác bản sao lệch version nhauCopies drifting apart in version

So sánh song songSide-by-side comparison

Yếu tốFactor01 DB chung01 Shared DB02 DB-per-tenant03 Instance-per-tenant
Mức cô lậpIsolationYếu (logic trong app)Weak (app logic)Chặt (DB riêng)Strong (separate DBs)Chặt nhất (tách mọi thứ)Strongest (separate everything)
Chi phíCostThấp nhấtLowestTrung bìnhMediumCao nhấtHighest
Rủi ro rò rỉLeak riskCao (một bug là lộ)High (one bug leaks)Rất thấpVery lowGần như khôngAlmost none
Số tenant tối đaMax tenantsHàng nghìnThousandsHàng chục–hàng trămTens–hundredsVài tenantA handful
Sao lưu từng tenantPer-tenant backupKhóHardDễEasyDễEasy
Công bảo trìMaintenance effortThấp nhấtLowestTrung bìnhMediumCao nhấtHighest
Tuỳ biến theo tenantPer-tenant customisationKhóHardMột phầnSomeToàn bộTotal
tenant_id trên bảng nghiệp vụtenant_id on business tablesCó, ở mọi bảngYes, everywhereKhôngNoKhôngNo
MigrationChạy một lầnRun onceMột lần cho mỗi tenantOnce per tenantMột lần cho mỗi bản triển khaiOnce per deployment
Phù hợp nhất choBest forNhiều tenant nhỏ/rủi ro thấpMany small/low-risk tenantsDữ liệu nhạy cảm, số lượng vừaSensitive data, moderate countÍt khách hàng lớn/khắt kheFew large/strict customers

Case study

Slack

Mô hình 01 · DB dùng chungModel 01 · shared DB

Hàng triệu workspace — database-per-tenant không quản lý nổi. Dựa vào hạ tầng chung với tenant ID và nhiều lớp bảo vệ nội bộ; mức cô lập phụ thuộc vào code và kiểm thử cực kỳ chắc chắn.Millions of workspaces — database-per-tenant would be unmanageable. It leans on shared infrastructure with tenant IDs and heavy internal safeguards; isolation depends on rock-solid code and testing.

App trường họcThe school app

Mô hình 02 · DB riêngModel 02 · DB per tenant

Dữ liệu học sinh (điểm số, chuyên cần, liên lạc phụ huynh) rất nhạy cảm, số trường vừa phải. Mỗi tenant một database là điểm cân bằng: cô lập chặt mà không tốn tiền của các bản triển khai riêng.Student data (grades, attendance, parent contacts) is highly sensitive, and the school count is moderate. A database per tenant is the sweet spot: strong isolation without paying for separate deployments.

Sở giáo dục nhà nướcState education department

Mô hình 03 · instance riêngModel 03 · own instance

Luật có thể bắt buộc dữ liệu nằm trên hạ tầng tách biệt vật lý. Chi phí thêm chấp nhận được vì yêu cầu tuân thủ không thể thương lượng.The law may require data on physically separate infrastructure. The extra cost is acceptable because compliance is non-negotiable.

Mô hình laiHybrid

Trộn nhiều mô hìnhMixing models

Phần lớn khách ở mô hình rẻ, gói cao cấp có DB hoặc instance riêng. App trường học thực chất là vậy: mặc định 02, 03 khi khách yêu cầu.Most customers on the cheap model, a premium tier with a dedicated DB or instance. The school app is effectively this: 02 by default, 03 on request.

Lưới an toàn chồng lớpLayered safety nets

Ngay trong mô hình 02, app trường học không dựa vào một lớp duy nhất. Đây cũng là thứ chặn bug "control plane trả nhầm database" trong phần thử ở trên.Even within model 02, the school app doesn't rely on a single layer. These are also what stop the "control plane returns the wrong database" bug in the simulator above.

Sai lầm hay gặpCommon mistakes

Sai lầmMistakeVì sao saiWhy it's wrongSửa thế nàoFix
Nghĩ multi-tenancy = chỉ tenant_idThinking multi-tenancy = only tenant_idCả ba mô hình đều là multi-tenancy; chúng chỉ đặt tường ở chỗ khác nhauAll three models are multi-tenancy; they put the wall in different placesChọn chỗ đặt tường theo độ nhạy cảm dữ liệu và số tenantPlace the wall by data sensitivity and tenant count
Mô hình 01 chỉ dựa vào code nhớ lọcModel 01 relying on code to remember the filterQuên một lần là lộ dữ liệu tenant khácForget once and another tenant's data leaksBật Postgres RLS để database tự áp điều kiện lọcEnable Postgres RLS so the database enforces the filter
Không index tenant_idNot indexing tenant_idMọi truy vấn đều lọc theo cột nàyEvery query filters on itIndex tenant_id trên mọi bảng nghiệp vụIndex tenant_id on every business table
Chạy migration tay cho từng DBRunning migrations by hand per DBMô hình 02 có N database cùng cấu trúc; sót một cái là lệch schemaModel 02 has N identically-shaped databases; miss one and schemas driftMột lệnh CLI chạy migration cho mọi tenantOne CLI command migrates every tenant
Thêm tenant mà không tính dung lượng (sizing)Adding tenants without sizingMỗi DB tốn kết nối và bộ nhớ; server có trầnEach DB costs connections and memory; servers have a ceilingTính trước mỗi server chứa được bao nhiêu tenantWork out how many tenants fit per server up front

Ba câu cần nhớThree things to remember