Cache (P1/3): WiredTiger cache là gì

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

Bài này trả lời hai câu: WiredTiger cache giữ những gì, và khi nó đầy thì chuyện gì xảy ra. Phần này định nghĩa cache và working set; Lab: hit, miss và working set đo chúng; Dirty page, eviction và chọn kích thước cache đi vào eviction và cách chọn kích thước.

Nhiều bài trước để lại cùng một lời hứa, mỗi bài từ một phía. Bài Mental Model đo mọi thứ khi cache đã warm (đã nạp sẵn dữ liệu) và hứa đo cold cache (cache còn trống, chưa nạp dữ liệu). Bài Data Modeling đoán rằng "9 document rải ở 3 collection" sẽ đắt hơn "1 document" nhiều hơn tỉ lệ đo được khi dữ liệu không còn nằm trong RAM. Bài Schema Design Patterns thấy một document 10,9 MB chiếm 11,8 MB cache. Bài CRUD & Query Model dừng ở một COLLSCAN còn nằm gọn trong cache. Bài Index Fundamentals nói mỗi index tranh chỗ trong cache. Bài Transactions & Atomicity dừng ở lỗi TransactionTooLargeForCache. Tất cả đều trỏ về một chỗ.

Bài này mở hộp đó. Câu hỏi xuyên suốt, và là sợi chỉ của cả series: working set của bạn có vừa trong cache không? Hai database cùng 100 GB có thể khác nhau nhiều lần về độ trễ, vì thứ quyết định không phải 100 GB mà là phần bạn thật sự chạm tới.

Bài này nằm ở đâu

  • Cần biết trước: Mental Model (WiredTiger cache, working set, cache hit và miss ở mức mô hình), WiredTiger & Compression (page, B-tree, dữ liệu nén trên đĩa và không nén trong RAM: bài này dùng các khái niệm đó, không dạy lại), Index Fundamentals (index cũng là B-tree nằm trong cache)
  • Giới thiệu: chuỗi dataset → working set → cache → RAM → hit/miss → disk I/O → latency, kích thước cache mặc định và cách đọc serverStatus().wiredTiger.cache, clean page và dirty page, ngưỡng eviction, eviction thread và application thread bị kéo vào eviction, cách đo working set so với cache, cách quyết định thêm RAM hay sửa mô hình
  • Không dạy ở đây: journal và checkpoint, tức cách dirty page thành dữ liệu bền (bài Journal & Checkpoint); lịch sử phiên bản và history store (bài WiredTiger MVCC, chỉ nhắc một câu); ticket và lock (bài Concurrency); connection pool (bài Connection Pool & Capacity)
  • Dẫn tới: Journal & Checkpoint

Môi trường lab: MongoDB 8.3.11 trong Docker (image mongo:8), host Apple M4, mongosh 2.12.0, standalone (không replica set), mỗi container 2 CPU và WiredTiger cache 0,5 GB (--wiredTigerCacheSizeGB 0.5). Cache nhỏ có chủ ý: để làm working set lớn hơn cache ta chỉ cần vài GB dữ liệu. Có hai cấu hình cùng volume dữ liệu: main (RAM 2 GB, đĩa nhanh của Docker Desktop) và slow (RAM 1,25 GB để filesystem cache của OS không giữ nổi file dữ liệu, kèm giới hạn 3.000 IOPS đọc bằng --device-read-iops, tức một chiếc đĩa mạng mô phỏng, không phải đĩa thật). Dữ liệu chính: collection items 1.000.000 document, mỗi cái khoảng 2 KB (1.956 MB logic, 1.174 MB trên đĩa sau snappy, khoảng 2,2 GB khi nằm trong cache), tức khoảng 4,3 lần cache. Client là mongosh trong container riêng, dùng chung network namespace với mongod, đọc tuần tự một luồng (closed loop).

Ba điều phải nói trước. (1) Docker Desktop không cho xoá filesystem cache của cả máy, nên mỗi lần cần cache OS ở trạng thái cold tôi dùng posix_fadvise(DONTNEED) chỉ trên các file của volume lab (dd iflag=nocache) và kiểm tra bằng rbytes trong io.stat của cgroup; có lần chạy "giữ cache OS" vẫn đọc đĩa vì máy ảo bị các lab khác chiếm bộ nhớ. (2) VM Docker dùng chung CPU và đĩa với các lab khác chạy cùng lúc, nên số tuyệt đối dao động giữa các lần chạy, nhất là số ghi. Tôi giữ cả hai lần chạy khi chúng khác nhau. (3) Mọi con số là cận dưới của cái giá thật: đĩa dưới Docker Desktop nhanh hơn nhiều đĩa mạng của production.

