Cache (P3/3): Dirty page, eviction và chọn kích thước cache

9 phút đọcSeries: MongoDB: từ gốc đến internals

Ở phần trước: một COLLSCAN quét cả collection làm thao tác/s của hot set giảm gần một nửa, từ 3.724 xuống 1.928. Dữ liệu vượt cache thì page đang dùng bị bỏ rồi đọc lại.

Lab 5: dirty page và áp lực eviction

Hot reader (61.000 document, đã warm-up) chạy 75 giây; từ giây 25, các writer chạy 40 giây trên cùng 1.000.000 document: updateOne ngẫu nhiên đặt lại body 2 KB (mỗi lần sửa một document ở page ngẫu nhiên, phải đọc page lên rồi biến nó thành dirty page), hoặc insertOne 2 KB nối đuôi. Mỗi giây ta ghi lại độ trễ của luồng đọc và các counter (e4-dirty.out, e4-dirty-run1-nocounters.out, e4-dirty-rerun.out). Độ trễ lấy trong cửa sổ giây 30–64:

Tải ghip50 đọc (trước → trong)Thao tác đọc/sDirty tối đaCache đầy tối đaApplication thread tự evictApplication thread tự đọc page
1 writer, 160 sửa/s0,16–0,23 → 0,17–0,23 ms≈ không đổi4,1–6,5%72–81%01,6–2,1 s tổng
4 writer sửa ngẫu nhiên (lần 1)0,17 → 0,774.970 → 74016,7%90%149 ms tổng45 s
4 writer sửa ngẫu nhiên (lần 2)0,19 → 1,223.791 → 1866,3%85%076 s
8 writer sửa ngẫu nhiên (lần chạy lại)0,15 → 0,975.061 → 65215,9%95%0110 s
4 writer insert nối đuôi0,12–0,26 → 0,41–0,492.519–5.908 → 1.554–1.7534,4–6,3%85–91%01,7–3,5 s

Cảnh báo về độ nhiễu: hai lần chạy cùng cấu hình "4 writer sửa" cách nhau gấp 4 lần về thao tác/s đọc ([suy luận] do VM dùng chung đĩa với các lab khác). Hình dạng thì ổn định:

  1. Dirty bám quanh eviction_dirty_target. Với một writer nhẹ, dirty tối đa 4,1–6,5% cache, quanh mốc 5%; [suy luận] eviction worker liên tục ghi dirty page xuống để giữ dirty ở mức đó. Dirty chỉ lên 16–17% khi tải ghi ngẫu nhiên nặng, rồi bị kéo xuống. Ở hai lần còn counter cuối, "number of times dirty trigger was reached" là 0, và ở mọi lần dirty tối đa 16,7% chưa tới 20%: lab này không tái hiện được vách đá của eviction_dirty_trigger, nên phần mô tả mốc 20% ở phần trước (WiredTiger cache là gì) chỉ dựa vào tài liệu.
  2. Thời gian application thread bị kéo đi evict gần như bằng 0 (tối đa 149 ms trong 35 giây), trong khi thời gian chúng tự đọc page lên cache là 45–110 giây cộng dồn (bộ đếm gộp cả luồng đọc lẫn các writer). [suy luận] Ở lab này, thứ làm chậm luồng đọc không phải "bị kéo vào eviction" mà là tranh nhau CPU và đĩa (hai CPU, 8 writer, mỗi lần sửa kéo cả page vào) và chờ đọc page. Đây là điểm cần tách khi đọc counter ở production: application thread time evicting tăng mới là dấu hiệu bị kéo vào eviction; còn application threads page read from disk to cache time tăng là cache miss.
  3. Ghi nặng còn có thể làm mongod hết bộ nhớ. Ở container 2 GB, lần chạy lại 8 writer sửa và 4 writer insert bị OOM-kill ở cuối (docker inspect: OOMKilled=true, exit 137). Số liệu theo từng giây vẫn còn nên vẫn nằm trong bảng, nhưng counter cuối (như số lần chạm dirty trigger) bị mất. Cache 0,5 GB không phải trần bộ nhớ của mongod: [tài liệu WiredTiger] cursor, session, data handle và bộ nhớ tạm khi đọc ghi page nằm ngoài cache, và page bị evict chỉ trả bộ nhớ về allocator, không làm tiến trình nhỏ lại. Lab không xác định phần nào đã đẩy mongod qua 2 GB.
  4. Ghi nối đuôi dễ hơn ghi ngẫu nhiên nhiều. 4 writer insert đẩy 570–710 MB xuống đĩa mà dirty chỉ 4–6%, trong khi sửa ngẫu nhiên đẩy dirty lên 16–17% ở hai lần chạy. [suy luận] Page cuối B-tree luôn nằm sẵn trong cache và dùng lại được; sửa ngẫu nhiên thì mỗi lần biến một page khác thành dirty, và phải đọc page đó lên trước.

Giới hạn 20% và TransactionTooLargeForCache

Bài Transactions & Atomicity nói phần dirty bị giới hạn 20%. Thử lại trên replica set một node, cache 0,256 GB (20% = 51,2 MB), một transaction insert liên tục document 0,5 MB (e7-ttlfc.out):

