Thứ Hai đầu tháng, dashboard Grafana hiện CPU Postgres chạm 92% liên tục. Bảng orders đã vượt 2 tỷ row, index B-tree sâu đến mức mỗi lần INSERT phải traverse qua 5-6 level. Autovacuum chạy không kịp vì bảng quá lớn, dead tuple tích tụ làm sequential scan chậm thêm. Team đã thêm 3 read replica nhưng primary vẫn nghẽn vì write traffic tăng 40% mỗi quý, replica chỉ giảm tải read, không giúp gì cho write. Vertical scaling? Con máy hiện tại đã là db.r6g.8xlarge, 32 vCPU, 256 GB RAM. Máy lớn hơn tồn tại nhưng giá tăng gấp đôi mà throughput chỉ tăng 30-40%, và luôn có trần: không tồn tại máy có 10,000 vCPU.
Đây là lúc sharding, chia data ra nhiều database instance, trở thành lựa chọn không thể tránh. Nhưng sharding không phải “bật switch” mà là thay đổi kiến trúc sâu, ảnh hưởng đến mọi query, mọi transaction, mọi migration sau này. Bài này đi vào chi tiết: khi nào thực sự cần shard, chọn shard key thế nào, các chiến lược phân phối data, và những cạm bẫy mà team nào cũng gặp ít nhất một lần.
Vertical scaling có trần
Trước khi nói sharding, cần hiểu rõ vì sao “mua máy to hơn” không phải giải pháp vĩnh viễn.
Vertical scaling, thêm CPU, RAM, SSD nhanh hơn cho cùng một máy, hoạt động tốt đến một ngưỡng. Từ 4 vCPU lên 16 vCPU, throughput tăng gần tuyến tính vì bottleneck thường nằm ở CPU. Nhưng từ 64 vCPU lên 128 vCPU, Amdahl’s Law bắt đầu chi phối: phần sequential trong database engine (WAL write, lock manager, buffer pool management) không thể song song hoá, nên throughput tăng chậm dần. Đến một điểm, tăng gấp đôi tiền chỉ tăng 20-30% performance.
RAM cũng vậy. Khi working set (phần data được truy cập thường xuyên) vừa RAM, mọi read là cache hit, nhanh. Khi working set vượt RAM, mỗi cache miss là random IO xuống SSD. 256 GB RAM nghe nhiều nhưng bảng 2 tỷ row với index có thể chiếm 500 GB+ trên disk, working set không bao giờ fit hoàn toàn.
Và quan trọng nhất: vertical scaling không giải quyết được write bottleneck. Dù máy to cỡ nào, PostgreSQL vẫn có một WAL writer, một set of lock structures. Write-heavy workload trên một instance luôn có trần, và đó chính xác là lý do sharding tồn tại.
Sharding là gì, và không phải là gì
Hiểu nôm na thì sharding giống như chia sách trong thư viện ra nhiều kệ riêng biệt, thay vì nhét tất cả sách lên một kệ dài đến mức tìm mãi không ra, mỗi kệ chứa một phần theo vần chữ cái, nhân viên biết ngay chạy về kệ nào.
Sharding là horizontal partitioning, chia data của một bảng logic ra nhiều database instance vật lý, mỗi instance giữ một subset của data. Bảng orders 2 tỷ row có thể chia thành 8 shard, mỗi shard giữ ~250 triệu row trên một Postgres instance riêng. Mỗi shard có CPU, RAM, disk riêng, write throughput scale gần tuyến tính khi thêm shard.
Cần phân biệt rõ sharding với table partitioning của PostgreSQL (hoặc MySQL). Table partitioning chia bảng thành nhiều partition nhưng vẫn trên cùng một instance, giúp vacuum, query pruning, và quản lý data lifecycle, nhưng không giải quyết CPU hay write bottleneck. Sharding chia data ra nhiều instance, giải quyết cả read, write, và storage.
Postgres)] Router -->|user_id % 4 = 1| S1[(Shard 1
Postgres)] Router -->|user_id % 4 = 2| S2[(Shard 2
Postgres)] Router -->|user_id % 4 = 3| S3[(Shard 3
Postgres)]
Mô hình trên minh hoạ hash-based sharding đơn giản: application tính user_id % 4 để biết data nằm ở shard nào, route query đến đúng instance. Mỗi shard là một Postgres database đầy đủ, có thể có read replica riêng.
Shard key, quyết định quan trọng nhất
Nếu toàn bộ bài này chỉ còn một đoạn, thì đây là đoạn đó. Shard key quyết định mọi thứ: data phân bố đều hay lệch, query có cần đi qua nhiều shard hay chỉ một, có thể join được hay không, resharding sau này đau đến đâu.
Tiêu chí chọn shard key
Shard key tốt phải thoả mãn ba tiêu chí đồng thời, và đây là lý do chọn shard key khó, vì ba tiêu chí này đôi khi mâu thuẫn nhau.
Phân bố đều (uniform distribution): data phải chia đều giữa các shard. Nếu 80% order thuộc về 10% user (whale accounts), shard key user_id với hash modulo sẽ tạo hotspot, shard chứa whale bị tải gấp nhiều lần shard khác. Phân bố đều không có nghĩa mỗi shard có chính xác cùng số row, mà là tải (QPS, CPU, IO) tương đối đều.
Locality, query chỉ cần đến một shard: phần lớn query của ứng dụng nên chỉ cần truy cập một shard duy nhất. Nếu query phổ biến nhất là “lấy tất cả order của user X”, thì shard key user_id hoàn hảo, mọi order của user X nằm trên cùng shard. Nhưng nếu query phổ biến là “tổng doanh thu theo ngày”, shard key user_id buộc phải query tất cả shard rồi aggregate, chậm và phức tạp.
Ổn định (immutability): shard key không nên thay đổi sau khi record được tạo. Nếu shard key là region và user chuyển region, record cần migrate từ shard này sang shard khác, phức tạp, tốn resource, và có thể gây inconsistency trong lúc migrate.
Ví dụ shard key theo domain
Trong hệ thống SaaS multi-tenant, tenant_id thường là shard key tốt nhất. Mọi data của một tenant nằm trên cùng shard, query trong context tenant chỉ đến một shard, join giữa các bảng trong cùng tenant hoạt động bình thường, và tenant không bao giờ “chuyển” sang tenant khác. Nhược điểm: tenant lớn (enterprise customer) có thể tạo hotspot.
Trong social network, user_id phổ biến. Post, comment, notification của user nằm cùng shard. Nhưng news feed lại cần data từ nhiều user (bạn bè), cross-shard query. Đây là trade-off kinh điển và là lý do các hệ thống social thường denormalize feed thành bảng riêng, shard theo viewer_id.
Trong e-commerce, order_id nghe hợp lý nhưng thực tế có vấn đề: query “tất cả order của user X” phải scatter tất cả shard. user_id tốt hơn cho phần lớn use case, nhưng query “tất cả order trong ngày” cho admin dashboard lại phải scatter. Giải pháp thường là shard operational data theo user_id, và replicate sang analytical store (data warehouse) cho reporting.
Chiến lược phân phối data
Range-based sharding
Chia data theo khoảng giá trị: shard 0 chứa user_id 1–1,000,000, shard 1 chứa 1,000,001–2,000,000, v.v. Ưu điểm là range query (WHERE user_id BETWEEN 500000 AND 600000) chỉ cần đến một shard. Dễ hiểu, dễ implement.
Nhược điểm nghiêm trọng: dễ tạo hotspot. User mới đăng ký có ID cao nhất, tất cả write dồn vào shard cuối cùng. Bảng log theo timestamp cũng vậy, shard chứa “hiện tại” luôn nóng nhất. Range-based sharding chỉ phù hợp khi data distribution tương đối đều theo range tự nhiên, ví dụ shard theo region khi mỗi region có traffic tương đương.
Hash-based sharding
Tính hash của shard key, modulo cho số shard: shard_id = hash(user_id) % num_shards. Hash function phân bố đều nên data tự động phân bố đều giữa các shard, không phụ thuộc vào pattern của key. Đây là chiến lược phổ biến nhất.
Nhược điểm: range query không thể prune, WHERE user_id BETWEEN 100 AND 200 phải scatter tất cả shard vì hash phá vỡ thứ tự tự nhiên. Và khi thêm shard (thay đổi num_shards), hầu hết data phải di chuyển vì hash(key) % 4 khác hoàn toàn hash(key) % 5.
Directory-based sharding
Dùng lookup table (directory) ánh xạ từng key đến shard: “user 42 → shard 2”, “user 43 → shard 0”. Linh hoạt nhất, có thể di chuyển bất kỳ key nào sang bất kỳ shard nào mà chỉ cần update directory. Phù hợp khi cần handle tenant lớn bằng cách cho tenant đó shard riêng.
Nhược điểm: directory là single point of failure và potential bottleneck. Mỗi query cần lookup directory trước, thêm một hop. Directory phải có availability cực cao và latency cực thấp, thường cache trong memory. Nếu directory corrupt hoặc mất, toàn bộ hệ thống không biết data ở đâu.
So sánh nhanh
Range-based đơn giản, hỗ trợ range query, nhưng dễ hotspot. Hash-based phân bố đều, nhưng mất range query và resharding đau. Directory-based linh hoạt nhất, nhưng thêm complexity và single point of failure. Trong thực tế, hash-based là lựa chọn mặc định cho phần lớn hệ thống, với directory-based bổ sung khi cần handle special case (whale tenant).
Consistent hashing, giảm đau khi thêm shard
Vấn đề lớn nhất của hash modulo đơn giản: khi thêm một shard (từ 4 lên 5), hash(key) % 4 và hash(key) % 5 cho kết quả khác nhau cho hầu hết key. Nghĩa là phần lớn data phải di chuyển, với 2 tỷ row, đó có thể là hàng ngày downtime hoặc migration phức tạp.
Consistent hashing giải quyết vấn đề này. Hình dung một vòng tròn (ring) từ 0 đến 2^32. Mỗi shard được gán một hoặc nhiều vị trí trên ring (gọi là virtual node). Mỗi key được hash ra vị trí trên ring, rồi đi theo chiều kim đồng hồ đến shard gần nhất.
Khi thêm shard D, chỉ data nằm giữa vị trí shard D và shard kế trước trên ring cần di chuyển, khoảng 1/N tổng data thay vì gần 100%. Với 4 shard, thêm shard thứ 5 chỉ cần di chuyển ~20% data thay vì ~80% với modulo đơn giản. Đây là sự khác biệt giữa “resharding mất vài giờ” và “resharding mất vài ngày”.
Virtual node (mỗi shard có nhiều vị trí trên ring) giúp phân bố đều hơn, nếu mỗi shard chỉ có một vị trí, data có thể lệch đáng kể vì vị trí trên ring random. 100-200 virtual node per shard thường cho phân bố đủ đều.
Consistent hashing được dùng rộng rãi: DynamoDB, Cassandra, Riak, memcached, và nhiều hệ thống cache phân tán khác.
Cross-shard query, nỗi đau không tránh được
Khi data nằm trên nhiều shard, mọi query cần data từ nhiều shard đều phải scatter-gather: gửi query tới tất cả (hoặc nhiều) shard, chờ tất cả trả kết quả, rồi merge ở application layer.
Scatter-gather
Query “top 10 order giá trị cao nhất toàn hệ thống” trên 8 shard nghĩa là: gửi query tới 8 shard song song, mỗi shard trả top 10 local, application nhận 80 row rồi sort lại lấy top 10 global. Latency của scatter-gather bằng latency của shard chậm nhất (tail latency), nếu 7 shard trả trong 10ms nhưng shard thứ 8 đang GC pause mất 500ms, toàn bộ query mất 500ms.
Scatter-gather với aggregation (SUM, COUNT, AVG) phức tạp hơn. SUM và COUNT có thể tính từng shard rồi cộng lại. AVG thì không, phải tính SUM và COUNT từ mỗi shard rồi chia. DISTINCT còn tệ hơn: phải merge toàn bộ distinct set từ mỗi shard.
Với hệ thống 4-8 shard, scatter-gather chấp nhận được cho query không thường xuyên (admin dashboard, report). Nhưng nếu query phổ biến nhất của ứng dụng cần scatter, bạn đã chọn sai shard key, quay lại xem xét lại.
Denormalization để tránh cross-shard
Cách phổ biến nhất để tránh cross-shard query là denormalize: copy data sang shard cần dùng, chấp nhận data redundancy để đổi lấy query locality.
Ví dụ: bảng orders shard theo user_id, bảng products không shard (nhỏ, ít thay đổi). Thay vì join orders với products cross-shard, copy thông tin product cần thiết (tên, giá tại thời điểm order) vào bảng orders. Mỗi shard có đủ thông tin trả về order detail mà không cần query shard khác hay bảng product.
Trade-off rõ ràng: storage tăng, và khi product thay đổi (tên, ảnh), bản copy trong order cũ vẫn giữ giá trị cũ. Nhưng đối với order history, đây thường là hành vi đúng, order nên giữ giá tại thời điểm mua, không phải giá hiện tại.
Denormalization cần discipline: phải rõ ràng field nào là copy, field nào là source of truth. Nếu không, developer sẽ cập nhật bản copy khi data gốc thay đổi, tạo ra hai source of truth, đây là anti-pattern kinh điển dẫn đến data inconsistency.
Global table, bảng không shard
Một số bảng nhỏ và ít thay đổi không cần shard: countries, currencies, product_categories, config. Thay vào đó, replicate toàn bộ bảng này tới mọi shard. Mỗi shard có bản copy đầy đủ, join với bảng global là local join, không cần cross-shard.
Vitess gọi đây là “reference table”, Citus gọi là “reference table” luôn. Yêu cầu: bảng phải nhỏ (dưới vài triệu row) và ít write. Nếu bảng global có write thường xuyên, phải broadcast write tới mọi shard, consistency giữa các bản copy trở thành bài toán phân tán.
Cross-shard transaction, tránh nếu có thể
Transaction trên nhiều shard cần two-phase commit (2PC) hoặc tương đương: coordinator gửi “prepare” tới mọi shard liên quan, chờ tất cả acknowledge, rồi gửi “commit”. Nếu bất kỳ shard nào fail ở bước prepare, toàn bộ transaction phải rollback trên mọi shard.
Vấn đề thực tế của 2PC nhiều đến mức hầu hết hệ thống production tránh cross-shard transaction bằng cách thiết kế shard key sao cho transaction chỉ cần đến một shard.
Latency: 2PC có ít nhất 2 round-trip giữa coordinator và mỗi shard, nếu shard ở region khác, mỗi round-trip thêm hàng chục millisecond. Transaction local trên PostgreSQL mất microsecond, 2PC cross-shard có thể mất hàng chục đến hàng trăm millisecond.
Availability: trong 2PC, nếu coordinator crash sau khi gửi “prepare” nhưng trước “commit”, mọi shard giữ lock và chờ, in-doubt transaction. Shard không tự quyết commit hay rollback được, phải chờ coordinator hồi phục. Thời gian chờ đó, row bị lock, query khác bị block.
Throughput: lock giữ lâu hơn (vì round-trip latency), contention tăng, throughput giảm. Với workload write-heavy, cross-shard transaction có thể giảm throughput xuống dưới mức single-instance vì overhead coordination.
Giải pháp thiết kế: đảm bảo mọi business transaction (ví dụ “tạo order + trừ inventory + charge payment”) nằm trên cùng shard, bằng cách chọn shard key sao cho tất cả entity liên quan cùng shard key. Nếu order, order_item, payment đều shard theo user_id, một order transaction chỉ cần đến shard chứa user đó.
Khi cross-shard transaction thực sự không thể tránh, dùng saga pattern, chuỗi local transaction với compensating action khi fail. Saga không cho atomicity như 2PC nhưng tránh được vấn đề lock và availability. Trade-off là phải viết compensating logic (undo) cho mỗi bước, phức tạp nhưng viable cho production.
Resharding, nỗi đau tất yếu
Bắt đầu với 4 shard, traffic tăng, cần 8 shard. Resharding, chia lại data cho số shard mới, là operation phức tạp nhất trong vòng đời của hệ thống sharded.
Vấn đề cốt lõi
Với hash modulo đơn giản (hash % 4 → hash % 8), hầu hết row phải di chuyển. Với consistent hashing, chỉ ~1/N data cần di chuyển, nhưng vẫn có thể là hàng trăm triệu row. Di chuyển data trong khi application vẫn đọc/ghi là bài toán online migration, khó và đắt.
Chiến lược online migration
Double-write: application ghi vào cả shard cũ và shard mới trong suốt quá trình migration. Background process copy data cũ từ shard nguồn sang shard đích. Khi copy xong, switch read sang shard mới, tắt write sang shard cũ. Ưu điểm: zero downtime. Nhược điểm: write throughput giảm (ghi 2 lần), logic double-write phức tạp, cần handle conflict khi data được update trong lúc đang copy.
Ghost table approach (tương tự gh-ost cho schema migration): tạo bảng mới trên shard đích, copy data batch nhỏ, đồng thời capture change qua CDC (Change Data Capture) hoặc trigger để không bỏ sót update. Khi catchup xong (lag < threshold), cutover: switch traffic sang shard mới trong một khoảng thời gian ngắn (vài giây). Đây là cách Vitess xử lý resharding.
Expand-by-splitting: thay vì thay đổi hàm hash, chia mỗi shard cũ thành 2 shard mới (4 → 8). Mỗi shard cũ chỉ cần split data thành 2 phần, ít data movement hơn so với thay đổi hàm hash toàn bộ. DynamoDB và nhiều hệ thống NoSQL dùng cách này.
Dù chiến lược nào, resharding luôn cần planning cẩn thận, testing kỹ trên staging, và rollback plan rõ ràng. Đây không phải operation chạy ad-hoc lúc 4 giờ chiều thứ Sáu.
Shard provisioning trước
Một kỹ thuật giảm đau resharding: provision nhiều shard logic hơn cần thiết từ đầu, nhưng đặt nhiều shard logic trên cùng một instance vật lý.
Ví dụ: thay vì bắt đầu với 4 shard, tạo 64 shard logic nhưng chỉ chạy trên 4 server (mỗi server host 16 shard logic). Khi cần scale, di chuyển shard logic từ server này sang server mới, data không cần chia lại, chỉ cần move nguyên shard. Từ 4 server lên 8 server: mỗi server cũ chuyển 8 shard logic sang server mới, mỗi server giờ host 8 shard.
Kỹ thuật này đơn giản hoá scale-out đáng kể, nhưng cần chọn số shard logic hợp lý từ đầu, quá ít thì vẫn phải reshard, quá nhiều thì overhead quản lý tăng.
Hotspot, khi phân bố không đều
Dù chọn shard key cẩn thận và dùng hash phân bố đều, hotspot vẫn có thể xảy ra vì data access pattern không đều.
Celebrity problem
Trên social network, user có 10 triệu follower tạo post mới, shard chứa user đó nhận burst write (notification, feed fanout). Dù data phân bố đều theo user count, traffic phân bố theo follower count, và follower count tuân theo power law, không phải uniform distribution.
Giải pháp cho celebrity problem thường là application-level: detect celebrity user (follower > threshold), xử lý riêng, fanout theo pull model thay vì push, hoặc dùng queue để spread write over time. Không có shard key nào giải quyết được bài toán này, vì vấn đề nằm ở logic, không phải data distribution.
Time-based skew
Bảng events shard theo hash(event_id), data phân bố đều. Nhưng query phổ biến nhất là “events trong 24 giờ qua”, event mới nằm rải đều trên mọi shard nhờ hash, nên query phải scatter tất cả shard. Nếu shard theo timestamp thì query chỉ cần shard “hiện tại”, nhưng shard đó nhận 100% write, hotspot.
Đây là trade-off kinh điển giữa write distribution và read locality cho time-series data. Giải pháp phổ biến: composite shard key hash(entity_id, time_bucket), phân bố write đều hơn pure timestamp, đồng thời giới hạn scatter cho query time-range vì chỉ cần query shard chứa time bucket liên quan.
Mitigation chung
Monitor per-shard metric riêng biệt: CPU, QPS, latency P99 cho từng shard. Hotspot lộ rõ khi một shard consistently cao hơn các shard khác. Nếu hotspot do data skew (một key quá lớn), giải pháp là split key đó thành sub-key hoặc di chuyển key lớn sang shard riêng (directory-based override).
Nếu hotspot do access pattern (celebrity problem), giải pháp nằm ở application layer, không phải database layer, caching, rate limiting, async processing.
Application-level vs database-level sharding
Có hai cách triển khai sharding: tự viết logic routing trong application, hoặc dùng middleware/extension xử lý transparent.
Application-level sharding
Application biết shard key, tính shard ID, chọn database connection tương ứng. Code rõ ràng, kiểm soát hoàn toàn, nhưng logic sharding phải implement trong mọi service truy cập database. Mỗi lần thêm shard, sửa config ở mọi service. Query cross-shard phải viết scatter-gather thủ công.
Ưu điểm: không phụ thuộc middleware, hiểu rõ hành vi vì code nằm trong tay. Nhược điểm: effort lớn, dễ sai khi có nhiều service, và mọi feature mới phải tính đến sharding.
Middleware và extension
Vitess (YouTube, Slack, GitHub dùng) đứng giữa application và MySQL, route query tự động dựa trên shard key trong query. Application giao tiếp với Vitess như một MySQL instance duy nhất, Vitess lo routing, resharding, schema migration. Trade-off: thêm một layer cần vận hành, debug khó hơn khi query đi qua proxy.
Citus là extension của PostgreSQL, biến một Postgres cluster thành distributed database. Bảng được khai báo distributed với shard key, Citus tự động route query, xử lý cross-shard join khi cần. Ưu điểm lớn: vẫn dùng Postgres SQL đầy đủ, ORM và tool vẫn hoạt động phần lớn. Nhược điểm: một số query pattern không được hỗ trợ hoặc chậm, và Citus có learning curve riêng.
ProxySQL cho MySQL hoặc PgBouncer + custom routing cho PostgreSQL, nhẹ hơn Vitess/Citus nhưng ít feature hơn, thường chỉ lo connection routing mà không xử lý cross-shard query hay resharding.
Lời khuyên: nếu mới bắt đầu shard và dùng PostgreSQL, thử Citus trước, nó giảm đáng kể effort so với application-level sharding. Nếu dùng MySQL và cần resharding thường xuyên, Vitess là lựa chọn production-proven. Application-level sharding chỉ nên cân nhắc khi có yêu cầu đặc biệt mà middleware không đáp ứng.
Khi nào KHÔNG nên shard
Sharding là giải pháp cuối cùng, không phải giải pháp đầu tiên. Trước khi shard, hãy chắc chắn đã thử hết các cách ít phức tạp hơn, mỗi cách đều rẻ hơn sharding cả về effort triển khai lẫn complexity vận hành.
Index optimization: kiểm tra query plan, thêm index thiếu, sửa query full table scan. Một index đúng có thể giảm query time từ 10 giây xuống 10 millisecond, giải quyết bottleneck mà không đụng gì đến kiến trúc. Bảng 2 tỷ row mà thiếu index trên cột filter phổ biến thì thêm index là việc đầu tiên, không phải shard.
Query optimization: N+1 query, missing join, SELECT * khi chỉ cần 3 cột, subquery có thể viết lại thành JOIN, những thứ này cải thiện throughput đáng kể mà không cần thay đổi infrastructure.
Read replica: write traffic OK nhưng read quá tải? Thêm read replica. PostgreSQL streaming replication setup trong vài giờ, application route read query sang replica. Với workload read-heavy (90% read), 3-5 replica có thể tăng read capacity 3-5 lần.
Connection pooling: PgBouncer hoặc PgCat trước database giảm connection overhead. PostgreSQL process-per-connection model tạo overhead lớn khi có hàng nghìn connection, pooler giảm xuống vài chục backend connection, tăng throughput đáng kể.
Table partitioning: PostgreSQL native partitioning chia bảng thành partition theo range (thời gian phổ biến nhất) hoặc list. Vacuum chạy nhanh hơn trên partition nhỏ, query pruning skip partition không liên quan, drop partition cũ thay vì DELETE hàng triệu row. Vẫn trên cùng instance nhưng cải thiện manageability đáng kể.
Archive old data: bảng orders 2 tỷ row nhưng query production chỉ cần order 2 năm gần nhất? Move order cũ sang bảng archive hoặc cold storage (S3 + Parquet cho analytics). Bảng chính giảm xuống 500 triệu row, working set fit RAM, performance cải thiện mà không cần shard.
Vertical scaling: trước khi shard, thử upgrade instance lên size lớn hơn. Nếu chi phí acceptable và performance đủ cho 6-12 tháng tới, đó là giải pháp đơn giản nhất. Sharding là quyết định kiến trúc dài hạn, không nên làm chỉ vì tiết kiệm tiền instance trong ngắn hạn.
Progression hợp lý mà phần lớn hệ thống nên đi qua trước khi shard:
optimization] --> B[Connection
pooling] B --> C[Read replica] C --> D[Table
partitioning] D --> E[Archive
old data] E --> F[Vertical
scaling] F --> G[Sharding]
Nếu đã đi qua hết progression trên mà vẫn bottleneck, thường là write throughput hoặc storage vượt khả năng single instance, thì mới shard. Sharding quá sớm nghĩa là chịu complexity của distributed system khi chưa cần, trong khi đáng lẽ chỉ cần thêm index hoặc read replica.
Anti-pattern
Shard quá sớm
Startup mới ra mắt, 10,000 user, database 5 GB, đã shard thành 8 instance. Mỗi shard chỉ có vài trăm MB data, chạy trên instance nhỏ nhất vẫn thừa resource. Nhưng team phải maintain 8 database, schema migration chạy 8 lần, monitoring 8 instance, backup 8 instance (lúc này mới nhận ra “distributed system” không phải badge of honor mà là khoản thuế vận hành phải trả mỗi ngày). Chi phí vận hành tăng 8 lần cho vấn đề chưa tồn tại. Khi cần thêm column mới, migration phải coordinate 8 shard, thay vì ALTER TABLE một lần trên single instance.
Shard khi cần, không shard vì sợ. “Premature optimization is the root of all evil”, câu này đặc biệt đúng với sharding vì chi phí undo (merge shard lại) cực cao.
Chọn sai shard key
Shard theo order_id khi 90% query là “tất cả order của user X”. Mỗi query user profile phải scatter tất cả shard, latency tăng, code phức tạp với scatter-gather logic. Nếu chọn user_id từ đầu, query phổ biến nhất chỉ cần một shard.
Sửa shard key sau khi đã shard, đó chính là resharding, operation đắt nhất. Nên dành nhiều thời gian phân tích query pattern trước khi chọn shard key. List top 10 query theo QPS, kiểm tra mỗi query cần bao nhiêu shard với shard key ứng cử, query phổ biến nhất phải single-shard.
Ignore cross-shard join
Thiết kế schema với nhiều foreign key join, shard xong mới phát hiện join không hoạt động cross-shard. Refactor lại schema để denormalize, effort lớn, bug nhiều, và data model phình to. Trước khi shard, phải review tất cả JOIN trong codebase và quyết định: join nào có thể eliminate bằng denormalize, join nào có thể chuyển sang application-level, join nào bắt buộc phải trên cùng shard (đặt cùng shard key).
Không monitor per-shard
Sau khi shard, team chỉ monitor aggregate metric (tổng QPS, tổng error rate) mà không monitor từng shard riêng. Một shard có thể overload 90% CPU trong khi 7 shard khác chỉ 20%, aggregate hiện 30%, trông “bình thường”. Alert per-shard là bắt buộc: CPU, memory, disk, QPS, latency P99, replication lag (nếu shard có replica) cho từng shard instance.
Sharding và các pattern liên quan
Sharding không đứng riêng lẻ, nó tương tác với nhiều pattern kiến trúc khác.
CQRS (Command Query Responsibility Segregation): shard write store theo entity key, replicate sang read store tối ưu cho query pattern khác. Ví dụ: order data shard theo user_id cho write, replicate sang Elasticsearch không shard (hoặc shard theo thời gian) cho search theo product, date range, status. Mỗi store tối ưu cho use case riêng.
Event sourcing: event store shard theo aggregate_id (thường là entity key). Mọi event của một aggregate nằm trên cùng shard, replay nhanh, consistency đảm bảo. Projection (materialized view) có thể shard khác hoặc không shard, tuỳ query pattern.
Multi-tenancy: shard theo tenant_id tự nhiên map với isolation requirement. Tenant lớn có thể có shard riêng (dedicated shard), tenant nhỏ share shard. Directory-based sharding phù hợp ở đây vì cần flexibility mapping tenant → shard.
Operational concern sau khi shard
Sau khi shard, nhiều operation quen thuộc trên single database trở nên phức tạp hơn.
Schema migration: ALTER TABLE phải chạy trên mọi shard. Nếu migration lỗi ở shard 5/8, 5 shard có schema mới, 3 shard còn schema cũ, application phải handle cả hai version hoặc rollback 5 shard. Dùng tool migration có dry-run và atomic rollback per shard. Vitess xử lý tốt vấn đề này; với application-level sharding, phải tự implement coordination.
Backup và restore: backup mỗi shard riêng biệt, nhưng cần point-in-time consistency giữa các shard nếu muốn restore toàn bộ hệ thống về cùng một thời điểm. Không trivial, mỗi shard có WAL position khác nhau. Giải pháp pragmatic: backup mỗi shard với timestamp gần nhau, chấp nhận slight inconsistency giữa shard (vài giây), cho hầu hết use case, điều này chấp nhận được.
Auto-increment ID: SERIAL trên PostgreSQL cho ID tăng dần trong mỗi shard, nhưng shard khác nhau sẽ có ID trùng. Giải pháp: dùng UUID (random, không trùng nhưng lớn và không sortable), Snowflake ID (64-bit, chứa timestamp + machine ID + sequence, sortable, không trùng, nhỏ hơn UUID), hoặc allocate range per shard (shard 0 dùng ID 0–999,999, shard 1 dùng 1,000,000–1,999,999).
Monitoring: dashboard phải hiện per-shard metric. Alert phải fire per shard, không chỉ aggregate. Trace phải tag shard ID để khi debug biết query đi đến shard nào. Log cũng vậy, shard_id là field bắt buộc trong structured log sau khi shard.
Shard key sai thì resharding, và resharding luôn tốn hơn dự kiến về effort, thời gian, và rủi ro. Dành đủ thời gian phân tích top query theo QPS trước khi chọn shard key, khai thác hết các giải pháp đơn giản hơn trước khi shard, và khi shard thì monitor per-shard ngay từ ngày đầu, aggregate metric che khuất mọi vấn đề thực sự.