Lịch sử System Design · Phần I · về mục lục · chương trước

Chương 5. Mở rộng cơ sở dữ liệu

bản sao chỉ đọc, failover, partitioning và sharding

Chương này phân tích các cách mở rộng một cơ sở dữ liệu quan hệ khi một máy không còn đủ. Tình huống nghiên cứu là hai sự cố của Chợ Phố sau khi thêm bản sao chỉ đọc. Khách đọc lại địa chỉ cũ ngay sau khi sửa, và một buổi diễn tập failover để ứng dụng ghi vào máy cũ. Từ đó chương trình bày độ trễ sao chép, đọc dữ liệu vừa ghi, sao chép đồng bộ, failover, partitioning và sharding, rồi tính xem Chợ Phố đã cần sharding hay chưa.

Thời gian đọc khoảng 25 phút Chương sau Chương 6. Xử lý bất đồng bộ Cập nhật 27/10/2026

Tình huống: địa chỉ cũ và primary cũ

Khoảng tháng thứ 20 kể từ khi mở cửa, Chợ Phố phục vụ khoảng 200.000 người dùng mỗi ngày. Bộ nhớ đệm (cache) dùng chung ở chương 4 đã gánh phần lớn truy vấn (query) đọc dữ liệu sản phẩm. Cơ sở dữ liệu (database) PostgreSQL 17 vẫn chạy trên một máy chủ (server), tên là db-1. Một tháng trước, An bổ sung hai máy db-2 và db-3, mỗi máy giữ một bản sao (replica) của db-1 qua cơ chế sao chép luồng (streaming replication) bất đồng bộ. Ứng dụng được sửa để gửi truy vấn đọc sang hai máy mới, còn mọi thao tác ghi vẫn đi tới db-1.

Thứ Sáu 20:04
Trong đợt khuyến mãi, một khách sửa địa chỉ giao hàng rồi bấm thanh toán ngay. Trang xác nhận hiện địa chỉ cũ, và đơn hàng được tạo với địa chỉ đó.
Hai tuần sau
Bộ phận hỗ trợ khách hàng ghi nhận 17 đơn đã giao tới địa chỉ cũ. Mọi trường hợp đều rơi vào giờ khuyến mãi.
Chủ nhật 03:00
Nhóm diễn tập tình huống db-1 hỏng: nâng db-2 lên làm máy nhận ghi.
03:02
db-2 nhận vai trò mới. Ứng dụng không báo lỗi nào.
03:14
Các đơn hàng mới nằm trên db-1 và không có trên db-2. Ứng dụng vẫn ghi vào địa chỉ IP của máy cũ.

Cấu hình kết nối (connection) của ứng dụng tại thời điểm diễn tập:

properties# cấu hình ứng dụng trước khi sửa: địa chỉ IP ghi cứng
chopho.db.write-url=jdbc:postgresql://10.0.2.20:5432/chopho
chopho.db.read-urls=jdbc:postgresql://10.0.2.21:5432/chopho,jdbc:postgresql://10.0.2.22:5432/chopho
Chương này trả lời ba câu hỏi. Vì sao khách thấy địa chỉ cũ ngay sau khi đã lưu địa chỉ mới? Vì sao ứng dụng vẫn ghi vào máy cũ sau khi vai trò đã chuyển, và hậu quả là gì? Khi dữ liệu và lượng ghi tiếp tục tăng, đến lúc nào một máy nhận ghi không còn đủ?

Sao chép và bản sao chỉ đọc

Sao chép (replication) là việc duy trì cùng một dữ liệu trên nhiều máy. Trong PostgreSQL, máy nhận mọi thao tác ghi gọi là primary. Mỗi thay đổi trên primary được ghi trước vào nhật ký ghi trước (write-ahead log, WAL), cơ chế được trình bày ở chương 21. Với sao chép luồng, primary gửi liên tục các bản ghi WAL tới những máy standby. Mỗi standby phát lại (replay) các bản ghi đó để có cùng dữ liệu với primary.

Một standby cho phép chạy truy vấn đọc trong lúc phát lại được gọi là hot standby. Tham số hot_standby mặc định bật. Bản sao chỉ đọc (read replica) là cách dùng hot standby để chia tải: ứng dụng gửi truy vấn đọc tới standby và chỉ gửi thao tác ghi tới primary. Mỗi bản sao giữ trọn dữ liệu, nên thêm bản sao tăng khả năng đọc nhưng không tăng khả năng ghi hay giảm dung lượng mỗi máy.

Mỗi người dùng tạo khoảng 250 MB ÷ 50.000 người = 5 KB dữ liệu mỗi ngày, với 1 KB = 1.000 byte. Từ mức của chương 1, dung lượng cơ sở dữ liệu nay khoảng 60 + 215 ≈ 275 GB. Cụm ba máy vì vậy giữ 3 × 275 = 825 GB.

Thời điểm primary trả lời ứng dụng quyết định hai chế độ sao chép. Trong sao chép bất đồng bộ (asynchronous replication), primary xác nhận giao dịch (transaction) ngay khi WAL đã ghi xuống ổ đĩa (disk) cục bộ, không chờ standby. Trong sao chép đồng bộ (synchronous replication), primary chờ thêm ít nhất một standby xác nhận. Theo tài liệu PostgreSQL, sao chép luồng mặc định là bất đồng bộ. Chế độ này không làm tăng độ trễ (latency) của thao tác ghi, đổi lại standby luôn đi sau primary một khoảng.

Bản sao không thay thế sao lưu (backup) đã thiết kế ở chương 1. Một lệnh xoá nhầm trên primary được chuyển tới mọi standby giống như mọi thay đổi khác. Khoảng thời gian standby đi sau primary là nguồn gốc của sự cố địa chỉ giao hàng.

Độ trễ sao chép và cách đo

Độ trễ sao chép (replication lag) là khoảng thời gian từ khi một giao dịch được xác nhận trên primary đến khi truy vấn trên standby thấy nó. PostgreSQL đo đại lượng này trên primary qua view pg_stat_replication, mỗi dòng ứng với một standby đang có kết nối sao chép. An chạy truy vấn sau trong giờ khuyến mãi:

sql-- Trên primary db-1: mỗi dòng ứng với một standby đang nhận WAL
SELECT application_name, client_addr, state, sync_state,
       write_lag, flush_lag, replay_lag,
       pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS replay_bytes
FROM pg_stat_replication;

-- kết quả minh hoạ
 application_name | client_addr | state     | sync_state | write_lag       | flush_lag       | replay_lag      | replay_bytes