Nhãn: [tài liệu] theo tài liệu chính thức (manual MongoDB, tài liệu WiredTiger); [quan sát] đo trong lab này (8.3.11); [suy luận] rút ra từ quan sát, không kiểm tra riêng; [chi tiết cài đặt] cách server đang làm, không phải cam kết API; [hình dung] mô hình để dễ nhớ.

Ý chính

Cache của WiredTiger là vùng RAM giữ các page đã đọc hoặc đã sửa của collection và index. Cần một page: có sẵn trong cache (hit) thì xong trong vài micro giây; không có (miss) thì phải đọc từ file, giải nén, rồi mới dùng được. Cache có kích thước cố định, nên khi đầy, WiredTiger evict bớt page cũ (bỏ page đó khỏi cache để lấy chỗ). Page chưa bị sửa (clean) thì chỉ cần bỏ đi; page đã bị sửa trong cache nhưng chưa ghi xuống đĩa (dirty) phải ghi xuống trước.

Working set là phần dữ liệu và index mà ứng dụng thường xuyên chạm tới. Nếu nó vừa cache, hầu hết thao tác là hit và database nhanh dù tổng dữ liệu lớn bao nhiêu. Nếu không vừa, các page đang được dùng liên tục bị evict rồi đọc lại, và độ trễ tăng theo số lần phải xuống đĩa. Khi cache vừa đầy vừa nhiều dirty page, chính các thread phục vụ query còn bị kéo vào làm việc eviction.

Hình dung trước: kho hàng và cái bàn làm việc

Một kho hàng có 2.000 thùng trên kệ. Nhân viên kho chỉ làm việc trên một cái bàn nhỏ, vừa 100 thùng. Khách gọi món nào, nhân viên ra kệ khiêng thùng đó về bàn, mở nén ra (hàng trên kệ được ép chặt cho gọn), rồi xử lý đơn. Thùng đã mở nằm lại trên bàn: khách sau hỏi cùng món thì có ngay. Bàn đầy thì phải bớt thùng đi: thùng nào lâu không ai đụng thì cất lại. Thùng chỉ được xem mà không bị sửa thì chỉ cần bỏ đi; thùng bị ghi chú, sửa số lượng thì phải mang về kệ trước. Giữa kệ và bàn còn một hành lang (filesystem cache của OS) để những thùng vừa khiêng chưa kịp xếp lại kệ.

Nhà kho                                   MongoDB
──────────────────────────────────────    ──────────────────────────────────────
kệ hàng, thùng ép chặt                    file .wt trên đĩa, page nén
hành lang trước kệ                        filesystem cache của OS (page nén)
cái bàn làm việc, thùng đã mở             WiredTiger cache (page chưa nén)
khách gọi món có sẵn trên bàn             cache hit
khách gọi món phải ra kệ lấy              cache miss: đọc file, giải nén
thùng chỉ xem, lâu không dùng             clean page, bỏ đi được ngay
thùng đã sửa chưa cất lại kệ              dirty page, phải ghi xuống trước
người chuyên dọn bàn                      eviction thread
bàn quá đầy, nhân viên phải tự dọn         application thread bị kéo vào eviction
100 món khách hay gọi nhất                working set

[hình dung] Đây chỉ là cách hình dung. WiredTiger không xếp "thùng" theo từng document: nó đọc theo page, một khối chứa nhiều document (ở cỡ document nhỏ, hàng xóm đi nhờ vào cache; ở cỡ 10 MB thì mỗi document là cả khối riêng, đúng như bài Schema Design Patterns đã đo). Việc "chọn thùng lâu không dùng" cũng không phải một người nhìn quanh: WiredTiger chỉ xấp xỉ LRU. Và cái bàn không chứa toàn bộ bộ nhớ của mongod: cursor, session và nhiều cấu trúc khác nằm ngoài cache.

Mental model: chuỗi từ dữ liệu đến độ trễ

dataset (toàn bộ dữ liệu + index)           ví dụ minh hoạ: 100 GB
   │
   │  phần ứng dụng thật sự chạm tới
   ▼
