Mọi tối ưu trên một máy đơn — index, phân vùng, nén, pool — đều có trần. Khi lượng ghi và dữ liệu vượt sức một máy chủ mạnh nhất, lựa chọn còn lại là sharding: chia dữ liệu ngang qua nhiều máy, mỗi máy giữ một phần. Đây là bước nhảy về độ phức tạp lớn nhất trong sê-ri. Bài này đo thật cơ chế sharding bằng postgres_fdw và làm rõ đánh đổi cốt lõi: shard key quyết định thành bại.

Khi nào cần sharding

Trước sharding, hãy vắt kiệt các lựa chọn rẻ hơn: nâng cấp máy (vertical scaling — có trần RAM/CPU/đĩa), thêm replica đọc (mở rộng đọc, nhưng ghi vẫn dồn về một primary). Chỉ khi lượng ghi hoặc dữ liệu vượt sức một máy — replica không cứu được ghi, và một máy không chứa/xử lý nổi — thì mới cần chia ngang qua nhiều máy.

Có ba cách tiếp cận:

  • postgres_fdw (native): bảng phân vùng mà một số partition là foreign table ở server khác — PostgreSQL tự route truy vấn theo shard key. (Demo bài này.)
  • Citus (extension): biến PostgreSQL thành CSDL phân tán thực thụ — coordinator + worker nodes, tự phân mảnh và fan-out song song.
  • Application-level: ứng dụng tự chọn shard theo khóa và tự gộp kết quả — linh hoạt nhất nhưng dồn phức tạp vào code.

Ảnh chụp đoạn mã SQL nền tối minh hoạ sharding chia dữ liệu ngang qua nhiều máy khi một máy không đủ, khi nào cần một máy đã hết cách vertical scaling nâng cấp một máy có trần RAM CPU đĩa tối đa replica đọc giúp mở rộng đọc nhưng ghi vẫn dồn về một primary khi dữ liệu ghi vượt sức một máy sharding chia ngang qua nhiều máy shard key bằng id shard 1 id 1 đến 500k shard 2 id 500k đến 1M bảng phân tán don máy chủ A máy chủ B, ba cách tiếp cận 1 postgres_fdw native bảng phân vùng với partition là foreign table ở server khác PostgreSQL tự route theo shard key demo bài này 2 Citus extension biến PostgreSQL thành CSDL phân tán tự phân mảnh fan-out truy vấn song song coordinator cộng worker nodes 3 application-level ứng dụng tự chọn shard theo khóa tự gộp kết quả, demo native partition foreign trỏ sang shard khác CREATE SERVER shard2_srv FOREIGN DATA WRAPPER postgres_fdw OPTIONS host dbname shard2 CREATE FOREIGN TABLE don_s2 PARTITION OF don FOR VALUES FROM 500001 TO 1000001 SERVER shard2_srv truy vấn theo shard key chỉ chạm đúng shard Foreign Scan, thách thức lớn nhất truy vấn không theo shard key lọc theo shard key id route tới một shard nhanh lọc theo cột khác kh fan-out chạm mọi shard rồi gộp đắt chọn shard key theo cách truy vấn phổ biến nhất thường là tenant_id user_id

Hình 1: Sharding chia dữ liệu ngang qua nhiều máy theo shard key khi một máy không đủ. Ba cách tiếp cận: postgres_fdw (native), Citus (phân tán), application-level. Thách thức lớn nhất là truy vấn không theo shard key.

Đo thật: bảng phân tán qua postgres_fdw

Tôi dựng một shard thứ hai (database shard2), rồi tạo bảng phân vùng don với partition đầu cục bộ và partition thứ hai là foreign table trên shard2:

CREATE SERVER shard2_srv FOREIGN DATA WRAPPER postgres_fdw
  OPTIONS (host '...', dbname 'shard2');
CREATE FOREIGN TABLE don_s2 PARTITION OF don
  FOR VALUES FROM (500001) TO (1000001) SERVER shard2_srv;

Bảng don giờ có 1 triệu dòng: 500k cục bộ (don_s1) + 500k trên shard2 (don_s2). Truy vấn xuyên suốt như một bảng. Đo thật cách truy vấn route:

