이 글에서는 approximate kNN search 에서의 성능 튜닝 가이드를 제공함:

 

근사 kNN 검색의 특성:

  • 쿼리 벡터에 가장 가까운 k개의 벡터를 효율적으로 찾는 방법임.
  • ES 의 다른 쿼리와는 다르게 작동하므로 특별한 성능 고려사항이 있음.

 

여기서 말하는 성능 효과:

  • 검색 속도 향상
  • 인덱싱 속도 개선

 

Reduce vector memory foot-print

벡터 필드의 메모리 사용량을 줄이는 방법을 다룸. 그 방법으로 양자화 (quantization) 에 대해서 다룬다.

 

기본적으로 벡터의 element_type은 float 인데 이건 4바이트임. 양자화하면 1바이트 (-128 ~ 127 범위까지 커버) 하는 걸로 줄일 수 있음.

 

고차원 벡터 (384 이상) 인 경우에는 양자화를 사용하는 걸 권장함.

 

 

Reduce vector dimensionality

근사 kNN 검색 성능 향상을 위해서 벡터 차원을 축소시키는 걸 말함. 유사도 계산할 때 모든 벡터 값이 다 고려되기 때문임.

 

이를 위해 PCA(Principal Component Analysis)와 같은 차원 축소 기법을 실험해보는 것도 좋음. 이러한 기법은 데이터의 주요 특성을 유지하면서 차원을 줄일 수 있다.

 

물론 이런 방법을 적용했을 때 성능적으로 영향이 있는지 실험을 해보는 게 중요.

 

 

Exclude vector fields from _source

kNN 검색 성능을 향상 시키기 위해서 dense_vector 필드를 _source 에서 제외시키는 걸 말함.

  • source 필드에는 오리지날 JSON 문서가 저장됨. 이게 인덱싱을 할 때 같이 저장된다. 여기서 벡터 필드는 제외시키는걸 말함.

 

고차원 dense_vector 필드에서는 문서의 source 필드 값이 매우 클 수 있음. 이 경우에는 excludes 매핑 파라미터를 통해서 dense_vector 필드에서 _source 에 저장되지 않도록 하는게 좋음.

 

 

Ensure data nodes have enough memory

Elasticsearch는 근사 kNN 검색에 HNSW(Hierarchical Navigable Small World) 알고리즘을 사용함.

 

그래프 검색은 모두 메모리에 있을 경우 효과적이므로 충분한 RAM 이 필요함.

 

Analyze index disk usage API를 사용하여 벡터 데이터의 크기를 확인할 수 있다. 이걸로 필요한 메모리를 확인하면 됨.

 

대략적인 추측을 하려면 num_vectors * 4 (num_dimensions + 12) 바이트 로 계산 때리면 됨.

 

물론 양자화를 한다면 num_vectors * (num_dimensions + 12) 바이트 로 계산하면 된다.

 

이런 데이터들은 Page Cache 를 사용하므로 자바의 Heap 메모리를 말하는 건 아님.

 

 

Warm up the filesystem cache

Elasticsearch에서 파일 시스템 캐시를 예열(warm up)하는 방법과 근사 kNN 검색에 사용되는 파일 확장자에 대해 다룸.

 

파일 시스템 캐시의 중요성:

  • Elasticsearch가 실행되는 머신이 재시작되면 파일 시스템 캐시가 비워짐.
  • 운영 체제가 인덱스의 중요 영역을 메모리에 로드하는 데 시간이 걸리고, 이로 인해 초기 검색 작업이 느려질 수 있음.

 

캐시 예열 방법:

  • index.store.preload 설정을 사용하여 특정 파일 확장자를 가진 파일을 메모리에 미리 로드하도록 지시할 수 있다.

 

주의사항:

  • 너무 많은 인덱스나 파일을 미리 로드하면 파일 시스템 캐시가 충분히 크지 않을 경우 검색 속도가 느려질 수 있음.

 

근사 kNN 검색에 사용되는 파일 확장자:

  • vec와 veq: 벡터 값 저장
  • vex: HNSW 그래프 저장
  • vem, vemf, vemq: 메타데이터 저장

 

 

Reduce the number of index segments

인덱스 세그먼트 수를 줄이는게 중요한 이유에 대해 먼저 다룸.

 

