BE-commerce 프로젝트
약 4분 분량 검색·추천이벤트·메시징

검색 색인이 상품 변경을 반영하는 데 10분 걸리던 것을 1.1초로 줄였다

목차

상품·가격·재고 변경이 검색 결과에 보이기까지 최대 10분 걸리던 것을 p95 1.12초로 줄였습니다. 데이터베이스 변경 로그를 읽어 흘려주는 CDC(Change Data Capture)로 바뀐 상품만 색인에 다시 넣었고 600건을 흘려 놓친 변경은 0건이었습니다. 같은 방식으로 Elasticsearch에 반영한 쪽도 1.20초라 엔진은 앱 안 Lucene을 유지했습니다.

개요

이 쇼핑몰의 검색 색인은 앱 프로세스 안에 있고 10분마다 DB에서 상품 전체를 읽어 통째로 다시 만듭니다. 새 상품은 최대 10분 뒤에야 검색에 나옵니다. 가격과 재고는 검색 결과를 화면에 그릴 때 DB에서 다시 채우니 화면 숫자는 늘 최신이었습니다. 그런데 검색어와 함께 가격·재고 필터를 걸게 되면서(ch45) 이 지연이 필터 결과로 번졌습니다. 품절된 상품이 “재고 있음” 결과에 최대 10분 남습니다.

변경이 색인에 들어가는 길. 10분 재색인은 30초 안에 한 건도 반영하지 못했고 CDC로 바뀐 상품만 다시 넣자 Lucene p95 1.12초, Elasticsearch 1.20초였다

무엇을 붙였나

MySQL binlog를 읽는 Debezium 커넥터가 products와 stock 표의 변경을 Kafka 토픽 하나로 보냅니다. 메시지 키는 상품 id입니다.

받는 쪽은 메시지 내용을 쓰지 않고 상품 id로 DB를 다시 읽어 색인에 넣습니다. 재고 이벤트에는 상품 이름이 없고 두 표의 이벤트는 순서가 바뀌거나 두 번 올 수 있습니다. 지금 DB를 다시 읽으면 세 문제를 따로 다룰 필요가 없습니다. 대가는 변경마다 DB 조회 한 번입니다.

엔진받는 방식검색에 보이는 시점
Lucene(앱 안)인스턴스마다 모든 변경을 받습니다(부팅마다 새 컨슈머 그룹)1초마다 검색기를 다시 열 때
Elasticsearch색인기 하나가 받습니다(공유 컨슈머 그룹)엔진의 1초 refresh 뒤

Lucene은 색인이 인스턴스마다 있으니 인스턴스마다 모든 변경을 받아야 합니다. 10분 전체 재색인은 놓친 이벤트를 메우는 안전망으로 남겼습니다. 재색인 중에 들어온 변경은 모아 두었다가 새 색인에 다시 넣은 뒤 갈아 끼웁니다. 새 색인은 적재를 시작한 순간의 DB를 담고 있어서 그 사이 변경을 다시 넣지 않으면 갈아 끼우는 순간 사라집니다.

어떻게 쟀나

변경 셋을 DB에 직접 쓰고 커밋 시각부터 모든 인스턴스의 검색 API 결과가 바뀔 때까지 50ms 간격으로 확인했습니다. 종류마다 200건, 초당 20건입니다.

  • 새 상품: 고유한 검색어로 검색해 나타나면 반영
  • 가격 인하: 카탈로그 최저가보다 낮게 바꾸고 그 가격 이하 필터에 나타나면 반영
  • 품절: 재고를 0으로 바꾸고 “재고 있음” 필터에서 빠지면 반영
방식인스턴스30초 안에 못 한 변경p50p95
10분 재색인(이전)160/60해당 없음해당 없음
Lucene + CDC20/600873ms1,122ms
Elasticsearch + CDC20/600735ms1,197ms

판정 기준은 측정 전에 p95 3초 이하, 놓친 변경 0건으로 정했고 둘 다 넘었습니다. 둘 다 통과하면 Lucene을 유지하기로 했으니 엔진은 바꾸지 않았습니다.

차이가 난 곳은 인스턴스 사이입니다. Lucene은 인스턴스마다 검색기를 다시 여는 1초 타이머가 어긋나 있어서 두 인스턴스의 반영 시각이 중앙값 405ms 벌어졌습니다. 그 사이 두 인스턴스가 다른 결과를 냅니다. Elasticsearch는 색인이 하나라 이 차이가 40ms로, 확인 간격의 오차 수준이었습니다.

측정하다 잡은 결함 셋

셋 다 단위 테스트로는 보이지 않았고 실제 경로로 재자 드러났습니다. 모두 머지 전에 고쳤습니다.

결함어떻게 드러났나
Elasticsearch 색인기가 영어가 아닌 문자가 든 상품을 넣지 못했다첫 측정에서 20건을 놓쳤습니다. bulk 요청 본문에 문자셋이 없어 기본 문자셋으로 인코딩됐습니다
문서 하나가 거절되면 배치 전체가 재시도에 묶였다거절된 문서 때문에 같은 배치의 다른 변경까지 늦었습니다. 문서 거절은 세고 넘기고 엔진이 못 받을 때만 다시 시도하게 나눴습니다
Lucene 인스턴스가 기동할 때마다 토픽 전체를 처음부터 다시 읽었다반영한 수가 변경 수의 두 배였습니다. 변경 없이 기동만 해도 2,299건을 반영했습니다. 적재 시각으로 위치를 찾다가 못 찾으면 전역 설정(처음부터)으로 떨어지고 있었습니다

세 번째는 결과가 맞게 나와서 더 늦게 보였습니다. 다시 읽어도 DB에서 지금 값을 읽으니 색인은 맞았고 숫자 하나(반영 수)만 이상했습니다.

정리

  • 상품 변경은 CDC로 색인에 반영합니다. 반영 p95는 1.12초입니다
  • 인스턴스마다 모든 변경을 DB에서 다시 읽으니 그 비용은 인스턴스 수에 비례합니다. 카탈로그를 통째로 다시 넣는 몰림(수만 건)은 재지 않았습니다
  • 다른 서비스가 같은 색인을 읽어야 하거나 인스턴스가 많아져 재조회가 부담이 되면 Elasticsearch로 옮깁니다. 반영 지연 쪽에서는 옮길 이유가 나오지 않았습니다

색인이 1초 안에 바뀌게 되면서 새 질문이 하나 생겼습니다. 사용자가 검색 결과의 쪽을 넘기는 그 몇 초 사이에도 색인이 바뀝니다. 그 이야기는 ch52에서 다룹니다.

프로필 사진
작성자 @범수

오늘의 노력이 내일의 전문성을 만든다고 믿습니다.

댓글

댓글 수정/삭제는 GitHub Discussions에서 가능합니다.