Skip to content

ADR-0001. Các mức cô lập multi-tenancy (Pool / Bridge / Silo)

TrườngGiá trị
Trạng tháiDraft (Đề xuất)
Ngày2026-05-22
Người quyết địnhPhat Nguyen
Phạm viXuyên suốt - datasource @nx/core, mọi service, triển khai
Thay thế-
Bối cảnh sản phẩmChiến lược Multi-Tenancy (PRD)

Bối cảnh

Bài toán

  • BANA chạy một database dùng chung (nx_seller) với một connection pool tĩnh mỗi service (PostgresCoreDataSource gọi new Pool() một lần). Tenant chỉ được phân biệt bằng lọc merchantId / organizerId trong query repository - tức mô hình Pool.
  • Ta cần hỗ trợ mức cô lập mạnh hơn (DB riêng, stack riêng) cho một số Org trong khi giữ Pool rẻ cho số đông, có thể di chuyển Org giữa các mô hình - gồm cả chiều khó: gộp Silo → Pool.

Điều thúc đẩy

  • Quy hoạch triển khai & vận hành dài hạn. Số tenant và độ đa dạng hợp đồng đang tăng; chốt cứng một mô hình toàn cục ngay bây giờ sẽ rất đắt để gỡ về sau.

Hiện trạng (AS-IS)

Khía cạnhHiện tại
Cô lập DBMột nx_seller chung, một pool tĩnh/service
Cột tenantmerchantId (chính), organizerId (cha) trên ~52 bảng
Cách thực thi cô lậpLọc ở tầng ứng dụng; không RLS, không schema-per-tenant
Phân giải tenantJWT claim (organizers[], merchants[]) → lọc theo request context
Định tuyếnTheo token; không theo subdomain/host
IDSnowflake (toàn cục duy nhất) qua IdGenerator
Triển khaiK8s trên VNPAY Cloud (Kustomize, cluster staging/prod tách biệt); gateway Traefik

Quyết định

Áp dụng mô hình lai (hybrid), phân bậc trong đó mức cô lập là thuộc tính theo từng Org, quyết định lúc chạy - không phải hằng số cho cả hệ thống. Đơn vị cô lập là Organizer.

MứcServiceDatabaseMặc định cho
POOLChungnx_seller chung, lọc organizerIdMọi Org (mặc định)
BRIDGEChungMột DB mỗi OrgOrg cần cô lập dữ liệu
SILOStack riêngMột DB mỗi OrgEnterprise / on-prem

Ba cơ chế nền tảng giúp điều này khả thi - tất cả nằm trong @nx/core, không đụng tới code nghiệp vụ:

  1. Tenant Registry - bảng orgId → { isolationTier, datasourceRef }. Mọi Org mặc định là POOL.
  2. Connection Resolver - nâng PostgresCoreDataSource từ một pool tĩnh thành bộ phân giải pool theo tenant, đọc registry và cache kết nối. Đây là thay đổi chặn duy nhất; mọi thứ khác xây dựng trên nó.
  3. Bộ cấp phát Snowflake WorkerId - cấp worker/node ID tập trung để các silo chạy độc lập không bao giờ sinh ID trùng (chuẩn hoá quy ước APP_ENV_NODE_ID sẵn có, xem payment ADR-0002).

Hướng di chuyển

ChiềuĐộ khóCơ chế
POOL → SILO (tách)DễSao chép có lọc theo organizerId (logical replication / pg_dump --where), cutover, chuyển registry
SILO → POOL (gộp)Khó nhưng khả thiNhập dữ liệu giữ nguyên Snowflake ID, kiểm tra không có FK mồ côi, chuyển registry, khoá silo cũ

Vì sao gộp an toàn ở đây: Snowflake ID toàn cục duy nhất nên khi nhập các dòng của silo vào bảng chung sẽ không trùng PK - đúng đặc tính khiến việc gộp dựa trên auto-increment gần như bất khả. Voucher sequence theo merchant được scope bởi merchantId nên số chứng từ dễ đọc cũng không trùng chéo giữa các Org.

Hệ quả

LợiHại
Cô lập trở thành một "núm xoay" theo Org, không phải viết lạiConnection Resolver làm phức tạp thêm hot path của tầng dữ liệu
Org mới được tiếp nhận tức thì (POOL mặc định)Cần cache kết nối theo tenant (vòng đời, thu hồi)
Snowflake ID giúp gộp Silo→Pool khả thiWorkerId phải được quản trị tập trung nếu không việc gộp sẽ vỡ
Code nghiệp vụ/repository không đổi giữa các mứcMigration schema phải trải ra N database (Bridge/Silo)
Khớp topology K8s/Kustomize + Traefik sẵn cóViệc provision Bridge/Silo cần tự động hoá trước khi scale

Phương án đã cân nhắc

Phương ánLợiHạiVì sao loại
Chỉ PoolVận hành đơn giản nhấtKhông cô lập được, không lên on-premMất khách enterprise/tuân thủ
Chỉ SiloCô lập tối đaChi phí cao nhất, tiếp nhận chậm, gộp tốn kémSai cho POS SMB đại trà
Chốt một mức toàn cục ngayQuyết định đơn giảnBị khoá cứng; trả "thuế migration" về sau dưới áp lựcQuá sớm; nhu cầu vẫn đang thăm dò
Định tuyến tenant theo subdomain/hostMẫu hình SaaS phổ biếnPhải làm lại URL client + mô hình tokenPhân giải theo token đã chạy; không cần
Postgres RLS thay cho lọc ứng dụngCô lập do DB thực thiDi chuyển lớn mọi đường queryNgoài phạm vi quyết định này; ghi nhận là câu hỏi mở

Câu hỏi mở

  • Thị trường mục tiêu (SMB vs enterprise) - nghiêng về lai (hybrid), vẫn đang thăm dò.
  • Ràng buộc tuân thủ / data-residency / on-prem - chưa xác định; nếu có sẽ khiến SILO + Helm chart di động trở thành bắt buộc.
  • BRIDGE là mức cố định hay chỉ là bước trung chuyển sang SILO?
  • Dùng RLS hay giữ lọc tầng ứng dụng cho POOL?

Hoàn thành khi

  • [ ] Tenant Registry tồn tại; mọi Org có isolationTier (mặc định POOL).
  • [ ] PostgresCoreDataSource phân giải kết nối theo tenant context từ registry.
  • [ ] Snowflake worker ID được cấp phát tập trung (không có hai node trùng nhau).
  • [ ] Có runbook cho việc tách (Pool→Silo) và gộp (Silo→Pool).
  • [ ] Nâng trạng thái Draft → Accepted sau khi đã trả lời các câu hỏi về thị trường & tuân thủ.

Tham chiếu

  • Chiến lược Multi-Tenancy (PRD)
  • packages/core/src/datasources/postgres-core.datasource.ts - pool tĩnh cần nâng cấp
  • packages/core/src/utilities/request.utility.ts - trích xuất tenant-context hiện tại
  • Payment ADR-0002 - tiền lệ phân vùng Snowflake NODE_ID
  • AWS SaaS Lens - mẫu hình cô lập Pool / Bridge / Silo

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.