Elasticsearch 샤드와 세그먼트:

  • Elasticsearch 샤드는 여러 세그먼트로 구성되고, 세그먼트는 인덱스 내의 내부 저장 요소임.

 

근사 kNN 검색에서의 세그먼트 역할:

  • 각 세그먼트의 벡터 값은 별도의 HNSW 그래프로 저장됨.
  • kNN 검색도 각 세그먼트를 통해 확인함.

 

kNN 검색의 병렬화:

  • 최근 kNN 검색의 병렬화로 여러 세그먼트를 검색하는 속도가 크게 향상되긴함.

 

세그먼트 수와 검색 속도:

  • 세그먼트 수가 적을수록 kNN 검색이 최대 몇 배 더 빠를 수 있음.

 

기본 세그먼트 병합 프로세스:

  • Elasticsearch는 기본적으로 백그라운드 병합 프로세스를 통해 작은 세그먼트들을 더 큰 세그먼트로 주기적으로 병합하긴한다.

 

추가적인 세그먼트 수 감소 방법:

  • 기본 병합 프로세스가 충분하지 않은 경우, 인덱스 세그먼트 수를 줄이기 위한 명시적인 조치를 취할 수 있긴함.

 

고려사항:

  • 세그먼트 병합은 리소스를 소비하는 작업이고, 일시적으로 인덱싱 성능이 저하될 수 있음.

 

 

Force merge to one segment

여기서는 force merge 작업을 통해 인덱스를 단일 세그먼트로 병합하는 방법과 그에 따른 장단점을 다룸.

 

Force Merge 작업:

  • 인덱스 병합을 강제로 수행하는 작업임. 단일 세그먼트로 병합하면 kNN 검색 속도가 향상되니까

 

성능 영향:

  • dense_vector 필드의 force merge는 비용이 많이 드는 작업임. 완료하는데 상당히 시간이 걸릴 수 있음.

 

Force Merge 권장 사항:

  • 읽기 전용 인덱스에서만 force merge를 수행하는 것이 좋음. 즉, 더 이상 쓰기 작업을 받지 않는 인덱스에 적용하는 것이 좋음.

 

문서 업데이트 및 삭제 처리:

  • 문서가 업데이트되거나 삭제될 때, 기존 버전은 즉시 제거되지 않는다.
  • 대신 소프트 삭제되고 "톰스톤(tombstone)"으로 표시됨.
  • 이런 툼스톤은 일반적인 세그먼트 병합 중에 자동으로 정리된다.

 

Force Merge 문제점:

  • 소프트 삭제된 문서의 수가 급격히 증가할 수 있음. 디스크 사용량이 급증할 수 있게 되고, 검색 성능 저하로 이어질 수 있음.
  • 그래서 쓰기 작업이 있는 인덱스에 정기적으로 force merge 를 하게되면 문제가 될 수 있음.

 

 

Create large segments during bulk indexing

Bulk Indexing 중 큰 세그먼트를 생성하는 방법에 대해 다룸.

 

Elasticsearch 에서 초기에 큰 세그먼트를 생성하도록 유도할 수 있음.

 

주요 전략은 다음과 같음:

  • 검색 비활성화 및 refresh 간격 조정:
    • 대량 업로드 중에는 검색을 수행하지 않도록 함.
    • index.refresh_interval을 -1로 설정하여 비활성화한다. 이로인해 refresh 작업을 방지하고 추가 세그먼트 생성을 피한다.
  • 인덱싱 버퍼 크기 증가:
    • 더 큰 인덱스 버퍼를 이용해서 flush 전에 더 많은 문서를 세그먼트에 수용하도록 하는 것.

 

index refresh 작업:

  • refresh 는 인덱싱 된 문서를 검색 가능하게 만드는 프로세스임.
  • 작동 원리:
    • 새로운 문서나 업데이트된 문서는 먼저 인메모리 버퍼에 저장됨.
    • 다음으로 Refresh 작업이 수행되면, 이 버퍼의 내용이 새로운 세그먼트로 쓰여진다.
    • 이 새 세그먼트는 디스크에 완전히 쓰여지지 않고, 파일 시스템 캐시에 존재한다. 그리고 검색이 가능해짐.
  • refresh 작업은 주기적으로 이뤄지는데 이를 통해서 인덱싱 된 이후 실시간에 가깝게 검색이 되게 만들어짐.
  • 빈번한 refresh는 최신 데이터의 가시성을 높이지만, 시스템 리소스를 더 많이 사용한다. refresh 를 줄이면 성능적으로 이익이 있긴함.
  • 그리고 각 Refresh 는 새로운 세그먼트를 생성하는거임.

 

