🏗️🧠 Read/Write Splitting: tăng tốc database trước khi cần CQRS - System Design P29
Read/Write Splitting: Tăng Tốc Database Khi Chưa Cần Đến CQRS
1. Khi "Trái tim" của hệ thống bắt đầu hụt hơi
Hãy tưởng tượng bạn đang vận hành một hệ thống thương mại điện tử. Mọi thứ vẫn ổn cho đến khi chiến dịch Sale lớn nhất năm bắt đầu. Traffic tăng vọt, nhưng có một điều kỳ lạ: lượng đơn hàng (Write) chỉ tăng gấp 5 lần, trong khi lượng người dùng vào xem sản phẩm, đọc review và kiểm tra trạng thái đơn hàng (Read) lại tăng gấp 50 lần.
Lúc này, Database bắt đầu "khóc". CPU spike liên tục ở mức 90%, Latency của các API tăng từ 50ms lên 2s, kéo sập trải nghiệm người dùng. Phản ứng bản năng của nhiều đội ngũ kỹ thuật là tìm cách Sharding hoặc "đập đi xây lại" toàn bộ theo kiến trúc CQRS (Command Query Responsibility Segregation) vì nghe nói nó giúp scale luồng đọc.
"Mọi vấn đề về hiệu năng database đều có thể giải quyết bằng cách thêm Cache hoặc Sharding."
Đây là một trong những tư duy sai lầm phổ biến nhất trong System Design. Cache không phải là 'viên đạn bạc' cho mọi loại dữ liệu, và Sharding là một "cơn ác mộng" về vận hành mà bạn không nên dấn thân vào quá sớm.
Câu hỏi đặt ra là: Liệu có nhất thiết phải chọn những con đường tốn kém đó ngay lập tức? Dưới góc nhìn của một kỹ sư trưởng, chúng ta cần tìm một "đòn bẩy thực dụng" hơn để giải quyết bài toán Throughput vs. Latency mà không làm thay đổi toàn bộ mô hình lập trình. Đó chính là Read/Write Splitting.
2. Bản chất của Read/Write Splitting: Phân hóa trách nhiệm
Dưới góc nhìn kiến trúc dòng chảy dữ liệu (Data Flow), Read/Write Splitting là việc tách rời luồng truy vấn dựa trên mô hình Primary-Replica. Tuy nhiên, một Senior Engineer cần nhìn thấy sự khác biệt cốt lõi giữa hai tầng:
- Replication (Tách ở mức vật lý): Là việc đồng bộ dữ liệu từ node Primary sang một hoặc nhiều node Replica. Đây là tính năng có sẵn của DB Engine.
- Routing (Tách ở mức logic): Đây mới là nơi "nghệ thuật" kiến trúc bắt đầu. Hệ thống phải đủ thông minh để điều phối: các thao tác thay đổi trạng thái (Ghi) vào Primary, và các thao tác truy vấn (Đọc) vào Replica.
Tại sao đây là bước tiến hóa cần thiết?
- Giảm tải cho Primary: Giúp node quan trọng nhất tập trung hoàn toàn vào việc ghi và đảm bảo tính toàn vẹn, không bị nghẽn bởi các query báo cáo đắt đỏ.
- Tăng khả năng chịu lỗi (Availability): Khả năng failover linh hoạt khi node Primary gặp sự cố.
- Scale-out luồng đọc độc lập: Bạn có thể thêm nhiều Replica tùy ý theo nhu cầu traffic mà không ảnh hưởng cấu trúc node Primary.
Lưu ý từ Production: Việc thêm Replica không hề "miễn phí". Theo tư duy hệ thống (Rule 06), bạn đang tăng áp lực lên Connection Management. Càng nhiều Replica, ứng dụng càng phải duy trì nhiều kết nối, dễ dẫn đến tình trạng cạn kiệt Connection Pool nếu không có chiến lược quản lý hoặc Proxy phù hợp.
3. Tại sao không phải là CQRS?
Read/Write Splitting là điểm dừng chân lý tưởng trong lộ trình tiến hóa (Evolutionary Architecture) trước khi tiến tới CQRS. Đây là bài toán đánh đổi về chi phí và độ phức tạp vận hành.
| Tiêu chí | Read/Write Splitting | CQRS (Command Query Responsibility Segregation) |
|---|---|---|
| Độ phức tạp code | Rất thấp. Logic nghiệp vụ gần như giữ nguyên. | Rất cao. Tách biệt hoàn toàn Model cho Read và Write. |
| Infrastructure Cost | Trung bình. Chỉ cần thêm Replica cho cùng loại DB. | Cao. Thường dùng đa dạng DB (Polyglot Persistence). |
| Consistency Model | Eventual Consistency (mức thấp). | Eventual Consistency (mức cao, qua Message Queue). |
| Operational Overhead | Thấp. Quản lý Replication lag và Routing. | Rất cao. Quản lý Event Store, Data Sync, và đa nền tảng. |
Read/Write Splitting là giải pháp mang lại 80% giá trị của việc tách biệt đọc/ghi nhưng chỉ tốn 20% công sức triển khai so với CQRS.
4. "Cái bẫy" mang tên Replication Lag và Stale Reads
Trong hệ thống phân tán, không có bữa trưa nào miễn phí. Khi áp dụng kỹ thuật này, bạn buộc phải chấp nhận đổi Consistency (Tính nhất quán) lấy Performance.
Cơ chế Replication thường là bất đồng bộ (Asynchronous). Khi dữ liệu vừa ghi vào Primary, nó cần thời gian để lan truyền tới Replica. Khoảng trễ này gọi là Replication Lag, dẫn đến hiện tượng Stale Reads (Đọc phải dữ liệu cũ).
**Kịch bản thất bại điển hình:**User cập nhật Profile (Ghi vào Primary). Ngay lập tức, họ F5 trang để xem thay đổi. Request "Đọc" gửi tới Replica chưa kịp sync dữ liệu mới. Kết quả: User thấy thông tin cũ và tưởng rằng hệ thống bị lỗi.
**Nguyên nhân gây Lag không chỉ là do Network:**Từ kinh nghiệm thực chiến, Lag thường xuyên xảy ra do Heavy Selects on Replica. Một truy vấn "đọc" quá nặng trên Replica có thể chiếm dụng tài nguyên, khiến tiến trình apply log từ Primary bị chậm lại, gây ra độ trễ hàng giây hoặc thậm chí hàng phút.
5. Chiến lược Routing thông minh và Session Consistency
Để xử lý hiện tượng Stale Reads, chúng ta cần các chiến lược Routing tinh tế hơn:
Hướng tiếp cận hạ tầng
- Application-level Routing: Developer tự quản lý nhiều DataSources trong code. Cách này kiểm soát tốt nhưng làm "bẩn" code và khó scale.
- Proxy-level Routing: Sử dụng Middleware như ProxySQL hay MaxScale. Ứng dụng chỉ kết nối tới một Endpoint, Proxy sẽ tự tách Query. Đây là cách tiếp cận "Sạch" và chuyên nghiệp nhất cho các hệ thống lớn.
Giải pháp cho "Read-your-writes Consistency"
Để đảm bảo người dùng luôn thấy dữ liệu họ vừa ghi, chúng ta áp dụng kỹ thuật Session Consistency:
- Sử dụng một Session-based flag (ví dụ lưu trong Cache/Cookie) đánh dấu User vừa thực hiện lệnh Ghi.
- Trong khoảng X giây tiếp theo, mọi request Đọc của User đó sẽ được ép (force) sang node Primary.
- Nâng cao hơn, có thể so sánh Log Sequence Number (LSN) giữa Primary và Replica để đảm bảo Replica đã "đuổi kịp" trước khi cho phép đọc.
6. Khi nào nên và không nên áp dụng?
Đừng vội vàng áp dụng Read/Write Splitting chỉ vì "nghe nói nó nhanh". Hãy dùng bộ khung ra quyết định sau:
Checklist sẵn sàng:
- [ ] Tỉ lệ Read/Write lệch hẳn về phía Read (thường > 80% Read).
- [ ] Primary Database gặp bottleneck về Disk I/O hoặc CPU do lượng SELECT quá lớn.
- [ ] Hệ thống có thể chấp nhận Eventual Consistency ở một số tính năng không quan trọng.
Cảnh báo (Tuyệt đối không dùng nếu):
- Hệ thống yêu cầu Strong Consistency tuyệt đối cho mọi query (ví dụ: khớp lệnh chứng khoán, giao dịch ví tiền).
- Write-heavy workloads: Nếu node Primary đã quá tải vì lượng Ghi, việc thêm Replica sẽ phản tác dụng. Lúc này, Primary phải tốn thêm CPU/Network để Log Shipping (đẩy dữ liệu sync) cho các Replica, khiến nó sập nhanh hơn. Khi đó, Sharding mới là câu trả lời.
7. Kết luận & Key Takeaways
Read/Write Splitting là bước đệm quan trọng, giúp hệ thống "sống sót" qua những đợt tăng trưởng nóng mà không cần một cuộc đại tu kiến trúc.
3 điểm cốt lõi cần nhớ:
- Bản chất là sự kết hợp giữa Replication vật lý và Routing logic.
- Luôn phải đối mặt với Stale Reads và rủi ro từ Replication Lag.
- Ưu tiên sử dụng Proxy-level routing và kỹ thuật Session Consistency để giữ hệ thống ổn định và minh bạch.
Khi Read/Write Splitting cũng chạm tới giới hạn của nó—nơi mà chi phí quản lý hàng chục Replica và áp lực ghi lên Primary trở nên quá lớn—đó là lúc chúng ta cần bàn tới Serverless Database hoặc Infrastructure as Code (IaC) để quản lý sự phức tạp. Đó sẽ là câu chuyện của tập tiếp theo.
🚀 Tiếp tục hành trình cùng TechCraft
Bài viết này là một phần trong hành trình khám phá Backend Engineering, System Design và Production Systems tại TechCraft.
Nếu bạn muốn học theo một lộ trình rõ ràng hơn, TechCraft đang xây dựng Dev Insider như nơi tập trung các series chuyên sâu hơn về:
- Backend Internals
- Database Internals
- Transaction & Consistency
- Distributed Systems
- Production System Design
- AI-Proof Engineer
🚀 Dev Insider
https://www.patreon.com/techcraft_official/posts/vi-sao-dev-ra-161163881?collection=2220113
📘 Facebook
https://www.facebook.com/techcraft.official
🎥 YouTube
https://www.youtube.com/@techcraft.official
🎵 TikTok
https://www.tiktok.com/@techcraft.official
Hiểu hệ thống. Không chỉ framework.
All Rights Reserved