boogie의 가벼운 개발 일기

[mongoDB] AggregationPipeline에 $project 사용 시 주의사항 본문

MongoDB

[mongoDB] AggregationPipeline에 $project 사용 시 주의사항

boogie 2021. 6. 30. 23:32

현재 재직중이 회사에서 검색 색인 파일을 추출하기 위해 몽고DB의 aggregationPipeline을 사용중이다.
커머스 중에서도 굉장히 복잡한 데이터 스키마를 가진 편이기에, 파이프라인이 복잡한것은 물론이고
lookup하는 컬렉션은 수십개가 되고, 그중 몇몇 컬렉션은 단일 컬렉션의 도큐먼트만 수십억건이 있다.

그렇게 AggregationPipeline을 사용하던 중 의문이 생겼는데, 상식적으로 RDS에서던 NoSQL에서던,
추출되는 결과 컬럼의 갯수를 줄여 꼭 필요한 컬럼만을 추출하는 것이 (SELECT * 을 사용하지 않고 SELECT절에 꼭 필요한 컬럼명만 지정) 성능이 좋다는 것은 튜닝의 기본중의 기본인데
이상하게 문제가 된 대형 Aggregation Query에서는 각 Stage에 $project 연산자를 통해 결과 컬럼을 제한하면 오히려 성능이 떨어지는 것이다!

굉장히 중요한 모듈이고 까딱 성능이 저하되거나 정합성에 문제가 되면 매출에 즉각적인 영향이 가는데다가 복구하는데도 시간이 상당히 소요되기 때문에 섣불리 건드리지 못하고
필요이상의 필드들을 fetch해 오는것을 늘 찜찜해하다가, 최근 해당 모듈에 주요 로직이 몇가지 추가되면서 성능 튜닝을 진행하던중 해답을 얻었다.

 

주요 참고 자료 : https://dba.stackexchange.com/questions/198444/how-mongodb-projection-affects-performance

 

How mongoDB projection affects performance?

From MongoDB documentation it is mentioned that: When you need only a subset of fields from documents, you can achieve better performance by returning only the fields you need How filtering f...

dba.stackexchange.com

 

결론은..
projection 연산을 할 때 mongoDB는 해당 document가 메모리에 full로 fetch되어 있지 않다면 메모리에 모두 올린다.
그리고 메모리상에서 projection에 지정된 필드들을 뽑아내는 연산을 한다.
단, projection 지정된 필드와 match조건들을 모두 커버하는 커버 인덱스가 있다면, full document 에 접근하지 않고 index만으로 결과를 필터링하여 반환하게 된다
즉, projection을 지정하는것은 네트워크 대역폭을 절약하게 해 주지만 (결과 데이터의 크기가 감소), 경우에 따라 더 많은 메모리와 CPU자원을 사용한다.
--> 사실 여기까지는 기존에 알고 있던 내용.

그러나 중요한 것은 이제부터. 몽고의 Aggregation Pipeline은 RDS에서 join 연산하는것과 달리 집계를 위한 '파이프라인' 이므로 동작하는 방식이 다르다.
하나의 스테이지에서 필터링/가공하여 얻어진 데이터들이 다음 스테이지로 넘어가고, 그 데이터를 input으로 하여 다음 스테이지가 동작하고, 그 결과는 또 다음 스테이지로 넘겨지고...

그러다보니 자연스럽게 Aggregation 쿼리 작성시 각 컬렉션을 조회하는 스테이지 (lookup 스테이지) 에서 $projection을 하기 마련인데, 그 방식이 굉장히 비효율적으로 돌아가고 있었던 것.
특정 데이터를 lookup(index를 통해 document의 물리주소 get) --> projection에서 도큐먼트를 물리데이터에 접근하여 메모리에 올리고 연산수행 --> 다시 다음 스테이지 --> 또 메모리에 올리고... x 반복 반복

결국 더 여러차례 보조 메모리(hdd, ssd)에 접근하게 되고 IOPS, 메모리/CPU 자원사용 및 연산이 증가하였고, 메모리를 거의 풀로 사용하게 되면서 극단적으로 느려졌던 것.

그래서 바로 간단한 projectionBuilder를 만들어서 쿼리의 마지막에 한번에 lookup한 모든 collection.element들에 대해 projection을 지정하도록 변경해 보았다.
일단 눈으로 보기엔 훨씬 빠르다.
오늘은 늦었으니 내일 제대로 테스트 해 봐야겠다.