Computer >> 컴퓨터 >  >> 프로그래밍 >> 데이터베이스

MongoDB에서 사용하지 않는 인덱스 찾기: $indexStats 활용법과 6가지 핵심 고려사항

MongoDB에서 사용하지 않는 인덱스 찾기: $indexStats 활용법과 6가지 핵심 고려사항

MongoDB는 버전 3.2부터 모든 인덱스에 대한 사용 통계를 추적하고 있습니다. 이 통계에 접근하려면 MongoDB에서 제공하는 $indexStats 애그리게이션 파이프라인 스테이지를 활용하면 됩니다. 다음은 MongoDB에서 사용하지 않는 인덱스를 찾을 때 반드시 알아두어야 할 여섯 가지 고려사항입니다.

예를 들어, 아래 명령어를 실행하면 "test.foo" 컬렉션의 인덱스 통계를 확인할 수 있습니다.

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

$indexStats 출력 결과에 대한 상세한 설명은 이미 공식 문서와 여러 좋은 글에서 다루고 있으므로 생략하겠습니다. 대신, $indexStats 연산자를 실무에서 활용할 때 놓치기 쉬운 여섯 가지 핵심 사항을 소개합니다.

고려사항 1: 서비스 재시작 시 통계가 초기화됩니다

$indexStats 연산자를 사용할 때는 "accesses.since" 필드에 각별히 주의해야 합니다. 일부 쿼리 패턴은 실행 빈도가 낮을 수 있습니다. 예를 들어 하루가 끝날 때 실행되는 배치 작업이나 주간 리포트처럼요. 따라서 통계를 평가하려는 기간이 실제 쿼리 패턴을 충분히 포괄하는지 반드시 확인해야 합니다. 아래 스크립트는 임계값(시간 단위)을 설정하고, 미사용 인덱스가 해당 기준을 충족하지 못하면 경고 메시지를 출력합니다.

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

그렇다면 어떤 임계값이 적절할까요? 간단한 정답은 없습니다. 모든 쿼리 패턴이 동일한 빈도로 실행되는 것이 아니기 때문입니다. 적절한 임계값은 애플리케이션마다 다르며, 같은 데이터베이스 내에서도 컬렉션마다 달라질 수 있습니다.

고려사항 2: 세컨더리 읽기 처리

기본적으로 $indexStats는 프라이머리(Primary)에서 데이터를 읽습니다. 만약 애플리케이션이 세컨더리(Secondary)에서만 읽기를 수행한다면, 프라이머리를 대상으로 $indexStats를 실행했을 때 잘못된 결론에 도달할 수 있습니다. 세컨더리에서 결과를 읽으려면, 고려사항 1의 스크립트를 실행하기 전에 db.getMongo().setReadPref('secondary')를 먼저 실행하여 명령이 세컨더리에서 읽도록 강제하면 됩니다.

그렇다면 하나의 컬렉션이 프라이머리와 세컨더리 양쪽 모두에서 읽기를 받는 경우에는 어떻게 해야 할까요? 아래 스크립트를 사용하면 프라이머리와 세컨더리 어디에서도 사용되지 않는 인덱스만 idx 배열에 담을 수 있습니다.

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 배열에는 프라이머리와 세컨더리 양쪽 모두에서 사용되지 않는 인덱스가 담기게 됩니다. 그렇다면 통계 기준 기간(threshold_hours)은 어떻게 적용할까요? 고려사항 1의 스크립트를 약간 수정하면 시간 기준 검증까지 함께 적용할 수 있습니다. setReadPref를 사용해 프라이머리와 세컨더리 각각에 대해 아래 집계 구문으로 교체하여 실행하면 됩니다.

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

프라이머리와 세컨더리의 검증 결과가 서로 다르게 나온다면, 두 계층 모두 기준을 충족할 때까지 기다리는 것이 가장 안전한 접근 방식입니다.

세컨더리 읽기 선호도(read preference)를 사용하면, 최근 재시작된 세컨더리가 통계 수집 대상으로 선택되는 경우 부정확한 결과가 나올 수 있습니다. 이런 경우 db.serverStatus().uptime을 확인하여 가동 시간이 더 긴 세컨더리를 선택하는 것이 좋습니다. uptime 기반 접근 방식을 위한 스크립트 개선은 향후 이 글의 개정판에서 다룰 예정입니다.

고려사항 3: 레플리카 셋 태그

레플리카 셋 태그는 다양한 용도로 활용되지만, 주로 지역 복제(geo-replication) 환경에서의 읽기 지역성 확보나 특정 워크로드를 전용 노드에 배정하는 데 사용됩니다(예: 무거운 분석 작업). 워크로드 분산을 위해 태그를 사용하는 경우, 해당 태그가 붙은 노드는 반드시 별도로 점검해야 합니다. 다른 노드에서는 사용되지 않지만 특정 노드에서만 사용되는 인덱스가 있을 수 있기 때문입니다. 지역 복제 환경에서도 드물긴 하지만 비슷한 상황이 발생할 수 있습니다. 고려사항 1의 스크립트 맨 앞에 readPreference 설정을 추가하면 태그가 지정된 멤버도 점검할 수 있습니다. 다음은 분석(analytics) 전용으로 표시된 세컨더리에 읽기 선호도를 설정하는 예입니다.

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