working set                                  ví dụ: 5 GB
   │
   │  có vừa không?
   ▼
WiredTiger cache (một phần RAM)              ví dụ: 1 GB
   │
   ├── hit  ──▶ trả ngay                     ✓ vài µs
   │
   └── miss ──▶ RAM còn lại: filesystem cache (nén)
                   │
                   ├── có ──▶ giải nén, vào cache          ⚠ chục µs
                   │
                   └── không ──▶ đĩa: disk I/O             ✗ trăm µs đến ms
                                       │
                                       ▼
                                    latency

Hai database dưới đây cùng 100 GB, cùng cache 1 GB, nhưng khác nhau ở chỗ quan trọng nhất (con số minh hoạ, không đo):

                       A                         B
collection            100 GB                    100 GB
index                  30 GB                      5 GB
working set (data+index) 5 GB                   0,5 GB
cache                   1 GB                      1 GB
                       ──────                    ──────
working set / cache      5×                       0,5×
                       ✗ liên tục miss          ✓ gần như toàn hit

Kích thước database là cùng một con số, trải nghiệm thì không. Phần còn lại của bài đo từng mắt xích của chuỗi này.

WiredTiger cache là gì

Kích thước: mặc định và giới hạn

[tài liệu] Kích thước cache mặc định là lớn hơn của hai giá trị: 50% của (RAM − 1 GB), hoặc 0,256 GB. Ví dụ trong tài liệu: máy 4 GB RAM thì cache 1,5 GB. Giá trị cho phép nằm trong khoảng 0,256 GB đến 10.000 GB. Tài liệu khuyên tránh tăng cache vượt mặc định; nếu buộc phải, dùng storage.wiredTiger.engineConfig.cacheSizePct (tối đa 80% bộ nhớ khả dụng) thay vì tự tính GB. Phần RAM còn lại không bị bỏ phí: filesystem cache của OS dùng toàn bộ bộ nhớ trống.

[tài liệu] Khi chạy trong container có giới hạn RAM nhỏ hơn máy chủ, tài liệu cảnh báo WiredTiger "có thể không tính đến" giới hạn đó và bảo bạn kiểm tra giá trị mà nó dùng bằng lệnh hostInfo. [quan sát] Trên 8.3.11 trong lab (cgroup v2, VM 7,7 GB), không đặt cờ cache nào:

--memory=2g  →  memLimitMB 2048  →  cache_size=512M   (0,5 × (2048 − 1024) MB)
--memory=1g  →  memLimitMB 1024  →  cache_size=256M   (sàn 0,256 GB)

Tức là bản này đã dùng giới hạn của container, không dùng 7,7 GB của máy. Đừng suy ra điều đó cho mọi phiên bản, mọi runtime: kiểm tra hostInfo và maximum bytes configured trên môi trường của bạn. Lab của bài đặt cứng 0,5 GB (cache_size=512M trong dòng log "Opening WiredTiger").

Hai tầng: cache của WiredTiger và cache của OS

Bài Mental Model đã có bảng này; nhắc lại vì mọi số đo ở phần sau (Lab: hit, miss và working set) phụ thuộc vào nó. [tài liệu] Filesystem cache giữ dữ liệu cùng dạng với trên đĩa (nén); WiredTiger cache giữ dữ liệu collection chưa nén, với cấu trúc riêng trong bộ nhớ, và index trong cache có biểu diễn khác trên đĩa (vẫn hưởng prefix compression). Page nằm trong WiredTiger cache thì khỏi giải nén; page chỉ nằm trong cache của OS thì vẫn phải giải nén và chép vào cache trước khi dùng. Hệ quả cho việc đo: một cache miss của WiredTiger chưa chắc là một lần đọc đĩa. Lab 1 trong phần đó tách ba trường hợp.

Đọc serverStatus().wiredTiger.cache

Bảng dưới lấy từ chính server của lab, sau khi khởi động lại, đọc ngẫu nhiên 25 giây trên cả 1.000.000 document rồi sửa ngẫu nhiên 3.000 document (e0c-fields.out). [tài liệu] Trang serverStatus chỉ liệt kê sáu trường "key"; phần còn lại có trong output nhưng manual không mô tả, nên cột "ý nghĩa" của chúng là [suy luận] từ tên trường.