Ảnh chụp bảng kết quả đo thật nền tối bảng phân tán qua postgres_fdw truy vấn route theo shard key bảng don 1 triệu dòng 500k cục bộ don_s1 cộng 500k trên shard2 don_s2 FOREIGN PostgreSQL 16, truy vấn shard cục bộ id nhỏ hơn 1000 chỉ chạm don_s1 EXPLAIN ANALYZE SELECT count từ don WHERE id nhỏ hơn 1000 Seq Scan on don_s1 rows 999 shard2 bị prune không đụng tới Execution Time 13,8 ms, truy vấn shard xa id lớn hơn 600000 route sang shard2 Foreign Scan EXPLAIN ANALYZE SELECT count từ don WHERE id lớn hơn 600000 AND id nhỏ hơn 601000 Foreign Scan on don_s2 rows 999 gửi truy vấn tới máy shard2 Execution Time 13,2 ms, truy vấn không theo shard key kh bằng 5 fan-out cả hai shard EXPLAIN SELECT count từ don WHERE kh bằng 5 Append Seq Scan on don_s1 shard cục bộ Foreign Scan on don_s2 shard xa kh không phải shard key phải hỏi mọi shard rồi gộp đắt và không mở rộng tốt, bài học sharding chỉ thắng khi truy vấn lọc theo shard key route tới 1 shard truy vấn xuyên shard analytics tìm theo cột khác fan-out tới mọi máy chọn shard key khớp cách truy vấn đây là quyết định khó đảo ngược

Hình 2: Truy vấn id<1000 (shard cục bộ) chỉ Seq Scan on don_s1 (shard2 bị prune), 13,8 ms. Truy vấn id>600000 route sang shard2 qua Foreign Scan on don_s2, 13,2 ms. Truy vấn kh=5 (không phải shard key) fan-out: Append của Seq Scan cục bộ + Foreign Scan xa — chạm cả hai shard.

  • Truy vấn theo shard key (id<1000): chỉ chạm don_s1 cục bộ — shard2 bị prune hoàn toàn, không đụng tới. 13,8 ms.
  • Truy vấn shard xa (id>600000): PostgreSQL gửi truy vấn tới máy shard2 qua Foreign Scan on don_s2. 13,2 ms. Chỉ một shard được hỏi.
  • Truy vấn KHÔNG theo shard key (kh=5): phải fan-out — Append của Seq Scan on don_s1 (cục bộ) và Foreign Scan on don_s2 (xa). Chạm mọi shard rồi gộp.

Đây là bức tranh cốt lõi của sharding: truy vấn lọc theo shard key route tới đúng một shard (nhanh, mở rộng tuyến tính theo số máy). Truy vấn không theo shard key phải hỏi mọi shard — đắt và không mở rộng tốt.

Đánh đổi cần cân nhắc

Chọn shard key là quyết định khó đảo ngược nhất. Shard key quyết định dữ liệu nằm ở đâu và truy vấn nào route tới một máy (nhanh) so với fan-out mọi máy (chậm). Chọn theo cách ứng dụng truy vấn phổ biến nhất — thường là tenant_id hoặc user_id (đa số truy vấn theo một tenant/user). Chọn sai thì đổi lại nghĩa là reshard — di chuyển toàn bộ dữ liệu, cực tốn kém. Cân nhắc kỹ từ đầu.

Sharding đánh mất nhiều tiện ích của một máy đơn. JOIN xuyên shard tốn kém (dữ liệu ở máy khác); ràng buộc unique/khóa ngoại toàn cục khó (mỗi shard độc lập); giao dịch phân tán (two-phase commit) chậm và phức tạp; ANALYZE, backup, nâng cấp phải làm trên nhiều máy. Đây là cái giá thật của phân tán — đừng shard sớm.

Đừng shard khi chưa cần. Sharding là biện pháp cuối. Một PostgreSQL đơn hiện đại xử lý được hàng chục TB và hàng trăm nghìn TPS với phần cứng tốt + phân vùng + pool + tối ưu truy vấn. Phần lớn hệ thống không cần shard; những hệ thống cần thì thường dùng Citus (để đỡ tự dựng) hoặc thiết kế sharding vào ứng dụng từ đầu. Vắt kiệt một máy trước.

Ba ý mang về

  1. Sharding chia dữ liệu ngang qua nhiều máy khi một máy không đủ: đo thật với postgres_fdw, bảng phân tán 1 triệu dòng qua 2 shard truy vấn xuyên suốt như một bảng — nhưng là biện pháp cuối, sau khi đã vắt kiệt nâng cấp máy, replica, phân vùng và tối ưu truy vấn.
  2. Truy vấn theo shard key route tới một máy, không theo shard key thì fan-out mọi máy: đo thật, id<1000 chỉ chạm shard cục bộ, id>600000 route sang shard2 (Foreign Scan), còn kh=5 (không phải shard key) fan-out cả hai shard — sharding chỉ mở rộng tốt cho truy vấn khớp shard key.
  3. Chọn shard key là quyết định khó đảo ngược: chọn theo cách truy vấn phổ biến nhất (thường tenant_id/user_id); và nhớ sharding đánh mất JOIN/unique/giao dịch dễ dàng của một máy đơn — dùng Citus hoặc thiết kế từ đầu, đừng shard sớm.

Phần sau ta quay về một kỹ thuật thực dụng dùng cho cả nạp dữ liệu vào shard lẫn ETL hằng ngày: Phần sau đo COPY so với INSERT hàng loạt — vì sao COPY nhanh hơn nhiều lần khi nạp khối lượng lớn, và cách dùng đúng.