------------------+-------------+-----------+------------+-----------------+-----------------+-----------------+--------------
 db2              | 10.0.2.21   | streaming | async      | 00:00:00.000710 | 00:00:00.001930 | 00:00:02.411880 |      9437184
 db3              | 10.0.2.22   | streaming | async      | 00:00:00.000680 | 00:00:00.001870 | 00:00:00.041200 |        65536

Ba cột thời gian đo ba chặng, cùng tính từ lúc primary ghi WAL xuống ổ đĩa của nó. Flush là thao tác buộc hệ điều hành đẩy dữ liệu đang nằm trong bộ đệm của nó xuống ổ đĩa. Cột write_lag kết thúc khi standby báo đã ghi WAL, chưa flush. Cột flush_lag kết thúc khi standby báo đã flush WAL xuống ổ đĩa. Cột replay_lag kết thúc khi standby báo đã phát lại, tức là khi truy vấn trên standby thấy được thay đổi. Mỗi bản ghi WAL nằm ở một vị trí gọi là LSN (log sequence number), là độ lệch tính bằng byte và tăng dần theo thứ tự ghi. Cột replay_bytes là hiệu giữa LSN hiện tại của primary và LSN standby đã phát lại, tức lượng WAL standby còn phải phát lại.

Kéo ngang để xem hết hình

MỘT LẦN ĐỔI ĐỊA CHỈ VÀ MỘT LẦN ĐỌC NGAY SAU ĐÓ (KHÔNG THEO TỈ LỆ) Ứng dụng Primary db-1 Bản sao db-2 lưu địa chỉ mới COMMIT, ghi WAL ghi flush phát lại đọc trang thanh toán địa chỉ cũ write_lag flush_lag replay_lag
Hình 5.1. Ba thanh dưới cùng là ba cột thời gian của pg_stat_replication, cùng tính từ lúc primary ghi WAL. Lần đọc ở khung đỏ rơi vào khoảng giữa bước flush và bước phát lại: dữ liệu đã nằm trên ổ đĩa của bản sao nhưng truy vấn chưa thấy.

Kết quả cho thấy hai chặng đầu trên db-2 chỉ mất vài mili giây, trong khi replay_lag lên tới 2,4 giây. Mạng và ổ đĩa của db-2 vẫn theo kịp primary. Phần chậm là bước phát lại, và chỉ xảy ra trên db-2.

Lưu lượng đỉnh khuyến mãi nay khoảng 800 yêu cầu (request) mỗi giây, và khoảng 15% trong số đó có ghi dữ liệu. Primary vì vậy nhận khoảng 800 × 0,15 = 120 giao dịch ghi mỗi giây. Trong cửa sổ trễ lớn nhất, khoảng 120 × 2,4 ≈ 288 giao dịch đã được xác nhận với khách nhưng chưa hiện ra trên db-2. Khách sửa địa chỉ rồi bấm thanh toán trong khoảng một giây. Trang thanh toán đọc địa chỉ từ bản sao, nên nhận đúng bản ghi cũ.

Trên chính standby, có thể đo theo cách khác:

sql-- Trên bản sao db-2
SELECT pg_is_in_recovery()                     AS la_standby,
       pg_last_wal_receive_lsn()               AS da_nhan,
       pg_last_wal_replay_lsn()                AS da_phat_lai,
       now() - pg_last_xact_replay_timestamp() AS tre_uoc_luong;

Hàm pg_last_xact_replay_timestamp() trả về thời điểm primary tạo bản ghi commit của giao dịch cuối cùng đã được phát lại. Hiệu giữa now() và giá trị này chỉ là ước lượng. Hệ quả trực tiếp của định nghĩa là khi primary không có giao dịch mới, hiệu này vẫn tăng dù standby không đi sau. Vì vậy cảnh báo về độ trễ sao chép nên dựa trên replay_lag hoặc chênh lệch LSN đo từ primary.

Đi sâu: vì sao bước phát lại trên db-2 chậm

Trên db-2, bộ phận chăm sóc người bán chạy truy vấn xuất lịch sử đơn hàng kéo dài nhiều phút. Một truy vấn trên hot standby có thể xung đột với bản ghi WAL đang chờ phát lại. PostgreSQL giữ nhiều phiên bản của một dòng để truy vấn đang chạy vẫn đọc được trạng thái lúc nó bắt đầu, gọi là snapshot. Cơ chế này được trình bày ở chương 21. VACUUM dọn các phiên bản không còn ai cần trên primary. Xung đột điển hình xảy ra khi bản ghi dọn dẹp đó tới standby trong lúc snapshot của một truy vấn trên standby vẫn cần các phiên bản bị dọn.

Khi xung đột xảy ra, standby tạm dừng phát lại để truy vấn chạy tiếp, tối đa trong khoảng max_standby_streaming_delay, mặc định 30 giây. Hết khoảng đó, truy vấn bị huỷ. Khoảng này tính từ lúc standby nhận WAL, nên thời gian truy vấn được chạy thêm có thể ngắn hơn. Trong lúc tạm dừng, WAL vẫn đến và vẫn được flush, nên write_lag và flush_lag nhỏ trong khi replay_lag tăng. Đây đúng là dạng số đo của db-2.

Có hai hướng xử lý. Tham số hot_standby_feedback, mặc định tắt, cho standby báo primary giữ lại các dòng truy vấn còn cần, đổi lại bảng trên primary có thể phình ra. Hướng thứ hai là tách vai trò: db-3 chỉ phục vụ truy vấn của khách với độ trễ thấp, còn truy vấn báo cáo chạy trên db-2 và được phép làm db-2 đi sau. Chợ Phố chọn hướng thứ hai.

Khi hệ thống nhàn rỗi và standby đã phát lại hết, các cột độ trễ trở về NULL sau một thời gian ngắn. Script giám sát cần coi NULL là trạng thái bình thường.

Biết nguyên nhân chưa đủ, vì độ trễ sao chép không bao giờ bằng không. Ứng dụng cần một cách để người vừa ghi đọc được đúng dữ liệu mình vừa ghi.

Đọc dữ liệu vừa ghi

Đọc dữ liệu vừa ghi (read-your-writes) là bảo đảm rằng sau khi một người dùng ghi thành công, mọi lần đọc tiếp theo của chính người đó thấy giá trị mới. Bảo đảm này chỉ áp dụng cho người đã ghi. Người dùng khác vẫn có thể thấy giá trị cũ trong một khoảng ngắn. Đây là một trong các mức nhất quán được hệ thống hoá ở chương 12.

