spring

Mongo DB 조회 성능 개선기

e4g3r 2024. 12. 27. 00:48

https://www.mongodb.com/ko-kr/docs/manual/tutorial/sort-results-with-indexes/

 

복잡한 Join 탈출 - 결제 내역 조회 With Mongo DB

진행중인 프로젝트에선 사용자들이 Question이라는 상품을 구매할 수 있습니다. Question은 고등학교 과정의 문제를 의미합니다.(수학, 물리, 생명, 지구과학 등)따라서 사용자는 자신의 구매내역을

e4g3r.tistory.com

위 글에서 결제 내역 조회 시 복잡한 join 과정을 개선하기 위해 조회용 모델을 설계하고 MongoDB에 저장하여 조회하는 방식으로
변경하였습니다.

단순히 조회하는 Mongo DB 방식이 8개의 join을 처리하는 Maria DB 방식보다 미세하게 우세할 줄 알았으나
성능 테스트 결과 Maria DB 방식이 더 우세하였습니다. 

이전 포스팅 TPS 측정 내용

 

기존에는 k6를 통해 가상 이용자가 100명일 경우에서 테스트를 했었기 때문에 TPS가 그리 크게 차이나지 않았었는데요.

k6를 이용하여 측정할 때에는 가상 이용자를 100명 이상으로 하면 잘 진행이 되지 않아 JMeter를 통해 다시 진행해보았습니다.

JMeter 설정


JMeter는 가상 이용자 1000명 설정이 가능하여 더 높은 부하를 줄 수 있었습니다. 1000명의 이용자가 각 100번의 요청을 보내는
상황으로 설정하였으며 서버는 10만개의 요청을 처리해야합니다.

Mongo DB 방식 TPS 결과

Mongo DB 방식의 TPS는 1937로 측정되었습니다.

 

Maria DB 방식 TPS 결과

Maria DB 방식의 TPS는 3181로 측정되었습니다.

 

더 높은 부하를 주는 성능 테스트에서는 Maria DB 방식의 TPS가 약 1000 이상 더 높은 것으로 측정되었습니다.

K6로 측정 했을 때는 Mongo DB 방식과 Maria DB 방식의 TPS 차이는 별로 차이가 나지 않았기 때문에 조회 방식이 단순 해진 점과
낮아진 TPS는 트레이드 오프 영역으로 생각하기로 했었습니다.

하지만 JMeter가 보여주는 수치는 트레이드 오프 범주를 넘어 선 것 같았기 때문에 새로운 방식을 생각해야 할 지 고민되었습니다.

Mongo DB 도입을 취소 한다하더라도 Mongo DB를 너무 얕게 알고 도입한 것일까, 놓친 것이 있는 것은 아닐까 해서 일단 원인 분석을
하였습니다.

Sort With Index


결제 내역 조회 query가 부하를 발생시키는지 확인해보기 위해 어떻게 query를 생성하고 처리하는지 확인해보았습니다.
먼저 결제 내역 조회는 특정 유저의 결제 내역을 최신 순으로 조회되어야 합니다.
코드 상으로는 userId를 통해 1차적으로 데이터를 필터링하고 결제 번호인 _id를 내림차순으로 정렬합니다.
위 코드는 어떻게 Mongo DB에서 Query로 처리되는지 로그를 통해 확인해보았습니다.

 

로그 확인 결과 filter, sort, skip, limit을 이용해서 query가 작성되었음을 확인했습니다.


explain을 통해 확인해보면 userId 필터링이 먼저 진행되는 것을 확인할 수 있고 이 때 index userId가 사용됨을 확인할 수 있었습니다.

 

다음으로는 SORT 과정이 진행되는데요. 코드로 작성했던대로 결제번호(_id)를 내림차순으로 정렬하는 단계를 진행합니다.

 

마지막으로 SKIP 과정을 통해 페이징 처리를 합니다. mysql, mariadb로 치면 offset과 limit를 처리하는 과정입니다.

 

여기까지 아무 문제가 없어보였지만 SORT 과정이 포함되어있다면 이것은 index가 제대로 활용되지 않았을수도 있다는 것을
알게 되었습니다.

 

https://www.mongodb.com/ko-kr/docs/manual/tutorial/sort-results-with-indexes/


왜 문제일까 생각해보면 index 생성를 생성했다는 것은 이미 데이터는 정렬이 되어있다는 겁니다.
따라서 index를 활용하여 데이터를 조회했는데 정렬 과정이 진행된다면 index를 제대로 활용하지 않았다는 것입니다.
SORT 과정은 데이터를 불러온 다음 추가로 진행되는 작업이기 때문에 여기서 병목 현상이 나타난게 아닐까 예상해보았습니다.

https://www.mongodb.com/ko-kr/docs/manual/tutorial/sort-results-with-indexes/

 