기본 설정:

  • indices.memory.index_buffer_size는 기본적으로 힙 크기의 10%로 설정됨.
  • 32GB와 같은 상당한 힙 크기의 경우, 이 설정으로 충분할 수 있다.

 

추가 조정:

  • 전체 인덱싱 버퍼를 사용하려면 index.translog.flush_threshold_size 제한도 증가시켜야 한다.

 

index.translog.flush_threshold_size:

  • translog 에 대해 알아야함. 이건 아직 Lucene에 안전하게 저장되지 않은 모든 작업을 저장함. 목적은 복구를 위해서.
  • 큰 index buffer 를 사용하고 있는데 translog 가 한계점에 도달하게 되면 플러쉬 되면서 세그먼트를 만들게 될거임. 이 값이 성능 제약을 걸 수 있는 것. 그래서 이 사이즈도 같이 증가시키는게 필요할 수 있음.
  • 기본값은 512MB임.

 

 

Avoid heavy indexing during searchesedit

근사 kNN(k-Nearest Neighbors) 검색 중에 과도한 인덱싱을 피해야 한다는 내용과 그 대안에 대해 다룸.

 

동시 인덱싱과 검색의 문제점:

  • 활발한 문서 인덱싱은 근사 kNN 검색 성능에 부정적인 영향을 미친다.
  • 인덱싱 스레드가 검색에 필요한 컴퓨팅 리소스를 차지하기 때문임.

 

빈번한 Refresh 도 문제가 됨:

  • 동시에 인덱싱과 검색을 수행할 때 Elasticsearch는 자주 리프레시를 수행하게 되면 여러 개의 작은 세그먼트가 생성되면서 검색 성능을 떨어뜨림.

 

만약 벡터 임베딩 모델과 같이 재인덱싱(reindex) 가 필요하게 된다면 기존 문서에 업데이트 치기 보다는 새로운 별도의 인덱스를 생성해서 갈아끼우는 게 낫다고 함:

  • 기존의 인덱스에 문서를 업데이트 하게 되면 원래 문서는 소프트 삭제되고 새 문서가 추가되면서 병합 처리에서 성능에 악영향을 주기 때문.
  • 롤백의 용이성도 있음.

 

 

Avoid page cache thrashing by using modest readahead values on Linux

Elasticsearch의 성능을 최적화하기 위한 readahead 설정 값에 대해 이야기 함.

 

Elasticsearch 검색은 많은 랜덤 읽기 I/O를 유발할 수 있음.

 

블록 디바이스의 readahead 값이 높으면 불필요한 읽기 I/O가 많이 발생할 수 있다.

 

대부분의 Linux 배포판은 단일 일반 디바이스에 대해 128KiB의 적절한 readahead 값을 사용한다.

 

하지만 소프트웨어 RAID, LVM, dm-crypt 사용 시에 readahead 값이 MiB 단위로 커질 수 있음. 이는 페이지 캐시 thrashing 을 심각하게 유발해서 성능에 악 영향을 미칠 수 있다.

 

페이지 캐시 thrashing:

  • 시스템의 페이지 캐시(또는 파일 시스템 캐시)가 비효율적으로 사용되는 현상을 말함. 너무 자주 교체되는 경우.

readahead 값을 확인하려면 다음 명령어를 내리면 됨 

 

lsblk -o NAME,RA,MOUNTPOINT,TYPE,SIZE 명령을 사용하여 KiB 단위로 현재 값을 확인할 수 있음.

 

readahead 는 128KiB 값을 권장한다.

 

blockdev 사용 시 주의사항:

  • blockdev는 512바이트 섹터 단위의 값을 예상하지만, lsblk는 KiB 단위로 값을 보고한다.
  • 예: /dev/nvme0n1에 대해 임시로 readahead를 128KiB로 설정하려면 blockdev --setra 256 /dev/nvme0n1 명령을 사용하면 됨.

 

 

References:

+ Recent posts