WiredTiger MVCC (P3/4): Ba lab về snapshot bị giữ

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

Ở phần trước: update chain giữ các version của một key, mới nhất ở đầu, và sống trong cache. Khi eviction hoặc checkpoint ghi xuống, các version cũ đi vào history store.

Lab 1: giữ một snapshot mở trong lúc dữ liệu liên tục đổi

Kịch bản. 100 document (mỗi cái khoảng 1,1 KB: _id, một counter n, một chuỗi 1.000 ký tự), 300.000 lệnh updateOne({ _id: k }, { $inc: { n: 1 } }) trên các document chọn ngẫu nhiên, nhắm khoảng 1.000 lệnh mỗi giây. Mỗi cấu hình chạy trong một container riêng:

  • hold: mở một transaction snapshot, đọc một document, giữ nó mở tới khi hết 300.000 update rồi abortTransaction. Chạy hai lần.
  • control: cùng workload, không giữ gì.
  • Hold và control đầu tiên chạy song song, dùng minSnapshotHistoryWindowInSeconds = 30 (chỉ trong lab, để kết quả hiện ra trong vài phút). Lượt sau: hold chạy lại (kèm theo dõi kích thước file) song song với control dùng giá trị mặc định 300.

Sau khi update xong, lab lấy mẫu thêm 240 giây (420 giây với cửa sổ 300). Cứ 10 giây ghi một dòng gồm độ trễ từng lệnh update (đo trong container nên không có độ trễ mạng), các bộ đếm ở bảng trên và các timestamp. Output thô: e1-hold.out, e1b-hold-rerun.out, e1-ctl.out, e2-ctl-w300.out; tóm tắt e1-summary.out.

Một điều xác nhận trước: lúc hold hết update, snapshot đang giữ đọc tổng n của 100 document là 0, còn đọc mới nhất là 300.000 (read-check trong e1-hold.out).

History store lớn lên theo số update

Kích thước history store theo WiredTiger (file size in bytes, MB) khi số update đạt khoảng 100.000, 200.000 và 300.000:

Cấu hình~100.000 update~200.000300.000
hold, cửa sổ 30 s10,2621,9732,94
hold chạy lại, cửa sổ 30 s10,8921,0131,29
control, cửa sổ 30 s5,027,518,29
control, cửa sổ mặc định 300 s8,6617,4927,30

Và các con số lúc update vừa xong:

holdhold chạy lạicontrol (30 s)control (300 s)
thời gian chạy 300.000 update479 s403 s478 s373 s
history store insert calls870.454852.821532.745593.447
trong đó bản đầy đủ / reverse modify343.944 / 526.510330.368 / 522.453321.320 / 211.425327.079 / 266.368
page được ghi kèm bản ghi history store447506608735
stable − pinned (giây)48040430300
cache lớn nhất trong lúc update (MB, trên 1.024)120,7132,1111,3117,9
application thread time evicting0000

[quan sát] Điều đo được:

  1. Pinned timestamp lùi lại phía sau đúng bằng thời gian giữ snapshot: 480 giây ở hold (đúng bằng thời gian chạy), còn control cửa sổ 30 giây chỉ 30. Chênh lệch stable − pinned là chỉ báo trực tiếp "đang có ai giữ version cũ".
  2. History store của hold lớn khoảng 4 lần control cùng cửa sổ: 31 đến 33 MB so với 8,3 MB, với 853.000 đến 870.000 lần ghi so với 533.000. Hai lần chạy hold ra gần nhau (32,94 và 31,29).
  3. Nhưng control với cửa sổ mặc định 300 giây đã tới 27,3 MB, gần bằng hold cửa sổ 30 giây giữ 480 giây. Cửa sổ minSnapshotHistoryWindowInSeconds giữ oldest timestamp lùi sau 5 phút, nên mọi version của 5 phút gần nhất đều phải ở lại, dù không ai đọc chúng: về mặt history store, nó giống một snapshot luôn mở. [suy luận] Ở cấu hình mặc định, một transaction giữ mở chỉ làm pinned timestamp lùi xa hơn oldest timestamp khi nó mở lâu hơn 5 phút; lab không đo trường hợp hold với cửa sổ 300 giây.
  4. [quan sát] Control cửa sổ 30 giây vẫn ghi 532.745 version vào history store. [tài liệu] Version không cần chờ có ai đọc mới vào đó: chỉ cần nó chưa obsolete lúc page được ghi xuống thì reconciliation đã chuyển nó sang history store.

