AWS 기술 블로그

Amazon OpenSearch 3.3 업그레이드로 미리캔버스의 검색 성능 개선

부제: derived source와 Lucene 10.3으로 stored fields 병목을 해소하고, 무중단·안전 롤백 전략으로 메이저 버전을 올린 이야기

본 게시글은 미리디의 김민석, 최시온, 이동진, 김백규님과 함께 작성하였습니다.

미리디의 미리캔버스 소개

미리캔버스는 “누구나 쉽게, 함께 만드는 디자인”을 지향하는 실시간 협업 디자인 플랫폼입니다. 프레젠테이션, SNS 카드뉴스, 유튜브 썸네일, 포스터까지 다양한 시각 콘텐츠를 브라우저에서 바로 만들 수 있습니다. 현재 90만 개가 넘는 템플릿과 4,000만 개에 달하는 요소(이미지, 일러스트 등)를 제공하는데, 이 방대한 리소스에서 원하는 디자인 요소를 얼마나 빠르고 정확하게 찾아 주느냐가 서비스 경험을 좌우합니다.

이 글은 미리디의 검색 인프라 최적화 시리즈의 두번째 글입니다. 첫번째 글에서는 듀얼 벡터 검색(시맨틱 + 비주얼)을 설계하고, OpenSearch 2.19 환경에서 쿼리 최적화와 메모리 최적화 인스턴스 전환(m7g → r7g)으로 Read IOPS 병목을 완화하고 검색 레이턴시를 개선한 과정을 다뤘습니다. 다만 매일 새벽 대량 업데이트 배치가 끝난 직후 발생하는 세그먼트 병합 폭발만큼은 2.19 환경에서 끝내 풀지 못한 숙제로 남아 있었습니다. 이번 글은 바로 그 숙제를 풀기 위해 선택한 OpenSearch 3.3 메이저 버전 업그레이드 이야기입니다. 전체 배경과 2.19 단계의 최적화가 궁금하시다면 첫번째 글을 먼저 읽어보시길 권합니다.

OpenSearch 3.x의 등장

그러던 중 OpenSearch 3.x 릴리스 노트에서 눈에 띄는 변경 사항 몇 가지를 발견했습니다.

눈여겨본 변경 사항:

  • derived source: 벡터 데이터를 _source(stored fields)에 중복 저장하지 않고, 필요할 때 벡터 인덱스에서 거꾸로 재구성하는 기능입니다. Amazon OpenSearch Service에서는 OpenSearch 3.1부터 쓸 수 있으며, _source를 저장하지 않으니 스토리지가 줄어듭니다. 미리디가 가장 크게 고민하던 stored fields 비대화 문제를 직접 해결해 줄 방법이었습니다.
  • Lucene 10.3 기반 세그먼트 병합 개선: 3.3은 Lucene 10.3을 쓰며, 세그먼트 병합 효율과 압축이 개선됐습니다. 대량 업데이트 배치 직후 세그먼트 병합이 폭증하던 바로 그 문제와 맞닿아 있는 변경이었습니다.
  • concurrent segment search: 하나의 샤드 안에서 여러 세그먼트를 병렬로 탐색하는 기능으로, 3.0부터 k-NN 검색에 기본 적용됩니다. 세그먼트가 많아도 레이턴시를 낮게 유지할 수 있습니다.
  • 양자화(Quantization) 옵션: float32 대신 더 작은 데이터 타입으로 벡터를 저장해 메모리를 아끼는 선택지가 넓어졌습니다. 미리디는 버전 업그레이드(이하 버전업) 이후 단계에서 FP16 적용을 검토하고 있습니다(글 후반에서 다룹니다).

검증

물론 릴리스 노트만 보고 곧바로 버전업을 결정하지는 않았습니다. 미리캔버스 워크로드는 벡터 필드가 여러 개이고, 문서가 4,000만 건에 달하며, 듀얼 벡터 쿼리를 쓰는 등 일반적인 벤치마크와는 성격이 달라서 직접 검증이 꼭 필요했습니다.

그래서 테스트 환경에 3.3 클러스터를 띄우고 프로덕션 데이터의 서브셋으로 검증했습니다.

