Cả sê-ri này nói về đo lường trung thực. Bài này gặp công cụ đo tải chuẩn của PostgreSQL — pgbench — và cũng là bài mà mọi nguyên tắc đo của sê-ri hội tụ về một câu: một con số chỉ có nghĩa kèm điều kiện của nó. Tôi sẽ đo tps thật bằng pgbench, thấy nó tăng theo tải rồi bão hòa, và ghi lại lần tôi suýt khoe một con số hoàn toàn vô nghĩa.

Benchmark bằng pgbench

pgbench đo gì

pgbench là công cụ đi kèm PostgreSQL để đo thông lượng. Bạn khởi tạo dữ liệu bằng pgbench -i -s N (N là scale factor — quyết định kích thước dữ liệu), rồi chạy tải bằng pgbench -c C -T t: nó mô phỏng C client đồng thời gửi giao dịch trong t giây, và báo về tps (giao dịch mỗi giây), latency trung bìnhđộ lệch chuẩn của latency. Tải mặc định là một giao dịch kiểu ngân hàng (đọc một tài khoản, cập nhật số dư, ghi lịch sử); cờ -S đổi sang chỉ SELECT (read-only), -M prepared dùng prepared statement (bỏ chi phí parse mỗi câu). Ngoài ra -j đặt số luồng phía client (nên khớp số lõi để chính client không thành nút cổ chai), và -t chạy theo số giao dịch thay vì theo thời gian. Chọn tham số sai — ví dụ một luồng client phục vụ ba mươi hai kết nối — có thể khiến bạn đo giới hạn của chính pgbench thay vì của CSDL.

Tôi khởi tạo pgbench -i -s 10 (tạo bảng tài khoản ~150MB) rồi đo. Một chi tiết quan trọng ngay từ đây: scale factor 10 nghĩa là dữ liệu chỉ 150MB — nhỏ hơn RAM của máy rất nhiều. Con số này sẽ quay lại ám ảnh phần đo hớ, vì một benchmark mà toàn bộ dữ liệu nằm trong bộ nhớ đo một thứ rất khác với một benchmark mà dữ liệu phải đọc từ đĩa.

Đo: tps tăng theo tải rồi bão hòa

Chạy tải read-write (mặc định) với số client -c tăng dần:

-c    read-write tps
 1      4864
 4      9161
 8     10626
16     14213
32     16052

Tps tăng khi thêm client — nhưng không tuyến tính. Từ 1 lên 4 client, tps chỉ tăng 1,9 lần (không phải 4). Từ 16 lên 32 client (gấp đôi), tps chỉ nhích 14213 lên 16052 — 1,13 lần. Nó đang bão hòa: máy có 10 lõi, và một khi mọi lõi đều bận, thêm client chỉ làm chúng xếp hàng chờ, không tăng thông lượng. Đây là hình dạng kinh điển của mọi benchmark đồng thời — tăng, cong, rồi phẳng (đôi khi giảm vì chi phí tranh chấp). Hình dạng này lặp lại đúng thứ bài kế hoạch song song đã đo: thêm sức tính toán chỉ giúp tới một trần, đặt bởi tài nguyên dùng chung (ở đây là số lõi CPU và băng thông đĩa). Số lõi không phải một phép nhân thần kỳ cho thông lượng.

Đổi sang read-only (-S) cho con số cao hơn hẳn:

-c    read-only tps
 1     29124
16    193954
32    203189

Read-only nhanh hơn read-write khoảng 6 tới 14 lần, vì nó không ghi WAL, không fsync, không commit tốn kém — chỉ đọc thuần túy. Và nó bão hòa cao hơn (194k ở 16 client) vì nghẽn của nó là CPU, không phải đĩa. Đây đã là chỗ con số bắt đầu đánh lừa.

Một lần tôi đo hớ: con số 200 nghìn vô nghĩa

Thấy 203189 tps read-only, phản xạ của tôi là khoe: "CSDL này chạy hai trăm nghìn giao dịch mỗi giây!". Nhưng dừng lại một giây, con số đó gần như không nói gì về hiệu năng production, vì mọi điều kiện đo đều lệch về phía dễ:

