Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> ฐานข้อมูล

วิธีระบุ Index ที่ไม่ได้ใช้งานใน MongoDB เพื่อประสิทธิภาพสูงสุด

ตั้งแต่เวอร์ชัน 3.2 MongoDB เริ่มติดตามสถิติการใช้งานของทุก Index ผ่าน Aggregation Pipeline Stage ชื่อ $indexStats นี่คือ 6 ข้อพิจารณาเมื่อหา Index ที่ไม่ได้ใช้งานใน MongoDB

ตัวอย่างคำสั่งสำหรับดูสถิติ Index ของ Collection "test.foo":

db.foo.aggregate( [ { $indexStats: { } } ] )

เราจะไม่อธิบาย Output ของ $indexStats เพราะมีเอกสารและบทความเยอะมากครอบคลุมอยู่แล้ว เราจะเน้น 6 ข้อพิจารณาในมุมมองต่างๆ เมื่อใช้ $indexStats Operator

ข้อพิจารณา 1: สถิติจะ Reset ทุกครั้งที่ Restart Service

ต้องใส่ใจ field accesses.since เมื่อใช้ $indexStats Pattern บางอย่างอาจรันไม่บ่อย เช่น End-of-day Batch Process หรือ Weekly Report ให้แน่ใจว่าช่วงเวลาของสถิติที่จะประเมิน ครอบคลุมความต้องการของคุณ สคริปต์ด้านล่างตั้ง Threshold (ชั่วโมง) และแจ้งเตือนหาก Index ที่ไม่ได้ใช้งานไม่ผ่านเกณฑ์

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');};}})

Threshold ที่เหมาะสมคือเท่าไหร่? ไม่มีคำตอบง่ายๆ ไม่ใช่ทุก Statement Pattern ที่รันด้วยความถี่เท่ากัน Threshold ที่ถูกต้องแตกต่างไปตามแอปพลิเคชัน และอาจต่างกันต่อ Collection ภายใน Database เดียวกัน

ข้อพิจารณา 2: การอ่านจาก Secondary

โดย Default $indexStats อ่านจาก Primary หากแอปอ่านจาก Secondary เท่านั้น การรัน $indexStats ต่อ Primary จะนำไปสู่ข้อสรุปผิด ในการอ่านจาก Secondary ใช้สคริปต์จากข้อพิจารณา 1 แต่ Execute db.getMongo().setReadPref('secondary') ก่อนรัน เพื่อบังคับให้อ่านจาก Secondary

คำถามคือ "ถ้า Collection รับทั้ง Primary และ Secondary Reads จะทำอย่างไร?" สคริปต์ต่อไป Populate Array idx ด้วย Index ที่ไม่ได้ใช้งานทั้ง Primary และ 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);};}})

เนื้อหา idx Array จะมี Index ที่ไม่ได้ใช้งานทั้ง Primary และ Secondary ส่วน Compliance Window (threshold_hours)? ปรับสคริปต์จากข้อพิจารณา 1 เล็กน้อย ให้รองรับ Time Compliance ได้ โดยแทนที่ Aggregation Part ด้วย:

db.foo.aggregate( [ { $indexStats: { } } ,{$match:{"name" : {$in:idx}}}] ).

หาก Primary และ Secondary คืนค่า Compliance ต่างกัน แนวทางที่ดีคือรอให้ทั้งสอง Tier ผ่านเกณฑ์

การใช้ Secondary Read Preference อาจให้ "Incorrect Results" หาก Secondary เพิ่ง Restart และสคริปต์เลือก Secondary นั้น แนวทางที่ดีคือเช็ค db.serverStatus().uptime และเลือก Secondary ที่ Uptime สูงกว่า วิธีนี้ต้องการ Approach ต่างในสคริปต์ จะทำใน Revision หน้า

ข้อพิจารณา 3: Replica Set Tags

Replica Set Tags ใช้ในหลาย Use Case แต่หลักคือ Read Locality ใน Geo-replication และ Targeting Workload เฉพาะไปยัง Node เฉพาะ (เช่น Heavy Analytics) สำหรับ Workload Targeting Tagged Nodes ต้องตรวจสอบแยกต่างหาก เพราะอาจใช้ Index ที่ Node อื่นไม่ใช้ กรณี Geo-replication ก็อาจเกิดขึ้นแต่อย่างนادر สคริปต์จากข้อพิจารณา 1 ใช้เช็ค Tagged Member ได้ โดยเพิ่ม ReadPreference ต้นสคริปต์ ตัวอย่างตั้ง Read Preference ไป Secondary ที่ Tag workload:analytics:

 db.getMongo().setReadPref('secondary', [ { "workload": "analytics" } ] ) . 

