색인 후 검색 응답이 늦어지는 이슈 해결 과정

May Han·2022년 3월 24일
3

Elasticsearch

목록 보기
4/4

쇼핑몰에서 자동완성이란 빼놓을 수 없지요!
정성스레 만들어 사용중인 자동완성API가 주기적으로 응답 시간이 지연되는 현상이 발생했습니다.. 😥

보통은 10-30ms 응답 속도를 유지했었는데 일정 주기로 약 30초간은 응답 속도가 5초~15초까지 올라가버렸어요.

이슈 해결을 위한 과정을 적어보려고 합니다.


현재 수집 로직

자동완성 같은 경우에는 1시간 주기로 자동완성 데이터를 수집, 정적 색인을(static index) 하고 있습니다.
timestamp을 포함한 인덱스명(my_index_202203241550)으로 새로운 인덱스를 생성하고 정적 색인이 완료되면 alias를 변경하고 기존 인덱스를 close하는 방식으로 수집하고 있습니다.


이슈 파악

1시간 간격으로 자동완성 응답시간이 5초 이상 지연되는 현상이 발생합니다.
확인 결과, 자동완성 정적 색인 완료 후 alias가 새로운 인덱스로 교체 직후 약 30초간 응답이 지연되는 현상이 발생하는 것을 파악하였다.
즉, 교체되는 새로운 인덱스에 원인이 있다는 것으로 보여졌습니다.


원인 파악

루씬의 색인

Elasticsearch의 Lucene 기반으로 만들어졌으며 엘라스틱 서치의 샤드는 루씬 인덱스의 확장이고 세그먼트(segment) 기반 내부 동작을 그대로 검색에 활용하고 있습니다.

원인 설명에 앞서 루씬의 색인 흐름에 대해 먼저 간단히 알아보겠습니다.

루씬에 색인 작업이 요청되면 전달된 데이터는 인메모리 버퍼에 순서대로 쌓이고, 정책에 따라 내부 버퍼에 일정 크기 이상의 데이터가 쌓이거나 일정 시간이 흐를 경우 버퍼에 쌓은 데이터를 한꺼번에 모아 처리합니다.

엘라스틱서치는 위와 같은 루씬의 작업(Flush, Commit, Merge)에 대해 확장하여 API로 제공있고 설정을 통해 튜닝도 가능합니다.


Refresh API
엘라스틱서치는 대용량의 데이터를 색인하고 실시간 검색에 가깝게 동작하기 위해 주기적으로 인메모리 버퍼에 대해 루씬의 Flush 작업을 수행합니다. 이러한 루씬의 Flush 작업을 엘라스틱서치에서 Refresh라고 부르며 세그먼트가 생성되면 커널 시스템 캐시에 세그먼트가 캐싱되어 읽기가 가능해집니다.

즉, 데이터를 검색 가능하게 만들어줍니다.
단, 이 시점까지는 디스크에 영속적으로 저장된 것은 아닙니다.

클러스터에 존재하는 모든 샤드에서 기본적으로 1초(default)마다 Refresh 작업이 수행하고 있습니다.
index.refresh_interval 설정을 통해 refresh주기를 변경할 수도 있으며 refresh API를 통해 수동으로 실행할 수 있습니다.

원인 추측

자동완성는 요구사항에 따라 데이터 처리 및 가공하는 작업을 여러번 거쳐서 색인이 됩니다. 이 과정에서 update도 많이하게 되는데 이로 인해 세그먼트가 많아지고, 검색 성능에도 문제가 생기는 것이 아닐까 생각했습니다.


시도

1. refresh를 주기 위해 alias 교체 시간 늦추기

우선 refresh 작업이 필요할 것이라고 생각을 했고, 공식 문서에는 refresh를 직접적으로 호출하는 것보다 indexAPI의 refresh=wait_for 옵션을 사용하는 것을 권장하고 있습니다.

하지만 저희는 정적 색인으로 bulk를 여러번 하고 있어서 옵션을 사용하기 보다는 단순하게 index.refresh_interval을 기다릴 수 있도록 alias 교체 전에 30초 delay를 주었습니다.


결과는 실패였습니다. 지연되는 현상이 동일하게 발생했습니다. refresh문제가 아니면 도대체 무엇일까 고민하다가 다음 방법을 생각하게 됩니다.

2. forcemerge로 segments 강제 병합하기

segment 개수는 검색 성능에 영향을 주는 요소 입니다.
이번에는 _forcemerge?max_num_segments=1 옵션을 주어 강제 병합하는 로직을 alias 교체 전 추가해주었습니다.

그 결과, 응답 지연 시간이 800ms까지 줄었으나 역시 평균(10-30ms)보다는 많은 시간이 걸립니다. 😖

무엇이 문제일까.. 열심히 서칭을 시작했습니다.


3. 유레카! refresh API를 호출하자

열심히 서칭하다 보니 엘라스틱서치 개발자가 달아놓은 코멘트에 아주 중요한 정보를 찾게 되었습니다!!!! 유레카!

In 7.x series we changed to way we are running refresh. If there is no search activity, then the refresh is not called IIRC.
If you have a search request coming then the refresh is triggered before the search is actually ran.

엘라스틱서치 7버전 이후에는, 검색 요청이 없는 경우 IIRC는 refresh를 호출하지 않는다고 합니다.
그리고 검색 요청이 들어왔을 때 검색 실행 이전에 refresh를 호출한다고 하네요!!

그러니까, 우리가 새로 만들든 자동완성 인덱스는 search request가 한번도 없었기 때문에 refresh가 되지 않았고 위에 1번 시도에도 index.refresh_interval을 기다리기 위해 delay를 주었더라도 사실 refresh가 된 적이 없던거죠 😛



이슈 해결

여러 시도 끝에 정적색인 이후 alias 교체 전에 refresh API를 client에서 직접 호출해주도록 하였습니다.
반영 이후로 부터는 자동완성 응답 지연 현상은 발생하지 않고 평균 응답 속도(10-30ms)를 유지하고 있답니다.


읽어주셔서 감사합니다!




Reference

profile
🚢 크루즈승무원 출신 백엔드 개발자, 기록하는 것을 좋아합니다.

2개의 댓글

comment-user-thumbnail
2022년 12월 12일

잘 읽고 갑니다.

답글 달기
comment-user-thumbnail
2023년 1월 14일

잘 보고 갑니다 :)

답글 달기