그리고 공식 문서를 다시 읽어보니 복합 인덱스 정렬을 알게 되었습니다. 간략히 요약하자면 query에서 여러 필드가 사용될 때 정렬을 하기 위해선 해당 필드들이 복합 인덱스로 지정되어야 한다는 뜻입니다.

 

 

query를 보면 userId를 이용해 1차적으로 데이터를 필터링하고 그 이후 _id를 기준으로 정렬합니다.
따라서 query에서 userId_id가 사용되었기 때문에 index를 통해 정렬을 수행시키려면 복합 인덱스를 생성해야 합니다.

 

복합 인덱스를 생성해주었습니다. (복합 인덱스 생성 시 순서가 중요하므로 자세한 내용은 공식문서를 확인해주세요.)

 

복합 인덱스 (userId + _id) 추가 후 query의 실행 계획을 보면 새로 추가한 복합 인덱스가 사용됨을 확인할 수 있었습니다.

 

추가로 SKIP 과정이 진행되기 전에 SORT 과정이 사라졌음을 확인할 수 있었습니다.

성능이 개선 되었길 바라며 부하 테스트를 통해 TPS를 측정 해보았습니다.


1937 -> 2580로 약 500정도 수치가 증가했음을 확인했습니다.

하지만 maria DB 방식에 비해서는 아직까지 처리 성능이 아쉬운 것 같습니다.

Custom Document Converter

도저히 납득이 되지 않는 결과지만 원인 조차 알 수 없었기 때문에 너무 답답하여 참여중인 오픈채팅방 커뮤니티에서 다른분들께 질문을
해보았습니다. 이 때 다른 분들도 단순히 조회 하는 Mongo DB의 속도가 이렇게 느릴수는 없다고 생각하셨고 Mongo DB의 문제가
맞는지 mongo DB문제가 아닐 수 있으니 프로파일링으로 병목 지점을 분석해보는 게 좋을 것 같다고 하셨습니다.

 

제일 오랫동안 처리되는 부분은 결제 내역을 조회하는 부분이 맞았습니다. 따라서 결제 내역 조회를 개선해야 한다는 의미입니다.

 

call tree를 보면 Mongo DB로 부터 불러온 데이터를 매핑하는 과정이 있었는데요.

 

Spring Data MongoDB Slow MongoTemplate.find() Performance

I'm having performance issues when querying ~12,000 user documents, indexed by 1 column, (companyId), no other filter. The whole collection only has ~27000. It took me about 12 seconds to get the ...

stackoverflow.com

stack over flow에서는 BSON -> DOCUMENT -> POJO(Entity)로 변환 하는 과정에 OverHead가 있다는 의견이 있었습니다.

 

Object Mapping :: Spring Data MongoDB

This section covers the fundamentals of Spring Data object mapping, object creation, field and property access, mutability and immutability. Note, that this section only applies to Spring Data modules that do not use the object mapping of the underlying da

docs.spring.io

공식문서에 따르면 Spring Data Mongo의 기본 converter는 내부적으로 Reflection 및 생성자를 이용하여 매핑을 진행합니다.
이 부분에서 타입 검증 및 여러 부가적인 로직이 오버헤드를 발생시키는 것으로 판단되었습니다.

 

Custom Conversions :: Spring Data MongoDB

Generally, we inspect the Converter implementations for the source and target types they convert from and to. Depending on whether one of those is a type the underlying data access API can handle natively, we register the converter instance as a reading or

docs.spring.io

Custom Converter를 통해 매핑을 진행할 수도 있기 때문에
공식문서에 나와있는 대로 Custom 
Conveter를 이용하여 매핑을 처리하도록 진행하였습니다.

import org.springframework.core.convert.converter.Converter;

 

구현하고 있는 Converter는 위와 같습니다.


그리고 공식 문서의 설명대로 converter를 등록해줍니다.


이 후 브레이크 포인트를 걸어 실제로 매핑 과정에 사용되는지 확인해보았고 잘 작동되는 것을 볼 수 있었습니다.

사실 매핑 과정이 querydsl을 이용하는 8단 join 방식과 다름이 없는 것 같지만 Mapper를 작성한 것이라고 생각해야겠습니다 ㅎㅎ..

그리고 위 Converter 코드가 구현 코드에 작성되는 것이 아니라 나름 괜찮은 것 같습니다.

 

 

tps 역시 4000에 가까운 속도를 보여주고 있었습니다. 이제는 maria DB만을 사용하여 처리할 때 보다 더 높은 TPS 수치를 보여줍니다.

정리

1. query에 이용되는 두개의 필드를 복합 인덱스로 설정하지 않아 Sort 과정이 별도로 진행이 되어 부하 / 병목 발생 -> 복합 인덱스 생성 

2. Spring Data Mongo이 기본으로 제공하는 Converter 매핑은 병목 현상이 발생할 수 있음 -> CustomConverter 등록