WiredTiger & Compression: một collection nằm trên đĩa như thế nào

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

Các Phần trước của series nhìn MongoDB từ phía ứng dụng, và ở mọi chỗ ta đều gặp cùng một nhân vật đứng sau màn: WiredTiger. Bài Mental Model nhắc rằng dữ liệu có ba dạng (nén trên đĩa, nén trong cache của OS, chưa nén trong WiredTiger cache). Bài Index Fundamentals hứa rằng hình dạng page, cách tách page và cách nén key là chuyện của WiredTiger. Mấy bài về transaction nhắc "storage engine giữ nhiều phiên bản", "journal", "ticket" mà chưa mở ra.

Phần WiredTiger mở những cái hộp đó, mỗi bài một hộp. Bài này là bản đồ: một collection và một index thật sự là gì trên đĩa, một page trong RAM khác một page trên đĩa chỗ nào, và dữ liệu được nén ở đâu, bằng gì, với giá bao nhiêu, kèm lab 1 triệu document với bốn compressor để câu "nén tốn CPU nhưng tiết kiệm đĩa" có con số đi kèm.

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

  • Cần biết trước: Mental Model (storage engine, WiredTiger cache, working set), Index Fundamentals (B-tree, RecordId, kích thước index), BSON & ObjectId (document chiếm bao nhiêu byte)
  • Giới thiệu: mỗi collection và mỗi index là một file .wt, RecordId và clustered collection, B-tree gồm internal page và leaf page, page trong bộ nhớ và page trên đĩa (reconciliation, mức khái niệm), block compression (snappy, zstd, zlib, none) và prefix compression, cấu hình theo toàn cục, theo collection, theo index, đo dung lượng, CPU và tốc độ đọc theo compressor
  • Không dạy ở đây: cache, eviction và working set (bài Cache, Eviction & Working Set); journal và checkpoint (bài Journal & Checkpoint); update chain, history store (bài WiredTiger MVCC); lock, latch, ticket (bài Concurrency). Bài này chỉ chỉ đường tới chúng ở cuối
  • Dẫn tới: Cache, Eviction & Working Set

Môi trường lab: MongoDB 8.3.11 trong Docker (image mongo:8), bốn container standalone (không phải replica set) mongo-wt20-snappy, -zstd, -zlib, -none, mỗi container 2 CPU, 2 GB RAM, WiredTiger cache 1 GB và volume dữ liệu riêng, mới tinh, để dung lượng trên đĩa so sánh được. Host Apple M4, mongosh 2.12.0, database lab20. Mỗi lần đo chỉ một container làm việc, nhưng máy host lúc đó còn chạy lab của bài khác và vài container không liên quan (load average của host có lúc lên tới 55), nên thời gian dao động; bài ghi median và khoảng dao động. Ổ đĩa là ổ ảo của Docker trên macOS. Mọi số đo trong bài đều đo thật và có file output thô kèm theo (thư mục lab lab20/); riêng bảng tốc độ đĩa giả định ở Lab 4 là phép tính minh hoạ, có ghi rõ.

Một lần chạy hỏng: lần nạp đầu vào container none bị giết vì hết bộ nhớ (exit 137, OOMKilled=true) sau khi build xong bốn index, trước khi lệnh fsync cuối trả về. Sau khi mongod khởi động lại, số đếm document trong thống kê của collection báo 870.000 thay vì 1.000.000 đã nạp (manual cảnh báo thống kê size và count có thể lệch sau khi tắt đột ngột; tôi không kiểm tra dữ liệu có thật sự mất không). Tôi bỏ lần đó, drop và nạp lại từ đầu; mọi số của none trong bài là của lần chạy lại.

