[System Design Version 1 - Bài 9] Scale Database: Replication (Master-Slave), Sharding (Phân mảnh) và những cạm bẫy khi chia nhỏ dữ liệu
Chào anh em. Trong vòng đời phát triển của một hệ thống, có một sự thật đau lòng mà mọi kỹ sư backend đều phải đối mặt: Database luôn là nút thắt cổ chai (bottleneck) khó nhằn nhất.
Khi server chạy code (Application Server) bị quá tải, bạn chỉ cần ném thêm tiền cho AWS hoặc Google Cloud để bật thêm vài con server mới, gắn Load Balancer vào là xong. Code là Stateless (không lưu trạng thái).
Nhưng Database thì có trạng thái (Stateful). Bạn không thể copy paste cái Database ra làm 3 bản rồi mong chúng tự động chạy đồng bộ với nhau một cách màu nhiệm được.
Hôm nay, với tư cách là một người từng đổ mồ hôi hột trong những đêm migration dữ liệu, tôi sẽ chia sẻ cho anh em hai chiến lược kinh điển để mở rộng Database, cùng những "cạm bẫy" chết người đi kèm.
1. Replication (Master-Slave): Phân thân chi thuật cho luồng Đọc
Thống kê thực tế cho thấy, trong các hệ thống e-commerce bán lẻ mỹ phẩm hay tin tức, tỷ lệ Đọc/Ghi (Read/Write Ratio) thường là 10:1 hoặc thậm chí 100:1. Có hàng ngàn người lướt xem sản phẩm, nhưng chỉ vài chục người thực sự bấm nút "Thanh toán".
Vậy tại sao phải bắt chung một con server gánh cả hai việc? Replication (Sao chép dữ liệu) ra đời để giải bài toán này.
- Cách hoạt động: Chúng ta thiết lập 1 server làm Master (Primary) và 1 hoặc nhiều server làm Slave (Replica).
- Quy tắc tối thượng: Chỉ được phép GHI (Insert/Update/Delete) vào Master. Mọi thao tác ĐỌC (Select) sẽ được đẩy sang các Slaves.
- Khi có một dòng dữ liệu mới chui vào Master, Master sẽ lập tức copy (ngầm) dòng đó đẩy sang cho các Slaves thông qua các file log (như binlog trong MySQL).
Ưu điểm: Luồng đọc được phân tải hoàn hảo. Nếu lượng người xem tăng đột biến, bạn chỉ cần gắn thêm nhiều Slaves. Nếu một Slave chết, hệ thống vẫn sống bình thường.
Cạm bẫy (Replication Lag): Việc copy dữ liệu từ Master sang Slave tốn một khoảng thời gian ngắn (từ vài mili-giây đến vài giây nếu mạng nghẽn). Tình huống dở khóc dở cười: User vừa đổi tên profile xong (ghi vào Master), màn hình load lại và query ngay lập tức (đọc từ Slave). Do dữ liệu chưa kịp đồng bộ, user vẫn thấy tên cũ của mình và tưởng hệ thống bị lỗi, bèn bão click vào nút "Lưu" thêm chục lần nữa. Cách khắc phục: Đọc từ Master (Read-after-write) đối với những dữ liệu user vừa chỉnh sửa, hoặc dùng Cache để "lấp liếm" khoảng thời gian lag này.
2. Sharding (Phân mảnh dữ liệu): Băm nhỏ con voi
Replication giải quyết được luồng Đọc. Nhưng nếu luồng Ghi quá lớn thì sao? Hãy tưởng tượng hệ thống thu phí tự động (AFC) của tuyến Metro. Mỗi ngày có hàng triệu lượt hành khách quẹt thẻ qua cổng Gate. 10 năm hoạt động sinh ra hàng tỷ dòng log giao dịch. Dung lượng phình lên hàng Terabyte. Một con Master không thể chứa nổi, mỗi lần tạo index mất nguyên một ngày, backup dữ liệu mất một đêm.
Lúc này, chúng ta phải dùng đến chiêu cuối: Sharding (Horizontal Partitioning).
- Cách hoạt động: Thay vì để 1 tỷ dòng trong 1 server, chúng ta bẻ nó ra làm 10 server (mỗi server gọi là 1 Shard), mỗi Shard chứa 100 triệu dòng.
- Chúng ta cần một Shard Key (Khóa phân mảnh). Ví dụ, dùng
UserID. Khách hàng có ID chẵn sẽ được lưu ở Shard A, ID lẻ sẽ lưu ở Shard B. Khi khách quẹt thẻ, code backend sẽ tính toán:Shard = UserID % 2để biết phải cắm request vào đúng database nào.
Ưu điểm: Mở rộng không giới hạn (Infinite Scalability). Vừa phân tải được cả luồng Đọc lẫn luồng Ghi. Hệ thống chịu lỗi tốt hơn (nếu Shard A sập, khách hàng ở Shard B vẫn đi Metro bình thường).
3. Những cạm bẫy "Máu và Nước mắt" của Sharding
Tôi thường khuyên các đàn em: Đừng Sharding cho đến khi bạn bị dí đến bước đường cùng. Bởi vì một khi đã Shard, code của bạn sẽ phức tạp lên gấp 10 lần.
Cạm bẫy 1: Sự biến mất của phép JOIN thần thánh
Bạn không thể JOIN hai bảng nằm ở hai máy chủ vật lý khác nhau! Nếu Bảng Users nằm ở Shard A, còn Bảng Orders của user đó không may lại bị phân vào Shard B (do chọn sai Shard Key). Chúc mừng, bạn phải query 2 lần ở 2 DB khác nhau, rồi dùng code PHP/C++ trên application server để tự lặp vòng for mà gộp dữ liệu lại.
Cạm bẫy 2: Hiện tượng Hotspot (Điểm nóng) Giả sử bạn chọn Shard Key theo "Ngày" (Giao dịch tháng 1 vào Shard 1, tháng 2 vào Shard 2...). Nghe có vẻ hợp lý? Thực tế: Shard của tháng hiện tại sẽ phải gánh 100% traffic Đọc/Ghi, trong khi các Shard của tháng trước nằm chơi xơi nước. Bạn Shard 10 server nhưng rốt cuộc vẫn chỉ có 1 server bị bốc khói. Chọn sai Shard Key là một án tử.
Cạm bẫy 3: Cơn ác mộng Re-sharding (Chia lại ván bài)
Hệ thống phát triển nhanh, 2 Shards bị đầy, bạn muốn gắn thêm Shard thứ 3. Khốn nỗi, thuật toán cũ là UserID % 2, bây giờ đổi thành UserID % 3. Mọi dữ liệu cũ bị sai lệch vị trí hoàn toàn! Bạn phải bảo trì hệ thống (Downtime), viết script để bốc hàng triệu dòng data từ Shard này chuyển sang Shard kia. Làm không khéo là mất trắng dữ liệu.
Lời kết
Mở rộng Database là một nghệ thuật của sự tốn kém và cẩn trọng. Hãy vắt kiệt sức mạnh của 1 server (Vertical Scaling). Tiếp theo, hãy tối ưu Index và Query. Sau đó, dùng Cache. Khi vẫn đuối, hãy dùng Master-Slave. Và chỉ khi mọi vũ khí đã cạn kiệt, mới nghĩ đến Sharding.
Đến đây, chúng ta đã tối ưu cả code, cả network, cả bộ nhớ và ổ cứng. Nhưng trong một kiến trúc Microservices phức tạp, việc các services gọi điện trực tiếp cho nhau (Synchronous HTTP Call) lại đẻ ra một nút thắt cổ chai khác. Nếu Service Bị chết, Service A gọi sang sẽ bị treo cứng lại, kéo sập cả dây chuyền.
Để giải quyết bài toán giao tiếp hệ thống, chúng ta phải tách rời chúng ra.
👉 Và đó là nội dung của bài tiếp theo: "Message Brokers (RabbitMQ, Kafka): Tại sao các hệ thống lớn không thể sống thiếu hàng đợi? Cách gỡ nút thắt cổ chai trong xử lý luồng dữ liệu lớn."
Anh em đã từng phải "chữa cháy" con server DB nào chạy 100% CPU chưa? Cùng bình luận kể khổ bên dưới nhé!
All rights reserved