Menu
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Đang tải...

Bài 50 — Database Scaling: Read Replica, Sharding

Technical Product Manager Bài 50/60

Mở đầu — vì sao bài này quan trọng

Hãy tưởng tượng bạn là PM của một sàn thương mại điện tử Việt Nam. Sản phẩm chạy ngon suốt hai năm đầu với khoảng 50.000 đơn mỗi ngày. Rồi một chiến dịch ngày đôi 11/11 ập tới, traffic tăng gấp tám lần trong vòng vài giờ. Trang chủ load chậm, giỏ hàng "quay vòng quay" mãi không xong, và đội kỹ thuật nhắn vào nhóm chiến dịch một câu khiến mọi PM rùng mình: "Database đang nghẽn, chúng ta phải scale."

Câu chuyện này không phải hiếm. Trong hầu hết các hệ thống, database là điểm nghẽn xuất hiện sớm nhất và đau đớn nhất. Bạn có thể nhân bản (clone) tầng ứng dụng dễ dàng — thêm mười server web chỉ mất vài phút. Nhưng database giữ trạng thái (state) của toàn bộ hệ thống: đơn hàng, tồn kho, số dư ví. Bạn không thể vô tư nhân bản nó như nhân bản server web, vì khi đó dữ liệu ở các bản sao phải khớp nhau — một bài toán khó hơn rất nhiều.

Là một Technical PM, bạn không cần tự tay viết câu lệnh sharding. Nhưng bạn bắt buộc phải hiểu các lựa chọn scaling database, vì chúng quyết định trực tiếp đến chi phí hạ tầng, thời gian phát triển tính năng, và cả những ràng buộc sản phẩm mà bạn buộc phải chấp nhận. Khi engineer nói "tính năng này khó vì dữ liệu đã shard theo user_id", bạn cần hiểu họ đang nói gì để đàm phán phạm vi (scope) một cách thông minh. Bài này trang bị cho bạn đúng ngôn ngữ và khung tư duy đó.

Khái niệm cốt lõi

Vertical scaling (Scale up) — Nâng cấp con server hiện tại

Cách đơn giản nhất để xử lý nhiều tải hơn là làm cho server database to hơn: thêm CPU, thêm RAM, đổi ổ cứng SSD nhanh hơn. Đây gọi là scale up. Ưu điểm là cực kỳ đơn giản — bạn gần như không cần đổi một dòng code nào, chỉ cần thay phần cứng (hoặc trên cloud thì đổi instance type, ví dụ từ db.r5.large lên db.r5.4xlarge).

Nhược điểm là có trần vật lý. Server lớn nhất trên thị trường vẫn có giới hạn về số core và dung lượng RAM. Quan trọng hơn, chi phí tăng phi tuyến: một server gấp đôi sức mạnh thường tốn hơn gấp đôi tiền. Đến một ngưỡng nào đó, bạn trả gấp ba tiền chỉ để được thêm 30% hiệu năng. Và còn một rủi ro chí mạng: dù server có to đến đâu, nó vẫn là một điểm chết duy nhất (single point of failure). Server sập là cả hệ thống sập.

Lời khuyên thực tế: scale up gần như luôn là bước đầu tiên nên làm, vì nó mua cho bạn thời gian mà không cần đụng vào kiến trúc. Đừng vội nhảy sang sharding khi một con server lớn hơn vẫn còn dư địa.

Horizontal scaling (Scale out) — Thêm nhiều server

Khi một server không đủ, ta thêm nhiều server và chia tải ra. Đây là scale out. Cách này không có trần vật lý lý thuyết — cần thêm thì thêm máy. Nhưng cái giá là độ phức tạp: dữ liệu giờ nằm rải rác trên nhiều máy, và bạn phải giải quyết bài toán đồng bộ. Có hai chiến lược scale out chính cho database: read replica và sharding.

Read Replica — Nhân bản để đọc