lần 1: 168 insert (≈ 84 MB), dirty 90,8 MB (35%) trước insert thất bại
lần 2:  50 insert (≈ 25 MB), dirty 27,0 MB (10,5%)
lần 3:  49 insert (≈ 25 MB), dirty 26,5 MB
lỗi: code 388, labels [], "transaction is too large and will not fit in the storage engine cache"
sau abort: dirty 0 MB

[tài liệu] Tham số transactionTooLargeForCacheThreshold (từ 6.2, mặc định 0,75) là tỉ lệ của phần dirty (20%) mà một transaction được phép chiếm để server còn tự thử lại: 15% tổng cache; transaction lớn hơn bị rollback vì áp lực cache thì nhận TransactionTooLargeForCache và không được thử lại. [quan sát] Điểm thất bại không phải một vạch sắc nét: 25 MB, 25 MB và 84 MB trên cùng cấu hình. Lần 2 và 3 thất bại ở 10,5% cache, dưới mốc 15%; lần 1 để dirty lên tới 35%, quá mốc 20%. Con số dùng được là: transaction chiếm quá một phần nhỏ cache có thể bị abort, không retry được. Import lớn thì chia lô, đúng như bài đó khuyên.

Chọn kích thước cache và theo dõi

Hit ratio không có sẵn, nhưng suy ra được

serverStatus không cho "cache hit ratio". Cái suy ra được và thật: miss mỗi thao tác = delta pages read into cache chia số thao tác. Trong Lab 1 ở phần trước (Lab: hit, miss và working set), mỗi lần tra _id luôn xin đúng 4 page, nên chia cho pages requested cho tỉ lệ miss theo page; ở workload khác số page mỗi thao tác khác nhau và con số chỉ là xấp xỉ. Hai chỉ số theo dõi theo thời gian, lấy delta chứ không lấy giá trị tuyệt đối:

pages read into cache / giây       ↑ kéo dài   → working set không vừa cache (hoặc vừa bị quét)
bytes currently in the cache       ≈ 80-90%    → bình thường, KHÔNG phải sự cố
tracked dirty bytes (% cache)      > 5% kéo dài, tiến về 20%  → ghi nhanh hơn eviction
application thread time evicting   > 0 và tăng → application thread bị kéo vào làm eviction
application threads page read ... time  tăng   → application thread chờ đọc page

Cache đầy 80% không phải dấu hiệu xấu: Lab 1 ở phần trước (Lab: hit, miss và working set) cho thấy nó là trạng thái bình thường. Dấu hiệu xấu là miss rate cao kéo dài cùng với latency tăng.

Thêm RAM hay sửa mô hình

Trước khi tăng cache, trả lời theo thứ tự:

1. Working set là gì? Phần nào ứng dụng thật sự chạm?
   (đừng lấy kích thước database)
2. Có phần nào trong cache không nên ở đó?
   → index thừa (mỗi index được dùng là working set), document rộng quá mức cần
     (projection không giảm byte vào cache), COLLSCAN định kỳ (đẩy hot set ra khỏi cache)
3. Sửa mô hình/index có đủ không?
   → subset pattern, index đúng, bỏ index chết, phân trang theo range
4. Vẫn không vừa? → thêm RAM (cache là phần của RAM; xem lại mặc định trước)
   hoặc shard để chia working set

Không khuyến nghị đổi cacheSizeGB như thuốc chữa bệnh. [tài liệu] Tài liệu MongoDB khuyên tránh tăng cache vượt mặc định, vì RAM còn lại là cache OS cho dữ liệu nén; Lab 5 cho thấy mongod còn cần bộ nhớ ngoài cache và có thể bị OOM-kill. Nếu buộc phải, dùng cacheSizePct và kiểm tra hostInfo trong container. Về Atlas hoặc môi trường managed: chưa kiểm tra trong lab này, hãy đọc tài liệu của dịch vụ đó.

So với PostgreSQL

MongoDB (WiredTiger)PostgreSQL
Bộ đệm riêng của engineWiredTiger cache, mặc định lớn hơn của 50% × (RAM − 1 GB) và 0,256 GB; dữ liệu collection chưa nénshared_buffers, mặc định thường 128 MB; tài liệu gợi ý khởi đầu 25% RAM cho máy chuyên dụng từ 1 GB RAM
Phần còn lại của RAMfilesystem cache của OS, giữ dữ liệu nénPostgreSQL "cũng dựa vào cache của hệ điều hành" (tài liệu shared_buffers)
Đơn vịpage của B-tree (cache)page BLCKSZ, thường 8 KB
Lời khuyên về cỡtránh tăng quá mặc địnhkhó có lợi khi quá khoảng 40% RAM, vì còn cache của OS

[tài liệu] Cả hai hệ thống đều xếp một bộ đệm riêng của engine trên cache của OS, nên cả hai có "hai tầng". Điều đó không làm cơ chế eviction hay hiệu năng của chúng tương đương; bài không so cơ chế thay thế page của PostgreSQL và không đo PostgreSQL.