Future Approach จากข้อพิจารณา 2 สามารถขยายไปเช็ค Tagged Secondaries ได้

ข้อพิจารณา 4: TTL Indexes

TTL Indexes ให้บริการ Operation แต่ใช้หลักสำหรับ Data Pruning $indexStats อาจรายงานว่า Unused เพราะ TTL Monitor ไม่นับเป็น Index Operation ห้าม Drop TTL Index จากรายงาน $indexStats เด็ดขาด สคริปต์ต้อง Exclude Index ประเภทนี้ การปรับเล็กน้อยจากสคริปต์ Populate idx array (ข้อพิจารณา 2) ทำได้ โดยแทนบรรทัดที่ 2 ด้วย:

db.foo.getIndexes().forEach(function(f){if (f.expireAfterSeconds==undefined) {idx.push(f.name)}})

ขยายต่อ: _id ก็อยู่ในกลุ่ม Excluded Indexes เพราะ Drop _id Index ไม่ได้ แม้จะ Unused Collection ที่ไม่ทับ _id Index อาจต้อง Redesign

ข้อพิจารณา 5: Sharded Clusters

สำหรับ Sharded Clusters มี 2 ข้อพิจารณาก่อนประเมิน $indexStats Output ข้อแรก: $indexStats ต่อ Sharded Collection อาจจัด Shard Key Index ว่า Unused ตัวอย่าง Collection Write-heavy ที่ Shard บน {_id:"hashed"} (กระจาย Write) แต่ไม่มี Read/Update/Delete ที่ใช้ _id Index ทำให้ {_id:"hashed"} รายงานว่า Unused ห้าม Drop Index นี้ จะทำให้ Sharded Cluster พัง หากต้องการ Exclude Shard Key สำหรับ 'stats.foo' สคริปต์ต่อไปต่อท้ายข้อพิจารณา 2 (Secondary Reads) ได้:

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)}})

ข้อสอง: $indexStats คืนค่าสถิติจากทุก Shard ที่ Collection อยู่ แม่นادر แต่อาจมี Index รายงาน Unused ในบาง Shard แต่ Shard อื่นใช้งาน 原因อาจเป็น Poor Shard Key แต่สถานการณ์พบบ่อยคือ "Covering Indexes" ตัวอย่าง: Index {a:1} และ {a:1,b:1} ทั้งคู่ Serve Equality Match บน field 'a' ได้ หาก ShardA เลือก {a:1} และ ShardB เลือก {a:1,b:1} ทั้งสอง Index จะรายงาน Unused อย่างน้อย 1 Shard

ความท้าทายคือหา Index ที่ไม่ได้ใช้งาน Globally แทนที่ Aggregation Part (ตามด้วย forEach Loop) ของสคริปต์ข้อพิจารณา 2 (Secondary Reads) ด้วย:

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);};}})

อีกความท้าทายคือค้นหา Index ที่ใช้งานบางส่วน (Partially in Use) ข้อมูลนี้ช่วยเรื่อง Redundant Indexes หรือ Index Identification Malfunctions ใช้ idx Array Aggregation/Script นี้ Populate ด้วย Globally Unused Indexes และรายงาน 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)})

ข้อพิจารณา 6: Least-used Indexes

การหาและ Drop Unused Indexes สำคัญ แต่ประเมิน Least-used Indexes ก็สำคัญไม่แพ้กัน หาก Index ถูก Access แค่ 1-2 ครั้งต่อสัปดาห์หรือเดือน อาจหมายความว่าไม่จำเป็นหรือไม่คุ้มค่า ควรเช็ค Least-used Indexes เป็นระยะด้วย Aggregation:

db.foo.aggregate( [ { $indexStats: { } },{$match:{"accesses.ops":{$gt:0}}},{$group: {_id:"$name",number : {$sum:"$accesses.ops"}}},{$sort:{number:1}}] )

จากนั้นดำเนินการต่อไปตามเหมาะสม: Drop Index, เปลี่ยน Definition, หรือปรับ Schema/Application Logic ให้ Index เกลื่อน

เราอยู่ที่นี่เพื่อช่วยคุณ

เราพร้อมช่วย Clean Up Index สำหรับ MongoDB Instance ของคุณ เพียงสร้าง Ticket ไปที่ support@objectrocket.com เราจะช่วยปรับปรุง Index ของคุณ!