concurrent segment search와 세그먼트 병합 정책 검증: 앞서 말했듯 3.0부터 k-NN에 concurrent segment search가 기본 켜지면서 여러 세그먼트를 병렬로 탐색합니다. 예전에는 _forcemerge로 세그먼트 수를 줄여야 성능이 유지됐지만, 이제는 세그먼트가 많아도 병렬 탐색으로 레이턴시를 낮출 수 있습니다.

여기에 세그먼트 병합 정책 기본값도 함께 바뀌었습니다. floor_segment가 2MB에서 16MB로 8배 커져 작은 세그먼트가 더 적극적으로 병합되고, maxMergeAtOnce도 10에서 30으로 늘면서, 별도 설정 없이 버전만 올려도 같은 데이터의 세그먼트 수가 약 37% 줄었습니다(240개 → 152개).

검증 결과, 세그먼트 수가 줄어든 효과뿐 아니라 세그먼트 수를 똑같이 맞춰 비교해도 레이턴시가 좋아지는 것을 확인했습니다. concurrent segment search의 병렬 탐색 효율과 Lucene 10 엔진 자체의 성능 개선이 함께 작용한 결과로 보입니다. 덕분에 대량 업데이트 배치 직후처럼 세그먼트가 일시적으로 폭증하는 상황에서도 검색 성능이 크게 무너지지 않으리라는 기대를 가질 수 있었습니다.

지표 2.19 (실측) 3.3 (실측) 차이
CPU ~20% ~12% -40%
Search Latency ~12.5 ms ~5 ms -60%
요청량 ~9k ~9k 동일
세그먼트 수 240개 152개 -37%

[Table 1] OpenSearch 2.19와 3.3 사전 검증 비교 — 동일 요청량(~9k) 기준으로 CPU -40%, 검색 레이턴시 -60%, 세그먼트 수 -37% 개선 (2.19·3.3 모두 실측값 기준). 세그먼트 수 감소 역시 3.3 버전업으로 얻은 효과이므로, 정규화하지 않은 실측값으로 비교했습니다.

이 사전 검증 결과가 버전업을 결정한 가장 큰 근거가 되었습니다.

왜 3.0이 아니라 3.3이었나: 사실 3.0이 처음 나왔을 때 바로 뛰어들지는 않았습니다. 메이저 버전의 첫 릴리스에는 안정성 리스크가 따르고, 실제로 3.0에서 3.3에 이르는 마이너 릴리스마다 의미 있는 개선이 쌓였기 때문입니다.

  • Lucene 10.3의 세그먼트 병합 개선: 3.0은 Lucene 10.1 기반이지만 3.3은 Lucene 10.3으로 올라가면서 세그먼트 병합 효율과 압축이 좋아졌습니다. 대량 업데이트 배치 후 세그먼트 병합 폭발로 IOPS가 치솟던 게 가장 큰 페인 포인트였던 만큼, 이 개선이 들어간 3.3이 필요했습니다.
  • derived source 적용: 3.1부터 제공되는 derived source를 적용하면 _source(stored fields)에서 벡터 데이터를 덜어낼 수 있습니다. 공식 문서 기준 스토리지를 최대 50%까지 줄일 수 있으니, 앞서 짚은 stored fields 비대화 문제를 직접 해결하는 방법이었습니다.
  • 세 번의 마이너 릴리스를 거친 안정성: 3.0은 메이저 첫 릴리스라 안정성 리스크가 있었고, 3.1부터 3.3까지 k-NN 플러그인 버그 수정, derived source 관련 수정, 검색 안정성 개선 같은 패치가 여럿 반영됐습니다. 프로덕션에서 4,000만 건의 벡터 검색을 돌리는 입장에서는 충분히 검증된 버전이 필요했습니다.

버전업 전략

메이저 버전을 어떻게 올릴지는 버전업 자체만큼이나 중요한 결정이었습니다.

