Cache (P1/3): WiredTiger cache là gì
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: collectionitems1.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ằngrbytestrongio.statcủ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
│
▼
latencyHai 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 hitKí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ường | Giá trị | Ý nghĩa |
|---|---|---|
maximum bytes configured | 536.870.912 | Cache tối đa: 512 MB [tài liệu] |
bytes currently in the cache | 435.063.270 | Đang dùng: 415 MB, tức 81% [tài liệu] |
tracked dirty bytes in the cache | 38.505.153 | Phần dirty: 36,7 MB, 7,2% cache [tài liệu] |
pages currently held in the cache | 11.732 | Số page đang nằm trong cache |
pages requested from the cache | 325.009 | Số lần có ai xin một page (chia internal 162.264 và leaf 162.745) |
pages read into cache | 68.920 | Số page phải đọc từ file vào cache: các miss [tài liệu] |
bytes read into cache | 2.481.721.517 | 2,3 GB đọc vào cache từ lúc khởi động (chưa nén) |
pages written from cache | 2.363 | Số page ghi xuống file [tài liệu] |
unmodified pages evicted / modified pages evicted | 56.422 / 2.179 | Clean 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.855 | Application thread tự đọc page: trung bình 121 µs mỗi page |
application thread time evicting (usecs) | 3 | Thời gian application thread bị kéo vào làm eviction |
eviction worker thread active | 4 | Số eviction worker đang chạy |
number of times eviction trigger was reached / dirty trigger | 1 / 0 | Số lần chạm ngưỡng 95% / 20% dirty |
Ba cách đọc quan trọng nhất:
pages read into cachelà 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
_idxin 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 cachechia đô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 cachekhô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ằngrbytestrongio.statcủ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ưỡng | Mặc định WiredTiger [tài liệu] | Server 8.3.11 báo (% × 100) [quan sát] | Việc xảy ra |
|---|---|---|---|
eviction_target | 80% | 8000 | Eviction worker bắt đầu evict khi cache đầy từ mức này |
eviction_trigger | 95% | 9500 | Application thread bắt đầu phải evict; thao tác chậm lại |
eviction_dirty_target | 5% | 500 | Worker bắt đầu evict riêng phần dirty |
eviction_dirty_trigger | 20% | 2000 | Application 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?
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ì?