Cách thứ nhất là định tuyến theo cửa sổ thời gian. Sau mỗi thao tác ghi, ứng dụng lưu thời điểm ghi vào phiên của người dùng, nằm trong kho phiên dùng chung từ chương 3. Trong một khoảng sau đó, mọi truy vấn đọc của người này đi tới primary. Cửa sổ phải dài hơn độ trễ lớn nhất đã đo, kèm biên an toàn: 2,4 × 2 = 4,8, làm tròn lên 5 giây. Ở đỉnh khuyến mãi, số phiên đang đọc từ primary tại một thời điểm khoảng 120 × 5 = 600 phiên.

java// Sau mỗi thao tác ghi thành công: lưu thời điểm ghi vào phiên (kho phiên dùng chung)
session.setAttribute("lastWriteAt", System.currentTimeMillis());

// Khi chọn nguồn đọc cho một yêu cầu
static final long READ_YOUR_WRITES_MS = 5_000;   // dài hơn replay_lag lớn nhất đã đo, có biên an toàn

DataSource forRead(HttpSession session) {
    Long last = (Long) session.getAttribute("lastWriteAt");
    boolean recent = last != null && System.currentTimeMillis() - last < READ_YOUR_WRITES_MS;
    return recent ? primary : replicas.next();     // replicas: luân phiên db2, db3
}

Cách này đơn giản nhưng dựa vào giả định độ trễ không vượt quá cửa sổ. Khi bản sao chậm bất thường, chẳng hạn do xung đột vừa phân tích, giả định đó sai và lỗi quay lại.

Cách thứ hai dựa trên LSN, vị trí trong WAL đã định nghĩa ở mục trước. Ngay sau khi giao dịch ghi được commit, ứng dụng đọc vị trí WAL hiện tại của primary và lưu vào phiên. Vì WAL chỉ được ghi nối tiếp, vị trí này không nhỏ hơn vị trí bản ghi commit vừa tạo. Trước khi đọc từ bản sao, ứng dụng so vị trí đã lưu với vị trí bản sao đã phát lại tới. Nếu bản sao chưa tới, ứng dụng đọc từ primary.

sql-- 1. Trên primary, ngay sau COMMIT của giao dịch đổi địa chỉ; ứng dụng lưu kết quả vào phiên
SELECT pg_current_wal_lsn();                                   -- ví dụ 3A/7F0012C8

-- 2. Trên bản sao, trước khi đọc địa chỉ cho trang thanh toán
SELECT pg_last_wal_replay_lsn() >= '3A/7F0012C8'::pg_lsn AS da_kip;
-- da_kip = false: bản sao chưa phát lại tới thay đổi đó, đọc từ primary

PostgreSQL 17 chưa có lệnh dựng sẵn để chờ bản sao phát lại tới một LSN cho trước, nên phép so sánh này do ứng dụng thực hiện. Cách thứ hai chính xác hơn nhưng tốn thêm một truy vấn trước mỗi lần đọc.

Kéo ngang để xem hết hình

yêu cầu đọc của một khách khách vừa ghi trong cửa sổ thời gian? có primary db-1 luôn có dữ liệu mới nhất không bản sao db-2, db-3 chia đều tải đọc còn lại Riêng trang thanh toán: so LSN lưu trong phiên với pg_last_wal_replay_lsn() của bản sao, chưa tới thì đọc từ primary.
Hình 5.2. Quyết định dựa trên trạng thái của từng khách, lưu trong phiên. Phần lớn yêu cầu đọc vẫn đi tới bản sao vì đa số khách không vừa ghi gì.

Chợ Phố áp dụng cửa sổ thời gian cho mọi thao tác ghi, và áp dụng thêm phép so LSN cho riêng trang thanh toán, nơi một giá trị cũ gây thiệt hại trực tiếp. Một hướng khác là buộc primary chờ bản sao trước khi xác nhận ghi, tức sao chép đồng bộ.

Sao chép đồng bộ hay bất đồng bộ

Tham số synchronous_commit quyết định primary chờ tới bước nào trước khi báo commit thành công. Tham số này chỉ chờ standby khi synchronous_standby_names khác rỗng. Giá trị mặc định của synchronous_standby_names là chuỗi rỗng, tức không có standby đồng bộ nào.

Kéo ngang để xem hết hình

PRIMARY CHỜ TỚI BƯỚC NÀO TRƯỚC KHI BÁO COMMIT THÀNH CÔNG Primary WAL đã xuống đĩa cục bộ local Standby nhận WAL đã ghi vào hệ điều hành remote_write Standby flush WAL đã xuống đĩa standby on (mặc định) Standby phát lại truy vấn trên standby thấy remote_apply off: báo thành công trước cả khi WAL cục bộ xuống đĩa. Ba giá trị bên phải chỉ chờ standby khi synchronous_standby_names khác rỗng; nếu rỗng, chúng hoạt động như local.
Hình 5.3. Càng sang phải, commit càng chờ lâu và càng bảo đảm nhiều hơn. Chỉ bước cuối, khung màu nhấn, làm cho truy vấn trên standby thấy dữ liệu.

Khi có standby đồng bộ, remote_write chờ standby ghi WAL vào hệ điều hành, on chờ standby flush WAL xuống ổ đĩa, và remote_apply chờ standby phát lại xong. Chỉ remote_apply bảo đảm truy vấn trên standby thấy dữ liệu. Giá trị có thể đặt riêng cho từng giao dịch, nên chỉ giao dịch đổi địa chỉ phải trả chi phí chờ lâu nhất:

postgresql.conf# postgresql.conf trên primary: phương án được cân nhắc, chưa áp dụng
synchronous_standby_names = 'ANY 1 (db2, db3)'   # tên khớp application_name trong primary_conninfo của standby
synchronous_commit = on                          # mặc định: chờ một standby đồng bộ flush WAL
sql-- Chỉ giao dịch đổi địa chỉ chờ standby phát lại xong
BEGIN;
SET LOCAL synchronous_commit = remote_apply;
UPDATE addresses SET line1 = $1, ward = $2 WHERE id = $3 AND customer_id = $4;
COMMIT;