TrườngGiá trịÝ nghĩa
maximum bytes configured536.870.912Cache tối đa: 512 MB [tài liệu]
bytes currently in the cache435.063.270Đang dùng: 415 MB, tức 81% [tài liệu]
tracked dirty bytes in the cache38.505.153Phần dirty: 36,7 MB, 7,2% cache [tài liệu]
pages currently held in the cache11.732Số page đang nằm trong cache
pages requested from the cache325.009Số lần có ai xin một page (chia internal 162.264 và leaf 162.745)
pages read into cache68.920Số page phải đọc từ file vào cache: các miss [tài liệu]
bytes read into cache2.481.721.5172,3 GB đọc vào cache từ lúc khởi động (chưa nén)
pages written from cache2.363Số page ghi xuống file [tài liệu]
unmodified pages evicted / modified pages evicted56.422 / 2.179Clean page bị evict / dirty page bị evict (phải ghi trước) [tài liệu] cho trường đầu
application threads page read from disk to cache count / time (usecs)68.913 / 8.357.855Application thread tự đọc page: trung bình 121 µs mỗi page
application thread time evicting (usecs)3Thời gian application thread bị kéo vào làm eviction
eviction worker thread active4Số eviction worker đang chạy
number of times eviction trigger was reached / dirty trigger1 / 0Số lần chạm ngưỡng 95% / 20% dirty

Ba cách đọc quan trọng nhất:

  • pages read into cache là bộ đếm miss. Chia cho số thao tác cho ra "miss mỗi thao tác", thứ ta dùng suốt bài.
  • Mỗi thao tác tìm theo _id xin 4 page (hai page nội bộ và hai page lá; [suy luận] một cặp cho cây của collection, một cặp cho cây của index _id): pages requested from the cache chia đôi gần đúng nội bộ/lá, và ở Lab 1 mỗi thao tác luôn xin đúng 4 page.
  • bytes read into cache không phải byte đọc từ đĩa. Nó đếm byte sau khi giải nén ([quan sát] Lab 1: 89 MB vào cache cho 74 MB đọc từ file). Byte đọc từ file là wiredTiger.block-manager.bytes read (nén), và byte thật sự xuống thiết bị đo ngoài mongod, bằng rbytes trong io.stat của cgroup. Ba con số này khác nhau, và sự khác nhau chính là câu chuyện của OS cache và nén.

Clean page, dirty page và eviction

[tài liệu] (kiến trúc WiredTiger) Page clean giống hệt bản trên đĩa; page dirty đã bị sửa từ lúc đọc lên. evict clean page chỉ là giải phóng bộ nhớ. evict dirty page phải reconcile: chuyển từ dạng trong RAM sang dạng trên đĩa rồi ghi xuống (các phiên bản cũ đi vào history store, chuyện của bài WiredTiger MVCC). Việc ghi này tốn I/O, nên cache đầy dirty page nguy hiểm hơn cache đầy clean page.

[tài liệu] Một eviction server tìm ứng viên (xấp xỉ LRU: lấy một phần ba page cũ nhất trong số ứng viên) và đẩy vào hàng đợi; các eviction worker lấy từ hàng đợi để evict. Nếu cache vẫn tăng nhanh hơn tốc độ eviction giải phóng, application thread, tức chính thread đang phục vụ query của bạn, bị bắt làm việc của eviction worker trước khi đọc hay ghi tiếp. Tài liệu tinh chỉnh WiredTiger nói rõ hậu quả: "latency spikes".

Các ngưỡng, theo tài liệu tinh chỉnh WiredTiger và đối chiếu với server của lab:

NgưỡngMặc định WiredTiger [tài liệu]Server 8.3.11 báo (% × 100) [quan sát]Việc xảy ra
eviction_target80%8000Eviction worker bắt đầu evict khi cache đầy từ mức này
eviction_trigger95%9500Application thread bắt đầu phải evict; thao tác chậm lại
eviction_dirty_target5%500Worker bắt đầu evict riêng phần dirty
eviction_dirty_trigger20%2000Application thread bị throttle vì quá nhiều dirty page
eviction_updates_target / trigger(không nêu)250 / 1000 (2,5% / 10%)Cùng cơ chế cho byte của các update
cache 512 MB                              dirty (phần dirty page trong cache)