버전업 방식을 고를 때 세운 세 가지 기준:

  • 첫째, 서비스 무중단: 검색은 미리캔버스의 핵심 기능이라, 버전업 도중 검색이 멈추거나 품질이 떨어지는 상황은 절대 받아들일 수 없었습니다.
  • 둘째, 안전한 롤백: 메이저 버전 변경에는 예측하기 힘든 문제가 따릅니다. 문제가 생겼을 때 즉시 이전 상태로 되돌릴 수 있는 안전망이 필요했습니다.
  • 셋째, 충분한 검증 구간: k-NN 벡터 설정 변경(ef_search 기본값 등), 형태소 분석기 교체(Seunjeon → 대체 분석기), 쿼리 호환성 등 3.x에서 달라지는 부분이 많아, 프로덕션에 영향을 주지 않고 충분히 테스트할 환경이 필요했습니다.

Rolling Upgrade를 택하지 않은 이유: 가장 먼저 검토한 건 기존 클러스터의 노드를 하나씩 올리는 Rolling Upgrade였습니다. 가장 간단한 방식이지만, 위 세 기준 어디에도 맞지 않았습니다.

Rolling Upgrade는 프로덕션 트래픽이 흐르는 상태에서 진행해야 하니 서비스 영향을 완전히 피할 수 없고, 업그레이드 도중 일부 노드는 2.x, 일부는 3.x인 혼합 상태가 되면 샤드 버전 불일치 문제가 생길 수 있습니다. 무엇보다 문제가 발생했을 때 롤백이 매우 어렵습니다.

실제로 테스트 환경에서 Rolling Upgrade를 돌려봤을 때 이 리스크가 그대로 현실이 됐습니다. sunjeon 분석기를 쓰는 인덱스가 있었는데, sunjeon 관련 패치가 제대로 적용되지 않은 문제가 드러났습니다. 그런데 이 시점에 이미 클러스터의 모든 노드가 red 상태로 빠져 아무것도 할 수 없었고, 인덱스를 복구하는 데도 큰 어려움을 겪었습니다. 이 경험이 Rolling Upgrade를 프로덕션에서 배제한 결정적인 이유가 됐습니다.

신규 클러스터 + 도메인 스위칭: 결국 완전히 새로운 3.3 클러스터를 띄우고, 기존 클러스터의 데이터를 전부 옮긴 뒤, 준비가 끝나면 트래픽을 한 번에 전환하는 방식을 택했습니다. 이 방식은 앞서 세운 세 기준을 모두 만족합니다.

여기에 더해, 신규 클러스터에서 인덱스를 처음부터 다시 구성하면서 두 가지 부수적인 이점도 얻을 수 있었습니다.

  • 인덱스 설정 재정비: 기존 클러스터의 인덱스들은 오랜 기간에 걸쳐 추가되다 보니 샤드 수, 레플리카 설정, 벡터 엔진 파라미터가 초기 설정 그대로 남아 있는 경우가 있었습니다. 신규 클러스터에 처음부터 구성하면서 이런 기술 부채를 한꺼번에 정리할 수 있었습니다.
  • 벡터 인덱스 재빌드: 벡터 인덱스(HNSW 등)는 색인하는 시점에 그래프 구조가 만들어집니다. 신규 클러스터에서 다시 색인하면 3.3의 개선된 엔진으로 그래프가 새로 빌드되니, 벡터 검색 성능 개선 효과를 온전히 누릴 수 있습니다.

마이그레이션 시나리오: 실시간 인덱싱 양이 많은 서비스라, 마이그레이션이 길어질수록 기존 클러스터(A)와 신규 클러스터(B) 사이의 데이터 정합성을 맞추기가 까다로워집니다. 그래서 하루 안에 전체 작업을 끝내는 것을 목표로 시나리오를 짰습니다.

사전 준비
  1. 신규 클러스터(B) 생성
  2. 기존 클러스터(A)의 스냅샷 생성
  3. 스냅샷으로 B에 training data index 복원
  4. k-NN model 생성

마이그레이션 당일
  1. A로의 write 배치 중단
  2. element / template index 복원 (스냅샷 기반)
  3. 1차 쿼리 테스트 (기본 동작 검증)
  4. B 클러스터 버전 업그레이드
  5. element / template index 재인덱싱
  6. refresh / replica 설정 변경 + alias 변경
  7. 2차 쿼리 테스트 (최종 검증)
  8. custom domain endpoint A → B 스위칭