Cấu hình này chưa giải quyết bài toán địa chỉ giao hàng. Cú pháp ANY 1 (db2, db3) chỉ yêu cầu một trong hai standby xác nhận, trong khi truy vấn đọc có thể tới standby còn lại. Muốn mọi bản sao đều thấy dữ liệu thì primary phải chờ cả hai. Khi đó, chỉ cần một bản sao dừng là mọi commit chờ theo. Tài liệu PostgreSQL ghi rõ các commit như vậy có thể không bao giờ hoàn tất nếu một standby đồng bộ gặp sự cố, cho tới khi quản trị viên sửa synchronous_standby_names.

Chi phí thứ hai là độ trễ ghi. Mỗi commit chờ thêm ít nhất một vòng mạng tới standby, nên một kết nối thực hiện các commit nối tiếp bị giới hạn bởi thời gian vòng mạng. Trong cùng trung tâm dữ liệu, với vòng mạng 0,5 ms, giới hạn này là 1.000 ÷ 0,5 = 2.000 commit mỗi giây, chưa kể thời gian ghi ổ đĩa ở standby. Với standby ở vùng khác có vòng mạng 30 ms, giới hạn giảm còn 1.000 ÷ 30 ≈ 33 commit mỗi giây.

Đây là đánh đổi mà PACELC ở chương 0 mô tả: ngay cả khi mạng không bị phân hoạch, hệ thống sao chép vẫn phải chọn giữa độ trễ thấp và tính nhất quán.

Sao chép đồng bộ còn phục vụ một mục tiêu khác: không mất giao dịch đã xác nhận khi primary hỏng. Theo tài liệu PostgreSQL, với sao chép bất đồng bộ, lượng dữ liệu mất khi failover tỉ lệ với độ trễ sao chép tại thời điểm đó. Lượng này phải được so với mục tiêu điểm khôi phục (RPO) đặt ra ở chương 1. Với độ trễ thường ngày dưới một giây của db-3, nhóm giữ chế độ bất đồng bộ và giải bài toán đọc dữ liệu vừa ghi ở tầng ứng dụng. Lựa chọn này chỉ đứng vững nếu việc chuyển sang standby khi primary hỏng thật diễn ra đúng, điều buổi diễn tập lúc rạng sáng đã cho thấy là chưa.

Primary, standby và failover

Failover (chuyển đổi dự phòng) là việc chuyển vai trò primary sang một standby khi primary hỏng. Một quy trình failover gồm bốn việc. Thứ nhất, bảo đảm primary cũ không còn nhận ghi. Thứ hai, nâng một standby lên làm primary, thao tác gọi là promote. Thứ ba, cho các standby còn lại sao chép từ primary mới. Thứ tư, đưa ứng dụng tới primary mới. Buổi diễn tập lúc 03:00 chỉ thực hiện việc thứ hai:

shell# Buổi diễn tập chỉ chạy lệnh này, trên db-2
pg_ctl promote -D "$PGDATA"

# Cách tương đương từ một phiên SQL trên db-2: chờ tối đa 60 giây cho tới khi promote xong
#   SELECT pg_promote(wait => true, wait_seconds => 60);

Lệnh promote kết thúc chế độ standby của db-2, và db-2 bắt đầu nhận ghi trên một dòng thời gian (timeline) mới. db-1 không nhận được thông báo nào về việc này. Nó vẫn chạy với vai trò primary, và ứng dụng vẫn kết nối tới nó qua địa chỉ IP ghi cứng trong cấu hình. Lúc rạng sáng, primary nhận khoảng 4 giao dịch ghi mỗi giây. Từ 03:02 đến 03:14, db-1 vì vậy nhận 12 × 60 × 4 = 2.880 giao dịch mà db-2 không có.

Tình trạng hai máy cùng coi mình là primary và cùng nhận ghi gọi là split brain. Hai bản dữ liệu phân kỳ, và PostgreSQL không tự hợp nhất chúng. Trong một sự cố thật, dữ liệu ghi vào máy bị bỏ phải được đối chiếu và nhập lại bằng tay.

Kéo ngang để xem hết hình

TRONG BUỔI DIỄN TẬP SAU KHI SỬA ứng dụng trỏ cứng 10.0.2.20 db-1 (primary cũ) vẫn nhận ghi db-2 (primary mới) không nhận ghi nào hai máy cùng là primary: split brain db-3 vẫn sao chép từ db-1 ứng dụng chuỗi kết nối nhiều máy db-1 đã dừng (fencing) db-2 (primary) nhận mọi thao tác ghi db-3 theo primary mới qua primary_conninfo trình điều khiển bỏ qua máy không nhận ghi
Hình 5.4. Khung trái là trạng thái phát hiện trong buổi diễn tập. Khung phải cần cả hai thay đổi cùng lúc: primary cũ bị dừng, và ứng dụng tìm primary theo vai trò thay vì theo địa chỉ.

Lỗi của buổi diễn tập có hai lớp. Lớp thứ nhất là ứng dụng xác định cơ sở dữ liệu bằng địa chỉ IP thay vì bằng vai trò. Lớp thứ hai là primary cũ không bị ngăn ghi. Biện pháp cho lớp thứ hai gọi là fencing: trước khi standby được nâng lên, primary cũ phải bị dừng hoặc bị cắt khỏi mạng.

Có ba cách để ứng dụng tìm primary mới. Cách thứ nhất là một tên miền trỏ tới primary và được cập nhật khi failover, dựa trên DNS ở chương 26. JVM giữ kết quả phân giải tên trong một khoảng thời gian tuỳ cài đặt, nên thời gian chuyển phụ thuộc cả cấu hình này. Cách thứ hai là một địa chỉ IP ảo hoặc một proxy đứng trước cụm, do phần mềm quản lý cụm chuyển sang máy mới. Cách thứ ba là chuỗi kết nối nhiều máy, được Chợ Phố chọn vì không cần thêm thành phần nào:

properties# PgJDBC (ứng dụng Java): thử lần lượt từng máy, chỉ giữ kết nối tới máy nhận ghi
chopho.db.write-url=jdbc:postgresql://db1.chopho.internal:5432,db2.chopho.internal:5432,db3.chopho.internal:5432/chopho?targetServerType=primary

# libpq (script vận hành, công cụ dòng lệnh): cùng ý nghĩa
postgresql://db1.chopho.internal:5432,db2.chopho.internal:5432,db3.chopho.internal:5432/chopho?target_session_attrs=read-write

Với chuỗi này, trình điều khiển thử lần lượt từng máy theo thứ tự trong danh sách. Với libpq, target_session_attrs=read-write chỉ chấp nhận máy không ở chế độ hot standby và có default_transaction_read_only tắt. PgJDBC có tham số tương ứng là targetServerType=primary.