Quan sát quan trọng: hầu hết ứng dụng đọc nhiều hơn ghi rất nhiều — tỷ lệ đọc/ghi thường là 10:1, có khi 100:1. Trên một sàn thương mại điện tử, mỗi đơn hàng được ghi một lần nhưng được đọc hàng chục lần (xem chi tiết, kiểm tra trạng thái, báo cáo, vận chuyển...).

Read replica khai thác chính điều đó. Bạn giữ một database primary (còn gọi là master) chịu trách nhiệm mọi thao tác ghi (INSERT, UPDATE, DELETE). Sau đó tạo ra một hoặc nhiều bản replica (slave) — là bản sao của primary, chỉ phục vụ đọc (SELECT). Mỗi khi primary có thay đổi, nó sao chép (replicate) thay đổi đó sang các replica.

Lợi ích: tải đọc khổng lồ được phân bổ ra nhiều replica, primary được "giải phóng" để tập trung xử lý ghi. Cần thêm sức đọc thì thêm replica.

Nhưng có một cái bẫy phải nhớ nằm lòng: replication lag (độ trễ sao chép). Việc sao chép từ primary sang replica không tức thời — thường trễ vài mili-giây, nhưng khi tải nặng có thể lên vài giây. Hậu quả là hiện tượng read-after-write kinh điển: người dùng vừa cập nhật avatar (ghi vào primary), tải lại trang (đọc từ replica chưa kịp đồng bộ) và thấy avatar cũ. Họ tưởng bị lỗi. Giải pháp thường dùng: với những thao tác cần "đọc lại ngay cái vừa ghi", ta cho đọc thẳng từ primary thay vì replica.

Sharding — Chia nhỏ dữ liệu

Read replica giải quyết bài toán đọc, nhưng không giải quyết bài toán ghi: mọi thao tác ghi vẫn dồn về một primary duy nhất, và toàn bộ dữ liệu vẫn phải vừa trong một máy. Khi lượng ghi quá lớn hoặc dữ liệu quá to (hàng terabyte), ta cần sharding.

Sharding là chia một bảng/database lớn thành nhiều mảnh nhỏ (shard), mỗi shard nằm trên một server riêng và giữ một tập con dữ liệu. Ví dụ: thay vì một bảng users 100 triệu dòng trên một máy, ta chia thành 10 shard, mỗi shard giữ 10 triệu user trên một máy riêng. Giờ cả tải ghi lẫn dung lượng đều được phân bổ.

Câu hỏi mấu chốt là: chia theo cái gì? Đây gọi là shard key (khóa phân mảnh) — và là quyết định quan trọng nhất, khó đảo ngược nhất khi sharding.

  • Range-based sharding: chia theo khoảng giá trị. Ví dụ user_id 1–10 triệu vào shard 1, 10–20 triệu vào shard 2. Đơn giản, nhưng dễ gây hotspot — nếu user mới (id lớn) hoạt động nhiều nhất, shard cuối "gánh" hết tải.
  • Hash-based sharding: lấy hàm băm của shard key (ví dụ hash(user_id) % số_shard) để quyết định shard. Phân bổ rất đều, tránh hotspot. Nhược điểm: khó truy vấn theo khoảng, và việc thêm shard (resharding) rất đau vì hash thay đổi.
Cái giá lớn nhất của sharding là mất khả năng JOIN và transaction xuyên shard. Khi dữ liệu của hai user nằm trên hai máy khác nhau, một câu query JOIN giữa họ trở nên cực kỳ phức tạp hoặc bất khả thi. Báo cáo tổng hợp "doanh thu toàn hệ thống" giờ phải gom từ mọi shard rồi cộng lại ở tầng ứng dụng. Đây chính là lý do engineer "than khó" mỗi khi PM xin một tính năng cắt ngang dữ liệu.

CAP Theorem — ràng buộc nền tảng

