Từ phiên bản 3.2, MongoDB theo dõi thống kê sử dụng cho mọi index. Để truy cập thống kê này, MongoDB cung cấp stage aggregation pipeline $indexStats. Dưới đây là 6 cân nhắc khi tìm index không sử dụng trong MongoDB.
Ví dụ, lệnh sau cung cấp thống kê index cho collection "test.foo":
db.foo.aggregate( [ { $indexStats: { } } ] )
Chúng tôi không mô tả output $indexStats vì đã có nhiều tài liệu và bài viết tốt về chủ đề này. Thay vào đó, chúng tôi cung cấp 6 cân nhắc về các lĩnh vực khác nhau khi dùng toán tử $indexStats.
Cân nhắc 1: Thống kê reset khi restart service
Luôn chú ý trường accesses.since khi dùng $indexStats. Một số query pattern có thể hiếm gặp, như batch process cuối ngày hoặc báo cáo hàng tuần, nên hãy đảm bảo khoảng thời gian thống kê bạn đánh giá đáp ứng nhu cầu. Script sau cho phép đặt ngưỡng (giờ) và in cảnh báo nếu index không dùng không tuân thủ:
threshold_hours=24; db.foo.aggregate( [ { $indexStats: { } } ] ).forEach(function(f){if (f.accesses.ops==0) {if (ISODate()-f.accesses.since < threshold_hours*3600*1000) {print('Index: ' +f.name+ ' accessed: ' +f.accesses.ops+ ' times, Status:WARNING, The duration of statistics DOES NOT meet your compliance')} else {print('Index: ' +f.name+ ' accessed: ' +f.accesses.ops+ ' times, Status:OK The duration of statistics meet your compliance');};}})
Ngưỡng đúng cho bạn là gì? Không có câu trả lời đơn giản. Không phải pattern statement nào cũng thực thi cùng tần suất, nên ngưỡng đúng thay đổi theo ứng dụng và có thể khác nhau per collection trong cùng database.
Cân nhắc 2: Secondary reads
Mặc định $indexStats đọc từ Primary. Nếu ứng dụng chỉ đọc từ Secondary, chạy $indexStats trên Primary sẽ dẫn đến kết luận sai. Để đọc kết quả từ Secondary, dùng script từ cân nhắc 1 nhưng thực thi db.getMongo().setReadPref('secondary') trước khi chạy, ép lệnh đọc từ Secondary.
Câu hỏi: "Nếu collection nhận cả Primary và Secondary reads?" Script sau điền vào mảng idx các index không dùng trên cả Primary và Secondary:
idx=[]; db.foo.getIndexes().forEach(function(f){idx.push(f.name)}) db.getMongo().setReadPref('primary'); db.foo.aggregate( [ { $indexStats: { } } ] ).forEach(function(f){if (f.accesses.ops>0) { var index = idx.indexOf(f.name); if (index > -1) {idx.splice(index, 1);};}}) db.getMongo().setReadPref('secondary'); db.foo.aggregate( [ { $indexStats: { } } ] ).forEach(function(f){if (f.accesses.ops>0) { var index = idx.indexOf(f.name); if (index > -1) {idx.splice(index, 1);};}})
Nội dung mảng idx sẽ chứa index không dùng trên cả Primary và Secondary. Về compliance window (threshold_hours)? Sửa nhẹ script từ cân nhắc 1 cho phép áp dụng time compliance. Chỉ cần thay phần aggregation bằng cái dưới và chạy script trên Primary và Secondary dùng setReadPref:
db.foo.aggregate( [ { $indexStats: { } } ,{$match:{"name" : {$in:idx}}}] ).
Nếu Primary và Secondary trả về kết quả compliance khác nhau, cách tốt nhất là chờ cả hai tier đều comply.
Dùng Secondary read preference có thể dẫn đến "kết quả không chính xác" nếu Secondary vừa restart và script chọn Secondary đó để lấy thống kê. Cách tốt là check db.serverStatus().uptime và chọn Secondary có uptime cao hơn. Phương pháp uptime cần approach khác trên script sẽ implement trong bản sửa đổi sau của bài viết này.
Cân nhắc 3: Replica set tags
Replica set tags áp dụng nhiều use case nhưng chủ yếu dùng cho read locality trong geo-replication và targeting workload cụ thể đến node chuyên biệt (ví dụ: heavy analytics). Khi targeting workload, tagged node phải được kiểm tra riêng vì chúng có thể dùng index mà node khác không dùng. Điều này cũng có thể xảy ra trong geo-replication, dù hiếm hơn. Script từ cân nhắc 1 có thể dùng để check tagged member, thêm readPreference ở đầu script. Ví dụ set read preference đến Secondary marked cho analytics:
db.getMongo().setReadPref('secondary', [ { "workload": "analytics" } ] ) .
Cách tiếp cận tương lai, mô tả ở đoạn cuối cân nhắc 2, có thể mở rộng để check tagged Secondary.
Cân nhắc 4: TTL indexes
TTL indexes phục vụ operation nhưng mục đích chính là data pruning. Rất có thể $indexStats báo index này là unused vì TTL monitor không count như index operation. Bạn chắc chắn không muốn drop TTL index dựa trên báo cáo $indexStats nên script phải loại trừ loại index này.
Sửa nhỏ script populate idx array từ section 2 sẽ giải quyết. Thay dòng 2 bằng dòng sau thì idx array không chứa TTL index:
db.foo.getIndexes().forEach(function(f){if (f.expireAfterSeconds==undefined) {idx.push(f.name)}})
Tiến xa hơn về excluded indexes, ta có thể nói _id cũng rơi vào category này. Chắc chắn không thể drop _id index dù unused. Collection không bao giờ chạm _id index có thể cần redesign.
Cân nhắc 5: Sharded clusters
Với sharded clusters, có 2 mục cần cân nhắc trước khi đánh giá output $indexStats. Thứ nhất, khi dùng $indexStats trên sharded collection, shard key index có thể bị classify là unused. Ví dụ: collection write-heavy sharded trên {_id:"hashed"} (để phân phối write đều) nhưng không có read/update/delete dùng _id index. Lúc này {_id:"hashed"} báo unused. Không nên drop index vì sẽ break sharded cluster. Nếu muốn exclude shard key cho 'stats.foo', script sau attach vào method cân nhắc 2 (secondary reads) và shard key sẽ bị exclude khỏi idx array:
shardkey=db.getSiblingDB('config').collections.findOne({_id:'stats.foo'},{_id:0,key:1}); db.foo.getIndexes().forEach(function(f){if (JSON.stringify(f.key)!=JSON.stringify(shardkey.key)) {printjson(shardkey.key);printjson(f.key);idx.push(f.name)}})
Cân nhắc thứ 2 liên quan đến output $indexStats, nó trả về thống kê từ mọi shard mà collection có mặt. Dù hiếm, có case index báo unused ở vài shard mà shard khác dùng. Poor shard key có thể là nguyên nhân nhưng scenario phổ biến nhất là "covering indexes".
Ví dụ hiểu covering indexes: Indexes {a:1} và {a:1,b:1} đều serve equality match trên field 'a'. Nếu shardA optimizer chọn {a:1} và shardB chọn {a:1,b:1}, cả hai index sẽ báo unused cho ít nhất một shard.
Thách thức là tìm index không globally in use. Thay phần aggregation (kèm forEach loop) của script cân nhắc 2 (secondary reads) bằng cái sau sẽ giải quyết:
db.foo.aggregate( [ { $indexStats: { } } , {$group: {_id:"$name",number : {$sum:"$accesses.ops"}}}] ).forEach(function(f){if (f.number>0) { var index = idx.indexOf(f._id); if (index > -1) {idx.splice(index, 1);};}})
Thách thức khác: khám phá index partially in use. Thông tin này hữu ích cho redundant indexes hoặc index identification malfunctions. Dùng idx array, aggregation/script sau populate globally unused indexes và báo cáo partially-in-use indexes:
db.foo.aggregate( [ { $indexStats: { } },{$match:{name:{$nin:idx},"accesses.ops":0}}]).forEach(function(f){print("Index "+f.name+" reports as partially unused on shard/host " +f.host)})
Cân nhắc 6: Least-used indexes
Khám phá và drop unused indexes quan trọng nhưng cũng cần evaluate least used indexes. Nếu index chỉ accessed 1-2 lần trong tuần hoặc tháng, có thể nó không cần thiết hoặc không beneficial cho workload. Luôn good practice để định kỳ check least-used indexes dùng aggregation sau:
db.foo.aggregate( [ { $indexStats: { } },{$match:{"accesses.ops":{$gt:0}}},{$group: {_id:"$name",number : {$sum:"$accesses.ops"}}},{$sort:{number:1}}] )
Sau đó, thực hiện hành động phù hợp: drop index, thay đổi index definition, hoặc thay đổi schema/application logic để index trở nên redundant.
Chúng tôi sẵn sàng hỗ trợ
Chúng tôi giúp bạn dọn dẹp index cho MongoDB instance. Chỉ cần tạo ticket đến support@objectrocket.com và để chúng tôi giúp cải thiện index của bạn!