Files
sys-analysis-design/sa-output/e-commerce/02-architecture/adr/ADR-006_chien-luoc-du-lieu-rds-gop-schema-per-module.md
Canhchimlac 343ad8bbbc save
2026-09-15 16:02:30 +07:00

175 lines
11 KiB
Markdown

# ADR-006 — Một cụm RDS PostgreSQL chính, schema-per-module, chịu tải ghi/đọc gộp (trừ Payment)
*Tên file: `adr/ADR-006_chien-luoc-du-lieu-rds-gop-schema-per-module.md`*
| | |
|---|---|
| **Status** | `Proposed` *(🔴 radar ≥8 — theo `decision-radar.md §2`/§6, **không được chuyển `Accepted` khi chưa có `POC-02` PASS**)* |
| **Date** | 2026-09-15 |
| **Người quyết** | SA + Tech Lead/DBA (dữ liệu — `decision-radar.md §5`) |
| **Người đề xuất** | SA (qua skill `sa-2-architecture`, hoạt động `adr`) |
| **Điểm radar** | **~8/10** — 🔴 bắt buộc POC trước `Accepted` |
| **Supersedes** | — |
| **Superseded by** | — |
| **Liên quan** | `ASR-006` · `QAS-006` · `ARISK-02` · `POC-02` · `ADR-001` · `ADR-002` (Payment tách riêng, độc lập) |
> ⚠️ **ADR bất biến sau khi `Accepted`.** Muốn đổi quyết định thì viết ADR mới có
> `Supersedes: ADR-006` và đổi trạng thái bản cũ thành `Superseded by`.
---
## 1. Bối cảnh
Modular monolith P2 (`ADR-001`) cần một chiến lược dữ liệu nhất quán cho mọi module ngoài
Payment (đã tách riêng theo `ADR-002`). `ASR-006` ép ra một cụm RDS PostgreSQL Multi-AZ chính,
chia theo schema-per-module — chưa đo tải ghi (checkout) + đọc (catalog) gộp ở tải đỉnh flash
sale. Đây là quyết định đạt ngưỡng radar ≥8 vì chưa có bài đo thật trong ngữ cảnh này (`ASM-08`).
**Ràng buộc đang chi phối:**
| Nguồn | Nội dung |
|---|---|
| `ASR-006` | 1 RDS chính schema-per-module, chịu tải ghi/đọc gộp cho mọi module trừ Payment |
| `QAS-006` | p95 write (checkout) ≤300ms, p95 read (catalog) ≤200ms, CPU <75%, connection pool không exhaust |
| `ADR-001` | Modular monolith P2 — tiền đề cho việc gộp dữ liệu theo schema thay vì database-per-service |
| `ADR-002` | Payment có RDS riêng — không nằm trong phạm vi cụm RDS chính này |
**Cái đã biết chắc / cái còn là giả định:**
| Điều | 🟢 Đã kiểm chứng / 🔴 Giả định | Bằng chứng |
|---|---|---|
| Payment có RDS riêng, tách biệt hoàn toàn khỏi cụm chính | 🟢 Đã kiểm chứng | `ADR-002` |
| 1 cụm RDS `db.r6g.xlarge` Multi-AZ đủ chịu tải ghi/đọc gộp cho các module ngoài Payment ở kịch bản A/B | 🔴 Giả định (`ASM-08`) | `POC-02` — **chưa chạy** |
| Connection pool chia theo module đủ tránh một module chiếm hết pool | 🔴 Giả định | Chưa có thiết kế connection pool chi tiết — thuộc `DAT`, hoạt động 5 |
## 2. Phương án đã cân nhắc
### PA-1 — Database-per-service (mỗi module 1 RDS logic riêng)
| | |
|---|---|
| **Mô tả** | Mỗi module (Identity, Catalog, Cart & Order, Seller, Commission, Promotion, Review, Notification, Shipping) có 1 RDS instance/logic DB riêng — theo `SAD.md`/hồ sơ thầu (P1) |
| **Ưu** | Cô lập tải hoàn toàn — một module quá tải không ảnh hưởng module khác; dễ scale/tune riêng từng DB |
| **Nhược** | ~9-10 RDS instance phải patch/backup/monitor độc lập — vi phạm Conway với năng lực team hiện tại (`CON-05`); tăng chi phí vận hành đáng kể so với 1-2 instance |
| **Chi phí đảo ngược** | Cao — gộp lại thành 1 cụm sau này cần migrate dữ liệu qua nhiều DB, đồng bộ schema |
### PA-2 — 1 cụm RDS chính, schema-per-module (ranh giới logic, chung instance) *(chọn)*
| | |
|---|---|
| **Mô tả** | 1 cụm RDS PostgreSQL Multi-AZ, mỗi module có schema riêng trong cùng instance (ranh giới logic rõ ràng qua schema + quyền truy cập DB user riêng theo schema) |
| **Ưu** | Ít thành phần phải vận hành hơn (1 instance thay vì ~9-10) — khớp Conway với team hiện tại; vẫn giữ ranh giới logic rõ ràng qua schema (không phải 1 schema dùng chung hỗn loạn) |
| **Nhược** | Một module quá tải (VD Catalog bị truy vấn nặng mùa flash sale) có thể ảnh hưởng module khác dùng chung instance (Cart & Order, Commission…) — chưa đo tải gộp thật |
| **Chi phí đảo ngược** | Trung bình-cao — tách read replica/DB riêng cho từng module sau khi đã gộp là một dự án migration riêng |
### PA-3 — 1 cụm RDS, 1 schema chung (không phân tách theo module)
| | |
|---|---|
| **Mô tả** | Tất cả bảng của mọi module nằm chung 1 schema, không phân tách quyền truy cập theo module |
| **Ưu** | Đơn giản nhất về mặt cấu hình DB ban đầu |
| **Nhược** | Không có ranh giới logic nào giữa các module — vi phạm `D7` ("mỗi thực thể có đúng một chủ sở hữu" cần ranh giới rõ để enforce); dễ phát sinh coupling ngầm (module A viết trực tiếp bảng của module B) làm mất tác dụng modular monolith |
| **Chi phí đảo ngược** | Cao — tách schema sau khi code đã coupling chéo là công việc tái cấu trúc lớn |
### Bảng so sánh
| Tiêu chí | PA-1 (DB-per-service) | PA-2 (1 cụm, schema-per-module) | PA-3 (1 cụm, 1 schema) |
|---|---|---|---|
| Số thành phần phải vận hành | ~9-10 | 1 | 1 |
| Cô lập tải giữa module | ✅ Hoàn toàn | 🟠 Một phần (chung instance) | ❌ Không |
| Ranh giới logic rõ ràng (`D7`) | ✅ | ✅ (qua schema) | ❌ |
| Khớp Conway (`CON-05`) | ❌ | ✅ | ✅ |
## 3. Quyết định
> **Chọn PA-2 — 1 cụm RDS PostgreSQL Multi-AZ chính, schema-per-module, cho mọi module ngoại trừ
> Payment.**
**Vì sao:** Cân bằng giữa khớp Conway (`CON-05`, ít thành phần vận hành hơn database-per-service)
và vẫn giữ ranh giới logic rõ ràng qua schema (khác PA-3 gộp chung 1 schema mất kiểm soát ranh
giới dữ liệu).
**Phạm vi áp dụng:** Toàn bộ module ngoại trừ Payment (đã có RDS riêng theo `ADR-002`).
### Điều kiện chuyển `Proposed → Accepted`
| Điều kiện | Ai xác nhận | Trạng thái hiện tại |
|---|---|---|
| **`POC-02` PASS** — p95 write ≤300ms, p95 read ≤200ms, CPU <75%, connection pool không exhaust | Tech Lead/DBA | 🔴 **Chưa chạy** (`ARISK-02`) |
| Thiết kế connection pool theo module (giới hạn để tránh một module chiếm hết pool) | Tech Lead/DBA | Chưa thiết kế — thuộc `DAT` |
🔴 **Bắt buộc theo `decision-radar.md §2`/§6** — điểm radar ~8 (≥8) đồng nghĩa ADR này **không
được chuyển `Accepted`** cho tới khi `POC-02` chạy xong và PASS. Nếu `POC-02` FAIL: tách read
replica/DB riêng cho Catalog/Search sớm hơn dự kiến, cập nhật `TCO` (chi phí hạ tầng tăng, tiệm
cận P1 cho phần đó — đã cảnh báo ở `TCO §3.3`).
## 4. Phương án bị loại và lý do
| Phương án | Loại vì | Gắn với | Điều kiện nào thì xét lại |
|---|---|---|---|
| PA-1 — Database-per-service | Vi phạm Conway với năng lực team hiện tại; tăng chi phí vận hành đáng kể | `CON-05`, `OPT §4` tiêu chí 5 | Nếu `POC-02` FAIL nghiêm trọng (không thể khắc phục bằng read replica) **và** team đủ lớn để vận hành nhiều DB — xét lại cho riêng module gây nghẽn (VD tách Catalog) |
| PA-3 — 1 cụm, 1 schema chung | Không có ranh giới logic — vi phạm `D7`, dễ coupling chéo giữa module | `D7` (design-rules) | Không xét lại — đây là anti-pattern cho modular monolith, không phải trade-off hợp lý |
## 5. Hệ quả
**Hệ quả tích cực**
- Chỉ 1 RDS instance chính phải patch/backup/monitor (ngoài Payment) — khớp năng lực team, giảm
chi phí vận hành đáng kể so với database-per-service
- Ranh giới logic rõ ràng qua schema — enforce được `D7` (mỗi thực thể có đúng một chủ)
**Hệ quả tiêu cực phải sống chung**
- Một module quá tải (VD Catalog mùa flash sale) có thể ảnh hưởng module khác dùng chung instance
— cần connection pool giới hạn theo module để giảm thiểu
- Nếu `POC-02` FAIL, phải tách read replica sớm hơn dự kiến — tăng chi phí hạ tầng ngoài kế hoạch
ban đầu
**Cái quyết định này khoá lại**
| Muốn đổi về sau thì | Tốn |
|---|---|
| Tách một module ra RDS riêng sau khi đã gộp | Migration dữ liệu + đồng bộ song song trong thời gian chuyển đổi — dự án riêng, không miễn phí |
**Việc phát sinh**
| Việc | Chủ | Hạn | Ghi ở đâu |
|---|---|---|---|
| Chạy `POC-02` theo tiêu chí đã viết trước | Tech Lead/DBA | Trước khi `Accepted` | `ARISK_e-commerce_v1.0.md §6` |
| Thiết kế connection pool giới hạn theo module | SA (hoạt động `dat`) + DBA | GĐ2 tiếp theo | `DAT_e-commerce` |
| Kế hoạch tách read replica dự phòng nếu `POC-02` FAIL | Tech Lead/DBA | Khi có kết quả `POC-02` | `TDEBT` (GĐ3) hoặc cập nhật `TCO` |
## 6. Cách kiểm chứng quyết định này được tuân thủ
| Cách kiểm | Công cụ | Chạy ở đâu | `FIT` |
|---|---|---|---|
| `POC-02` — tải ghi/đọc gộp | Load test staging, cấu hình instance thật | Trước `Accepted`, lặp lại trước mỗi major release | — (ghi vào `ARISK §6`) |
| Không có bảng nào bị truy cập chéo schema (module A viết trực tiếp bảng module B) | DB user permission theo schema + code review/lint | CI (schema access lint) | `FIT-09` (ứng viên) |
| CPU/connection pool RDS trong ngưỡng ở production | CloudWatch metric + alarm | Production, liên tục | — |
## 7. Điều kiện xét lại
| Dấu hiệu | Ngưỡng | Ai theo dõi |
|---|---|---|
| `POC-02` FAIL | Không đạt tiêu chí PASS đã viết trước | Tech Lead/DBA |
| CPU RDS production vượt 75% thường xuyên | > 3 lần/tuần trong giờ cao điểm | SRE |
| Connection pool exhaust trong production | Bất kỳ lần nào | SRE |
## 8. Tham chiếu
- POC: `ARISK_e-commerce_v1.0.md §6 POC-02`
- Bài đo: `FIT-09` (ứng viên GĐ3)
- Tài liệu ngoài: `QAS_e-commerce_v1.0.md §A3 QAS-006`
- Thảo luận: `ASR_e-commerce_v1.0.md §B2 ASR-006`, `OPT_e-commerce_v1.0.md §8 ASM-08`
## 9. Review log (không đổi Status)
| Ngày | Người review | Vai trò | Quyết định | Lý do chưa chuyển `Accepted` |
|---|---|---|---|---|
| 2026-09-15 | Điều phối dự án (thay mặt Tech Lead, chế độ chạy thử) | Tech Lead (ký thay, ngoại lệ `DEC-01`) | `Reviewed (Proposed giữ nguyên)` — nội dung đủ làm cơ sở thiết kế tiếp (`icd`/`dat`/`sec`/`inf`/`fail`) | Radar 8 — bắt buộc `POC-02` (tải RDS gộp) PASS trước khi `Accepted`; chưa chạy |
> Đây là ghi nhận **review nội dung**, không phải "sign" theo nghĩa gate: `Status` giữ nguyên
> `Proposed`. Xem `00-index/ADL_e-commerce.md` (Change Log — Review 2026-09-15) và `ADL §8`
> "Việc phải làm" cho điều kiện chuyển `Accepted`. Confidence tổng thể của lượt review: 🔴 —
> `AG2` chưa ký.