Cache và độ trễ: không thấy ở quy mô này

  • Cache. Cache lớn nhất trong lúc update ở cả bốn lần chạy chỉ 111 đến 132 MB trên 1.024 MB, và application thread time evicting (thời gian thread của ứng dụng phải tự evict page vì cache đầy) bằng 0 ở mọi mẫu: không có cache pressure. Version cũ ở lab này nhỏ (history store 31 đến 33 MB) và nằm ở file trên đĩa.
  • Độ trễ update (trung vị của p50 theo cửa sổ 10 giây): hold 842 và 429 µs, control 897 và 394 µs: chênh gấp đôi giữa các lần chạy, theo cả hai chiều giữa hold và control. Trong cả bốn lần có lệnh đơn lẻ mất tới 6,2 đến 11,3 giây (kể cả control). [suy luận] Dao động của máy dùng chung và của checkpoint; tôi không tách được nguyên nhân. Ở quy mô này, độ trễ không phân biệt được hold với control.

Bản có áp lực cache

Để xem chuyện gì xảy ra khi lượng version lớn hơn cache nhiều lần, một lab nữa (e7-stress.js, e7-summary.out): 50 document 200 KB, mỗi update $set lại cả trường 200 KB, WiredTiger cache 0,25 GB, 200 lệnh mỗi giây trong 60 giây (khoảng 2,2 GB version), cửa sổ 5 giây cho cả hai. Hold giữ một snapshot suốt 61 giây.

holdcontrol
số update trong 61 s11.53711.822
history store (file size in bytes)2.171,8 MB2.222,8 MB
history store insert calls34.58423.662
cache lớn nhất197,7 MB187,6 MB
application thread time evicting (cộng dồn)202 ms279 ms
stable − pinned63 s5 s

[quan sát] Lần này history store lớn tới 2,1 GB (11.537 update × 200 KB cũng khoảng 2,1 GB), và control lớn bằng hold. Cache lên tới 188 đến 198 MB trên 256 MB và thread ứng dụng có tự evict 202 đến 279 ms trong 61 giây, nhưng cũng bằng nhau giữa hai bên. [suy luận] Với giá trị 200 KB và cache 0,25 GB, page bị evict liên tục, và mỗi lần evict thì mọi version chưa obsolete (cửa sổ 5 giây, mỗi document bị sửa khoảng 4 lần/giây) đều vào history store; snapshot giữ mở không thêm gì đáng kể trên nền đó. Lab này cho thấy một workload ghi nhanh vào giá trị lớn tự nó đã chuyển gần như toàn bộ version cũ sang history store. Nó không cho thấy giữ snapshot là vô hại: lab chỉ giữ một phút.

Sau khi giải phóng snapshot

