[요약] 2019년 네이버 검색 서비스에 사용된 MongoDB 시행착오 정리

 
 
"A Raphael painting of Buzz and Woody salute each other with majestic expressions in a grocery store." from DALL-E 2
 
[MongoDB 인덱스]
  • 컬렉션 당 최대 64개의 인덱스만 생성 가능
  • 너무 많은 인덱스를 추가하면 오히려 부작용 발생
  • 모든 인덱스를 메모리에 보관하기 때문에 인덱스가 많아지면 Frequent Swap 발생
  • 그러면 Write Performance 감소함
  • 인덱스가 너무 많으면 쿼리 플래너가 잘못된 인덱스를 선택할 가능성도 높아짐
 
[인덱스 prefix]
  • 여러 개의 항목에 대해 인덱스를 설정할 수 있음
  • 한 인덱스에 a, b, c 항목을 순서대로 정의하고 나중에 쿼리를 보낼 때, (a), (a,b), (a,b,c)를 조건으로 전달할 수 있음
  • 그러나, (b), (c), (b,c), (a,c)는 조건으로 전달해도 인덱스가 사용되지 않음
    • 단, (a,c)를 전달하면 a에대한 인덱스만 사용됨
 
[멀티소팅]
  • 정렬 키들은 반드시 인덱스와 같은 순서로 나열되어야만 한다.
  • 싱글 필드면 소팅 방향을 걱정할 필요 없음. 그러나, 복합 인덱스는 소팅 방향이 중요함
    • a:1, b:1로 복합 인덱스 설정시, (a:1, b:1), (a:-1, b:-1), (a:1), (a:-1)은 지원이 됨
 
[성능 높이기]
#컬렉션 나누기
  • 하나의 컬렉션을 여러 개의 컬렉션으로 나누기
  • 카디널리티를 높이기 위해 컬렉션을 나누는게 좋다. (예: 성별, 연령 등)
  • 카디널리티가 너무 낮은 항목에 대해 인덱스를 설정할 수 밖에 없는 상황이라면 별도의 컬렉션으로 빼는게 낫다.
#스레드를 이용해 대량의 다큐먼트 upsert
  • 한번 배치가 돌 때 upsert 치는 양이 8000개에서 만개정도 됨
  • 여러 개의 스레드에서 bulk operation으로 많은 다큐먼트를 한번에 write
#MongoDB 4.0
  • 이전에는 non-blocking secondary read 기능이 없었음
  • [기존] write가 primary에 반영되고 secondary들에 다 전달될 때 까지 secondary는 read를 block해서 데이터가 잘못된 순서로 read 되는 것을 방지함
  • [기존] 그래서 주기적으로 높은 global lock acquire count가 생기가 read 성능이 저하됨
  • [현재] data timestamp와 consistent snapshot을 이용해서 이 이슈를 해결함
    • non-blocking secondary read
 
[미운 인덱스]
  • 공연 컬렉션의 문서 수 : 9만건
 
"오늘 공연"
db.concert.count({serviceCode:{$in:[2,3]},startDate:{$lte:20190722},endDate:{$gte:20190722});
>>> 270ms
 
"서울 공연"
db.concert.count({serviceCode:{$in:[2,3]},region:"se",isNow:1});
>>> 290ms
 
#explain 결과
  • "오늘 공연"
    • {serviceCode:1,weight:1} 인덱스를 사용 (key examined: 88226) >> 거의 다 읽음
  • "서울 공연"
    • {region:1,weight:1} 인덱스를 사용 (key examined: 25322)
  • key examined의 값을 줄여야 함
 
#분석
  • "오늘 공연"
    • 오늘 공연의 결과 값은 123개였음. 123개를 위해 8만개의 문서를 탐색함
    • startDate, endDate부터 실행하고 serviceCode부터 뒤지면 123개만 탐색하면 가능함
    • 그러나 인덱스를 타지 않음
  • "서울 공연"
    • region이 "se"인 2만 5천개의 인덱스를 훑어보면서 메모리에서 isNow가 1이고 serviceCode가 2,3인 문서를 탐색함
    • isNow부터 검색하고 region, serviceCode를 검색하면 200여개만 탐색하면 됨
    • 그러나 인덱스를 타지 않음
 
#인덱스를 타지 않는 이유
  • 문제는 쿼리 플래너에 있었음
  • 이전에 실행한 쿼리 플랜을 캐싱해 놓음
  • 이전에 실행된 쿼리 플랜을 캐싱 해놓음. 캐싱된 쿼리 플랜이 없다면 가능한 모든 쿼리 플랜들을 조회해서 첫 batch (101개)를 가장 좋은 성능으로 가져오는 플랜을 다시 캐싱함
  • 만약 동점이 나올 경우, in-memory sort를 하지 않아도 되는 쿼리 플랜을 선택함
    • 몽고DB는 32MB가 넘는 결과 값들에 대해서는 in-memory sort가 불가능
 
[쿼리 실행 방법]
  • Hint 이용
    • 확실한 쿼리플랜 보장
    • 더 효율적인 인덱스가 생겨도 강제 고정
    • 데이터 분포에 대한 정보를 계속 follow 해야 함
    • 32MB가 넘는 응답결과를 sort해야할 경우 에러
  • 엄한 인덱스 지우기
    • 데이터에 따라 더 효율적인 인덱스가 생기면 자동 대응
    • 또 다른 엄한 케이스가 생길 수 있음
    • 삭제로 인해 영향을 받는 다른 쿼리가 생길 수 있음