앞서 고려사항 2 마지막 부분에서 언급한 uptime 기반 접근 방식은 태그가 지정된 세컨더리 점검으로도 확장 적용할 수 있습니다.

고려사항 4: TTL 인덱스

TTL 인덱스는 일반적인 쿼리 작업에도 사용되지만, 그 본래 목적은 데이터 정리(pruning)입니다. TTL 모니터의 실행은 인덱스 작업으로 집계되지 않기 때문에, $indexStats가 이러한 인덱스를 미사용으로 보고할 가능성이 매우 높습니다. $indexStats 결과만 보고 TTL 인덱스를 삭제해서는 절대 안 되며, 스크립트에서 이 유형의 인덱스는 반드시 제외해야 합니다.

고려사항 2에서 소개한 idx 배열 생성 스크립트를 살짝 수정하면 됩니다. 두 번째 줄을 다음과 같이 바꾸면 idx 배열에 TTL 인덱스가 포함되지 않습니다.

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

제외 대상 인덱스 논의를 한 단계 더 확장하면 _id 인덱스도 같은 범주에 속한다고 볼 수 있습니다. 당연히 _id 인덱스는 사용되지 않더라도 삭제할 수 없습니다. 만약 어떤 컬렉션에서 _id 인덱스가 한 번도 조회되지 않았다면, 해당 컬렉션의 설계를 다시 검토해야 한다는 신호일 수 있습니다.

고려사항 5: 샤딩된 클러스터

샤딩된 클러스터(sharded cluster)에서는 $indexStats 결과를 평가하기 전에 두 가지 사항을 먼저 확인해야 합니다. 첫째, 샤딩된 컬렉션에 $indexStats를 실행하면 샤드 키 인덱스가 미사용으로 분류될 수 있습니다. 예를 들어, 쓰기가 많은 컬렉션을 {_id:"hashed"}로 샤딩(쓰기 균등 분배 목적)했지만 _id 인덱스를 활용하는 읽기/수정/삭제 작업이 전혀 없다면, {_id:"hashed"} 인덱스는 미사용으로 보고됩니다. 하지만 이 인덱스를 삭제하면 샤딩된 클러스터 전체가 깨질 수 있으므로 절대 삭제해서는 안 됩니다. 'stats.foo' 컬렉션에서 샤드 키를 제외하고 싶다면, 아래 스크립트를 고려사항 2(세컨더리 읽기)의 방법에 추가하면 idx 배열에서 샤드 키가 제외됩니다.

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의 출력 방식과 관련이 있습니다. $indexStats는 해당 컬렉션이 존재하는 모든 샤드의 통계를 반환합니다. 흔하지는 않지만, 특정 인덱스가 일부 샤드에서는 미사용으로 보고되는데 다른 샤드에서는 사용되는 경우가 있습니다. 원인은 불량한 샤드 키일 수도 있지만, 가장 흔한 시나리오는 "커버링 인덱스(covering index)"입니다.

커버링 인덱스를 쉽게 이해하기 위해 예를 들어 보겠습니다. {a:1}과 {a:1,b:1} 인덱스는 모두 필드 'a'에 대한 동등 조건 매칭을 처리할 수 있습니다. 만약 shardA의 옵티마이저가 {a:1}을 선택하고 shardB의 옵티마이저가 {a:1,b:1}을 선택했다면, 두 인덱스 모두 최소 하나의 샤드에서는 미사용으로 보고됩니다.

여기서 핵심 과제는 전역적으로 사용되지 않는 인덱스를 찾아내는 것입니다. 고려사항 2 스크립트의 집계 부분(forEach 루프 포함)을 다음 코드로 교체하면 해결할 수 있습니다.

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

또 다른 과제는 부분적으로만 사용되는 인덱스를 발견하는 것입니다. 이 정보는 중복 인덱스 정리나 인덱스 식별 오류 진단에 유용하게 활용될 수 있습니다. idx 배열을 활용하면 다음 집계/스크립트로 전역적으로 미사용 인덱스를 구성하고, 부분적으로만 사용 중인 인덱스를 보고할 수 있습니다.

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: 가장 적게 사용되는 인덱스

미사용 인덱스를 찾아 제거하는 것도 중요하지만, 거의 사용되지 않는 인덱스를 평가하는 것 역시 중요합니다. 어떤 인덱스가 몇 주 또는 몇 달 동안 한두 번만 조회된다면, 해당 워크로드에 불필요하거나 실익이 없다는 의미일 수 있습니다. 다음 애그리게이션을 사용해 주기적으로 저사용 인덱스를 점검하는 것이 좋은 운영 습관입니다.

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

이후에는 적절한 조치를 취하면 됩니다. 인덱스를 삭제하거나, 인덱스 정의를 변경하거나, 아니면 스키마 및 애플리케이션 로직을 수정해 해당 인덱스가 불필요해지도록 개선하는 방법도 있습니다.

인덱스 관리, 함께 해결하세요

ObjectRocket은 MongoDB 인스턴스의 인덱스 정리를 도와드립니다. support@objectrocket.com으로 티켓을 생성해 주시면 인덱스 개선 작업을 함께 진행하겠습니다!