Một khái niệm bạn nên nắm để hiểu vì sao mọi thứ phức tạp: định lý CAP nói rằng trong một hệ phân tán, khi xảy ra phân vùng mạng (network partition — P, các máy mất liên lạc với nhau), bạn buộc phải chọn giữa Consistency (mọi máy thấy cùng một dữ liệu) và Availability (hệ thống vẫn trả lời). Bạn không thể có cả hai cùng lúc. Đây là lý do triết học giải thích vì sao replication lag tồn tại và vì sao "scale database mà vẫn đảm bảo dữ liệu luôn nhất quán tức thời" là điều không miễn phí.

Tình huống thực tế

Tình huống 1: Sàn TMĐT mùa sale — từ scale up đến read replica

Một sàn thương mại điện tử tại TP.HCM (giả định, quy mô tương tự Tiki giai đoạn đầu) chạy trên một PostgreSQL db.r5.2xlarge. Ngày thường 50.000 đơn/ngày, mọi thứ êm. Đến chiến dịch 11/11, traffic đọc (xem sản phẩm, kiểm tra giá, theo dõi đơn) tăng gấp 8 lần, CPU database chạm 95%, trang load 6 giây.

Đội kỹ thuật phản ứng theo hai bước. Bước một, họ scale up khẩn cấp lên db.r5.4xlarge — mua được vài tiếng nhưng CPU lại lên 90%. Bước hai, họ thêm 3 read replica và định tuyến toàn bộ truy vấn đọc (danh mục sản phẩm, chi tiết, tìm kiếm) sang replica, chỉ giữ ghi (đặt đơn, trừ kho) ở primary. CPU primary tụt về 40%, trang load còn 1,2 giây.

Bài học rút ra: Vì đặc thù TMĐT là đọc áp đảo ghi, read replica là "vũ khí" hiệu quả nhất và rẻ nhất cho mùa cao điểm. Là PM, bạn nên biết điều này để lên kế hoạch dung lượng trước mùa sale thay vì chữa cháy. Họ cũng học được một bài đau: trang "đơn hàng của tôi" ban đầu đọc từ replica, khiến khách vừa đặt xong vào xem lại không thấy đơn (replication lag) — sau phải sửa để trang đó đọc từ primary.

Tình huống 2: Ví điện tử — khi buộc phải shard

Một ví điện tử (giả định tương tự MoMo) có bảng transactions phình tới 4 tỷ dòng, 8 TB dữ liệu. Một con server không còn chứa nổi, và lượng ghi (mỗi giao dịch là một lần ghi) đạt 12.000 ghi/giây vượt khả năng của một primary. Read replica vô dụng ở đây vì vấn đề nằm ở ghidung lượng, không phải đọc.

Họ quyết định shard theo user_id với hash-based sharding, chia thành 64 shard. Lựa chọn shard key này rất khôn ngoan: 95% truy vấn của ví là "lịch sử giao dịch của một user" — và vì mọi giao dịch của một user nằm gọn trong một shard, các query này vẫn nhanh. Tải ghi được chia đều ra 64 máy.

Cái giá phải trả lộ ra khi đội tài chính cần báo cáo "tổng giá trị giao dịch toàn hệ thống theo giờ". Query này phải quét cả 64 shard rồi gộp lại — chậm và tốn kém. Giải pháp: họ tách riêng dữ liệu phân tích sang một hệ thống khác chuyên cho báo cáo, không bắt database giao dịch gánh.

Bài học rút ra: Chọn shard key = chọn truy vấn nào sẽ nhanh và truy vấn nào sẽ đau. Là PM, khi nghe "đã shard theo user_id", bạn phải hiểu ngay: mọi tính năng "theo user" sẽ dễ, mọi tính năng "cắt ngang nhiều user" sẽ khó. Hãy hỏi engineer về shard key trước khi hứa hẹn tính năng với khách hàng.

