Hai bài trước chỉnh cách planner ước lượng chi phí (effective_cache_size, random_page_cost). effective_io_concurrency thì khác: nó không đổi kế hoạch, nó đổi cách PostgreSQL thực thi việc đọc đĩa trong một Bitmap Heap Scan — cho phép nạp trước nhiều trang song song. Đây là một tham số SSD rất đáng bật. Nhưng bài này còn có một bài học lớn hơn cả bản thân tham số: tôi sẽ cho bạn thấy một phép đo thật mà eic=1 và eic=200 cho kết quả y hệt nhau — và vì sao đó chính là điều bạn phải hiểu để benchmark nó cho đúng.

effective_io_concurrency làm gì

Khi PostgreSQL chạy một Bitmap Heap Scan, nó có một lợi thế đặc biệt: từ bitmap dựng ở bước index, nó biết trước toàn bộ danh sách trang heap cần đọc. effective_io_concurrency (mặc định 1) nói cho nó biết được phép phát bao nhiêu yêu cầu đọc song song cho danh sách đó — qua lời gọi posix_fadvise(POSIX_FADV_WILLNEED) báo hệ điều hành nạp trước các trang. Thay vì đọc từng trang một rồi chờ, nó bảo HĐH "nạp trước 200 trang này đi" và xử lý các trang đến trước trong khi phần còn lại đang được đọc.

SHOW effective_io_concurrency;   -- mặc định 1 (gần như tắt prefetch)

Điểm mấu chốt: nó chỉ áp dụng cho Bitmap Heap Scan. Seq Scan dựa vào cơ chế readahead của HĐH; Index Scan thuần đi lắt nhắt từng dòng nên không prefetch hàng loạt được. Chỉ Bitmap Heap Scan mới biết trước cả tập trang để nạp song song hiệu quả.

Ảnh chụp đoạn mã SQL nền tối minh hoạ effective_io_concurrency đọc trước nhiều trang song song prefetch, SHOW effective_io_concurrency mặc định 1 trong Bitmap Heap Scan PostgreSQL biết trước danh sách trang heap cần đọc từ bitmap với eic lớn hơn 1 nó gọi posix_fadvise WILLNEED để yêu cầu HĐH nạp trước nhiều trang cùng lúc che giấu độ trễ đọc đĩa bằng cách đọc song song, nó chỉ áp dụng cho Bitmap Heap Scan Seq Scan dựa vào readahead của HĐH không dùng eic Index Scan plain đi lắt nhắt từng dòng không prefetch hàng loạt Bitmap Heap Scan biết trước cả tập trang prefetch hiệu quả nhất SET enable_seqscan off ép Bitmap để thấy eic có tác dụng SELECT count pad FROM eio WHERE grp nhỏ hơn 20000, con số bằng độ sâu hàng đợi phần cứng lưu trữ HDD 1 trục quay 1 đến 2 một đầu đọc song song vô ích RAID nhiều đĩa số trục quay SSD NVMe 200-300 hàng đợi sâu nhiều kênh mặc định 1 tắt prefetch quá thấp cho SSD eic 0 tắt hẳn prefetch, đặt cho SSD và người anh em maintenance_io_concurrency ALTER SYSTEM SET effective_io_concurrency 200 SSD ALTER SYSTEM SET maintenance_io_concurrency 200 mặc định 10 bản cho VACUUM pg_prewarm ANALYZE lưu ý prefetch chỉ giúp khi đọc thật từ đĩa có độ trễ nếu dữ liệu đã nằm trong cache eic gần như vô hại vô ích

Hình 1: effective_io_concurrency cho phép Bitmap Heap Scan nạp trước nhiều trang heap song song qua posix_fadvise. Con số phản ánh độ sâu hàng đợi I/O của phần cứng — SSD 200-300, HDD ~1. Mặc định 1 là tắt prefetch.

Đo thật: eic=1 và eic=200 cho thời gian y hệt