[quan sát] Giải phóng snapshot không làm file nhỏ đi ngay:

  • Ở hold, file bytes available for reuse đứng ở 4,38 MB tới giây 579 rồi nhảy lên 30,44 MB (trên 32,89 MB) ở giây 589, khoảng 100 đến 110 giây sau khi giải phóng (giây 479). Hold chạy lại: 190 đến 200 giây sau. Control cửa sổ 30 giây (từ 1,26 lên 6,63 MB): 110 đến 120 giây sau khi update xong. Control cửa sổ 300 giây (từ 3,52 lên 21,28 MB): 210 đến 220 giây. [suy luận] Có lẽ mỗi bước nhảy là một lần checkpoint (tài liệu: checkpoint xoá được page history store chỉ còn bản ghi obsolete); lab không ghi thời điểm checkpoint.
  • file size in bytes không nhỏ đi (32,89 MB sau 240 giây): phần trống được dùng lại cho history store về sau, không trả cho hệ điều hành ngay. Theo dõi bằng stat từ ngoài container cho hold chạy lại (cùng đơn vị MB như trên): đỉnh 31,29 MB lúc update xong, 27,32 MB khoảng 4 phút sau khi giải phóng, giữ nguyên tới lần đo cuối (6 phút sau). Khi kiểm tra lại, khoảng 24 phút sau khi update xong, WiredTigerHS.wt của cả hold chạy lại và control 300 giây chỉ còn 12.288 byte. Tôi không bắt được lúc nó co lại và không biết nguyên nhân.
  • Ở lab áp lực cache, 90 giây sau khi giải phóng phần dùng lại được chỉ 13,7 MB trên 2,1 GB (control: 10,2 MB): trong 150 giây theo dõi, gần như chưa có chỗ nào trống ra.

Tóm lại: version cũ trở thành xoá được sau khi pinned timestamp tiến lên, nhưng phần trống trong file chỉ xuất hiện sau khoảng 100 đến 220 giây, còn file trên đĩa co lại muộn hơn nhiều.

Lab 2: update chain dài làm đọc chậm bao nhiêu

Câu hỏi. Người đọc có snapshot cũ phải đi qua cả chain, người đọc mới dừng ở đầu chain. Chain dài bao nhiêu thì cái giá đó thấy được?

Thiết kế (e3-chain.js, e3-chain.out). Với mỗi K trong 0, 1.000, 10.000, 100.000: tạo một document mới, mở snapshot cũ, áp K lệnh $inc lên đúng document đó, mở snapshot mới, rồi mỗi bên đọc document 300 lần, hai đợt. Sau đó fsync (buộc một checkpoint chạy ngay, version cũ chuyển vào history store) và đo lại. Cả hai bên đọc trong transaction để cùng chi phí; snapshot cũ trả n = 0, snapshot mới trả n = K (đã kiểm). Số µs là p50 của 300 lần đọc (đợt 1 / đợt 2):

K (độ dài chain)đọc mới nhấtđọc snapshot cũsau checkpoint: mới nhấtsau checkpoint: cũ
0238 / 151160 / 135187 / 132136 / 117
1.000112 / 115115 / 122183 / 113146 / 110
10.000102 / 102147 / 144170 / 111165 / 157
100.000122 / 122235 / 244165 / 122122 / 130

[quan sát] Chỉ ở K = 100.000 mới thấy khác biệt rõ và ổn định giữa hai đợt: snapshot cũ chậm gấp khoảng hai lần (thêm 110 đến 120 µs), và chênh lệch biến mất sau checkpoint (cũ 122 / 130, mới 165 / 122 µs). [suy luận] Khớp với việc người đọc cũ phải đi qua phần chain dài còn trên page, và chain ngắn lại khi checkpoint chuyển version cũ sang history store. Tôi không tách riêng được điều đó:

  • Ở K = 10.000, snapshot cũ chậm hơn 42 đến 45 µs ở cả hai đợt, nhưng sau checkpoint đợt 2 vẫn còn khoảng cách tương tự (157 so với 111) còn đợt 1 thì không (165 so với 170), nên không gán được cho chain.
  • Ở K = 100.000, trong 26,6 giây update chạy, eviction và checkpoint nền đã chuyển phần lớn version sang history store (history store table insert calls tăng 243.134), và cả 600 lần đọc của snapshot cũ đều tính vào history store table reads. Sau checkpoint, 600 lần đọc cũ nữa cũng vào history store mà chỉ mất 122 đến 130 µs: ở lab này đọc từ history store không phải thứ chậm.
  • Đọc mới nhất dao động 102 đến 238 µs: nhiễu của máy cỡ bằng thứ ta muốn đo ở các K nhỏ.

