elasticsearch Fielddata & Aggregation

Fielddata

검색 결과를 정렬하려면, Elasticsearch 엔진은 매칭된 문서 모두에 대해 해당 필드의 값을 확인해야 한다. 역색인(inverted index) 구조는 검색 작업에는 효과적이나, 정렬 작업에는 효과적이지 않다.

  • 검색 작업: 텀(term) -> 문서(document) 매핑
  • 정렬 작업: 문서(documen) -> 문서 내 텀(term) 매핑

이러한 정렬작업을 효율적으로 처리하기 위해, 정렬 대상 필드의 모든 값들을 메모리로 로드한다. 이때 매칭된 문서의 필드 값만 로드하는 것이 아니라, 인덱스 내 모든 문서의 필드 값을 메모리로 로드한다. 이는 디스크에 저장된 인덱스의 역색인 형태를 다시 역으로 필드데이터 형태로 만들어서 메모리로 로드하는 과정이 느리기 때문이다.

Elasticsearch는 메모리에 로드한 필드데이터를 아래의 작업에서 활용한다.

  • 필드를 기준으로 검색 결과 정렬
  • 필드를 대상으로 aggregation
  • 일부 필터(geolocation 필터 등)
  • 필드를 참조하는 스크립트

 

필드데이터는 기본적으로 최초 검색 시점에 메모리에 로드된다. analyzed한 string 필드의 경우 문서가 많은 수의 텀을 가지므로, 필드데이터를 잘못 설정한 경우 너무 많은 메모리를 차지하거나 노드 전체가 다운(OutOfMemory exception)될 수 도 있다. 따라서 필드데이터가 사용할 메모리를 적절히 설정해야 한다. 또한 필드데이터를 메모리가 아닌 디스크에 저장할 수도 있다.

필드데이터는 세그먼트 단위로 동작한다. 따라서 새로운 세그먼트가 생성된 후 검색이 가능하게 되면, 기존의 세그먼트에 대한 필드데이터는 여전히 유효하며, 새로운 세그먼트만 필드데이터에 추가된다.

하지만 새로운 세그먼트의 크기가 큰 경우(또는 최초로 필드데이터를 메모리로 로드하는 경우), 필드데이터 생성에 오랜 시간이 걸리므로, 정렬이 필요한 검색 요청은 처리 속도가 저하된다. 이를 위해 검색 시점이 아니라, 사전에 필드데이터를 구성할 수도 있다.

Fielddata 메모리 사용량 확인

인덱스 단위로 확인

노드 단위로 확인

Fielddata & Aggregation

Aggregation의 경우, 매칭된 문서를 기준으로 필드데이터에서 값을 수집후 집계한다. 따라서 잘못 작성한 aggregation은 메모리를 과도하게 사용하여, 노드에 부하를 과중시키거나 다운시킬 수도 있다.

string 타입을 analyze하는 등 aggregation하려는 필드에 다수의 텀이 포함된 경우라면, 해당 aggregation은 너무 많은 메모리를 차지하게된다.

Fielddata 메모리 사용량 제한하기

필드데이터에 추가된 필드 값들은 절대로 메모리에서 삭제되지 않는다.

결국 메모리가 indices.breaker.fielddata.limit(기본값: 60%)까지 쌓이면, 새로운 세그먼트의 필드데이터는 메모리로 로드되지 않게되고, 검색되지 않게 된다.

이를 방지하려면, indices.fielddata.cache.size를 설정하여 LRU 필드데이터들을 삭제하고 새로운 필드데이터를 추가할 수 있도록 해야 한다.

indices.fielddata.cache.size

  • LRU 필드데이터를 삭제하지 않고 fielddata에 유지할 수 있는 최대 메모리 량.
  • 새로운 필드에 대한 검색 요청이 들어오면, 인덱스의 모든 문서에서 해당 필드의 값을 fielddata에 추가.
  • 최종 fielddata 크기가 indices.fielddata.cache.size보다 크면, LRU 메모리 회수 정책을 따름
  • 기본값은 unbounded다.

LRU 필드데이터를 삭제하고, 디스크에서 새로운 필드데이터를 추가하는 작업이 반복되면 무거운 DISK I/O가 발생하고, 무거운 GC가 발생하게 된다. 따라서 필드데이터가 커지면 메모리 증설 하는 것이 가장 좋다. 

절대로,

indices.fielddata.cache.expire는 절대로 설정하지 말아야 한다.

Fielddata Circuit Breaker

새로운 필드에 대한 검색 요청이 들어오면, 먼저 인덱스의 모든 문서에서 해당 필드의 값을 fielddata에 추가한 후, 변경된 fielddata의 사이즈를 검사한다.