Những lỗi thường gặp

  • Đo hiệu năng bằng kích thước database. Lab 1 ở phần trước (Lab: hit, miss và working set): cùng 2 GB, p50 0,12 ms ở 0,25x và 0,14 ms ở 4,3x, miss mỗi thao tác từ 0 lên 0,81; lệch 90/10 giữ miss ở 0,08.
  • Lo vì cache đầy 85%. Đó là trạng thái bình thường (target 80%, trigger 95%).
  • Đo cache miss bằng đĩa nhanh rồi kết luận miss rẻ. Lab 1 ở phần trước (Lab: hit, miss và working set): ở main miss rẻ vì cache OS che; cùng workload ở slow mất thêm 28% thao tác/s.
  • Thêm index "cho chắc". Index được dùng là working set; Lab 2: 8 index luân phiên làm query không liên quan chậm 32% ở main (5% ở slow).
  • Chạy COLLSCAN định kỳ cạnh dữ liệu ít được đọc. Lab 3: sau lần quét, dữ liệu đó phải đọc lại 128 page/giây trong 10 giây.
  • Một transaction hay một import cho cả lô. Lab 5: abort ở 25–84 MB trên cache 0,256 GB, không retry được.
  • Tăng cache sát RAM. Cache không phải trần bộ nhớ của mongod; lab bị OOM-kill ở 2 GB khi ghi nặng, dù cache chỉ 0,5 GB.

Cột mốc: Bạn đã có thể giải thích dirty page và eviction, biết dirty page được ghi xuống trước khi bị bỏ, và biết cache đầy 80% không phải lỗi. Bài này khép lại tại đây.

Hỏi & đáp

Trong lúc ghi nặng, application thread time evicting (usecs) gần 0 nhưng application threads page read from disk to cache time rất lớn. Chẩn đoán đúng?

  1. Application thread đang bị kéo vào eviction vì quá nhiều dirty page

    Ngược lại: thời gian evict của application thread gần 0 (tối đa 149 ms), dirty chỉ chạm 16–17%, chưa tới 20%. Xem Lab 5.

  2. Cache còn trống nhiều nên các con số này không có gì đáng lo

    Cache lúc đó đầy 85–95%; thời gian chờ đọc page lớn nghĩa là working set vượt cache. Xem bảng ở Lab 5.

  3. Application thread đang chờ đọc page: miss, không phải eviction

    Đúng. Lab 5: 45–110 giây cộng dồn chờ đọc page, vì mỗi lần sửa ngẫu nhiên phải kéo page lên. Xem Lab 5, điểm 2.

  4. Một transaction quá lớn sắp bị TransactionTooLargeForCache

    Lab này không dùng transaction; lỗi đó liên quan tới lượng dirty của một transaction. Xem mục "Giới hạn 20%".

Một dashboard có p95 tăng, pages read into cache cao kéo dài, working set ước tính gấp 3 lần cache. Việc đầu tiên nên làm?

  1. Tăng cacheSizeGB lên sát toàn bộ RAM của máy để mọi thứ vừa cache

    Cache không phải trần bộ nhớ của mongod; lab bị OOM-kill ở 2 GB khi ghi nặng, và RAM còn lại là cache OS cho dữ liệu nén. Xem mục "Thêm RAM hay sửa mô hình".

  2. Tạo thêm index trên mọi field dashboard lọc để mỗi query nhanh hơn

    Index được dùng là một phần working set: Lab 2, tám index luân phiên làm query không liên quan chậm 32% ở main.

  3. Bắt mỗi tuần một job quét cả collection để giữ cache luôn warm

    Lab 3: quét lớn hơn cache đẩy hot set ra và làm chậm cả hệ thống trong lúc chạy, không giúp cache warm hơn.

  4. Tìm phần working set thừa trước, rồi mới tính RAM

    Đúng. Index chết, document rộng, quét định kỳ đều làm working set to hơn cần thiết; xem thứ tự các bước ở mục "Thêm RAM hay sửa mô hình".

Nếu bỏ hết thuật ngữ: tốc độ phục vụ của một quầy phụ thuộc vào việc những thứ khách hay gọi có vừa trên quầy hay không, không phải kho to cỡ nào. Quầy luôn chừa một khoảng trống để thay món, và khi khách gọi món vượt sức chứa của quầy, người phục vụ phải chạy ra kho: nhanh hay chậm tuỳ kho gần hay xa. Một xe hàng đổ cả kho lên quầy trong một đêm sẽ làm những món ít gọi bị đẩy xuống đất, và sáng sau những món đó phải lấy lại từng cái.

Bài tiếp theo

Bài này cho thấy dirty page nằm trong cache và phải được ghi xuống. Nhưng ghi xuống khi nào, và nếu mất điện giữa lúc đó thì sao? Lệnh ghi đã được xác nhận nằm ở đâu trong lúc page chưa được ghi?

Journal & Checkpoint trả lời những câu đó: write-ahead log, j: true, chu kỳ checkpoint, và cách khôi phục sau crash. Cách giữ nhiều phiên bản của cùng một document trong cache, và vì sao một snapshot chạy lâu làm cache căng thẳng, là chuyện của bài WiredTiger MVCC.

Tài liệu tham khảo