Nhãn: [tài liệu] theo tài liệu chính thức (manual MongoDB hoặc 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ớ.

Giải thích trong 30 giây

Storage engine là phần của mongod chịu trách nhiệm cất và lấy dữ liệu; MongoDB dùng WiredTiger. Trong WiredTiger, mỗi collection và mỗi index là một table, và mỗi table là một B-tree nằm trong một file .wt. B-tree được cắt thành các page: page trên cao chỉ đường, page ở đáy giữ dữ liệu.

Một page tồn tại ở hai dạng: dạng làm việc trong RAM (đã mở ra, có thể đang mang các thay đổi chưa ghi) và dạng đóng gói trên đĩa. Khi ghi xuống đĩa, page được gói lại và (với collection) nén; khi đọc lên, nó được giải nén và mở ra. Block compression vì thế là chuyện của đĩa: page trong cache không nén, nên nó tiết kiệm dung lượng và I/O chứ không tiết kiệm RAM (prefix compression của index là ngoại lệ, sẽ gặp bên dưới). Cái giá là CPU lúc nén và giải nén, và lab sẽ cho thấy giá đó khác nhau rất nhiều giữa các compressor.

Hình dung trước: kho hàng

Một kho hàng khổng lồ. Mỗi loại hàng (mỗi collection, mỗi index) có một toà nhà kệ riêng. Bên trong, toà nhà chia thành các ngăn kệ (page), mỗi ngăn chứa cả chục thùng hàng. Ở đầu các dãy có bảng chỉ dẫn ("ngăn 1–40: mã A–C, ngăn 41–80: mã D–F"): muốn tìm một món, bạn đọc bảng tổng, rồi bảng chỉ dẫn, rồi đi thẳng tới đúng ngăn, không lục cả toà nhà.

Khi cần làm việc với một ngăn, nhân viên khiêng cả ngăn lên bàn làm việc (cache): mở các thùng, trải hàng ra để kiểm đếm, ghi chú những thay đổi mới bên cạnh. Khi trả ngăn về kho, họ đóng gói lại, hút chân không cho nhỏ, và xếp vào chỗ trống mới; chỗ cũ chỉ được giải phóng sau khi chỗ mới đã chắc chắn nằm yên.

Kho hàng                                    WiredTiger
─────────────────────────────────────       ───────────────────────────────────────────
một toà nhà kệ cho mỗi loại hàng            một file .wt cho mỗi collection, mỗi index
ngăn kệ                                     page (một nút của B-tree)
bảng chỉ dẫn đầu dãy                        internal page: chỉ có key và con trỏ tới page con
ngăn chứa hàng thật                         leaf page: key và value (document, hoặc RecordId)
khiêng ngăn lên bàn, mở thùng, trải hàng    đọc page vào cache, dựng dạng trong bộ nhớ
đóng gói, hút chân không, cất lại           reconciliation, rồi nén block khi ghi xuống đĩa
xếp vào chỗ trống mới, không đè chỗ cũ      block manager không ghi đè block đã ghi

[hình dung] Đây chỉ là cách hình dung. Kho thật không có chuyện "ngăn trên bàn và ngăn trong kho là cùng một dữ liệu ở hai dạng" mà ta phải tự giữ cho khớp; WiredTiger tự làm việc đó. "Hút chân không" cũng chỉ là block compression, và nó chỉ xảy ra lúc ghi xuống đĩa: ngăn đang nằm trên bàn không bao giờ ở dạng nén, nên bàn làm việc không rộng thêm chỉ vì kho dùng bao bì mỏng hơn. Phần lab sẽ đo đúng điểm này.

WiredTiger nằm ở đâu trong mongod

Ứng dụng
   │
   ▼
mongod
   ├── query layer, planner, execution     "tìm gì, bằng plan nào"
   │          │  xin: "cho tôi document có RecordId này", "ghi record này"
   │          ▼
   ├── storage engine API
   │          │
   │          ▼
   └── WiredTiger
          ├── các table = B-tree (mỗi collection, mỗi index một cái)     bài này
          ├── block manager + compression                                bài này
          ├── cache trong RAM, eviction                                  bài về cache
          ├── journal (write-ahead log) và checkpoint                    bài về journal
          ├── transaction, snapshot, phiên bản (MVCC)                    bài về MVCC
          └── lock, latch, ticket                                        bài về concurrency
                     │
                     ▼
        các file trên đĩa: collection-*.wt, index-*.wt, WiredTiger.wt, journal/ ...

[tài liệu] WiredTiger là storage engine mặc định (manual hiện chỉ còn WiredTiger và In-Memory, bản sau chỉ có ở Enterprise). [quan sát] File WiredTiger.turtle ghi WiredTiger 12.0.0: (November 15, 2024): phiên bản thư viện nằm bên trong mongod 8.3.11. Query layer không biết page hay block là gì; nó chỉ hỏi "document nào" và "key nào". Phần còn lại của bài đi xuống dưới ranh giới đó.

Lab 1: mỗi collection và index là một file

Sau khi nạp 1.000.000 đơn hàng vào lab20.orders (5 index: _id_ và bốn index phụ, chi tiết ở phần Setup bên dưới), nhìn thư mục dữ liệu của container snappy (e1-files.out, rút gọn):

-rw------- 176689152  collection-ce58c96d-3a7d-49ec-b820-c6974d3e9ce4.wt
-rw-------  40988672  index-eeb70adb-c344-4464-83e7-b8374d271e4c.wt
-rw-------  18726912  index-581981d0-7f95-4c93-a281-e0cf5f6d8676.wt
-rw-------  14540800  index-f0cd1cba-1bb5-4138-99f3-8200bbcfd9d6.wt
-rw-------  12832768  index-652a6bc8-c11e-4b3f-a3e1-8d17c1ea4e19.wt
-rw-------   6569984  index-5eb7f066-8e12-4cec-a611-f268274dd18c.wt
-rw-------    102400  WiredTiger.wt
-rw-------      4096  WiredTigerHS.wt
-rw-------     32768  _mdb_catalog.wt
-rw-------     32768  sizeStorer.wt
-rw-------      1576  WiredTiger.turtle
drwx------            journal/            (WiredTigerLog.0000000010: 104857600 byte)
drwx------            diagnostic.data/

(Đã lược bớt các file nhỏ: collection hệ thống, file lock, storage.bson, journal/WiredTigerPreplog.*.) $collStats với storageStats cho wiredTiger.uri của collection (statistics:table:collection-ce58c96d-...) và indexDetails.<tên>.uri của từng index: tên sau table: chính là tên file bỏ đuôi .wt. storageSize của orders là 176.689.152 byte, bằng đúng kích thước file, và 5 file index-*.wt khớp từng byte với indexSizes. Collection có 4 index phụ nên là 1 + 5 = 6 file: mỗi index là một table riêng, không có file index chung.

[tài liệu] File .wt là data file của table; WiredTiger.wt là file metadata; WiredTigerHS.wt là history store (manual MongoDB: lịch sử snapshot nằm ở đó); journal nằm ở thư mục journal/. diagnostic.data/ là nơi mongod ghi dữ liệu chẩn đoán FTDC (manual: Full Time Diagnostic Data Capture), không phải dữ liệu của bạn. [chi tiết cài đặt] _mdb_catalog.wt và sizeStorer.wt không có trang manual mô tả; ta chỉ thấy tên và kích thước. Tên file là collection-<uuid>.wt chứ không phải orders.wt, nên server phải giữ bảng ánh xạ namespace sang tên file. [suy luận] Bảng đó nằm trong catalog, suy từ tên _mdb_catalog.wt và từ stage $listCatalog (chạy được trên 8.3.11 nhưng tôi không tìm thấy trang manual cho nó, nên bài không dựa vào nó).

RecordId: tên của một document trong table

[quan sát] Trong creationString, table collection có key_format=q (số nguyên 64 bit, tức RecordId) và value_format=u (chuỗi byte, tức document BSON); table index có key_format=u.

lab.orders.find({}, { ref: 1 }).showRecordId().limit(3)             // quét collection
lab.orders.find({ userId: "u0007" }, { _id: 0, userId: 1, ref: 1 })
  .hint({ userId: 1 }).showRecordId().limit(4)                     // đi qua index userId
{ ref: 'orders/vn/2026/10/00000000', '$recordId': Long('1') }
{ ref: 'orders/vn/2026/10/00000001', '$recordId': Long('2') }
{ ref: 'orders/vn/2026/10/00000002', '$recordId': Long('3') }

{ userId: 'u0007', ref: 'orders/vn/2026/10/00000183', '$recordId': Long('184') }
{ userId: 'u0007', ref: 'orders/vn/2026/10/00000282', '$recordId': Long('283') }
{ userId: 'u0007', ref: 'orders/vn/2026/10/00000605', '$recordId': Long('606') }
{ userId: 'u0007', ref: 'orders/vn/2026/10/00000968', '$recordId': Long('969') }

Quét collection cho document theo thứ tự RecordId, tức thứ tự nhập. Đi qua index userId, các key đứng cạnh nhau nhưng RecordId rải khắp collection: mỗi FETCH là một cú nhảy sang chỗ khác của B-tree collection, đó là lý do index chậm dần khi phải fetch nhiều (bài Index Fundamentals đã đo). Và _id_ không phải nơi document nằm: document nằm ở table collection theo RecordId; _id_ chỉ là một index như mọi index.

Clustered collection: bỏ một tầng

[tài liệu] Từ MongoDB 5.3, clustered collection lưu document ngay trong B-tree theo thứ tự _id, trong một file duy nhất, thay vì một table theo RecordId cộng một index _id_ riêng. Chỉ tạo được lúc createCollection, key phải là { _id: 1 }, không chuyển đổi qua lại được (phải copy bằng $out/$merge hoặc dump/restore); manual khuyên key nhỏ và tăng dần.

lab.createCollection("c_clustered", { clusteredIndex: { key: { _id: 1 }, unique: true, name: "orders clustered key" } })

[quan sát] Cùng 200.000 đơn nạp vào một collection thường và một clustered (e6-clustered.out):

Collection thườngClustered
File collection + file _id_34.275.328 + 3.538.944 byte (36,06 MiB)35.794.944 byte + không có file index (34,14 MiB, ít hơn 5,3%)
key_format của tableq (RecordId)u (chính _id, dạng nhị phân)
Plan của find({ _id })EXPRESS_IXSCAN(_id_), keysExamined: 1EXPRESS_CLUSTERED_IXSCAN, keysExamined: 0

Với clustered collection "RecordId" chính là _id, và tra theo _id đi thẳng vào table dữ liệu. Manual nói thêm: một lần ghi thay vì hai cho insert/update/delete, một lần đọc thay vì hai cho query. Tôi không đo tốc độ của hai loại, nên chỉ nói điều đã thấy: ít một file, ít 5,3% dung lượng ở bộ dữ liệu này (chỉ có index _id), plan khác. [tài liệu] Manual cũng cảnh báo: khi clustered collection có index phụ, key clustered lớn làm các index phụ lớn hơn so với ở collection thường. Chọn clustered hay không là câu hỏi về cách truy cập (có hay tra theo _id, theo khoảng _id, có cần xoá theo TTL), không phải mặc định tốt hơn.

Page: B-tree trong RAM và trên đĩa

Cây và page

[tài liệu] Theo tài liệu WiredTiger, một table là một B-tree (chính xác là B+ tree) gồm các page. Root và internal page chỉ giữ key và tham chiếu tới page con; leaf page giữ key và value. Bản ghi luôn theo thứ tự key, và page tách đôi khi chạm giới hạn kích thước đã cấu hình. Đó là bức tranh B-tree của bài Index Fundamentals; điều mới là cùng cấu trúc đó chứa cả document của collection, không chỉ index.

                      [ root / internal page ]            chỉ key + con trỏ
                   ┌──────────┬──────────┬──────────┐
                   ▼          ▼          ▼          ▼
               [ leaf ]   [ leaf ]   [ leaf ]   [ leaf ]   ...    key + value, sắp theo key
               RecordId   RecordId   RecordId   RecordId
               1…48 ...   49…97 ...  ...        ...

Tách ở đâu? [tài liệu] Khi page đóng gói vượt kích thước tối đa, WiredTiger cắt thành nhiều page; split_pct (mặc định 90) quyết định cắt ở mức bao nhiêu phần trăm. Tài liệu giải thích: cập nhật ngẫu nhiên hợp với quanh 66% (chừa chỗ cho thay đổi sau), chỉ thêm vào cuối hợp với 100%, 90% là dung hoà. [quan sát] split_pct=90 có trong creationString của cả collection lẫn index.

Hai dạng của một page

[tài liệu] Page có dạng trong bộ nhớ và dạng trên đĩa, và hai dạng này khác nhau. Trong RAM, page ở dạng thuận tiện cho tra cứu và sửa, và mang thêm một cấu trúc ghi các insert, update chưa ghi xuống (đó là update chain của bài WiredTiger MVCC). Biến page trong RAM thành page để ghi xuống đĩa gọi là reconciliation; chiều ngược lại xảy ra khi đọc page lên.

        TRONG RAM (cache)                                  TRÊN ĐĨA (file .wt)

   ┌─────────────────────────────┐   reconciliation   ┌──────────────────────────────┐
   │ page "làm việc"             │ ─────────────────▶ │ page "đóng gói"              │
   │  • key/value đã mở ra       │                    │  • header + các cell          │
   │  • + các thay đổi chưa ghi  │ ◀───────────────── │  • key có thể bị cắt prefix   │
   │    (insert, update)         │    đọc + giải nén  │  • nén cả block (nếu bật)     │
   └─────────────────────────────┘                    │  • lưu thành block, kích thước │
        collection: KHÔNG nén                         │    là bội số của 4 KB          │
                                                      └──────────────────────────────┘

Về block manager (tầng đọc ghi block trên file), tài liệu nói: mỗi block chứa một page cộng header, kích thước là bội của allocation_size, và WiredTiger không ghi đè: page đã sửa, sau reconciliation, được ghi vào vị trí mới, block cũ được tái sử dụng sau (chính là "chỗ trống mới" ở kho hàng). [quan sát] allocation_size=4KB; collection snappy có 229.376 byte (0,1% file) đang chờ tái sử dụng. Khi nào page rời RAM (eviction khi cache đầy, checkpoint định kỳ) là của bài về cache và bài về journal. Ở đây chỉ cần giữ: dạng đĩa là dạng đóng gói, dạng RAM là dạng làm việc.

Kích thước page: MongoDB đặt gì

[tài liệu] Thư viện WiredTiger có memory_page_max, internal_page_max, leaf_page_max, mặc định của thư viện là 5 MB, 4 KB, 32 KB. MongoDB không nhất thiết dùng mặc định đó. [quan sát] creationString trong $collStats của lab (e3-sizes-snappy.out):

Tham sốTable collectionTable indexMặc định thư viện (tài liệu WT)
leaf_page_max32KB16k32 KB
internal_page_max4KB16k4 KB
memory_page_max10M5MB5 MB
block_compressorsnappy(trống)không nén
prefix_compressionfalsetruetắt
key_format / value_formatq / uu / u

[chi tiết cài đặt] Đây là cách MongoDB 8.3.11 đang tạo table, không phải cam kết và có thể khác giữa phiên bản. Điều cần nhớ: collection và index được cấu hình khác nhau ngay từ đầu; index không có block compressor và collection không có prefix compression. [suy luận] Lý do hợp lý của vế sau: khoá của collection là một số nguyên (RecordId), không có tiền tố chung nào để cắt.

Ba kiểu nén, ba chỗ khác nhau

[tài liệu] Tài liệu WiredTiger liệt kê ba cơ chế và nói rõ cái nào làm nhỏ cả RAM, cái nào chỉ làm nhỏ đĩa:

Cơ chếNén cái gìNhỏ hơn trên đĩaNhỏ hơn trong RAMTrong MongoDB
Prefix compressionphần đầu giống nhau của các key liền nhau, lưu một lần mỗi pagecócóbật mặc định cho index, tắt cho collection
Dictionarygiá trị giống hệt nhau, lưu một lần mỗi pagecócókhông dùng: dictionary=0 [quan sát]
Block compressioncả block nhị phân (một page đã đóng gói) khi ghi xuống đĩacókhôngsnappy mặc định cho collection; không có cho index

Manual MongoDB nói cùng ý: collection trong WiredTiger cache không nén; index trong cache có biểu diễn khác dạng đĩa nhưng vẫn hưởng lợi của prefix compression để giảm RAM. Hai câu này sẽ được thử bằng lab. Tài liệu WiredTiger cũng cảnh báo block compression "có thể tốn CPU đáng kể".

Cấu hình ở đâu

[tài liệu] Compressor mặc định của collection là snappy, đặt bằng storage.wiredTiger.collectionConfig.blockCompressor (hoặc cờ --wiredTigerCollectionBlockCompressor), giá trị none, snappy, zlib, zstd; với zstd còn có engineConfig.zstdCompressionLevel (mặc định 6, khoảng giá trị đổi ở 8.2: -7 đến 22). Prefix compression của index là storage.wiredTiger.indexConfig.prefixCompression (mặc định true). Journal có compressor riêng (engineConfig.journalCompressor, mặc định snappy), không liên quan tới compressor của collection; journal là chuyện của bài Journal & Checkpoint. Cả hai ghi đè được từng collection lúc createCollection và từng index lúc createIndex qua storageEngine:

db.createCollection("users", { storageEngine: { wiredTiger: { configString: "block_compressor=zlib" } } })
db.orders.createIndex({ ref: 1 }, { storageEngine: { wiredTiger: { configString: "prefix_compression=false" } } })

Điều tài liệu nhấn mạnh, và lab 6 kiểm: đổi giá trị toàn cục chỉ ảnh hưởng collection và index tạo sau đó; cái đã có "tiếp tục dùng compressor đã chọn khi được tạo".

Setup và dataset

MongoDB version : 8.3.11 (mongo:8), WiredTiger 12.0.0 (theo WiredTiger.turtle), mongosh 2.12.0
Hardware        : Apple M4 host, Docker VM ~7,7 GiB; mỗi container --memory=2g --cpus=2
Configuration   : mongod --wiredTigerCacheSizeGB 1 --wiredTigerCollectionBlockCompressor <snappy|zstd|zlib|none>
                  bốn container standalone, mỗi container một volume mới
Dataset         : lab20.orders, 1.000.000 document, trung bình 461 byte (440,4 MiB BSON)
Indexes         : _id_, { tenantId: 1, createdAt: -1 }, { userId: 1 }, { traceId: 1 }, { ref: 1 }

Schema là đơn hàng quen thuộc của series, thêm vài field để dữ liệu có cả phần lặp lại lẫn phần ngẫu nhiên:

{
  _id,                                   // ObjectId tăng dần (12 byte)
  tenantId: "t0042", userId: "u0007",    // 500 và 200 giá trị khác nhau
  status: "completed", createdAt, total, // status: 4 giá trị
  tags: ["gift", "cod"], items: [{ sku, qty, price }, ...],
  shipping: { city: "Đà Nẵng", street: "123 Lê Lợi 4" },   // 12 thành phố, 60 tên đường
  note: "Giao giờ hành chính. Gọi trước khi giao.",         // ghép từ 12 câu cố định
  traceId: "9f3a...",                    // 32 ký tự hex NGẪU NHIÊN (không nén được nhiều)
  ref: "orders/vn/2026/10/00000183"      // tăng dần, tiền tố chung dài
}

Tỉ lệ nén là tính chất của dữ liệu, không phải của compressor. Bộ dữ liệu này nằm giữa hai thái cực: phần lớn field lặp lại (dễ nén), traceId thì ngẫu nhiên (khó nén). Dữ liệu của bạn sẽ cho tỉ lệ khác, ở cuối bài có cách đo trên dữ liệu thật. Cả bốn container nạp đúng cùng một dãy document (sinh bằng seed cố định, _id cũng cố định), 100 lần insertMany mỗi lần 10.000 document, để thời gian chỉ cộng phần insertMany, không cộng thời gian sinh dữ liệu ở mongosh. Index tạo sau khi nạp xong.

Lab 2: dung lượng trên đĩa

Sau khi nạp và fsync (ép checkpoint), đọc $collStats với storageStats (e3-sizes-*.out; MiB = 1.048.576 byte):

CompressordataSize (BSON)storageSize (file)dataSize / storageSizeTổng index
snappy (mặc định)440,4 MiB168,5 MiB2,6189,3 MiB
zstd440,4 MiB93,8 MiB4,7089,3 MiB
zlib440,4 MiB109,1 MiB4,0489,7 MiB
none440,4 MiB457,6 MiB0,9689,3 MiB
  • dataSize là tổng kích thước BSON, giống hệt ở cả bốn container (461.796.477 byte). Chênh lệch ở storageSize là toàn bộ tác dụng của compressor: zstd nhỏ hơn snappy 44%. [quan sát] Ở bộ dữ liệu này zstd (mức mặc định 6) vừa nhỏ hơn zlib vừa rẻ hơn về CPU (lab 3); đừng khái quát sang dữ liệu khác.
  • none lớn hơn cả dataSize (thêm 3,9%): page vẫn có header, căn theo block 4 KB và có chỗ trống chờ tái sử dụng (4,6 MiB). "Không nén" không có nghĩa là "đĩa bằng dữ liệu".
  • Index không đổi: bốn index phụ có kích thước trùng từng byte giữa bốn container (13,87 / 6,27 / 39,09 / 12,24 MiB); chỉ _id_ dao động (17,86 đến 18,24 MiB). Đúng như creationString: compressor của collection không chạm vào index. Và với snappy, index (89,3 MiB) bằng 53% collection (168,5 MiB): tối ưu collection mà quên index là tối ưu một nửa bức tranh.

[quan sát] Bộ đếm nén của WiredTiger: snappy ghi 9.208 lần page, tất cả ở nhóm tỉ lệ nén "smaller than 4"; zstd và zlib ghi khoảng 4.040 lần, tất cả ở nhóm "smaller than 8"; không page nào nén thất bại. Số lần ghi chỉ bằng nửa không phải ngẫu nhiên: xem lab 4.

Lab 3: cái giá lúc ghi

Nạp 1.000.000 document ba lần liên tiếp mỗi container (drop rồi nạp lại), ghi median của tổng thời gian các lệnh insertMany (e2-load-*.out). Rồi ba lần nạp nữa vào một collection tạm chỉ có index _id_, đo CPU của mongod (user_time_us cộng system_time_us trong serverStatus) và ghi median (e2b-cpu.out):

CompressorThời gian insert 1 triệu (median 3 lần)CPU của mongod cho lần nạp + checkpoint (median 3 lần)
snappy9,1 s (khoảng 110.000 doc/s)2,76 s
zstd9,3 s5,86 s (2,1 lần snappy)
zlib9,5 s8,35 s (3,0 lần snappy)
none9,2 s2,82 s

Thời gian insert gần như không đổi (9,1 đến 9,5 s). Ba lần nạp thêm cho khoảng rộng hơn (median 9,2 / 11,3 / 10,1 / 9,6 s, máy bận), nên chỉ kết luận được: trên máy này, ở workload này, compressor không làm latency insert đổi một cách ổn định. CPU thì khác rõ: zlib gấp ba snappy, zstd gấp đôi, snappy và none ngang nhau. Chỉ có 3 lần đo, nhưng khoảng của chúng không chồng lên nhau giữa các nhóm: snappy 2,6–3,0 s, none 2,3–2,9 s, zstd 5,0–6,9 s, zlib 8,2–9,8 s.

[suy luận] Cách giải thích hợp lý: nén diễn ra khi page được ghi xuống (thread nền của WiredTiger, lúc eviction hoặc checkpoint), và cache 1 GB chưa bị áp lực nên thread của client không phải tự đi nén; chi phí hiện ở CPU chứ không ở latency. Tôi không kiểm tra riêng giả thuyết này. Nó hàm ý rằng khi máy thiếu CPU hoặc cache bị ép, compressor đắt có thể lộ ra thành latency, và đó là chuyện của bài về cache. Bộ đếm bytes written for leaf pages cho thấy lượng việc nén: snappy 470,5 MiB thành 172,5; zstd 470,1 thành 106,6; zlib 468,5 thành 111,6; none 471,6 thành 471,6.

Lab 4: cái giá lúc đọc

  • COLLSCAN với filter không khớp gì ({ "shipping.city": "Nowhere" }, hint({ $natural: 1 })): mọi document phải được giải nén và đọc, không có gì gửi về client.
  • Point read: findOne({ _id }) với _id ngẫu nhiên (5.000 lần cho trạng thái nguội, 20.000 lần × 5 pass cho nóng; thời gian gồm cả vòng đi về của mongosh).
  • Nguội = ngay sau docker restart: cache WiredTiger trống, nhưng page cache của OS không bị xoá (xem giới hạn bên dưới). Nóng = dữ liệu đã nằm trong WiredTiger cache.

Giới hạn quan trọng: tôi không xoá page cache của hệ điều hành (nó ảnh hưởng cả VM Docker mà các lab khác đang dùng), nên "nguội" chỉ nghĩa là cache WiredTiger trống. File dữ liệu vẫn nằm trong RAM của VM, "đọc đĩa" thực chất là sao chép từ RAM. Lab thấy được chi phí giải nén và số byte phải lấy từ file, nhưng không thấy thời gian chờ đĩa thật.

Median của 3 lần (nguội) hoặc 7 lần và 5 pass (nóng) (e4-*.out):

snappyzstdzlibnone
COLLSCAN nguội: thời gian455 ms684 ms1.098 ms452 ms
COLLSCAN nguội: byte đọc từ file168,5 MiB93,3 MiB108,5 MiB453,5 MiB
COLLSCAN nguội: byte đưa vào cache448,6 MiB448,2 MiB448,2 MiB449,1 MiB
5.000 point read nguội: µs mỗi lần190240349168
cùng 5.000 _id, lần 2 (đã nóng): µs134138147135
COLLSCAN nóng: thời gian193 ms204 ms191 ms222 ms
20.000 point read nóng: µs mỗi lần136142140129

Dao động của COLLSCAN nguội (3 lần): snappy 401–661 ms, none 386–860 ms, zlib 1.081–1.175 ms.

1. Khi dữ liệu đã ở trong cache, compressor không còn liên quan. COLLSCAN nóng 191 đến 222 ms, point read nóng 129 đến 142 µs: nằm trong dao động của máy (con số 222 ms của none tôi không giải thích được, nên không gán cho none chậm hơn). Page trong cache không nén, nên đọc nó giống nhau bất kể từng được nén bằng gì.

2. Khi page phải vào cache từ file, compressor trả giá bằng CPU theo thứ tự nhẹ đến nặng: none ≈ snappy (455 so với 452 ms) < zstd (1,5 lần none) < zlib (2,4 lần none). Point read cũng vậy: zlib 349 µs, none 168 µs. Byte đưa vào cache gần như bằng nhau (448 đến 449 MiB): giải nén cho ra cùng một dạng.

3. Cái được là số byte lấy từ file: 168,5 / 93,3 / 108,5 / 453,5 MiB. Trên máy này chúng gần như miễn phí vì đến từ RAM của VM; trên đĩa thật thì không. Ghép số byte đo được với hai tốc độ đĩa giả định (phép tính minh hoạ, không đo trên đĩa thật: cộng thời gian I/O với thời gian scan đã đo, coi như không chồng lên nhau):

Tốc độ đọc giả địnhsnappyzstdzlibnone
200 MiB/s (đĩa chậm hoặc nhiều tải)1,30 s1,15 s1,64 s2,72 s
2.000 MiB/s (NVMe nhanh)0,54 s0,73 s1,15 s0,68 s

Bài học nằm ở hình dạng chứ không ở từng ô: đĩa càng chậm thì nén mạnh càng có lợi, đĩa càng nhanh thì CPU giải nén càng lộ ra.

Nén mạnh hơn thì page trong cache to hơn. Chia byte đưa vào cache cho số page đọc: trung bình khoảng 27 KiB mỗi page với none (16.842 page), 50 KiB với snappy (9.274), và 114 KiB với zstd và zlib (khoảng 4.040). Trên đĩa, mỗi page chỉ khoảng 19 KiB (snappy) đến 28 KiB (none, zlib). [tài liệu] Khi bật block compression, "kích thước page đã cấu hình sẽ không khớp với kích thước thật trên đĩa", và lượng dữ liệu đưa vào nén dựa trên kết quả nén trước đó. [quan sát] leaf_page_max=32KB vẫn nằm trong creationString; page trên đĩa nằm dưới mức đó, nhưng một page với zstd hay zlib mở ra trong cache lớn gấp khoảng bốn lần none. Hệ quả thấy được ở point read nguội: cùng 5.000 lần đọc đưa vào cache 134 MiB với none, 246 MiB với snappy, 336 và 340 MiB với zstd và zlib. Mỗi point read ngẫu nhiên vào page nén mạnh phải giải nén cả page lớn để lấy một document, và [suy luận] đó là một phần lý do zlib 349 µs, zstd 240 µs, none 168 µs (tôi không tách riêng yếu tố này).

Nén không làm cache nhỏ đi

Đây là hiểu nhầm đắt nhất, và lab đã có bằng chứng. Sau khi mọi thứ nằm trong cache, bytes currently in the cache của collection orders:

snappyzstdzlibnone
Trên đĩa (storageSize)168,5 MiB93,8 MiB109,1 MiB457,6 MiB
Trong WiredTiger cache494,1 MiB492,9 MiB492,9 MiB495,8 MiB

Cache của collection gần như không đổi: 492,9 đến 495,8 MiB, trong khi trên đĩa none (457,6 MiB) lớn gấp 4,9 lần zstd (93,8 MiB). Với zstd, một collection chiếm 93,8 MiB trên đĩa phải có 492,9 MiB trong cache, gấp 5,3 lần; với snappy là 2,9 lần, và cũng lớn hơn 12% so với dataSize BSON (494,1 so với 440,4 MiB, [suy luận] phần chênh là cấu trúc quản lý của page trong bộ nhớ). Cache 1 GB của lab chứa trọn collection này cộng index _id_ (khoảng 26,5 MiB trong cache) mà vẫn còn dư, nhưng chỉ vì collection nhỏ. [tài liệu] Manual MongoDB nói đúng điều này: dữ liệu collection trong WiredTiger cache không nén và có biểu diễn khác dạng trên đĩa.

Hệ quả: dung lượng trên đĩa, nén hay không, không cho biết cần bao nhiêu RAM. Compressor mạnh làm nhỏ đĩa, I/O và bản backup, nhưng không làm working set vừa cache dễ hơn; câu "working set có vừa cache không" là chủ đề của bài Cache, Eviction & Working Set.

Lab 5: prefix compression cho index

Hai collection refs_on và refs_off chứa cùng 1.000.000 document nhỏ (_id, ref, traceId, tenantId, n) với ba index giống nhau; ở refs_off mỗi index tạo với configString: "prefix_compression=false" (e5-prefix-build.out). Ba loại key cho ba đầu của phổ:

  • ref: "https://shop.example.vn/api/v2/orders/2026/10/" (46 ký tự giống nhau) cộng 8 chữ số tăng dần.
  • traceId: 32 ký tự hex ngẫu nhiên, gần như không có prefix chung.
  • tenantId: "t0000" đến "t0499", 500 giá trị lặp lại 2.000 lần mỗi giá trị.

Kích thước trên đĩa, và lượng cache index chiếm sau khi quét hết nó một lần từ cache trống (e5b-prefix-cache.out, container snappy vừa restart):

IndexĐĩa, prefix bậtĐĩa, prefix tắtTắt / bậtCache, bậtCache, tắt
ref_1 (prefix dài)12,28 MiB65,96 MiB5,4 lần20,28 MiB72,99 MiB
tenantId_1 (500 giá trị lặp)7,82 MiB13,57 MiB1,7 lần15,91 MiB21,56 MiB
traceId_1 (ngẫu nhiên)39,09 MiB42,34 MiB1,08 lần46,58 MiB49,78 MiB
  1. Tác dụng khác nhau rất xa tuỳ key. Key có tiền tố chung dài (URL, đường dẫn, mã có tiền tố, dãy tăng dần) co 5,4 lần; key lặp lại co 42%; key ngẫu nhiên chỉ co 8%.
  2. Nó làm nhỏ cả cache, đúng như tài liệu (ref_1: 20,3 so với 73,0 MiB), khác với block compression ở lab trước.
  3. Dạng trong cache lớn hơn dạng trên đĩa ngay cả khi có prefix compression (20,3 so với 12,3 MiB): hai dạng của page là hai dạng thật.

Tốc độ. Tài liệu WiredTiger cảnh báo prefix compression làm duyệt ngược và tra key ngẫu nhiên tốn thêm CPU và bộ nhớ. Lab không thấy khác biệt ổn định: quét covered toàn index khi cache nóng, 9 lần xen kẽ, median 228 ms (bật) so với 226 ms (tắt) cho ref_1, 224 so với 224 ms cho traceId_1; quét ngược 227 so với 230 ms và 234 so với 223 ms (e5c-prefix-time.out, e5d-prefix-reverse.out). 20.000 lần tra ngẫu nhiên theo ref cho 2,998 s (bật) so với 3,789 s (tắt) nhưng dao động giữa các pass từ 2,7 đến 6,3 s (máy bận), nên không kết luận được gì. Tôi không có bằng chứng prefix compression chậm hơn ở quy mô này, và cũng không có bằng chứng ngược lại.

Lab 6: đổi compressor thì dữ liệu cũ thế nào

Hai thí nghiệm cho hai câu hỏi hay gặp.

Đổi cấu hình toàn cục có nén lại dữ liệu cũ không? Dừng container zlib, mở lại cùng volume bằng một mongod mới với --wiredTigerCollectionBlockCompressor none, rồi tạo collection mới và nạp 200.000 document (e7b-global-change.out):

setting now: {"blockCompressor":"none"}
{"coll":"orders","block_compressor_in_creationString":"zlib","count":1000000,"storageSizeBytes":114372608}
{"coll":"after_change","block_compressor_in_creationString":"none","count":200000,"storageSizeBytes":94892032}
old zlib blocks still readable: {...} count 99507     (countDocuments({ status: "pending" }) trên collection zlib)

orders vẫn mang block_compressor=zlib và vẫn 109,1 MiB; collection mới mang none. Dữ liệu cũ đọc được bình thường: [tài liệu] manual nói collection cũ "tiếp tục dùng compressor đã chọn khi được tạo"; tài liệu WiredTiger nói block trong file không ghi compressor nào đã nén nó, nên compressor phải được lấy từ cấu hình của table. [quan sát] đúng như vậy: creationString của orders vẫn là zlib.

Muốn nén lại dữ liệu cũ thì làm gì? Tạo collection mới với compressor mong muốn rồi copy dữ liệu sang. Từ collection snappy 200.000 document sang một collection tạo với configString: "block_compressor=zstd", bằng $merge (e7-override.out):

storageSize
c_regular (snappy)32,73 MiB
c_zstd (copy của nó, tạo với block_compressor=zstd)18,64 MiB

Copy 200.000 document mất khoảng 0,86 giây ở lab này. Với collection cỡ trăm GB đây là một việc vận hành lớn (thêm đĩa tạm, thêm ghi, chuyển ứng dụng sang tên mới), không phải "đổi một cờ". Đó là lý do nên đo trên mẫu trước: copy 1–5% dữ liệu thật sang bốn collection nhỏ, mỗi collection một compressor, so storageSize và CPU; ở quy mô nhỏ, đây là cách rẻ nhất để biết tỉ lệ nén của dữ liệu của bạn.

Chọn compressor thế nào

Từ lab 1 triệu đơn trên một máy (đều là [quan sát] ở bộ dữ liệu này):

  • snappy (mặc định) là điểm cân bằng: nén 2,6 lần với gần như không tốn CPU thêm so với không nén.
  • zstd cho đĩa nhỏ nhất ở đây, đổi lại CPU gấp đôi lúc ghi và COLLSCAN chậm gấp rưỡi khi page phải nạp từ file vào cache. Hợp với dữ liệu ít đọc, nơi đĩa hoặc backup là chi phí chính và CPU còn dư. [tài liệu] Manual đặt zstd làm mặc định cho time-series collection.
  • zlib ở đây vừa lớn hơn zstd vừa tốn CPU hơn. Lab này không cho lý do để chọn nó, nhưng đó là kết quả của dữ liệu này, đừng khái quát.
  • none chỉ hợp khi dữ liệu vốn đã nén hoặc đĩa cực nhanh và CPU cực hiếm; nhớ rằng nó lớn hơn dataSize.

Quy trình đáng tin: (1) copy mẫu dữ liệu thật vào bốn collection, mỗi collection một compressor; (2) so storageSize; (3) đo CPU khi nạp và khi quét nguội; (4) tính lại với tốc độ đĩa của bạn; (5) chỉ khi đó đổi cấu hình, biết rằng nó chỉ áp cho collection mới.

So với PostgreSQL

Cùng bài toán: lưu 1 triệu đơn hàng. [tài liệu] PostgreSQL lưu mỗi bảng (gọi là heap) và mỗi index như một dãy page kích thước cố định (thường 8 kB, chọn lúc biên dịch server); một dòng có thể nằm ở bất kỳ page nào của bảng (mọi page của bảng tương đương nhau về logic), và index là cấu trúc riêng trỏ tới vị trí dòng. Giá trị lớn đi đường TOAST: khi một dòng vượt khoảng 2 kB, server nén và/hoặc đẩy giá trị lớn sang bảng phụ; kiểu nén chọn theo từng cột. Durability đến từ WAL: thay đổi vào log trước, vào file dữ liệu sau. Với index B-tree, tài liệu mô tả deduplication cho các key trùng nhau.

So với WiredTiger: collection không phải heap mà là một B-tree theo RecordId (hoặc theo _id nếu clustered), page không cố định kích thước và cấu hình được theo từng table, nén là cả block của page lúc ghi xuống (collection) cộng prefix compression (index). Hai thiết kế giải cùng một nhu cầu bằng những đường khác nhau: TOAST nén từng giá trị lớn, block compression nén từng page, deduplication và prefix compression làm nhỏ index bằng hai kỹ thuật không tương đương. Bài này không đo PostgreSQL, nên không có kết luận "bên nào nhỏ hơn hay nhanh hơn".

Bản đồ: bốn cơ chế còn lại của Phần này

                    page trong RAM
                         │
     ┌───────────────────┼───────────────────┬──────────────────────┐
     ▼                   ▼                   ▼                      ▼
 cache đầy,         ghi log trước,       nhiều phiên bản        nhiều thread
 page nào đi?       checkpoint sau       của cùng một dữ liệu   cùng chạm một page
 [bài về cache]     [bài về journal]     [bài về MVCC]          [bài về concurrency]
  • Cache, Eviction & Working Set: cache chứa gì, page sạch và bẩn, khi nào page bị đẩy ra, vì sao working set quan trọng hơn kích thước database.
  • Journal & Checkpoint: làm sao thay đổi chỉ nằm trong RAM mà sống sót qua mất điện: write-ahead log, checkpoint, phục hồi sau crash.
  • WiredTiger MVCC: update chain trên page, history store (WiredTigerHS.wt trong danh sách file), vì sao snapshot cũ giữ lại các phiên bản.
  • Concurrency: Locks, Latches & Tickets: những thứ chặn nhau thật sự ở từng tầng, và ticket là gì.

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

  • "Nén làm MongoDB chậm" hoặc "nhanh", không đo. Lab cho cả hai chiều: khi dữ liệu nằm trong cache, compressor không còn liên quan (129 đến 142 µs mỗi point read nóng ở cả bốn); khi page phải vào từ file, zlib chậm gấp 2,4 lần none ở COLLSCAN nguội (cache WiredTiger trống, file vẫn trong page cache của OS). Chiều thứ ba là đĩa thật, không đo ở đây. Câu trả lời đúng là "tuỳ working set có nằm trong cache không".
  • Nhìn dataSize hoặc storageSize để đoán RAM cần có. Page trong cache không nén: collection 93,8 MiB trên đĩa (zstd) chiếm 492,9 MiB trong cache.
  • Chỉ nhìn collection, quên index. Index (89,3 MiB) bằng 53% collection snappy, không compressor nào chạm vào, và prefix compression làm nó đổi nhiều (5,4 lần cho key có tiền tố dài) hoặc gần như không (8% cho key ngẫu nhiên).
  • Đổi blockCompressor rồi mong dữ liệu cũ nhỏ lại. Cấu hình toàn cục chỉ ảnh hưởng collection và index tạo sau; muốn nén lại phải copy sang collection mới.
  • Lấy tỉ lệ nén của người khác cho dữ liệu của mình. 2,61 lần của snappy là của bộ dữ liệu này (nhiều field lặp, một field hex ngẫu nhiên). Đo trên mẫu.
  • Coi none là "không tốn gì". Nó lớn hơn dataSize 3,9% và đọc 453,5 MiB từ file thay vì 168,5 MiB cho cùng một COLLSCAN.
  • Tin số đo trên Docker laptop như số production. Lab không có độ trễ đĩa thật, chạy chung máy với lab khác, và có lần container bị OOM. Cái đáng tin là hình dạng (CPU tăng theo thứ tự nào, byte từ file giảm bao nhiêu), không phải từng mili giây.

Tóm tắt: mang theo gì sang bài sau

  • Collection và index là table riêng, mỗi table một B-tree trong một file .wt: orders với 5 index là 6 file, storageSize bằng đúng kích thước file. Document nằm ở table collection theo RecordId, _id_ chỉ là một index; clustered collection lưu document theo _id và bỏ file _id_.
  • Page có hai dạng: làm việc trong RAM (không nén, mang cả thay đổi chưa ghi) và đóng gói trên đĩa (reconciliation), nén theo block, ghi sang chỗ mới, không đè.
  • Collection: block compression, snappy mặc định. Index: prefix compression, không có block compression.
  • Lab (1 triệu đơn, một máy, 2 CPU): storageSize 168,5 / 93,8 / 109,1 / 457,6 MiB (snappy / zstd / zlib / none) cho 440,4 MiB BSON; CPU của mongod khi nạp 2,8 / 5,9 / 8,4 / 2,8 s; COLLSCAN nguội (cache WiredTiger trống, file vẫn trong page cache của OS) 455 / 684 / 1.098 / 452 ms; đọc nóng không khác nhau.
  • Cache không nhỏ đi vì nén: 493 đến 496 MiB cho cả bốn, gấp 2,9 đến 5,3 lần dung lượng đĩa. Prefix compression thì có làm nhỏ cache (ref_1: 20,3 so với 73,0 MiB).
  • Cấu hình mới chỉ áp cho collection và index tạo sau. Nén lại = copy sang collection mới.
  • Quyết định bằng đo trên mẫu dữ liệu thật, với tốc độ đĩa và CPU của bạn.

Hỏi & đáp

Collection orders (không clustered) có index _id_ và 4 index phụ. Chúng nằm trên đĩa ở đâu?

  1. Một file .wt chứa document, và một file .wt chung chứa cả 5 index

    Không có file index chung: mỗi index là một table riêng với B-tree riêng. Lab đếm đúng 6 file và kích thước từng file khớp indexSizes. Xem mục "Lab 1: mỗi collection và index là một file".

  2. Một file cho cả database, các collection và index được chia vùng bên trong

    Không có file theo database: collection-<uuid>.wt và index-<uuid>.wt là từng table của WiredTiger; tên file ánh xạ tới namespace qua catalog. Xem mục "Lab 1".

  3. Sáu file .wt: một cho collection, mỗi index thêm một file riêng

    Lab liệt kê đúng 6 file (collection-ce58c96d-... 176.689.152 byte và 5 file index-*), khớp với storageSize và indexSizes của $collStats. Tên file là ident (collection-<uuid>), không phải tên collection. Xem mục "Lab 1".

  4. Năm file cho 5 index, còn document nằm trong index _id_

    Chỉ clustered collection lưu document theo _id và không có file _id_ riêng. Collection thường lưu document ở table theo RecordId; _id_ chỉ là cặp _id → RecordId. Xem mục "RecordId: tên của một document trong table".

Cùng 1.000.000 đơn, nạp vào hai container: một dùng zstd (93,8 MiB trên đĩa), một dùng snappy (168,5 MiB). Sau khi quét hết, collection chiếm bao nhiêu trong WiredTiger cache?

  1. Gần như bằng nhau, khoảng 493 MiB, vì page trong cache không nén

    Lab đo 492,9 MiB (zstd) và 494,1 MiB (snappy), cả zlib và none cũng 493 đến 496 MiB. Block compression chỉ có trên đĩa. Xem mục "Nén không làm cache nhỏ đi".

  2. Khoảng 94 MiB với zstd và 169 MiB với snappy, vì cache lưu đúng thứ có trên đĩa

    Đó là hiểu lầm phổ biến nhất: cache của collection giữ dạng chưa nén, nên không nhỏ đi theo dung lượng đĩa. Xem bảng ở mục "Nén không làm cache nhỏ đi".

  3. Zstd chiếm ít hơn vì giải nén ít hơn, nhưng không đến mức bằng dung lượng đĩa

    Không có sự giảm nào ở cache: byte đưa vào cache là 448,2 (zstd) so với 448,6 MiB (snappy) trong cùng một COLLSCAN. Giải nén cho ra cùng một dạng. Xem mục "Lab 4: cái giá lúc đọc".

  4. Zstd chiếm nhiều hơn, vì phải giữ thêm bản nén để còn ghi lại

    Cache không giữ thêm bản nén nào; hai con số gần như trùng nhau (492,9 so với 494,1 MiB). Xem mục "Nén không làm cache nhỏ đi".

Bạn cần chọn compressor cho một collection 500 GB, và không biết dữ liệu của mình nén được bao nhiêu. Cách đáng tin cậy nhất?

  1. Lấy tỉ lệ nén từ bài benchmark phổ biến nhất, vì cùng WiredTiger thì cùng tỉ lệ

    Tỉ lệ nén là tính chất của dữ liệu: ở lab, một field hex ngẫu nhiên và nhiều field lặp cho 2,61 (snappy) đến 4,70 (zstd). Dữ liệu của bạn sẽ khác. Xem "Chọn compressor thế nào".

  2. Chọn compressor cho tỉ lệ nén cao nhất vì đĩa là chi phí duy nhất cần tối ưu

    CPU là chi phí thứ hai: zlib ngốn CPU gấp 3 snappy khi nạp và chậm gấp 2,4 lần none ở COLLSCAN nguội (cache WiredTiger trống), mà ở lab này còn lớn hơn zstd. Xem Lab 3 và Lab 4.

  3. Đổi cấu hình toàn cục sang compressor mới, rồi chờ collection tự được nén lại

    Cấu hình toàn cục chỉ áp cho collection mới. Lab 6: orders vẫn zlib 109,1 MiB sau khi đổi sang none. Xem mục "Lab 6".

  4. Copy 1–5% dữ liệu thật vào bốn collection, mỗi cái một compressor, rồi so kết quả

    Đúng, và nó rẻ: ở lab, copy 200.000 document sang collection block_compressor=zstd mất 0,86 giây và cho 32,7 xuống 18,6 MiB. Sau đó tính lại với tốc độ đĩa của bạn. Xem mục "Chọn compressor thế nào".

Bạn đổi storage.wiredTiger.collectionConfig.blockCompressor từ snappy sang zstd trong file cấu hình rồi restart mongod. Collection orders 500 GB đã có thì sao?

  1. Được nén lại dần bằng zstd khi các page bị ghi lại

    Manual nói collection hiện có tiếp tục dùng compressor đã chọn lúc tạo. Lab 6 kiểm lại: orders vẫn mang block_compressor=zlib sau khi đổi. Xem mục "Lab 6".

  2. Giữ snappy; chỉ collection tạo sau đó dùng zstd

    Đúng với cả manual lẫn lab: collection cũ vẫn đọc được bình thường và giữ nguyên kích thước. Muốn nén lại phải copy sang collection mới tạo với compressor mong muốn. Xem mục "Lab 6: đổi compressor thì dữ liệu cũ thế nào".

  3. Không đọc được nữa vì mỗi block ghi compressor cũ, đến khi chạy lệnh convert

    Không phải: collection cũ vẫn đọc tốt, và cấu hình table của nó giữ compressor đã chọn. Lab 6 đọc count và findOne trên collection zlib sau khi đổi sang none. Xem mục "Lab 6".

Một thư viện cất sách trong hộp ép chặt dưới tầng hầm; muốn đọc, nhân viên mang hộp lên bàn rồi mở sách ra. Thư viện đổi sang loại hộp ép chặt hơn, hầm chứa được gấp đôi số sách. Số sách để vừa trên bàn làm việc cùng lúc thay đổi ra sao?

  1. Gấp đôi, vì mỗi hộp nhỏ hơn nên bàn chứa được nhiều hộp hơn

    Trên bàn, sách đã mở ra khỏi hộp nên hộp nhỏ hay to không còn liên quan. Đó là cách hiểu sai về cache mà bài sửa. Xem "Nén không làm cache nhỏ đi".

  2. Không đổi: trên bàn sách luôn ở dạng đã mở

    Như WiredTiger cache: page ở dạng chưa nén bất kể trên đĩa nén bằng gì (lab: 493 đến 496 MiB cho cả bốn compressor). Cái đổi là chỗ ở hầm, công khuân và công mở hộp. Xem "Hình dung trước: kho hàng".

  3. Ít đi, vì hộp ép chặt hơn lúc mở ra cần bàn rộng hơn

    Sách mở ra chiếm cùng một chỗ dù hộp ép bằng cách nào; lab đo cache bằng nhau giữa zstd và snappy. Cái đắt hơn là công mở hộp (CPU). Xem Lab 4.

  4. Phụ thuộc vào loại sách, vì sách ít chữ mở ra chiếm ít chỗ hơn

    Đúng là kích thước sau mở phụ thuộc nội dung, nhưng nó không phụ thuộc cách ép hộp ở hầm. Với cùng một nội dung, cache bằng nhau. Xem "Nén không làm cache nhỏ đi".

Index { ref: 1 } trên 1.000.000 document, mọi key bắt đầu bằng cùng một URL dài 46 ký tự. Tạo nó với prefix_compression=false thay vì mặc định thì điều gì xảy ra ở lab?

  1. Index lớn gấp 5,4 lần trên đĩa (66,0 so với 12,3 MiB)

    Prefix compression lưu phần đầu chung một lần mỗi page. Lab: 12,28 so với 65,96 MiB trên đĩa, và cả trong cache: 20,28 so với 72,99 MiB. Xem "Lab 5: prefix compression cho index".

  2. Không đổi gì, vì index không được nén

    Index không có block compression, nhưng có prefix compression. Với key có tiền tố chung dài tác dụng rất lớn. Xem "Ba kiểu nén, ba chỗ khác nhau".

  3. Index nhỏ hơn, vì không phải lưu thông tin phụ của prefix

    Ngược lại, kích thước tăng 5,4 lần. Với key ngẫu nhiên như traceId chênh lệch chỉ 8% (39,09 so với 42,34 MiB), nhưng vẫn không nhỏ hơn. Xem bảng ở Lab 5.

Lab nạp 1 triệu document: thời gian insert chỉ 9,1 đến 9,5 s ở cả snappy, zstd, zlib và none, nhưng CPU của mongod là 2,8 / 5,9 / 8,4 / 2,8 s. Cách giải thích hợp lý nhất?

  1. Compressor chạy trong driver ở client nên insert không đổi, còn server chỉ phải giải nén khi đọc

    Việc nén là của WiredTiger bên trong mongod (CPU mongod tăng theo compressor); driver gửi BSON, không nén page. Xem "Lab 3: cái giá lúc ghi".

  2. Zlib thực ra không tốn CPU hơn; số khác nhau chỉ vì máy bận và bộ đếm CPU không đáng tin

    Ba lần đo cho cùng thứ tự (zlib 8,2 đến 9,8 s, snappy 2,6 đến 3,0 s), khoảng chênh lớn hơn nhiều so với dao động. Xem "Lab 3".

  3. Nén chạy khi page được ghi xuống; cache chưa bị ép nên client không đợi, chi phí hiện ở CPU

    Đây là giải thích [suy luận] trong bài, chưa kiểm tra riêng: nó khớp với dữ liệu và hàm ý rằng khi thiếu CPU hoặc cache bị ép, compressor đắt có thể lộ ra thành latency. Xem "Lab 3".

  4. Nén chỉ chạy một lần ở fsync cuối cùng, nên không nằm trong thời gian insert của client

    fsync sau lần nạp chỉ mất 55 đến 314 ms (rất nhỏ so với 9 s nạp), gợi ý rằng phần lớn dữ liệu đã được ghi xuống, và nén, trong lúc nạp. Xem "Lab 3: cái giá lúc ghi".

Nếu bỏ hết thuật ngữ: một kho hàng cất đồ trong thùng, mỗi loại hàng một toà nhà, mỗi toà chia thành các ngăn kệ có bảng chỉ dẫn. Làm việc thì phải khiêng ngăn lên bàn và mở thùng ra; trả về thì đóng thùng lại và cất vào chỗ trống mới. Hút chân không chặt hơn thì kho chứa được nhiều hơn và tiền thuê kho rẻ hơn, nhưng bàn làm việc không rộng ra chút nào, và mỗi lần mở thùng tốn thêm công. Muốn biết nên đóng gói thế nào, đừng nghe lời khuyên chung: lấy một ít hàng thật của mình ra thử.

Bài tiếp theo

Bài này nói WiredTiger làm gì với một page, chưa nói chuyện gì xảy ra khi RAM không đủ. Ta đã thấy 493 MiB trong cache cho một collection chỉ 94 MiB trên đĩa; nếu working set to gấp ba cache thì sao? Page nào bị đẩy ra? Page "bẩn" (đã sửa, chưa ghi xuống) khác page sạch thế nào, và vì sao thread của chính ứng dụng đôi khi bị kéo đi dọn cache?

Cache, Eviction & Working Set trả lời: kích thước cache, page sạch và bẩn, ngưỡng eviction, thread nền và thread ứng dụng, và một lab với working set lớn hơn cache để thấy độ trễ tăng ra sao khi hit chuyển thành miss.

Tài liệu tham khảo