따라서 indices.breaker.fielddata.limit(기본값: 60%)을 설정했더라도, 새로운 검색 요청으로 인해 추가되는 fielddata의 사이즈가 40%를 넘게 되면 노드의 힙 메모리 사이즈 범위를 벗어나게 되고, 결국은 OutOfMemoryException이 발생하여 노드가 장애 상태에 빠진다. 이를 방지하기 위해 Elasticsearch에서는 fielddata circuit breaker를 설정할 수 있다.

  • indices.breaker.fielddata.limit
    fielddata 메모리 최대량(기본값: 60%)
  • indices.breaker.request.limit
    검색 요청으로 인해 추가될 fieldaata 메모리 예측 최대량(기본값: 40%)
  • indices.breaker.total.limit
    indices.breaker.fielddata.limit + indices.breaker.request.limit(기본값: 70%)

 

검색 요청이 들어 왔을 때 각 circuit breaker는 설정값을 넘는지 검사 후, 초과하는 경우 예외를 리턴한다.

하지만 OutOfMemoryException는 발생하지 않으므로, 노드가 장애 상태에 빠지게 되는 일은 방지할 수 있다.

 Fielddata Filtering on aggregation

블로그 게시글 중, 인기 태그 순위를 집계해야 한다고 해보자. 이 경우 요건은

  • 인기 태그 Top N개와
  • 해당 태그를 포함하는 게시글의 수

를 term aggregation을 통해 쉽게 집계할 수 있다.

문제는 사용자가 입력하는 서로 다른 태그의 수는 엄청나게 많고, aggregation을 위해서는 이들 태그를 모두 필드데이터 메모리로 로드해야 한다는 점이다. 하지만, 겨우 1~2 문서에만 쓰인 태그는 Top N 인기 태그가 아니므로, 집계 대상이 아니므로 필드데이터로 로드할 필요가 없는 데이터다.

이러한 상황이라면, 해당 필드의 속성을 매핑할 때, 아래와 같이 필터를 설정할 수 있다.

필드데이터는 세그먼트 단위로 동작한다. 따라서 필터 또한 세그먼트 단위로 속성을 설정한다.

위의 경우, 세그먼트에 포함된 문서의 수가 500개 이상이고, 텀 빈도(TF)가 해당 세그먼트 내에서 1% 이상 등장하는 경우에만 필드데이터로 로드한다.

하지만 이처럼 필드데이터를 필터링하게 되면, 해당 데이터는 집계 대상에서 제외된다. 따라서 서비스 요건에 맞게 적절한 값으로 설정할 수 있도록 한다.

Doc Values

필드데이터는 메모리에 로드하므로, 로드할 수 있는 필드데이터 사이즈는 힙 사이즈를 넘을 수 없다. 따라서 필드데이터 사이즈가 커진다면, 노드를 증설해야 한다.

노드를 증설하는 대신 어느 정도의 검색 속도 저하를 감수할 수 있는 경우라면 필드데이터를 메모리가 아닌 디스크에 저장하는 방법을 택할 수 있다. 이러한 방법을 Doc Values라고 부른다.

Doc Values는 아래와 같이 필드 단위로 활성화할 수 있으며, analyzed 필드에는 적용할 수 없다.

In-Memory vs. Doc Values

 항목  In-Memory  Doc Values
 저장 위치  메모리  디스크
 생성 시점  첫 검색 호출시(default)* 사전 적재하는 경우에는 방법에 따라 다름  색인시
 검색 속도  Doc Values가 10~25% 가량 느림
 메모리 사용량  높음  낮음
 인덱스 사이즈  낮음  높음

Doc Values를 사용하면 검색 속도는 다소 느려지지만, 메모리를 사용하지 않으므로

  • ES_HEAP_SIZE를 더 낮게 설정할 수 있고
  • 해제된 메모리는 파일시스템 캐시로 사용될 수 있으므로, 더 많은 필터를 캐싱할 수 있게 된다
  • 무엇보다 필드데이터가 증가하더라도 메모리를 증설하지 않아도 되므로 부담이 덜하다

곧 doc values 방법이 필드데이터의 기본 방식이 될 가능성이 높다.

필드데이터 사전 적재

기본적으로 필드데이터는 lazy-loading을 따른다. 따라서 새로운 세그먼트가 생기더라도, 해당 세그먼트의 필드에 대한 최초 검색 요청이 발생한 시점에 필드데이터로 적재된다. 하지만 세그먼트 사이즈가 큰 경우, 최초 검색 요청 처리 시간이 지연될 수 있다. 이 경우 세그먼트를 사전에 적재하는 방법을 사용한다.

필드데이터를 사전 적재하는 방법에는 아래 3가지가 있다.

 Eager Loading

 Global Ordinals

 인덱스 워머(Index Warmer)

TBD

 Aggregation시 카테시안 곱(Cartesian Product) 연산 피하기

 Depath-First vs. Breadth-First

TBD

https://www.elastic.co/guide/en/elasticsearch/guide/current/_preventing_combinatorial_explosions.html#_preventing_combinatorial_explosions

 References

Leave a Reply

Your email address will not be published. Required fields are marked *