"Tỉ lệ trúng đệm phải trên 99%" là một trong những lời khuyên được lặp lại nhiều nhất về PostgreSQL. Phần này đo xem con số đó thật sự nói lên điều gì, và tìm ra rằng nó nói ít hơn nhiều so với người ta tưởng.

Hai lớp đệm, bằng chứng I/O, quét shared_buffers, và pg_buffercache

Có hai lớp đệm, thống kê chỉ thấy một

Khi PostgreSQL cần một trang dữ liệu, nó đi qua ba nơi:

shared_buffers (PostgreSQL)  ->  bộ đệm trang (hệ điều hành)  ->  đĩa

pg_statio_user_tables đếm heap_blks_hit khi trang có sẵn ở lớp thứ nhất, và heap_blks_read khi không có. Nhưng "không có ở lớp thứ nhất" không có nghĩa là "đọc từ đĩa" — lớp thứ hai thường trả về ngay.

Đây là toàn bộ nội dung của bài này.

Bằng chứng

Máy chủ với shared_buffers = 32MB, bảng 661 MB, bật track_io_timing:

Buffers: shared read=19184
I/O Timings: shared read=27.752
Execution Time: 115.745 ms

19.184 trang được đánh dấu là "read" — tức trượt đệm hoàn toàn. Nhưng tổng thời gian I/O chỉ 27,752 ms:

27,752 ms / 19.184 trang = 1,4 micro giây mỗi trang

Một lần đọc ngẫu nhiên từ SSD NVMe mất ít nhất 100 micro giây. Từ đĩa quay thì vài mili giây. 1,4 micro giây là tốc độ của một phép sao chép bộ nhớ.

Những trang đó đến từ bộ đệm của hệ điều hành, và pg_statio không biết điều đó.

Tăng shared_buffers: tỉ lệ nhảy vọt, thời gian gần như đứng yên

Cùng truy vấn, cùng dữ liệu, chỉ đổi shared_buffers:

shared_buffers Tỉ lệ trúng Thời gian Trang "read"
32 MB 0,00% 122 ms 109.998
128 MB 0,00% 140 ms 109.998
512 MB 83,33% 101 ms 18.333
1 GB 83,33% 94 ms 18.333

Tỉ lệ trúng đi từ 0% lên 83% — theo lời khuyên phổ biến thì đây là khác biệt giữa "thảm hoạ" và "chấp nhận được". Thời gian thì đi từ 122 ms xuống 94 ms: giảm 23%.

Nếu bạn chỉ nhìn tỉ lệ trúng, bạn sẽ kết luận rằng cấu hình 32 MB đang hỏng nặng. Nếu bạn nhìn thời gian, bạn thấy nó chậm hơn một phần tư.

Chú ý dòng 128 MB: tỉ lệ trúng vẫn 0% và thời gian tệ hơn 32 MB. Bảng 661 MB không vừa vào 128 MB, nên mỗi lần quét lại đẩy hết ra — đệm lớn hơn nhưng vẫn không đủ thì không giúp gì, mà lại lấy mất bộ nhớ của hệ điều hành.

Nhìn vào trong shared_buffers

Extension pg_buffercache cho biết chính xác cái gì đang nằm trong đệm:

create extension pg_buffercache;
select c.relname, count(*) as so_trang,
       pg_size_pretty(count(*) * 8192) as kich_thuoc,
       round(100.0 * count(*) / (select count(*) from pg_buffercache), 1) as phan_tram
from pg_buffercache b
left join pg_class c on c.relfilenode = b.relfilenode
group by c.relname order by count(*) desc limit 6;

Sau khi chạy một truy vấn quét bảng lớn:

to_             3.938 trang     31 MB     96,1%
(trống)            84 trang    672 kB      2,1%
pg_operator        11 trang     88 kB      0,3%

96,1% bộ đệm bị một bảng chiếm. Mọi thứ khác — kể cả catalog hệ thống mà mọi truy vấn đều cần — đã bị đẩy ra.

Đây là hiện tượng đáng biết: một truy vấn báo cáo quét bảng lớn có thể làm chậm mọi truy vấn khác trong vài phút sau đó, không phải vì nó chiếm CPU mà vì nó xoá sạch bộ đệm.

PostgreSQL có phòng vệ một phần cho chuyện này — quét tuần tự trên bảng lớn hơn một phần tư shared_buffers dùng một vùng đệm vòng nhỏ thay vì chiếm cả đệm. Nhưng như số đo cho thấy, nó không ngăn được hoàn toàn.

Đệm hai lớp: cùng dữ liệu nằm hai nơi

MemTotal:  16.354.680 kB
Cached:     8.332.588 kB