Chuỗi kết nối nhiều máy không thay thế được fencing. Nếu db-1 vẫn chạy với vai trò primary, nó đứng đầu danh sách và vẫn được chọn. Các kết nối đang mở trong nhóm kết nối (connection pool) của chương 0 cũng không tự chuyển sang máy mới. Chúng chỉ được tạo lại sau khi bị đóng, việc xảy ra ngay khi db-1 dừng.

  1. Fencing primary cũ. Dừng db-1 bằng pg_ctl stop -m fast. Nếu không truy cập được db-1, chặn cổng cơ sở dữ liệu của nó ở tường lửa.
  2. Chọn standby có nhiều dữ liệu nhất. So pg_last_wal_replay_lsn() của các standby và promote máy có giá trị lớn nhất.
  3. Nối các standby còn lại. Sửa primary_conninfo của db-3 trỏ sang db-2.
  4. Kiểm tra đường ghi. Trên máy mới, pg_is_in_recovery() trả về false, và đơn hàng mới xuất hiện trên máy này.
  5. Đưa db-1 trở lại. Dùng pg_rewind hoặc dựng lại db-1 làm standby của db-2.

Các công cụ như Patroni tự động hoá chuỗi bước trên. Patroni lưu trạng thái cụm trong một kho cấu hình phân tán, chẳng hạn etcd, Consul hoặc ZooKeeper. Chỉ máy đang giữ khoá leader trong kho này mới chạy với vai trò primary. Buổi diễn tập lần hai, theo đúng năm bước, chuyển vai trò mà không có giao dịch nào ghi vào máy cũ.

Đi sâu: dòng thời gian và pg_rewind

Mỗi lần promote, PostgreSQL tạo một dòng thời gian mới với số hiệu lớn hơn. WAL sinh ra sau thời điểm đó thuộc dòng thời gian mới. Một standby theo dõi dòng thời gian mới nhất khi recovery_target_timeline giữ giá trị mặc định latest, nhờ vậy db-3 theo được db-2 sau khi đổi primary_conninfo.

Phần WAL mà db-1 sinh ra sau điểm phân kỳ không tồn tại trên db-2. pg_rewind đồng bộ thư mục dữ liệu của db-1 với db-2 để db-1 trở lại làm standby. Công cụ này yêu cầu db-1 đã bật wal_log_hints hoặc được khởi tạo với data checksums. Khi tua lại, các thay đổi riêng của db-1 bị bỏ đi. Vì vậy các giao dịch ghi nhầm vào db-1 phải được xuất ra trước khi chạy pg_rewind.

Hai bí ẩn của chương đã có lời giải. Câu hỏi còn lại liên quan đến dung lượng: bảng lớn nhất, orders, tăng thêm mỗi ngày và chỉ một phần nhỏ của nó còn được đọc thường xuyên.

Partitioning: chia bảng theo cột và theo hàng

Partitioning là việc chia một bảng lớn thành nhiều phần nhỏ hơn trên cùng một máy chủ. Chia theo cột (vertical partitioning) tách các cột ít dùng hoặc có kích thước lớn sang một bảng riêng, nối với bảng gốc bằng cùng khoá chính. Chia theo hàng (horizontal partitioning) giữ nguyên các cột và đặt mỗi nhóm dòng vào một bảng con, gọi là phân vùng (partition).

PostgreSQL đã tự áp dụng một dạng chia theo cột cho giá trị lớn. Cơ chế TOAST chuyển giá trị lớn ra một vùng lưu riêng khi dòng rộng hơn ngưỡng TOAST_TUPLE_THRESHOLD, thường khoảng 2.000 byte trên trang dữ liệu mặc định 8.192 byte. Tách cột thủ công vẫn có ích khi một nhóm cột được đọc ít hơn hẳn phần còn lại. Ví dụ, nội dung phản hồi thô của cổng thanh toán chỉ cần khi đối soát, nên Chợ Phố chuyển nó sang bảng order_payment_raw.

Bảng orders hiện nhận khoảng 6.000 đơn × 30 ngày = 180.000 dòng mỗi tháng và đã có khoảng 1,5 triệu dòng. Phần lớn truy vấn chỉ chạm vào đơn gần đây: khách xem đơn đang giao, người bán xem đơn trong tuần. Chính sách mới giữ 12 tháng đơn hàng trong cơ sở dữ liệu chính, còn đơn cũ hơn được xuất ra kho lưu trữ phục vụ kế toán. Hai yêu cầu này phù hợp với chia theo hàng, mỗi tháng một phân vùng, bằng cơ chế phân vùng khai báo (declarative partitioning) của PostgreSQL:

sql-- Bảng cha: không chứa dòng nào, chỉ định nghĩa cách chia
CREATE TABLE orders (
    id           bigserial,
    customer_id  bigint      NOT NULL,
    shop_id      bigint      NOT NULL,
    status       text        NOT NULL,
    total_vnd    bigint      NOT NULL,
    created_at   timestamptz NOT NULL,
    PRIMARY KEY (id, created_at)          -- phải chứa cột của khoá phân vùng
) PARTITION BY RANGE (created_at);

-- Mỗi tháng một phân vùng: cận dưới được tính vào, cận trên thì không
CREATE TABLE orders_2027_10 PARTITION OF orders
    FOR VALUES FROM ('2027-10-01') TO ('2027-11-01');

-- Tạo trên bảng cha: PostgreSQL tạo chỉ mục tương ứng trên từng phân vùng
CREATE INDEX ON orders (customer_id, created_at);

Khoá chính phải chứa created_at, vì PostgreSQL yêu cầu mọi ràng buộc duy nhất trên bảng phân vùng chứa đủ các cột của khoá phân vùng. Chỉ mục (index) tạo trên bảng cha được tự động tạo trên từng phân vùng. Nếu một dòng mới không thuộc phân vùng nào và bảng không có phân vùng mặc định, lệnh ghi bị từ chối. Vì vậy một tác vụ định kỳ (cron job) tạo sẵn phân vùng của tháng sau.

Lợi ích thứ nhất là loại phân vùng (partition pruning). Khi điều kiện lọc giới hạn created_at, PostgreSQL bỏ qua các phân vùng không thể chứa dòng phù hợp. Tham số enable_partition_pruning mặc định bật. Truy vấn đơn hàng ba tháng gần nhất của một khách cho kế hoạch sau:

sqlEXPLAIN (COSTS OFF)
SELECT id, status, total_vnd FROM orders
WHERE customer_id = 48213
  AND created_at >= '2027-08-01' AND created_at < '2027-11-01';

 Append
   ->  Index Scan using orders_2027_08_customer_id_created_at_idx on orders_2027_08 orders_1
         Index Cond: ((customer_id = 48213) AND (created_at >= ...) AND (created_at < ...))
   ->  Index Scan using orders_2027_09_customer_id_created_at_idx on orders_2027_09 orders_2
         Index Cond: (...)
   ->  Index Scan using orders_2027_10_customer_id_created_at_idx on orders_2027_10 orders_3
         Index Cond: (...)
-- kết quả minh hoạ, rút gọn: chỉ ba phân vùng xuất hiện trong kế hoạch

Bảng hiện có 21 phân vùng, mỗi tháng một phân vùng kể cả tháng hiện tại. Truy vấn trên chỉ đọc 3 ÷ 21 ≈ 14% số phân vùng, và chỉ mục của các tháng gần đây đủ nhỏ để nằm trong bộ nhớ.

Kéo ngang để xem hết hình

BẢNG CHA orders VÀ MỖI THÁNG MỘT PHÂN VÙNG orders (bảng cha) PARTITION BY RANGE (created_at) tháng đầu tiên tháng hiện tại tách rồi xoá DETACH, DROP bị loại, không quét partition pruning được quét
Hình 5.5. Màu của từng ô cho thấy số phận của phân vùng với một truy vấn lọc theo ba tháng gần nhất và với chính sách giữ dữ liệu. Bảng cha không chứa dòng nào, chỉ định nghĩa cách chia.

Lợi ích thứ hai là xoá dữ liệu cũ. Cửa sổ giữ gồm mười hai tháng đã đóng cộng tháng hiện tại, nên số phân vùng được tách ra là 21 − 13 = 8 phân vùng. Tám tháng đầu tiên này chứa khoảng 252.000 dòng. Lệnh thứ hai dưới đây xoá vĩnh viễn dữ liệu của một tháng, nên chỉ chạy sau khi bản xuất đã được kiểm tra:

sql-- Sau khi đã xuất orders_2026_02 ra kho lưu trữ và kiểm tra bản xuất
ALTER TABLE orders DETACH PARTITION orders_2026_02 CONCURRENTLY;  -- không chạy trong khối giao dịch, bảng không có phân vùng mặc định
DROP TABLE orders_2026_02;                                        -- xoá vĩnh viễn dữ liệu của tháng đó

Tài liệu PostgreSQL ghi nhận rằng DROP TABLE trên một phân vùng hoặc DETACH PARTITION nhanh hơn nhiều so với một thao tác xoá hàng loạt. Hai lệnh này cũng tránh hoàn toàn chi phí VACUUM của lệnh DELETE lớn. Với cụm có bản sao, khác biệt còn rõ hơn. Theo cơ chế WAL, lệnh DELETE ghi thay đổi của từng dòng vào nhật ký, và mọi bản ghi đó phải được phát lại trên hai bản sao.

Partitioning cũng có chi phí. Truy vấn không lọc theo created_at, chẳng hạn tìm đơn theo mã đơn, phải dò mọi phân vùng. Theo tài liệu, quá nhiều phân vùng làm tăng thời gian lập kế hoạch và lượng bộ nhớ dùng. Bộ lập kế hoạch vẫn xử lý tốt vài nghìn phân vùng khi truy vấn điển hình loại được gần hết. Chuyển bảng orders đang chạy sang dạng phân vùng đòi hỏi tạo bảng mới và chép dữ liệu, một dạng schema migration trình bày ở chương 14.

Partitioning chia dữ liệu nhưng mọi phân vùng vẫn nằm trên một máy. Khi một máy không còn đủ, các phần phải được đặt trên nhiều máy.

Sharding: chia dữ liệu ra nhiều máy

Sharding là chia theo hàng trên nhiều máy chủ. Mỗi máy, gọi là một shard, giữ một tập con các dòng và nhận cả đọc lẫn ghi cho tập đó. Khác với bản sao, các shard giữ phần dữ liệu khác nhau, nên sharding chia được cả lượng ghi lẫn dung lượng. PostgreSQL lõi không tự phân phối một bảng ra nhiều máy, nên việc này do ứng dụng hoặc một phần mở rộng như Citus đảm nhận.

Khoá phân mảnh (shard key) là cột quyết định một dòng thuộc shard nào. Truy vấn có lọc theo khoá phân mảnh chỉ chạm một shard, còn truy vấn khác phải hỏi mọi shard. Với Chợ Phố, ứng viên tự nhiên là customer_id, vì khi đó mọi đơn hàng, địa chỉ và giỏ hàng của một khách nằm trên cùng shard.

Ba cách ánh xạ khoá sang shard

Theo khoảng, mỗi shard giữ một khoảng giá trị liên tục của khoá. Theo giá trị băm, shard được chọn bằng giá trị của hàm băm (hash) tính trên khoá, chia lấy dư cho số shard N. Theo bảng tra, một bảng riêng ghi khoá nào nằm ở shard nào.

Cách ánh xạƯu điểmNhược điểm
Theo khoảngTruy vấn theo khoảng chỉ chạm ít shardKhoá tăng dần như thời gian làm mọi ghi mới dồn vào shard cuối
Theo giá trị bămPhân bố đều, không cần bảng traTruy vấn theo khoảng phải hỏi mọi shard; đổi N làm di chuyển phần lớn dữ liệu
Theo bảng traDi chuyển được từng khoá, đặt riêng được khoá lớnThêm một bước tra cứu; bảng tra phải luôn sẵn sàng

Chia lại shard

Chia lại shard (resharding) là việc thay đổi số shard hoặc ranh giới giữa chúng khi dữ liệu tăng. Với cách theo giá trị băm và phép chia lấy dư, tăng từ bốn lên năm shard làm đổi shard của 80% số khoá. Một khoá chỉ giữ nguyên shard khi hai phép chia lấy dư cho cùng kết quả.

Kéo ngang để xem hết hình