Thứ nhất, nó là read-only — không có ghi, không có commit, không có fsync. Ứng dụng thật gần như luôn có ghi, và ghi là phần đắt nhất (như bài gom commit đã đo, fsync mỗi commit là nút cổ chai). Thứ hai, tôi đo trên đĩa máy ảo, nơi fsync gần như miễn phí — một điều tôi đã nhắc lại suốt sê-ri; trên đĩa vật lý thật, tps read-write sẽ thấp hơn nhiều vì bị chặn bởi tốc độ fsync. Thứ ba, 150MB dữ liệu nằm gọn trong RAM, nên không hề có đọc đĩa thật; một CSDL production với dữ liệu lớn hơn RAM sẽ phải đọc từ đĩa và chậm hơn hẳn.

Đổi bất kỳ điều kiện nào trong ba cái đó về phía thật — thêm ghi, đĩa thật, dữ liệu lớn hơn RAM — và 203k tps sụp xuống một con số hoàn toàn khác. Cái sai của tôi là đọc một con số như thể nó là thuộc tính của "CSDL", trong khi nó là thuộc tính của CSDL trong đúng điều kiện tôi đo: nhỏ, read-only, fsync rẻ. Một benchmark thiếu điều kiện là một con số nói dối — đúng bài học xuyên suốt sê-ri: "nhanh" vô nghĩa nếu chưa hỏi đo ở đâu, với gì, tải nào.

Cái bẫy thứ hai cũng ẩn trong bảng số: đừng ngoại suy tuyến tính. Nếu tôi lấy tps của 1 client (4864) rồi nhân cho 32 client, tôi dự đoán 155.648 tps. Thực đo ở 32 client chỉ 16.052 — sai mười lần. Vì bão hòa, một client không nói gì về ba mươi hai client. Đo ở đúng mức đồng thời mình quan tâm, đừng suy từ một điểm. Đường cong tps theo số client là dữ liệu; một điểm trên đường cong đó, nhân lên, là phỏng đoán — và phỏng đoán này gần như luôn quá lạc quan vì bỏ qua chính cái trần mà benchmark sinh ra để tìm.

Vì sao điều này quan trọng khi lập trình

Hệ quả đầu tiên: benchmark phải giống production ở những trục quan trọng. Scale factor đủ lớn để dữ liệu vượt RAM (nếu production vậy), tỉ lệ đọc/ghi giống tải thật, chạy trên phần cứng giống đích, ở mức đồng thời thật. Một con số đẹp trên laptop với dữ liệu tí xíu không hứa hẹn gì về máy chủ thật. Như bài kết nốibài checkpoint đã cho thấy, các nút cổ chai (fsync, I/O đĩa, cache) chỉ lộ ra ở đúng quy mô và đúng phần cứng.

Hệ quả thứ hai: đọc tps kèm latency và độ lệch chuẩn, và đo đường cong, không đo một điểm. Con số mang theo: pgbench đo tps/latency của một tải; tps tăng theo số client rồi BÃO HÒA ở số lõi (read-write 4864→16052 tps từ 1 tới 32 client, read-only tới 203k vì nghẽn ở CPU) — nhưng con số cao ở đây vô nghĩa cho production vì đo read-only trên đĩa VM fsync rẻ với 150MB vừa RAM; benchmark chỉ có nghĩa ở ĐÚNG điều kiện (scale > RAM, đĩa thật, tải có ghi), và ngoại suy 1 client lên 32 sai 10 lần vì bão hòa. Trước khi tin một con số benchmark, hỏi nó đo ở điều kiện nào.

Thử ba mươi giây

Nếu có PostgreSQL, chạy pgbench -i -s 5 để khởi tạo, rồi pgbench -c 1 -T 5pgbench -c 8 -T 5 — so hai con số tps. Bạn sẽ thấy 8 client không cho tps gấp 8 lần 1 client; tỉ số nhỏ hơn nhiều, và càng tăng -c tỉ số càng co lại khi chạm số lõi máy. Rồi thêm -S (read-only) và chạy lại: tps vọt lên vì bỏ hết phần ghi. Nửa phút đó cho bạn thấy hai điều: thông lượng bão hòa theo số lõi, và một con số tps thay đổi hoàn toàn tùy bạn đo đọc hay ghi — nên đừng bao giờ trích một con số tps mà không kèm điều kiện đo nó.