Tôi dựng bảng eio 1,9GB (8 triệu dòng, lớn hơn nhiều shared_buffers 128MB) với cột grp rải rác khắp bảng. Truy vấn grp < 20000 khớp 320.000 dòng nằm rải rác — buộc Bitmap Heap Scan phải đọc hàng nghìn trang heap ở khắp nơi. Để bắt đầu từ cache nguội, tôi restart container trước mỗi lần đo (xoá sạch shared_buffers), và ép Bitmap Heap Scan bằng enable_seqscan=off:

SET enable_seqscan = off;
SELECT count(pad) FROM eio WHERE grp < 20000;   -- Bitmap Heap Scan, read=10121 trang

Ảnh chụp bảng kết quả EXPLAIN ANALYZE nền tối đo thật effective_io_concurrency trên lab đã cache tác dụng xấp xỉ bằng không Bitmap Heap Scan bảng eio 1,9GB 8 triệu dòng lớn hơn shared_buffers 128MB restart để xoá shared_buffers mỗi lần PostgreSQL 16, Bitmap Heap Scan cùng một kết quả read eic 1 so với 200 Bitmap Heap Scan on eio grp nhỏ hơn 20000 320000 dòng Buffers shared read 10121 cùng số trang đọc cả hai lần eic 1 actual time 5.526 đến 40.414 Execution Time 40,4 ms eic 200 actual time 5.957 đến 39.541 Execution Time 39,5 ms lặp lại nhiều lần 40,4 trên 39,5 ms chênh lệch nằm trong nhiễu đo, bảng effective_io_concurrency 1 mặc định tắt prefetch read 10121 40,4 ms 200 SSD read 10121 39,5 ms xấp xỉ như nhau, vì sao không thấy khác biệt và đây là bài học quan trọng prefetch posix_fadvise WILLNEED chỉ có ích khi mỗi lần đọc trang có độ trễ thật để che giấu ở lab này container trên máy nhanh các read được phục vụ từ page cache của máy ảo ở tốc độ gần RAM không có độ trễ nào để prefetch che eic vô tác dụng cạm bẫy benchmark trên dữ liệu đã nằm trong cache sẽ luôn thấy eic không có tác dụng đó là lỗi đo không phải kết luận đúng, khi nào eic thật sự giúp đọc nguội thật từ lưu trữ có độ trễ SSD NVMe hàng đợi sâu HDD Bitmap Heap Scan quét hàng nghìn trang rải rác eic 200 cho phép hàng trăm lần đọc chạy song song che độ trễ có thể nhanh 2-3 lần SSD nên đặt 200-300 mặc định 1 là bỏ phí nhưng phải đo trên tải thật với cache nguội mới thấy không đo được trên dữ liệu cache

Hình 2: Bitmap Heap Scan đọc read=10121 trang. eic=1 → 40,4 ms; eic=200 → 39,5 ms — chênh lệch nằm trong nhiễu đo. Trên lab đã cache, prefetch không có độ trễ nào để che nên tác dụng gần như bằng không.

Kết quả trung thực: eic=1 cho 40,4 ms, eic=200 cho 39,5 ms — cùng read=10121 trang, chênh lệch nằm trong sai số đo. Lặp lại nhiều lần vẫn vậy. Trên lab này, effective_io_concurrency không có tác dụng đo được.

Vì sao không thấy khác biệt — và bài học benchmark

Đây mới là phần đáng giá. Prefetch chỉ có ích khi mỗi lần đọc trang có độ trễ thật để che giấu. Container này chạy trên máy nhanh, và các trang "read" mà PostgreSQL báo thật ra được phục vụ từ page cache của máy ảo ở tốc độ gần RAM. Khi mỗi lần đọc gần như tức thì, posix_fadvise(WILLNEED) chẳng có độ trễ nào để giấu — nó thành lời khuyên vô nghĩa. Tệ hơn, prefetch một trang đã nằm trong cache là thao tác không làm gì cả.

