오늘은 검색 기능 고도화 과정을 정리하고, 성능이 얼마나 개선되었는지 테스트해보았다!
의도된 내용은 아니었지만, 이번 서비스에서 검색 엔진이 순차적으로 업그레이드 되었다.
처음에는 DB에서 직접 조회를 하고, 공간 인덱스를 생성해보고, Redis Geospatial을 도입해보고, 마지막에는 Elasticsearch를 사용했다.
이렇게 순차적으로 검색 기능을 고도화시켰는데, 이 과정에 대해 정리해보고, 성능이 얼마나 개선되었는지를 테스트해보았다.
우선 테스트를 위해 더미데이터를 만들었다.
사실 어느 정도의 데이터 크기가 적절한지 몰라서 우선 10만 건을 만들었지만, 나중에 와서 생각해보니 데이터의 크기가 너무 크다는 생각이 들었다.
다음에 테스트를 진행할 때에는 꼭 서비스에 적절한 데이터의 크기가 어느 정도일지 잘 생각해보고 테스트를 진행해야겠다.
저번에 DB 조회와 Redis GEO를 통한 조회의 성능 테스트를 진행할 때는 1만건의 데이터를 가지고, 모두 OPEN 상태로 세팅해두고 진행했다.
그런데, 이번에는 모임 상태도 OPEN 70%, CLOSED 20%, COMPLETED 10%로 세팅하였고, 수도권에 70%가 밀집되도록, 그 외 30%는 외곽에 분산되도록 세팅해두었다.
키워드 검색도 테스트하기 위해 10%에 매칭되는 상위 키워드, 1~5%에 매칭되는 중간 키워드, 0.1%에 매칭되는 희귀 키워드로 나누었다.
테스트를 진행할 때에는 조건을 하나씩 추가해가며 단계별로 테스트를 진행했는데,
기본적으로 모임의 상태 여부와 OPEN 상태에 대해서만 조회하고 있고, C0에서는 위치 필터링, C1에서는 위치와 카테고리 필터링, C2에서는 필터링과 함께 키워드 검색까지 추가하였다.
요청 시나리오는 Hot, Warm, Cold로 나누었는데,
Hot 요청은 인기 지역 집중 트래픽 상황을 가정하고, 전체 요청의 35~40%를 할당하였다.
Warm 요청은 주변 탐색 및 스크롤 이동 상황을 가정하고, 전체 요청의 40%를 할당하였다.
그리고 마지막으로 Cold 요청은 신규 지역 탐색 상황을 가정하고, 전체 요청의 20%를 할당하였다.
그리고 30초 간 20명, 1분 간 50명, 1분 간 100명의 가상 유저가 동시에 요청을 보내도록 하고,
요청 사이에는 0.1초의 지연 시간을 설정하였다.
이렇게 테스트 환경을 세팅하고, 순수 DB 조회, 공간 인덱스를 활용한 조회, Redis GEO로 1차 필터링을 한 후 조회, Elasticsearch를 통한 조회로 4가지 상황을 나누고, C0, C1, C2의 단계별 테스트를 진행했다.
① 목록 조회 API 성능 지표 비교
| 테스트 단계 | 사용 방법 | avg | p95 | req/s |
|---|---|---|---|---|
| C0 | DB | 6.38s | 13.16s | 7.54 |
| 공간 인덱스 | 6.68s | 13.21s | 7.25 | |
| Redis GEO | 3.24s | 8.09s | 14.22 | |
| ES | 11.77ms | 29ms | 396.75 | |
| C1 | DB | 955ms | 2.01s | 44.18 |
| 공간 인덱스 | 967ms | 2.03s | 43.04 | |
| Redis GEO | 2.21s | 5.74s | 20.43 | |
| ES | 14.32ms | 39ms | 332.53 | |
| C2 | DB | 501ms | 1.08s | 77.29 |
| 공간 인덱스 | 492ms | 1.05s | 76.95 | |
| Redis GEO | 2.09s | 5.58s | 21.55 | |
| ES | 9.3ms | 20ms | 369.88 |
② 지도 조회 API 성능 지표 비교
| 테스트 단계 | 사용 방법 | avg | p95 | req/s |
|---|---|---|---|---|
| C0 | DB | 4.32s | 8.33s | 10.79 |
| 공간 인덱스 | 3.76s | 7.48s | 12.34 | |
| Redis + DB | 469ms | 1.3s | 81.25 | |
| ES | 16.45ms | 58ms | 338 | |
| C1 | DB | 557ms | 1.15s | 70.68 |
| 공간 인덱스 | 555ms | 1.15s | 70.93 | |
| Redis + DB | 342ms | 872ms | 104.53 | |
| ES | 12ms | 29ms | 399 | |
| C2 | DB | 338ms | 816ms | 105.75 |
| 공간 인덱스 | 321ms | 665ms | 106.98 | |
| Redis + DB | 337ms | 848ms | 105.98 | |
| ES | 10ms | 25ms | 410 |
전체적인 테스트 결과는 위 표를 통해 확인할 수 있는데, 테스트 결과가 꽤나 놀라웠다.
우선 DB 조회 시 결과를 보면,
나는 당연히 C0 → C1 → C2로 갈수록 처리 시간이 느려질 것이라고만 생각했다.
하지만, 테스트를 진행해보니 오히려 C1, C2의 결과가 C0보다 훨씬 더 빨랐다.
내가 판단했을 때는 DB 조회 시에 병목이 조회 자체에 있기보다는 집계와 order 처리 등에 있었기 때문인 것 같다.
C0에서는 위치에 대해서만 조회하기 때문에, Hot 요청과 Warm 요청에 대해서 수만건의 데이터가 조회되었을 것이고, 이로 인해 DB에서는 수만건의 데이터를 상대로 집계와 정렬 등의 처리를 해야만 했다.
그런데, C1과 C2에서는 추가 조건으로 조회 결과가 줄었고, 이로 인해 훨씬 적은 양의 데이터만을 상대로 처리할 수 있었다.
또, Redis 조회 결과도 상당히 충격적이었는데,
지도 조회 결과에서는 Redis가 DB보다 빠르고, 어떤 조건에서든 안정적인 모습을 보였지만,
목록 조회 시, C1과 C2에서는 오히려 Redis가 DB보다 훨씬 느린 모습을 보였다.
이 결과가 너무 이상해서 이런 저런 가설을 세워보았는데, 그 중 Redis 필터링의 반환 결과가 너무 커서 DB에서 in절을 처리하는 데에 너무 오랜 시간이 걸렸다는 이유가 가장 합리적이었다.
그래서 Redis 반환 결과에 제한을 걸어서 다시 테스트를 해보았는데, Redis 필터링 결과를 1,000개로 제한했을 때는 Redis가 DB보다 훨씬 더 빠른 결과를 보였다.
테스트 시, 10만 건의 데이터 중 70%를 수도권에 밀집시켜두었는데, 수도권의 데이터를 필터링 할 때는 그 결과가 수만건 이었을 것으로 예상된다.
Redis를 도입하면서 Redis 반환 결과에 제한을 둘까 고민해보았지만, Redis 반환은 거리가 가까운 순서로만 정렬이 가능하다.
하지만, 우리 서비스에서는 최신순, 인기순, 마감 임박순 등의 추가 정렬 조건을 제공하고 있다.
그래서 Redis에서부터 제한을 걸게 되면, 이후 다른 정렬에서 정확한 값을 반환하지 못하게 될 것이라는 판단을 했었다.
Elasticsearch를 도입할 때, 사실 ES GEO의 성능이 Redis보다는 훨씬 좋지 않을 것이라 예상했었다.
그런데 ES 위치 정보 처리도 생각보다 너무 빨라 굉장히 놀랐었다.
Redis의 반환 개수를 제한해서 C0에 대한 속도를 ES와 비교해보았으면 좋았겠지만, 시간이 많지 않아 이렇게까지는 테스트를 진행해보지 못했다.
그래도 ES와 Redis의 GEO 처리 성능 차이가 그리 심하지 않았을 것이라 예상되고,
그렇다면 다양한 검색 조건을 한 번에 처리하고, 검색 부하를 DB에서 완전히 분리할 수 있는 ES를 사용하는 것이 훨씬 합당하다고 판단했다.
물론 ES를 도입하게 되면서 운영 복잡도가 훨씬 올라가게 되었고, ES를 사용하는 것이 처음이기 때문에, 학습에도 많은 시간을 투자해야했다.
하지만, ES의 성능이 너무 너무 좋았고, ES를 사용하며 훨씬 간단하게 조건을 처리할 수 있었기 때문에 ES를 도입하기로 결정하였다.
테스트에 대한 자세한 내용은 노션을 통해 작성해두었다.
Notion 자료 보러가기
위에는 엄청 거창하게 ES 도입 이유를 작성했지만, 사실 ES가 써보고 싶었기 때문에 ES를 도입하였다ㅎㅎ
발표 준비를 위해 위의 내용처럼 테스트를 진행해봤는데, 테스트를 진행하고, 증명해가는 과정들도 너무 재미있었다.
그치만 다음부터는 이런걸 먼저 준비한 다음에 기술 도입을 시작해야겠다..