Tình huống 3: Startup SaaS chọn sai thời điểm sharding

Một startup SaaS B2B ở Singapore mới có 200 khách hàng doanh nghiệp nhưng đội kỹ thuật, vì đọc nhiều blog của các công ty lớn, quyết định shard database ngay từ đầu "cho chuẩn tương lai". Kết quả: mỗi tính năng mới mất gấp đôi thời gian phát triển vì phải xử lý logic xuyên shard, các bug liên quan transaction phân tán liên tục xuất hiện, trong khi dữ liệu thực tế chỉ vài chục GB — thừa sức nằm gọn trên một server với một read replica.

Sau sáu tháng vật lộn, họ gỡ bỏ sharding, quay về một primary + một replica. Tốc độ ra tính năng tăng gấp đôi.

Bài học rút ra: Sharding là công cụ của bài toán quy mô lớn thật sự. Tối ưu hóa quá sớm (premature optimization) trả giá bằng tốc độ sản phẩm — thứ mà một startup không thể đánh đổi. PM có vai trò quan trọng ở đây: khi đội kỹ thuật muốn "làm cho hoành tráng", hãy hỏi "dữ liệu hiện tại bao nhiêu, scale up còn dư địa không?"

Hướng dẫn từng bước

Khi hệ thống của bạn bắt đầu chạm giới hạn database, đây là trình tự ra quyết định nên theo, từ rẻ-đơn-giản đến đắt-phức-tạp:

  • Đo trước, đoán sau. Xác định nghẽn nằm ở đâu: CPU, RAM, disk I/O, hay số kết nối? Đọc nghẽn hay ghi nghẽn? Đừng scale mù khi chưa có số liệu.
  • Tối ưu trước khi scale. Nhiều khi vấn đề không phải thiếu máy mà là query tệ hoặc thiếu index. Thêm một index đúng chỗ có thể nhanh gấp trăm lần và rẻ hơn mọi giải pháp scaling. Luôn vét cạn bước này trước.
  • Thêm caching. Một tầng cache (như Redis) đứng trước database hấp thụ phần lớn truy vấn đọc lặp lại. Đây thường là bước hiệu quả-chi-phí cao nhất. (Chủ đề caching có bài riêng — ở đây chỉ cần biết nó là tuyến phòng thủ trước khi đụng vào database.)
  • Scale up. Nâng cấp lên instance lớn hơn. Đơn giản, không đổi code, mua thời gian. Làm bước này khi caching và tối ưu query đã hết dư địa.
  • Thêm read replica nếu nghẽn ở đọc. Định tuyến đọc sang replica, giữ ghi ở primary, và xử lý kỹ những điểm cần read-after-write.
  • Sharding — bước cuối cùng, chỉ dùng khi nghẽn ở ghi hoặc dữ liệu quá lớn cho một máy. Đầu tư nghiêm túc vào việc chọn shard key, vì đảo ngược nó cực kỳ tốn kém.
Với vai trò PM, đóng góp của bạn ở mỗi bước là: đặt câu hỏi đúng, đảm bảo đội không nhảy cóc xuống bước phức tạp khi bước đơn giản còn dư địa, và hiểu rõ ràng buộc sản phẩm mà mỗi bước tạo ra.

Lỗi thường gặp & mẹo

Lỗi 1: Sharding quá sớm. Như tình huống 3, đây là sai lầm kinh điển. Mẹo: hỏi "scale up lên instance lớn nhất rồi vẫn không đủ chứ?" Nếu chưa thử, chưa đến lúc shard.

Lỗi 2: Quên replication lag khi thiết kế tính năng. PM thường vẽ luồng "user cập nhật → user xem lại ngay" mà không biết replica trễ. Mẹo: với mọi luồng "ghi rồi đọc lại tức thì", hãy hỏi engineer "cái này đọc từ primary hay replica?".