Hệ điều hành đang giữ 8,3 GB trong bộ đệm trang. PostgreSQL giữ thêm 32 MB nữa trong shared_buffers — và phần lớn 32 MB đó là bản sao của dữ liệu đã có trong 8,3 GB kia.

Đó là lý do đặt shared_buffers rất lớn không phải lúc nào cũng tốt: mỗi byte bạn cấp cho PostgreSQL là một byte hệ điều hành không dùng được, và nếu dữ liệu bị lưu hai lần thì bạn mất một nửa bộ nhớ hiệu dụng.

Quy tắc 25% RAM phổ biến chính là từ chỗ này. Nó không phải con số tối ưu về mặt lý thuyết — nó là điểm cân bằng giữa "PostgreSQL kiểm soát được cái gì nằm trong đệm" và "để hệ điều hành làm phần còn lại".

effective_cache_size không cấp phát gì

Tham số này hay bị nhầm với shared_buffers. Nó không cấp phát một byte nào — nó chỉ là con số bạn nói với bộ lập lịch: "tổng cộng có khoảng chừng này bộ nhớ đệm, kể cả của hệ điều hành".

Bộ lập lịch dùng nó để ước lượng chi phí đọc chỉ mục: nếu đệm lớn, khả năng trang đã có sẵn cao, nên quét chỉ mục rẻ hơn.

Tôi thử đổi từ 128MB lên 4GB:

effective_cache_size = 128MB  ->  Bitmap Heap Scan  (cost=282.18..49314.17 rows=25000)
effective_cache_size = 4GB    ->  Bitmap Heap Scan  (cost=282.18..49314.17 rows=25000)

Kế hoạch không đổi, chi phí ước lượng cũng không đổi tới hai chữ số thập phân.

Tôi không kết luận rằng tham số này vô dụng — tôi kết luận rằng trên truy vấn cụ thể này, với độ chọn lọc này, nó không đủ để lật lựa chọn. Nó có ảnh hưởng ở vùng ranh giới giữa quét chỉ mục và quét tuần tự, và truy vấn của tôi không nằm ở vùng đó.

Đặt nó bằng khoảng 50–75% RAM là an toàn, và nó miễn phí vì không cấp phát gì.

Cách đọc tỉ lệ trúng cho đúng

Tỉ lệ trúng vẫn có ích, nhưng phải đọc cùng thứ khác:

select relname,
       heap_blks_hit, heap_blks_read,
       round(100.0 * heap_blks_hit / nullif(heap_blks_hit + heap_blks_read, 0), 2) as ti_le
from pg_statio_user_tables
where heap_blks_read > 0
order by heap_blks_read desc limit 10;

Rồi với bảng đứng đầu, chạy truy vấn thật với EXPLAIN (ANALYZE, BUFFERS)bật track_io_timing:

alter system set track_io_timing = on;
select pg_reload_conf();

Con số cần nhìn là I/O Timings chia cho số trang read:

Thời gian mỗi trang Nghĩa là
Dưới 10 µs Đến từ bộ đệm hệ điều hành — không phải vấn đề
50–200 µs Đọc SSD thật
Trên 1 ms Đọc đĩa quay, hoặc đĩa mạng, hoặc máy chủ đang quá tải I/O

Chỉ ở hai hàng cuối thì tỉ lệ trúng thấp mới là vấn đề đáng sửa.

Ba việc theo thứ tự

  1. Đặt shared_buffers khoảng 25% RAM rồi dừng lại. Đây là điểm khởi đầu hợp lý, không phải con số cần tinh chỉnh.
  2. Đặt effective_cache_size khoảng 50–75% RAM. Miễn phí, và nó giúp bộ lập lịch chọn đúng.
  3. Bật track_io_timing. Chi phí gần như bằng không trên máy chủ hiện đại, và không có nó thì bạn không phân biệt được đệm hệ điều hành với đĩa thật.

Điều không nên làm là tăng shared_buffers lên 50% hay 75% RAM để đuổi theo con số tỉ lệ trúng. Số đo ở đầu bài cho thấy phần thưởng nhỏ, và phần 32 đã đo một trường hợp mà cấp thêm bộ nhớ còn làm chậm đi.

Thử ba mươi giây

select name, setting, unit from pg_settings
where name in ('shared_buffers','effective_cache_size','track_io_timing');

Nếu track_io_timing đang off, bật nó. Đó là thay đổi rẻ nhất trong bài, và nó biến cột read từ một con số không diễn giải được thành một con số nói cho bạn biết chính xác dữ liệu đến từ đâu.

Phần sau đo WAL và checkpoint: cấu hình nào làm máy chủ dừng lại vài giây một lần, và cách nhận ra nó.