SHARD CỦA 20 KHOÁ ĐẦU TIÊN KHI CHIA THEO h mod 4 VÀ THEO h mod 5 khoá h h mod 4 h mod 5 0 0 0 1 1 1 2 2 2 3 3 3 4 0 4 5 1 0 6 2 1 7 3 2 8 0 3 9 1 4 10 2 0 11 3 1 12 0 2 13 1 3 14 2 4 15 3 0 16 0 1 17 1 2 18 2 3 19 3 4 giữ nguyên shard phải chuyển sang shard khác
Hình 5.6. Mẫu lặp lại sau mỗi chu kỳ bằng bội chung nhỏ nhất của hai số shard, nên tỉ lệ khoá giữ nguyên trên một chu kỳ cũng là tỉ lệ trên toàn bộ dữ liệu.

Với dữ liệu hiện tại, khối lượng phải chuyển qua mạng khoảng 80% × 275 GB ≈ 220 GB. Cách phổ biến để giảm khối lượng này là chia trước thành nhiều shard logic, chẳng hạn 1.024, rồi gán các shard logic cho máy vật lý. Khi thêm máy thứ năm, chỉ cần chuyển khoảng 1 ÷ 5 = 20% số shard logic sang máy mới. Băm nhất quán (consistent hashing), do Karger và cộng sự công bố tại hội nghị STOC năm 1997, đạt tính chất tương tự mà không cần bảng gán.

java// Chia trước thành 1.024 shard logic; bảng gán shard logic sang máy vật lý nằm trong cấu hình
static final int LOGICAL_SHARDS = 1024;

int logicalShard(long customerId) {
    return Math.floorMod(hash64(customerId), LOGICAL_SHARDS);   // hash64: hàm băm ổn định giữa các phiên bản
}

DataSource shardFor(long customerId) {
    return physical[assignment[logicalShard(customerId)]];      // thêm máy: chỉ sửa assignment cho một phần shard logic
}

Khoá nóng

Khoá nóng (hot key) là một giá trị khoá phân mảnh nhận lượng truy cập lớn bất thường so với phần còn lại. Giả sử Chợ Phố phân mảnh theo shop_id để người bán xem đơn nhanh hơn. Một đợt giảm giá chớp nhoáng của một gian hàng chiếm 30% số đơn trong giờ đó sẽ dồn vào một shard. Shard này nhận 30% + 70% ÷ 4 = 47,5% lượng ghi, tức khoảng 47,5 ÷ 25 ≈ 1,9 lần mức của phân bố đều.

Kéo ngang để xem hết hình

PHẦN LƯỢNG GHI MỖI SHARD NHẬN TRONG ĐỢT GIẢM GIÁ CHỚP NHOÁNG, KHOÁ PHÂN MẢNH LÀ shop_id shard 0 (gian hàng nóng) 47,5% 47,5% shard 1 17,5% 17,5% shard 2 17,5% 17,5% shard 3 17,5% 17,5% mức của phân bố đều
Hình 5.7. Vạch đứt là phần mỗi shard nhận khi tải phân bố đều. Thêm shard chỉ làm phần tải chia đều nhỏ đi, còn mọi đơn của gian hàng nóng vẫn đi tới đúng một shard.

Cách xử lý thường gặp là tách khoá nóng: thêm một hậu tố vào khoá để rải dữ liệu của gian hàng đó ra nhiều shard, đổi lại phải gộp kết quả khi đọc. Với cách theo bảng tra, một gian hàng lớn có thể được đặt riêng trên một shard.

Sharding chuyển một phần việc của cơ sở dữ liệu sang ứng dụng. Phép join giữa hai shard phải thực hiện trong ứng dụng. Giao dịch ghi vào nhiều shard cần những giao thức như two-phase commit, trình bày ở chương 12. Ràng buộc duy nhất toàn cục và việc sinh mã đơn hàng không còn do cơ sở dữ liệu bảo đảm, nên ứng dụng phải tự đảm nhận. Thay đổi cấu trúc bảng và sao lưu phải thực hiện trên từng shard. Các chi phí này chỉ đáng trả khi một máy thật sự không còn đủ, và câu hỏi đó cần được trả lời bằng số liệu.

Chợ Phố đã cần sharding chưa

Sharding giải quyết hai giới hạn của một máy: lượng ghi vượt khả năng của primary, và dung lượng vượt khả năng lưu trữ. Lượng đọc đã được các bản sao chia sẻ. Hai giới hạn trên được ước lượng như sau, với tốc độ tăng người dùng quan sát được trong một năm qua thay vì tốc độ dự báo ở chương 1.

  1. Tốc độ tăng. Số người dùng mỗi ngày tăng gấp bốn từ tháng thứ 8 đến nay. Tốc độ tăng mỗi tháng là 4^(1/12) ≈ 1,12, tức khoảng 12% mỗi tháng, cao hơn mức 8% mà chương 1 dự báo.
  2. Lượng ghi. Sau khi truy vấn đọc chuyển sang bản sao, mức sử dụng (utilization) CPU của primary ở đỉnh khuyến mãi là 25%. Khoảng dự trữ tới ngưỡng nâng cấp của chương 1 là 70 ÷ 25 = 2,8 lần. Thời gian chạm ngưỡng là ln 2,8 ÷ ln 1,12 ≈ 9,1 tháng.
  3. Mở rộng theo chiều dọc (scale up). Một lần nhân đôi số CPU kéo dài thêm ln 2 ÷ ln 1,12 ≈ 6,1 tháng. Tổng cộng khoảng 9,1 + 6,1 ≈ 15 tháng trước khi lượng ghi chạm giới hạn của primary.
  4. Dung lượng. Dữ liệu mới hiện là 200.000 × 5 KB = 1 GB mỗi ngày, tức khoảng 30 GB mỗi tháng. Sau hai năm tăng trưởng như trên, phần thêm vào là 30 GB × (1,12²⁴ − 1) ÷ 0,12 ≈ 3.545 GB, và tổng dung lượng là 275 + 3.545 ≈ 3,8 TB.
  5. Giới hạn của PostgreSQL. Với kích thước trang mặc định, một quan hệ tối đa 32 TB. Mỗi phân vùng là một quan hệ riêng. Ngay cả toàn bộ cơ sở dữ liệu dự báo cũng nhỏ hơn giới hạn của một quan hệ: 32 TB ÷ 3,8 TB ≈ 8 lần.

Mức sử dụng CPU được định nghĩa ở chương 0, còn mở rộng theo chiều dọc được phân tích ở chương 1. Kết luận định lượng là Chợ Phố chưa cần sharding trong khoảng một năm tới. Giới hạn đến trước là lượng ghi, và nó vẫn còn dư địa nhờ mở rộng theo chiều dọc. Dung lượng còn xa giới hạn, và partitioning giữ cho từng bảng gọn.