Kết luận dùng được: chỉ khi 100.000 update dồn lên một hot document (document bị sửa liên tục) trong lúc snapshot cũ còn mở, người đọc cũ mới chậm thêm thấy được, khoảng 0,1 ms mỗi lần đọc; từ 10.000 trở xuống không tách được khỏi nhiễu ở máy này. Đó là kịch bản của một counter được cộng liên tục cộng với một transaction báo cáo chạy lâu.

Lab 3: các hàng rào chống version cũ tích tụ vô hạn

Hai tham số đã gặp ở bài trước chặn việc version cũ tích tụ từ hai đầu.

transactionLifetimeLimitSeconds (mặc định 60 giây, bài Transactions & Atomicity). [tài liệu] Manual nói tham số này "giúp giảm áp lực cache" và một tiến trình nền định kỳ abort transaction quá hạn, mỗi transactionLifetimeLimitSeconds/2 giây hoặc ít nhất mỗi 60 giây. [quan sát] e5-guard.js đặt giới hạn 10 giây (chỉ trong lab), mở transaction snapshot rồi để nó rảnh; cứ 2 giây đọc transaction read timestamp of the oldest active reader (e5-guard.out):

sec 0 .. 14   oldestActiveReader = 1791548026|2      <- còn bị giữ
sec 16 .. 38  oldestActiveReader = 0                 <- biến mất
lệnh kế tiếp trong transaction: NoSuchTransaction (251), "has been aborted"

Reader biến mất giữa giây 14 và 16 (quá hạn 10 giây cộng nhịp 5 giây của tiến trình nền). Từ đó transaction này không còn giữ pinned timestamp; stable − pinned ở lab này vẫn tăng, [suy luận] vì container mới chạy chưa đủ 300 giây của cửa sổ mặc định. Đây là hàng rào cho transaction của ứng dụng.

minSnapshotHistoryWindowInSeconds (mặc định 300). Nó là hàng rào ngược chiều: bảo đảm WiredTiger luôn giữ 5 phút snapshot history cho atClusterTime / snapshot ngoài transaction (Lab 1: 27,3 MB history store mà không ai giữ gì). [tài liệu] Manual: tăng giá trị này tăng dung lượng đĩa, mức tăng tuỳ workload; giảm về 0 trên config server của sharded cluster có thể làm hỏng thao tác nội bộ.

Cửa sổ đặt sàn cho lượng snapshot history được giữ; transactionLifetimeLimitSeconds đặt trần cho thời gian một transaction ứng dụng được giữ mở.

Cột mốc: Bạn đã có thể đọc số liệu của một snapshot bị giữ, so độ trễ đọc khi update chain dài, và nói vì sao history store không co lại ngay. Tiếp theo: So với PostgreSQL và lỗi thường gặp.

Hỏi & đáp

Lab 1: một transaction snapshot được giữ mở trong lúc 300.000 lệnh $inc chạy trên 100 document. Trước khi giải phóng, tổng n mà chính snapshot đó đọc được là?

  1. 300.000, vì snapshot luôn tự cập nhật theo lệnh ghi mới

    Chính các lệnh ghi đó là thứ snapshot không được phép thấy. Đây là hiểu nhầm giữa snapshot và read committed. Xem mục "Lab 1", đoạn "Một điều xác nhận trước".

  2. Giữa 0 và 300.000, tuỳ lúc đọc, vì cursor đọc dần

    Đó là hành vi của cursor ngoài snapshot (bài Isolation & Snapshot). Trong transaction snapshot, mọi lần đọc dùng cùng một read timestamp. Xem mục "Lab 1".

  3. Một lỗi SnapshotTooOld, vì đã quá cửa sổ 30 giây

    SnapshotTooOld áp cho đọc ở mốc đã cũ hơn oldest timestamp; transaction đang mở giữ pinned timestamp ở read timestamp của nó nên version cần đọc vẫn còn. Lab đã nâng transactionLifetimeLimitSeconds lên 3600 để transaction không bị abort. Xem Quan sát các timestamp và mục "Lab 1".

  4. 0, vì snapshot thấy dữ liệu như lúc nó được lấy

    Lab: snapshot đang giữ đọc tổng n là 0, còn đọc mới nhất là 300.000. Mọi version mới đều ở trên read timestamp của snapshot nên bị bỏ qua. Xem mục "Lab 1", đoạn "Một điều xác nhận trước".