Lỗi 3: Chọn shard key kém dẫn đến hotspot. Shard theo ngày tháng khiến mọi traffic hôm nay dồn vào một shard. Mẹo: shard key tốt phải phân bổ tải đềukhớp với mẫu truy vấn phổ biến nhất.

Lỗi 4: Tưởng read replica giúp được tải ghi. Replica chỉ giúp đọc. Nếu ghi nghẽn, thêm replica vô ích — thậm chí còn tệ hơn vì primary phải sao chép sang nhiều replica hơn.

Lỗi 5: Coi replica như backup. Replica đồng bộ lỗi cũng nhanh như đồng bộ dữ liệu đúng. Lỡ xóa nhầm bảng, replica cũng mất theo. Backup là một việc riêng biệt.

Mẹo PM vàng: Khi planning một tính năng mới, hãy thêm một câu hỏi vào checklist: "Dữ liệu này được sharding/replicate thế nào, và tính năng của tôi có cắt ngang shard không?" Câu hỏi này giúp bạn ước lượng độ phức tạp trước khi cam kết với stakeholder.

Bài tập thực hành

  • Phân loại nghẽn: Với mỗi tình huống sau, hãy quyết định nên dùng scale up, read replica, hay sharding — và giải thích: (a) Một blog tin tức có 2 triệu lượt đọc/ngày nhưng chỉ đăng 20 bài/ngày. (b) Một game mobile ghi 50.000 sự kiện/giây vào database, dữ liệu đã 10 TB. (c) Một app nội bộ 500 nhân viên, database 5 GB, thỉnh thoảng chậm.
  • Chọn shard key: Bạn là PM của một ứng dụng gọi xe. Bảng trips (chuyến đi) cần shard. Có ba ứng viên shard key: trip_id, driver_id, city_id. Với mỗi lựa chọn, hãy chỉ ra truy vấn nào sẽ nhanh và truy vấn nào sẽ đau (gợi ý: nghĩ về "lịch sử chuyến của một tài xế" và "tất cả chuyến trong một thành phố").
  • Bắt lỗi replication lag: Hãy rà soát sản phẩm bạn đang làm và liệt kê 3 luồng có dạng "user ghi rồi xem lại ngay". Với mỗi luồng, ghi chú nó có nên đọc từ primary không.
  • Đối thoại với engineer: Viết ra 5 câu hỏi bạn sẽ hỏi tech lead trong buổi review kiến trúc khi đội đề xuất "chúng ta nên shard database". Mục tiêu: kiểm tra xem quyết định có chín muồi chưa.

Tóm tắt

Database là điểm nghẽn xuất hiện sớm nhất vì nó giữ trạng thái hệ thống, không thể nhân bản vô tư như tầng ứng dụng. Có hai hướng scaling: vertical (scale up) — nâng cấp một server, đơn giản nhưng có trần vật lý; và horizontal (scale out) — thêm nhiều server, không có trần nhưng phức tạp.

Trong scale out có hai công cụ chính. Read replica nhân bản dữ liệu để chia tải đọc — hiệu quả vì hầu hết hệ thống đọc nhiều hơn ghi, nhưng phải cảnh giác với replication lag. Sharding chia dữ liệu thành nhiều mảnh để chia tải ghi và dung lượng — mạnh nhưng đắt: mất JOIN/transaction xuyên shard, và việc chọn shard key là quyết định khó đảo ngược nhất.

Trình tự ra quyết định nên theo: đo nghẽn → tối ưu query và index → caching → scale up → read replica → và cuối cùng mới là sharding. Là Technical PM, giá trị của bạn không phải tự viết code scaling, mà là đặt đúng câu hỏi, ngăn đội tối ưu hóa quá sớm, và hiểu được các ràng buộc sản phẩm mà mỗi lựa chọn kiến trúc tạo ra — để bạn cam kết với stakeholder bằng hiểu biết, chứ không phải bằng hy vọng.