사전 준비: IVF/PQ 기반 벡터 인덱스는 training data로 k-NN model을 먼저 만들어야 색인할 수 있습니다. 이 model 생성에는 시간이 꽤 걸려서, 마이그레이션 당일이 아니라 미리 끝내 뒀습니다. 스냅샷으로 training data index를 B 클러스터에 복원하고 k-NN model을 만드는 데까지가 사전 준비 단계입니다.

마이그레이션 당일: 당일 작업의 핵심은 “A 클러스터의 write를 멈춘 시점부터 B 클러스터로 트래픽을 넘기기까지의 시간을 최대한 짧게 가져가는 것”이었습니다.

먼저 A로 들어가던 write 배치를 멈추고 스냅샷 기반으로 인덱스를 B에 복원합니다. 이때 1차 쿼리 테스트로 기본 동작을 확인한 뒤, B 클러스터를 3.3으로 올립니다. 업그레이드를 마치면 3.3 엔진의 효과를 온전히 살리려고 재인덱싱을 수행하고, refresh interval과 replica 수를 운영 설정으로 되돌린 다음 alias를 바꿉니다. 2차 쿼리 테스트로 최종 검증까지 마친 뒤, custom domain의 endpoint를 A에서 B로 넘기면 마이그레이션이 끝납니다.

롤백 시나리오: 만일에 대비한 롤백 계획도 미리 세워 뒀습니다. A 클러스터는 스위칭 이후에도 한동안 그대로 유지하기 때문에, 문제가 발견되면 custom domain endpoint를 다시 A로 되돌리는 것만으로 곧바로 복구할 수 있습니다.

버전업 이후, 달라진 것들

메모리 사용량과 IOPS Throttling

가장 크게 체감한 변화는 메모리 상황이 한결 나아졌다는 점입니다. 3.3에서 derived source가 적용되면서, 벡터 데이터가 더 이상 _source(stored fields)에 저장되지 않고 필요할 때 벡터 인덱스에서 거꾸로 꺼내 쓰는 방식으로 바뀌었습니다. 앞서 진단했듯 IOPS 문제의 근본 원인이 _source에 실린 벡터 데이터의 비대화였는데, derived source가 이를 직접 해결한 것입니다.

stored fields 크기가 크게 줄면서 OS 페이지 캐시에 여유가 생겼고, 그동안 문제였던 페이지 캐시 스래싱도 크게 줄었습니다. 자연히 디스크 I/O가 줄었고, 버전업 전 가장 큰 문제였던 IOPS Throttling은 완전히 사라졌습니다. 메모리 부족 → 페이지 캐시 미스 → 디스크 I/O 폭증 → IOPS Throttling으로 이어지던 악순환이 뿌리째 끊긴 것입니다.

Throttling까지 가지 않더라도 순간적으로 IOPS가 튀는 microbursting 수치도 눈에 띄게 낮아졌습니다. 예전에는 특정 쿼리 패턴이나 배치 작업에서 IOPS가 순간적으로 치솟으며 Throttling 경계선에 근접하곤 했는데, 버전업 후에는 MAX 값이 5,250에서 3,500으로 떨어지면서 I/O 스파이크가 안전한 수준으로 안정화되었습니다.

지표 Before After 변화
Read IOPS Microbursting (AVG) 4,550 3,190 -30%
Read IOPS Microbursting (MIN) 3,550 2,270 -36%
Read IOPS Microbursting (MAX) 5,250 3,500 -33%

[Table 2] 3.3 버전업 전후 Read IOPS Microbursting 비교 — derived source 적용으로 stored fields가 줄면서 순간적인 IOPS 급등(MAX) -33%, Throttling 경계선과의 여유 확보

CPU 안정화

버전업 전에는 CPU 사용률이 간헐적으로 최대 80%까지 튀는 일이 모든 시간대에서 일어났는데, 3.3 버전업 이후 이 스파이크가 크게 가라앉았습니다.

CPU 스파이크의 주범도 결국 페이지 캐시 스래싱이었습니다. 페이지 캐시가 부족한 상태에서 여러 검색 쿼리가 동시에 캐시 미스를 내면, OS가 디스크 read를 한꺼번에 처리해야 합니다. 이 과정에서 I/O 인터럽트 처리와 컨텍스트 스위칭이 폭증하며 순간적으로 CPU가 치솟았고, 그래서 시간대를 가리지 않고 스파이크가 나타났습니다. 새벽 배치 시간대에는 여기에 세그먼트 병합까지 겹쳐 스파이크가 더 심해지곤 했습니다.