Snapshot giữ mở 8 phút vừa được giải phóng. History store của lab là 32,89 MB. Vài phút sau bạn kỳ vọng gì?

  1. Không đổi gì cho tới khi restart mongod, vì chỗ trống trong history store chỉ được lấy lại lúc khởi động

    Không cần restart: lab thấy phần trống tăng từ 4,38 lên 30,44 MB khoảng 100 đến 110 giây sau khi giải phóng, mongod vẫn chạy. Xem mục "Sau khi giải phóng snapshot".

  2. File WiredTigerHS.wt nhỏ lại ngay về vài KB, vì không còn ai cần version cũ

    Không: file size in bytes vẫn 32,89 MB tới hết 240 giây theo dõi; file chỉ co lại muộn hơn nhiều. Xem mục "Sau khi giải phóng snapshot".

  3. Phần trống bên trong file tăng lên sau vài phút, nhưng kích thước file chưa nhỏ đi

    Lab: file bytes available for reuse nhảy từ 4,38 lên 30,44 MB khoảng 100 đến 110 giây sau khi giải phóng, kích thước file vẫn 32,89 MB. Vì vậy hãy đo phần trống, đừng chỉ nhìn kích thước file. Xem mục "Sau khi giải phóng snapshot".

  4. Phải chạy compact mới lấy lại được chỗ trống để dùng cho history store, nếu không thì file chỉ lớn thêm mãi mà không dùng lại chỗ cũ

    Lab không chạy compact và vẫn thấy phần trống tăng lên: chỗ trống bên trong file được dùng lại mà không cần lệnh nào. Xem mục "Sau khi giải phóng snapshot".

Bạn để mặc định minSnapshotHistoryWindowInSeconds = 300 và không bao giờ giữ transaction nào mở. Workload của Lab 1: 300.000 $inc vào 100 document trong khoảng 6 phút. History store ra sao so với cấu hình cửa sổ 30 giây có một snapshot giữ mở 8 phút?

  1. Cỡ gần nhau, vì cửa sổ 300 giây tự nó đã giữ mọi version của 5 phút gần nhất

    Lab: 27,3 MB (cửa sổ 300 giây, không giữ gì) so với 31 đến 33 MB (cửa sổ 30 giây, giữ snapshot). Oldest timestamp lùi sau 5 phút nên mọi version trong 5 phút gần nhất bị giữ; control cửa sổ 30 giây chỉ 8,3 MB. Xem mục "History store lớn lên theo số update".

  2. Gần như rỗng, vì không có transaction nào giữ mở nên không version nào phải lưu

    Version vào history store khi page được ghi xuống mà version chưa obsolete, không phải chỉ khi có người giữ. Lab: 593.447 lần ghi và 27,3 MB. Xem Từ page trong cache tới history store.

  3. Nhỏ hơn rất nhiều so với snapshot giữ mở, khoảng 8 MB như control 30 giây

    8,3 MB là control cửa sổ 30 giây. Cửa sổ 300 giây giữ snapshot history dài gấp mười, nên 27,3 MB. Xem bảng ở Lab 1.

  4. Lớn hơn nhiều, vì cửa sổ rộng buộc mọi page phải ở trong cache cho tới hết 300 giây

    Cửa sổ rộng làm history store lớn hơn trên đĩa, không làm cache lớn hơn: cache lớn nhất 117,9 MB trên 1.024 MB và application thread time evicting = 0. Xem mục "Cache và độ trễ: không thấy ở quy mô này".