Bài học: benchmark effective_io_concurrency trên dữ liệu đã nằm trong cache sẽ LUÔN cho kết quả "không có tác dụng" — và đó là lỗi phương pháp đo, không phải kết luận đúng về tham số. Rất nhiều người kết luận nhầm "eic vô dụng" chỉ vì họ đo trên dữ liệu nóng. Tham số này chỉ bộc lộ giá trị khi đọc nguội thật từ lưu trữ có độ trễ thật.

Khi nào eic thật sự giúp

Trên storage thật với độ trễ mỗi lần đọc đáng kể — HDD, hay SSD/NVMe khi khai thác hàng đợi sâu — một Bitmap Heap Scan quét hàng nghìn trang rải rác được lợi rõ: eic=200 cho phép hàng trăm lần đọc chạy song song, che độ trễ chồng lên nhau, có thể nhanh gấp 2-3 lần so với đọc tuần tự từng trang. Con số nên phản ánh độ sâu hàng đợi của thiết bị:

ALTER SYSTEM SET effective_io_concurrency = 200;      -- SSD/NVMe
ALTER SYSTEM SET maintenance_io_concurrency = 200;    -- mặc định 10, cho VACUUM/prewarm
SELECT pg_reload_conf();

Có một người anh em ít biết: maintenance_io_concurrency (mặc định 10) là biến thể dùng cho công việc bảo trì — VACUUM, pg_prewarm, một số thao tác ANALYZE. Trên SSD cũng nên nâng nó lên tương ứng.

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

Trên HDD một trục quay, để mặc định 1. Đĩa cơ chỉ có một đầu đọc; phát nhiều yêu cầu song song không giúp gì mà còn có thể làm đầu đọc nhảy loạn. Giá trị cao chỉ hợp với thiết bị thật sự xử lý song song được (RAID nhiều trục, SSD nhiều kênh).

Đây không phải song song hoá truy vấn. effective_io_concurrency chỉ là prefetch I/O trong một tiến trình, khác hẳn parallel query (max_parallel_workers_per_gather) vốn dùng nhiều CPU worker. Đừng nhầm hai thứ.

Phải đo trên tải thật, cache nguội — không đo được trên dữ liệu cache. Như chính lab này cho thấy, nếu working set nằm gọn trong RAM/OS cache thì eic gần như vô hình. Nó chỉ đáng chỉnh khi hệ thống của bạn thường xuyên đọc dữ liệu vượt cache từ đĩa — và cách kiểm chứng đúng là đo trên chính tải đó, sau khi cache nguội, không phải trên bảng vừa quét xong.

Ba ý mang về

  1. effective_io_concurrency cho Bitmap Heap Scan nạp trước nhiều trang heap song song qua posix_fadvise — nó biết trước cả tập trang từ bitmap; mặc định 1 tắt prefetch, SSD nên đặt 200-300 (và maintenance_io_concurrency cho VACUUM/prewarm).
  2. Đo thật trên lab đã cache: eic=1 và eic=200 cho thời gian y hệt (40,4 so với 39,5 ms, cùng read=10121) — vì prefetch chỉ che được độ trễ đọc đĩa thật, mà ở đây reads phục vụ từ page cache ở tốc độ gần RAM nên không có gì để che.
  3. Bài học benchmark: đo eic trên dữ liệu đã cache sẽ luôn thấy nó "vô dụng" — đó là lỗi phương pháp; tham số chỉ bộc lộ giá trị (có thể 2-3 lần) khi đọc nguội thật từ storage có độ trễ, và chỉ trên HDD một trục quay mới nên để mặc định 1.

Phần sau ta chuyển sang mảng ghi và độ bền: Phần sau mổ xẻ WAL và checkpoint — vì sao mọi thay đổi ghi vào WAL trước, checkpoint dồn ghi ra sao, và chỉnh chúng thế nào để tránh sốc I/O.