100% ┤ ✗ thao tác bị chặn (stall)           20% ┤ ✗ application thread bị throttle
 95% ┤ ⚠ application thread phải evict       5% ┤ ⚠ worker bắt đầu ghi dirty page xuống
 80% ┤ ⚠ eviction worker bắt đầu evict       0% ┤ ✓ không ai phải làm gì thêm
  0% ┤ ✓ ai cũng đủ chỗ

Trang parameters của MongoDB, ở phần về transaction quá lớn (TransactionTooLargeForCache, xem Lab 5 ở Dirty page, eviction và chọn kích thước cache), cũng viết: dirty cache bị giới hạn ở 20% tổng cache. Hai điều thuộc loại [chi tiết cài đặt]: server mở WiredTiger với eviction=(threads_min=4,threads_max=4) (dòng log "Opening WiredTiger" của lab, và eviction worker thread active = 4), và tài liệu WiredTiger tự cảnh báo bản Architecture Guide "không nhất thiết đúng cho mọi release". Đừng biến các số này thành cam kết; chúng là điểm xuất phát để đọc counter, không phải núm để vặn. Manual còn cảnh báo thẳng rằng tham số wiredTigerEngineRuntimeConfig (nơi đổi các ngưỡng) chỉ nên đụng vào khi có hướng dẫn của kỹ sư MongoDB.

Cột mốc: Bạn đã có thể phân biệt hit với miss, nói vì sao clean page bỏ đi được còn dirty page phải ghi trước, và hỏi working set có vừa cache không. Tiếp theo: Lab: hit, miss và working set.

Hỏi & đáp

Hai database cùng 100 GB dữ liệu và cùng cache 1 GB. Database A có working set 5 GB, database B có working set 0,5 GB. Điều nào đúng nhất?

  1. Hai database có hiệu năng đọc tương đương vì cùng dung lượng và cùng cache

    Dung lượng không phải thứ quyết định. Lab 1 đọc cùng collection 2 GB: 0,08 miss mỗi thao tác khi 90% truy cập rơi vào 5% dữ liệu, 0,81 khi đọc đều. Xem Lab cold cache, hit và miss.

  2. B nhanh hơn vì dữ liệu của B nén tốt hơn trên đĩa của nó

    Đề không nói gì về nén. Cache giữ dữ liệu chưa nén, nên điều quyết định là lượng dữ liệu được chạm tới. Xem mục "Hai tầng".

  3. A miss nhiều hơn hẳn: chạm gấp 5 lần cache

    Đúng. Working set so với cache quyết định miss: 5x thì page đang dùng liên tục bị evict rồi đọc lại, 0,5x thì gần như toàn hit. Xem chuỗi ở mục "Mental model".

  4. Chỉ A cần lo vì B đã có sẵn index nên không còn phụ thuộc cache

    Index cũng nằm trong working set và tranh chỗ trong cache. Lab 2: tám index được dùng làm một query không liên quan chậm 32% ở main. Xem Lab index, kích thước document và working set.

Cache 512 MB của một mongod, sau khi chạy lâu, bytes currently in the cache ổn định ở khoảng 430 MB. Đó là gì?

  1. Bình thường: eviction giữ cache dưới trần, thường ở 80–90%

    Đúng. Cache đầy 80–88% là trạng thái bình thường của cache chịu áp lực. Dấu hiệu cần lo là pages read into cache cao kéo dài hoặc thời gian application thread evict tăng. Xem Lab cold cache, hit và miss.

  2. Dấu hiệu rò rỉ bộ nhớ, vì cache lẽ ra phải đầy đủ 512 MB

    Lab 1: dù dữ liệu gấp 4,3 lần cache, cache dừng ở 411–448 MB: eviction target 80% và trigger 95% giữ nó dưới trần. Xem mục "Clean page, dirty page và eviction".

  3. Cache chưa warm-up xong, phần còn lại đang được nạp dần

    Cache dừng ở 80–88% dù còn nhiều dữ liệu để nạp: eviction bỏ page cũ khỏi cache đúng bằng lượng nạp vào. Xem bảng 1 ở Lab cold cache, hit và miss.

  4. WiredTiger chừa phần còn lại cho filesystem cache của OS dùng

    Filesystem cache nằm ngoài WiredTiger cache, dùng RAM trống của máy; nó không chiếm phần còn lại của cache. Xem mục "Hai tầng".

Chủ đề

Bạn thấy bài này thế nào?