3.3 버전업 후 스토리지가 최적화되고 세그먼트 수가 줄면서 페이지 캐시 스래싱이 풀렸고, 스레드의 실행 패턴이 예측 가능해지면서 CPU 사용도 고르게 분산돼 스파이크가 안정됐습니다.

검색 레이턴시

사전 검증에서 확인했던 레이턴시 개선이 프로덕션에서도 그대로 재현됐습니다.

지표 Before After 변화
요청량 (AVG) 30.6 /s 33.1 /s +8% (유사)
Latency (AVG) 51.9 ms 21.4 ms -59%
Latency (MAX) 175 ms 26.7 ms -85%

[Table 3] 3.3 버전업 전후 프로덕션 검색 레이턴시 비교 — 유사한 요청량 수준에서 평균 레이턴시 -59%, 최대 레이턴시 -85% 개선되어 tail latency가 거의 사라짐

요청량은 거의 비슷한 수준인데, 평균 레이턴시는 59%, 최대 레이턴시는 85% 줄었습니다. 특히 MAX가 175ms에서 26.7ms로 극적으로 떨어진 건, 그동안 I/O 스파이크 탓에 생기던 tail latency가 거의 사라졌다는 뜻입니다.

레이턴시 개선의 가장 직접적인 원인은 세그먼트 수 감소로 봅니다. 같은 데이터인데 세그먼트가 240개에서 152개로 37% 줄면서, 쿼리마다 뒤져야 할 세그먼트 수와 결과를 합치는 오버헤드가 함께 줄었습니다. 게다가 세그먼트가 크고 적으면 각 세그먼트의 k-NN 그래프(HNSW/IVF)에 더 많은 데이터 포인트가 담겨 그래프 품질이 좋아지고, 적게 탐색해도 좋은 결과를 찾을 수 있습니다.

여기에 페이지 캐시 히트율 향상(세그먼트가 크고 적으면 캐시 재사용률이 올라갑니다), Lucene 10.3의 쿼리 처리 최적화, 백그라운드 병합 활동이 줄면서 I/O와 CPU 경쟁이 완화된 점까지 함께 맞물린 결과로 보입니다.

3.3 버전업 이후, 추가 개선 가능성

버전업으로 안정을 찾은 지금은, 3.3에서 새로 열린 개선 방향들을 살펴보고 있습니다.

FP16 (Half-Precision) 도입 검토: 현재 벡터 데이터는 float32(FP32)로 저장하고 있고, 메모리 제약 탓에 IVF/PQ 기반 근사 검색을 주로 써 왔습니다. IVF/PQ는 메모리 효율은 좋지만 근사 알고리즘이라 정확도(리콜)에 한계가 있습니다.

OpenSearch 3.x에서 강화된 FP16(Half-Precision) 옵션을 적용하면 벡터당 저장 용량이 절반으로 줄어듭니다. 여기서 기대하는 것은 단순한 메모리 절감만이 아닙니다. FP16으로 메모리가 반으로 주는 만큼, 그동안 메모리 제약 때문에 쓰지 못하던 HNSW처럼 정확도가 높은 알고리즘을 적용할 여지가 생깁니다. 같은 메모리 예산 안에서 IVF/PQ 대신 HNSW를 쓰거나, 기존 IVF/PQ의 파라미터(nprobes 등)를 더 공격적으로 높여 리콜을 끌어올릴 수 있게 됩니다. 품질 손실이 허용 범위인 필드부터 단계적으로 적용하는 방안을 검토하고 있습니다.

정기 재인덱싱 도입: 3.3으로 올리면서 재인덱싱으로 새 엔진에 벡터 인덱스를 다시 빌드한 것이 성능에 큰 효과를 냈습니다. 이 경험을 바탕으로, 정기 재인덱싱을 운영 프로세스로 도입하는 방안을 검토하고 있습니다.

