Sharding, federation, denormalization: cách đọc database của khách trước khi viết câu truy vấn đầu tiên
Một câu JOIN chạy đúng trên laptop vẫn có thể trả về con số sai ở hệ thống của khách nếu bạn chưa biết họ chia dữ liệu ra sao.
Mỗi shard chỉ nên trả về SUM và COUNT; chỉ tính trung bình sau khi đã gộp, nếu không con số sẽ lệch.
Đồ hoạ: FDE Times
Tóm tắt nhanh
- Shard key là quyết định thiết kế quan trọng nhất và khó đảo ngược nhất, nên nó cho bạn biết khách thường hỏi dữ liệu theo trục nào.
- Federation khiến JOIN giữa hai database trở nên phức tạp. Denormalization giúp đọc nhanh hơn nhưng làm ghi chậm hơn, và các bản sao dễ lệch nhau.
- Hệ thống sharded thường chấp nhận eventual consistency, nên số liệu bạn tổng hợp qua nhiều shard có thể chưa khớp nhau ở cùng một thời điểm.
Thử hình dung tuần đầu ở một khách hàng bán lẻ. Bạn được cấp quyền read-only và được giao làm một agent trả lời câu hỏi “doanh thu tháng trước theo vùng là bao nhiêu”. Bạn viết SELECT ... JOIN customers ... GROUP BY region, chạy thử, rồi nhận về một con số thấp hơn báo cáo của phòng tài chính đến mức khó hiểu.
Câu truy vấn không sai cú pháp, database cũng không hỏng. Vấn đề là bạn mới chạm vào một phần dữ liệu. Có thể bảng orders bạn đang đọc chỉ là một shard. Có thể bảng customers nằm ở một database khác hẳn. Cũng có thể cột region trong orders là bản chép từ lâu và chưa từng được cập nhật.
Với một FDE, đọc hiểu cách khách đã mở rộng database là việc phải làm trước câu truy vấn đầu tiên. Tin tốt là việc này có quy trình rõ ràng, và bạn có thể tập cho thành thạo.
Vì sao database của khách không còn là một khối?
Khi dữ liệu và lượng người dùng tăng lên, một server đơn lẻ sẽ chạm trần. Tài liệu kiến trúc của Microsoft Azure cho biết scale theo chiều dọc, tức là thêm ổ đĩa, CPU, RAM và băng thông mạng, chỉ trì hoãn tạm thời giới hạn đó. Đến một lúc nào đó, khách buộc phải scale theo chiều ngang bằng cách thêm node vào hệ thống.
Có ba cách phổ biến, và mỗi cách để lại một kiểu dấu vết riêng. Sharding chia các dòng dữ liệu: mỗi shard có cùng schema nhưng giữ một tập con riêng và nằm trên một node lưu trữ.
Federation, còn gọi là functional partitioning, tách database theo chức năng, ví dụ users một nơi, products một nơi, orders một nơi. Denormalization được áp lên một database vốn đã chuẩn hóa để đổi lấy hiệu năng: đọc nhanh hơn, nhưng ghi chậm hơn.
Ba kỹ thuật này hay đi cùng nhau. Chính Azure khuyên các thiết kế sharded nên denormalize để những thực thể thường được truy vấn chung, như khách hàng và đơn hàng của họ, nằm trên cùng một shard, nhờ đó giảm số lần đọc riêng lẻ. Gặp một kỹ thuật thì bạn nên đi tìm luôn hai kỹ thuật còn lại.
Shard key cho bạn biết khách hỏi dữ liệu theo trục nào
Azure gọi shard key là quyết định thiết kế quan trọng nhất trong một hệ thống sharded, và đây là quyết định rất khó đảo ngược. Vì thế, khi đọc hệ thống của khách, shard key cho bạn thấy đội kỹ thuật của họ đã đặt cược rằng phần lớn truy vấn sẽ đi theo trục nào.
Nếu khách shard theo customer_id, mọi câu hỏi kiểu “đơn hàng của khách X” sẽ rơi vào đúng một shard và chạy rất nhanh. Còn câu hỏi của bạn, doanh thu theo vùng trên toàn bộ khách hàng, phải đi qua tất cả các shard.
Azure cũng cảnh báo rằng truy vấn và transaction xuyên shard rất tốn kém, và phần lớn hệ thống sharded tránh distributed transaction để dùng eventual consistency.
Kiểu sharding cũng là điều nên hỏi. Range sharding, chẳng hạn chia theo khoảng ngày, hợp với truy vấn theo khoảng nhưng không chia tải đều và khó rebalance. Hash sharding tránh được hotspot. Nhưng nếu khách dùng hash(key) mod N thì mỗi lần thêm hoặc bớt shard, phần lớn key sẽ bị gán lại và kéo theo một đợt di chuyển dữ liệu lớn.
Bạn có thể tự tính. Khi tăng từ 4 lên 5 shard, một key chỉ đứng yên nếu h mod 4 == h mod 5. Trong 20 giá trị dư có thể có, chỉ 4 giá trị thỏa điều kiện này, nên khoảng 80% key phải chuyển chỗ.
Con số đó liên quan trực tiếp đến bạn. Nếu khách đang rebalance, một đơn hàng có thể tạm thời nằm ở shard cũ, ở shard mới, hoặc ở cả hai. Hãy hỏi trước khi tin vào một con số tổng.
Ví dụ từ đầu đến cuối: doanh thu theo vùng
Quay lại khách bán lẻ giả định. Sau một buổi đọc connection string và hỏi DBA, bạn vẽ được bức tranh: orders được shard theo customer_id ra 3 shard. customers và products nằm ở hai database riêng theo kiểu federation. Trong orders có cột customer_region, được chép từ customers lúc tạo đơn hàng.
Nhờ cột chép trùng này, bạn không cần JOIN sang database khách hàng, và đó chính là lý do người ta denormalize. Việc còn lại là fan-out tới từng shard rồi gộp kết quả:
from collections import defaultdict
SHARDS = ["orders_shard_0", "orders_shard_1", "orders_shard_2"]
SQL = """
SELECT customer_region AS region,
SUM(total) AS revenue,
COUNT(*) AS n_orders
FROM orders
WHERE order_date >= %s AND order_date < %s
GROUP BY customer_region
"""
def revenue_by_region(start, end):
agg = defaultdict(lambda: {"revenue": 0, "n_orders": 0})
for dsn in SHARDS:
for region, revenue, n in run_query(dsn, SQL, [start, end]):
agg[region]["revenue"] += revenue
agg[region]["n_orders"] += n
for r in agg.values():
r["avg_order"] = r["revenue"] / r["n_orders"]
return dict(agg)
Chi tiết đáng chú ý là dòng tính avg_order. Mỗi shard chỉ trả về SUM và COUNT, còn giá trị trung bình được tính sau khi gộp.
Giả sử shard 0 có 10 đơn với trung bình 100, shard 1 có 1.000 đơn với trung bình 50. Trung bình của hai trung bình là 75, trong khi con số đúng là (1.000 + 50.000) / 1.010, xấp xỉ 50,5.
Giờ đến cột denormalized. customer_region là vùng của khách tại thời điểm đặt hàng. Nếu khách chuyển từ Hà Nội vào TP.HCM, các đơn cũ vẫn mang nhãn Hà Nội. Đây không phải lỗi kỹ thuật mà là câu hỏi nghiệp vụ: phòng tài chính muốn tính theo vùng lúc mua hay vùng hiện tại? Hãy hỏi trước khi agent của bạn tự trả lời thay họ.
Giả sử họ cần thêm doanh thu theo danh mục sản phẩm, mà danh mục chỉ có trong database products. Đây là chỗ federation gây khó, vì JOIN giữa hai database là việc phức tạp. Một cách làm thực tế là kéo bảng ánh xạ product_id → category (thường nhỏ) vào bộ nhớ hoặc vào warehouse, rồi gộp ở tầng ứng dụng thay vì cố JOIN xuyên database.
Năm bước trước câu truy vấn đầu tiên
Bước đầu là lập bản đồ: có bao nhiêu database, mỗi cái giữ chức năng gì, bảng nào bị chia shard. Connection string, file cấu hình ORM và những tên database kiểu orders_shard_07 là manh mối nhanh nhất. Bước hai là tìm shard key và kiểu sharding (hash, range hay theo bảng tra), rồi hỏi xem có đợt rebalance nào đang chạy không.
Bước ba là liệt kê các cột bị chép trùng và tìm nguồn gốc của từng cột: cột ấy được cập nhật khi nào, bằng job nào, có bị trễ không.
Bước bốn là xếp mỗi câu hỏi nghiệp vụ vào một trong ba nhóm: chỉ chạm một shard, phải fan-out, hoặc phải ghép dữ liệu từ nhiều database chức năng.
Bước cuối là thống nhất với khách về độ trễ chấp nhận được, vì hệ thống eventual consistency không hứa con số khớp tuyệt đối ở mọi thời điểm.
| Kỹ thuật | Dấu vết thường gặp | Truy vấn của bạn phải làm gì |
|---|---|---|
| Sharding | Nhiều database cùng schema, tên đánh số | Fan-out, gộp bằng SUM/COUNT, cẩn thận lúc rebalance |
| Federation | Mỗi database một miền nghiệp vụ | Ghép ở tầng ứng dụng hoặc warehouse, không JOIN trực tiếp |
| Denormalization | Cột trùng tên ở nhiều bảng hoặc nhiều database | Xác định cột gốc và cột chép mang giá trị tại thời điểm nào |
Với hệ thống NoSQL như MongoDB, câu hỏi vẫn vậy, chỉ khác hình thức. Quan hệ ở đó được dựng bằng cách nhúng (embed) hoặc tham chiếu dữ liệu thay vì dùng foreign key. Một document nhúng chính là một quyết định denormalize, nên bạn vẫn phải hỏi bản nhúng có được cập nhật khi bản gốc thay đổi hay không.
Những lỗi FDE hay mắc
Lỗi phổ biến nhất là tưởng mình đang đọc toàn bộ dữ liệu trong khi thật ra chỉ đọc một shard. Lỗi thứ hai là lấy trung bình của các trung bình, hoặc chạy COUNT(DISTINCT) trên từng shard rồi cộng lại, trong khi cùng một giá trị có thể xuất hiện ở nhiều shard.
Lỗi thứ ba khó thấy hơn: chạy truy vấn fan-out nặng vào giờ cao điểm trên database production. Azure nhấn mạnh rằng sharding mang lại độ phức tạp vận hành lâu dài, từ giám sát, backup từng shard đến thay đổi DDL trên mọi shard. Đội vận hành của khách sẽ không vui nếu truy vấn của bạn làm chậm mọi shard cùng lúc.
Hãy hỏi xem có read replica không. Azure cũng cho rằng khi nút thắt nằm ở phía đọc, nên dùng read replica và cache trước khi tính đến sharding.
Lỗi cuối cùng là đề xuất “gom hết về một database cho gọn” ngay tuần đầu. Shard key gần như không đảo ngược được. Hệ thống của khách là kết quả của những đánh đổi có thật, và việc của bạn là làm việc được với nó.
Thể hiện kỹ năng này trên CV thế nào?
Khi đọc JD của các vị trí FDE hay solutions engineer, nếu thấy yêu cầu tích hợp với hệ thống dữ liệu có sẵn của khách hoặc làm việc trên nhiều loại database khác nhau, đó là lúc kỹ năng này được dùng tới.
Trên CV, đừng chỉ ghi “biết sharding”. Hãy viết một dòng cụ thể, chẳng hạn: đã dựng lớp truy vấn fan-out qua N shard và thống nhất với khách cách xử lý số liệu trễ do eventual consistency.
Lần tới khi được cấp quyền vào database của khách, đừng viết SELECT ngay. Hãy dành một giờ vẽ bản đồ shard, các database chức năng và những cột bị chép trùng trước, vì phần lớn câu trả lời đúng nằm ở đó.