Nhóm ghi lại ba tín hiệu để đánh giá lại. Tín hiệu thứ nhất là CPU của primary vượt ngưỡng nâng cấp khi đã ở cấu hình lớn nhất có thể thuê. Tín hiệu thứ hai là độ trễ sao chép của bản sao phục vụ khách tăng kéo dài dù bản sao không chạy truy vấn nặng. Dấu hiệu này cho thấy primary sinh WAL nhanh hơn khả năng phát lại. Tín hiệu thứ ba là thời gian khôi phục thực tế vượt mục tiêu đã đặt vì dung lượng quá lớn. Khi một tín hiệu xuất hiện, phương án đầu tiên vẫn là tách bớt dữ liệu ra khỏi primary trước khi phân mảnh.

Mỗi loại dữ liệu một loại cơ sở dữ liệu

Tách bớt dữ liệu ra khỏi primary là việc Chợ Phố đã làm dần qua các chương trước. Mỗi loại dữ liệu được đặt vào kho phù hợp với cách nó được đọc và ghi. Bảng dưới tổng hợp hiện trạng.

Dữ liệuNơi lưuLý do
Đơn hàng, tài khoản, tồn khoPostgreSQL: primary và hai bản saoCần giao dịch và ràng buộc toàn vẹn
Phiên đăng nhậpRedis, làm kho phiên (chương 3)Truy cập theo khoá, tự hết hạn
Dữ liệu sản phẩm đọc nhiềuRedis, làm bộ nhớ đệm (chương 4)Bản sao tạm của PostgreSQL, chấp nhận cũ trong thời gian ngắn
Ảnh sản phẩmKho đối tượng (chương 2)Tệp lớn, đọc nguyên tệp, không cần truy vấn

Mỗi kho thêm vào là thêm một hệ thống phải vận hành và giám sát, và thêm một nơi dữ liệu có thể lệch với nguồn gốc. Nguyên tắc của Chợ Phố là chỉ thêm kho mới khi PostgreSQL không đáp ứng được một yêu cầu đã đo được. Các nhóm cơ sở dữ liệu NoSQL, cùng các hệ NewSQL tự phân mảnh dữ liệu mà vẫn giữ SQL và giao dịch, được phân tích ở chương 20.

Hướng tiếp theo

Sau chương này, Chợ Phố có một primary, hai bản sao chỉ đọc với vai trò tách riêng, và quy trình failover đã diễn tập lại với fencing cùng chuỗi kết nối nhiều máy. Bảng orders được chia theo tháng. Sharding đã được đánh giá và hoãn lại kèm điều kiện xem xét lại.

Khi xem lại số đo của trang thanh toán, An nhận thấy phần lớn thời gian của yêu cầu thanh toán không nằm ở cơ sở dữ liệu. Ứng dụng gửi thư xác nhận đơn hàng ngay trong luồng (thread) xử lý yêu cầu, và mỗi khi máy chủ thư phản hồi chậm thì khách phải chờ theo. Việc xuất báo cáo doanh thu cho người bán cũng chạy trong luồng xử lý yêu cầu và giữ luồng đó trong nhiều giây.

Những việc nào thật sự phải xong trước khi trả lời khách? Phần còn lại có thể đưa vào một hàng đợi (queue) để xử lý sau, nhưng nếu thư bị gửi hai lần hoặc bị mất thì sao? Chương 6 trình bày xử lý bất đồng bộ.

Tóm tắt chương

  1. Bản sao chỉ đọc chia tải đọc, không chia tải ghi và dung lượng. Mỗi bản sao giữ trọn dữ liệu và không thay thế sao lưu.
  2. Sao chép luồng của PostgreSQL mặc định bất đồng bộ. Bản sao luôn đi sau primary một khoảng gọi là độ trễ sao chép.
  3. Đo độ trễ trên primary bằng pg_stat_replication. write_lag và flush_lag nhỏ trong khi replay_lag lớn cho thấy bước phát lại là chỗ chậm, thường do xung đột với truy vấn trên hot standby.
  4. Đọc dữ liệu vừa ghi cần thiết kế ở tầng ứng dụng. Định tuyến về primary trong một cửa sổ thời gian, hoặc so LSN đã ghi với LSN bản sao đã phát lại.
  5. synchronous_commit = remote_apply làm commit chờ standby phát lại. Cái giá là độ trễ ghi và nguy cơ commit bị treo khi standby đồng bộ hỏng.
  6. Failover gồm bốn việc: fencing primary cũ, promote standby, nối lại các standby khác, đưa ứng dụng tới primary mới. Thiếu fencing dẫn tới split brain.
  7. Ứng dụng nên tìm primary theo vai trò. Chuỗi kết nối nhiều máy với target_session_attrs=read-write hoặc targetServerType=primary, tên DNS, hoặc IP ảo.
  8. Partitioning theo tháng giúp loại phân vùng khi truy vấn và xoá dữ liệu cũ bằng DETACH, DROP.
  9. Sharding chia cả ghi lẫn dung lượng nhưng chuyển join, giao dịch và vận hành sang ứng dụng. Chọn khoá phân mảnh, chuẩn bị cho chia lại shard và khoá nóng.
  10. Quyết định sharding dựa trên phép tính. Với số liệu hiện tại, Chợ Phố còn khoảng 15 tháng trước khi lượng ghi chạm giới hạn của một primary.

Tài liệu tham khảo

  • PostgreSQL 17 Documentation: chương "High Availability, Load Balancing, and Replication" (mục Log-Shipping Standby Servers, Hot Standby).
  • PostgreSQL 17 Documentation: view pg_stat_replication, các hàm quản trị sao chép và phục hồi, tham số synchronous_commit, synchronous_standby_names, hot_standby_feedback, max_standby_streaming_delay.
  • PostgreSQL 17 Documentation: libpq "Connection Strings" và target_session_attrs, pg_ctl, pg_rewind, "Table Partitioning", "TOAST", "Appendix K. PostgreSQL Limits".
  • PgJDBC Documentation: "Connecting to the Database", các tham số targetServerType và hostRecheckSeconds.
  • Tài liệu Patroni và tài liệu Citus về mô hình phân phối dữ liệu.
  • D. Karger và cộng sự, "Consistent Hashing and Random Trees", STOC 1997.

Chợ Phố là tình huống giả định. Các phép tính được kiểm lại bằng _build/c05_calc.py; giá trị mặc định, tên tham số và hành vi phần mềm được đối chiếu tài liệu chính thức trong _build/c05-verify.md.