오래 운영하다 보면 삭제 마킹된 문서가 쌓이고, 세그먼트가 파편화되고, 벡터 그래프 구조가 최적이 아닌 상태로 남는 등 기술 부채가 누적됩니다. 주기적으로 재인덱싱하면 세그먼트 수 관리, stored fields 크기 최적화, 벡터 인덱스 재빌드 효과를 꾸준히 유지할 수 있습니다. 미리디의 사전 검증에서 3.3의 재인덱싱 소요 시간과 클러스터 부담이 2.19보다 크게 줄어드는 것을 확인했기 때문에, 정기적으로 돌리기에도 현실적인 선택지가 되었습니다.

마무리

이번 OpenSearch 3.3 버전업은 단순히 버전 숫자를 올린 작업이 아니라, 서비스가 성장하며 쌓인 검색 인프라의 한계를 근본부터 풀어낸 과정이었습니다.

듀얼 벡터 검색이라는 도전적인 기능을 도입하면서 메모리와 I/O의 한계에 부딪혔고, 인스턴스 스펙 조정과 쿼리 최적화로 버텨왔지만 세그먼트 병합 폭발이라는 근본 문제 앞에서는 새로운 해법이 필요했습니다. OpenSearch 3.3의 Lucene 10.3 기반 개선들이 이 문제에 직접적인 답을 줬고, 신규 클러스터 + 도메인 스위칭 전략으로 안전하게 전환할 수 있었습니다.

미리캔버스의 검색은 지금도 진화하고 있습니다. FP16, 정기 재인덱싱처럼 3.3에서 새로 열린 가능성을 하나씩 적용해 가며, 더 빠르고 정교한 검색 경험을 만들어 갈 계획입니다.

대규모 벡터 검색을 운영하면서 비슷한 메모리·I/O 병목을 겪고 있다면, Amazon OpenSearch Service의 k-NN 플러그인으로 직접 시작해 보시길 권합니다. 인덱스의 stored fields 구성과 세그먼트 병합 패턴부터 점검한 뒤, 이 글에서 소개한 derived source와 concurrent segment search 같은 최신 엔진 기능을 적용하면 같은 비용으로도 더 안정적인 검색 성능을 확보할 수 있습니다. 자세한 내용은 아래 관련 리소스의 k-NN 문서와 도메인 업그레이드 가이드를 참고해 주세요.

[관련 리소스]

김민석 (Minseok Kim)

김민석 백엔드 팀 리드는 검색과 데이터 파이프라인 설계 경험을 바탕으로, 안정적인 서비스 운영과 팀의 성장을 함께 이끌며 미리캔버스의 핵심 기능을 만들어가고 있습니다.

최시온 (Sion Choi)

최시온 백엔드 엔지니어는 벡터 검색 쿼리 설계부터 클러스터 성능 최적화까지 검색 시스템 전반의 경험을 바탕으로, 대규모 검색 환경에서 검색 품질과 서비스 안정성을 동시에 확보하기 위해 노력하고 있습니다.

이동진 (Dongjin Lee)

이동진 미리캔버스 검색팀 백엔드 엔지니어는 사용자가 원하는 템플릿과 디자인 콘텐츠를 더 빠르고 정확하게 찾을 수 있는 검색 경험을 만들어가고 있습니다. OpenSearch를 기반으로 검색 품질, 성능, 운영 안정성을 개선하는 방법에 관심을 가지고 있습니다.

김백규 (Baekgyu Kim)

김백규 미리캔버스 검색파트 백엔드 엔지니어는 디자인 콘텐츠를 검색엔진에 안정적으로 색인하여 유저가 원하는 검색 결과를 빠르게 제공할 수 있는 환경을 구축하고 있습니다. 대규모 데이터에 대한 정확하고 신뢰할 수 있는 검색 시스템 설계에 관심을 가지고 있습니다.

Yujin Cho

Yujin Cho

조유진 테크니컬 어카운트 매니저는 다양한 데이터베이스의 운영과 데이터 분석 경험을 바탕으로 고객이 데이터 기반의 비즈니스 목표를 달성할 수 있도록 고객과 함께 효율적인 아키텍처와 안정적인 운영 환경을 구성하기 위해 노력하고 있습니다.