KimDoubleB / LAB

 ᕙ(•̀‸•́‶)ᕗ

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

엘라스틱서치 실무 가이드

KimDoubleB opened this issue · comments

엘라스틱서치 실무 가이드 책을 읽고 정리

챕터 별 바로가기

1장 - 검색 시스템 이해하기
2장 - 엘라스틱서치 살펴보기
3장 - 데이터 모델링
4장 - 데이터 검색
5장 - 데이터 집계
9장 - 엘라스틱서치와 루씬 이야기
10장 - 대용량 처리를 위한 시스템 최적화
11장 - 장애 방지를 위한 실시간 모니터링


docker

https://github.com/KimDoubleB/LAB/tree/master/docker/elastic-search


버전 별 변경사항

Elasticsearch 7

Elasticsearch 8


한글 검색용 필터

https://github.com/yainage90/hanhinsam

8.10.2 사용 시 참고사항

  • 버전에 맞게 gradle 수정: 8.1.2 -> 8.10.2

  • elasticsearch 버전이 올라가며 코드 수정이 이뤄졌음 (특히, AbstractTokenFilterFactory)

  • 고로 FilterFactory들을 다음과 같이 코드 수정해줘야 함. (elastic/elasticsearch#88113, 8.4.0 이후인 듯)

- super(indexSettings, name, settings);
+ super(name, settings);
  • ./gradlew clean assemble 빌드 후, build/distributions/hanhinsam-0.1.zip 확인
  • bin/elasticsearch-plugin install file:///file-path/hanhinsam-0.1.zip 설치

검색 FE 예제 코드

https://github.com/KimDoubleB/es-movie-finder

1장 - 검색 시스템 이해하기

검색 시스템의 기본 요소

수집기 > 색인기 > 스토리지 > 검색기

수집기

  • 웹에서 필요한 정보를 수집하는 프로그램 (크롤러, 스파이더, 웜, 웹로봇 등).

스토리지

  • 검색엔진에서 색인한 데이터를 보관하는 공간.

색인기

  • 수집기가 수집한 정보를 사용자의 질의에 대해 대답할 수 있도록 검색 가능한 구조로 가공하고 저장.
  • 다양한 형태소 분석기를 조합해 정보에서 의미있는 용어를 추출, 검색에 유리한 역색인 구조로 데이터 저장.

검색기

  • 사용자의 질의를 받아 색인기에서 저장한 역색인 구조에서 일치하는 문서를 찾아 결과로 반환.
  • 사용자 질의를 바로 사용하지 않고 색인기와 마찬가지로 질의에서 유의미한 용어를 추출해 검색 (형태소 분석기를 이용).

관계형 데이터베이스와의 차이점

RDBMS에서는 검색을 위해 LIKE Query를 이용. 하지만 텍스트 매칭을 통한 단순한 검색만 가능함.

  • 텍스트를 여러 단어로 변형하거나, 여러 개의 동의어/유의어를 활용한 검색은 불가능.

검색엔진은 데이터베이스에서 불가능한 여러 검색이 가능하다.

  • 위에서 말한 여러 단어변형/동의어/유의어 활용 검색.
  • 비정형 데이터 색인 및 검색
  • 형태소 분석을 이용해 사람이 구사하는 자연어 처리가 가능해짐
  • 역색인 구조로 빠른 검색속도 보장

엘라스틱 서치가 강력한 이유

  • 오픈소스 검색엔진: 루씬을 기반으로 개발된 오픈소스 검색엔진
  • 고차원적인 전문 검색: 내용 전체를 색인해 특정 단어가 포함된 문서를 검색하는 것
  • 통계분석 가능/키바나를 이용한 실시간 시각화 가능
  • 스키마리스/Document-Oriented
  • Restful API 지원
  • 역색인
  • 확장성과 가용성: 분산구성/확장 가능. 샤드 단위로 분산되어 빠르게 처리 가능.

약점?

  • 실시간이 아님.
    • 색인된 데이터는 약 1초 뒤에서나 검색이 가능해짐. 즉, 준 실시간임.
  • 트랜잭션/롤백이 없음
    • 전체적인 클러스터의 성능 향상을 위해 트랜잭션/롤백을 지원하지 않음.

2장 - 엘라스틱서치 살펴보기

기본 용어

image

인덱스

  • 데이터 저장 공간
  • 인덱스 이름으로 문서 데이터를 검색하며, 여러 개의 인덱스를 동시에 검색하는 것도 가능
  • 인덱스가 없는 상태에서 데이터가 추가되면, 인덱스가 자동으로 생성됨
  • 이게 번역본이라 영어로 보다보면 헷갈리는 점들이 있을 수 있다
    • index: 색인 데이터
    • indexing: 색인 과정
    • indices: 매핑정보를 저장하는 논리적인 공간
    • 즉, 번역본에서 인덱스는 indices를 의미한다.

샤드

  • 색인된 문서는 하나의 인덱스에 담기는데, 인덱스 내부에 색인된 데이터는 물리적인 공간에 여러 개의 파티션으로 나뉘어 구성됨 → 이 파티션을 샤드라고 함.
  • 엘라스틱서치가 분산환경이면, 하나의 인덱스가 여러 노드에 분산되어 저장
    • 기본적으로 5개의 프라이머리 샤드와 1개의 레플리카 샤드를 생성
    • 업데이트 됨
      • 버전 7부터는 기본적으로 프라이머리 샤드 1개, 레플리카 샤드 1개 생성됨.
      • 인덱스 당 최대 1024개의 샤드 생성가능 (사고를 막기 위해 지정된 값).
        • ES_JAVA_OPTS="-Des.index.max_number_of_shards={number}"로 제한 가능

타입

  • 인덱스의 논리적 구조
  • 하나의 인덱스 당 하나의 타입을 가짐

문서

  • 데이터가 저장되는 최소 단위
  • 기본적으로 JSON 포맷으로 저장됨
  • 중첩구조가 지원되어 문서 안에 문서를 저장하는 것도 가능

필드

  • 문서를 구성하기 위한 속성
  • 문서당 필드명은 중복될 수 없음
  • RDB Column의 차이는 동적인 데이터 타입이라는 것
    • 목적에 따라 다수의 데이터 타입을 가질 수 있음 (값, 초성 등)

매핑

  • 문서의 필드와 필드의 속성을 정의하고, 그에 따른 색인방법을 정의하는 프로세스

노드의 종류

마스터 노드

  • 클러스터 관리
  • 노드 추가/제거 같은 클러스터 전반적인 관리
  • 하나의 노드만이 마스터 노드로 결정되어 동작

데이터 노드

  • 실질적인 데이터 저장, 샤드 배치
    • 리소스 많이 소모하니 리소스 모니터링 필요
  • 검색/통계 같은 데이터 관련 작업 수행

코디네이팅 노드

  • 사용자의 요청을 받아 처리. 라운드로빈으로 업무 분산
  • 클러스터 관련 요청은 마스터 노드에게, 데이터 관련 요청은 데이터 노드에게 전달

인제스트 노드

  • 문서의 전처리 담당
    • 데이터 포맷변경을 위해 슼릡트로 전처리 파이프라인을 구성해 활용할 수 있음
  • 인덱스 생성 전 문서의 형식 변경

여러가지 작업 팁

샤드 개수

인덱스 생성 시, 샤드 개수를 설정할 수도 있다.

number_of_shards: 프라이머리 샤드의 개수
number_of_replicas: 레플리카 샤드의 개수

스키마리스 기능은 사용하지 말자

스키마리스는 데이터만 보고 알아서(default로) 매핑이 되도록 설정하는 것

  • 데이터 공간 낭비: 기본적으로 모든 필드가 text/keyword 타입 동시에 지원하도록 설정됨
  • 원하지 않는 Analyzer 결과: Standard Analyzer가 사용되어 원하지 않는 방향으로 결과가 도출될 수 있음

인덱스 생성 시, 주의점

한번 생성된 매핑정보는 변경할 수 없다.

  • 만약 잘못 생성했거나 변경해야하는 경우, 데이터를 삭제하고 다시 색인하는 수 밖에 없다.

3장 - 데이터 모델링

한번 생성한 매핑 타입은 변경할 수 없다. 그렇기에 아래 사항을 고민해 매핑정보를 설정해야한다.

  • 문자열을 분석할 것인가?
  • _source 에 어떤 필드를 정의할 것인가?
  • 날짜 필드를 가지고 있는 필드는 무엇인가?
  • 매핑에 정의되지 않고 유입되는 필드는 어떻게 처리할 것인가?

매핑정보

매핑생성

  • PUT {index_name}

매핑확인

  • GET {index_name}/_mapping

매핑 파라미터

  • analyzer: 형태소 분석기
    • String인데 따로 지정하지 않으면 Standard Analyzer 형태소 분석기 사용됨
  • normalizer: term query 분석기
    • ex) cafe, Cafe, ….
  • coerce: 색인 시 자동변환 여부. 문자열 숫자가 들어오면 숫자로 처리할 것인가?
  • copy_to: 들어오는 값 복사해 다른 필드로 추가하고 싶을 때 사용
  • 캐시
    • fielddata
    • doc_values
  • dynamic: 매핑에 필드 추가할 때 동적으로 생성할지/생성하지 않을지를 결정
  • enabled: _source 에서 검색이 되지만 색인은 하지 않는 경우
  • format: 날짜/시간 문자열 포맷
  • ignore_above: 문자열 크기 설정. 해당 크기 넘으면 빈 값으로 색인
  • ignore_malformed: 잘못된 데이터 타입 색인 시, 해당 데이터 무시

메타 필드

  • _index : 인덱스 이름. 인덱스 정보 조회 시, 사용.
  • _type : 매핑의 타입정보
  • _id : 문서를 식별하는 유일 키 값
  • _uid : # 을 이용해 _type_id 를 조합하여 사용 (내부적으로 사용되는 값)
  • _source : 문서의 원본 데이터
  • _all (deprecated)
  • _routing : 특정 문서를 특정 샤드에 저장하기 위해 사용자가 지정하는 메타 필드

필드 데이터 타입

  • keyword, text
    • keyword
      • 별도의 분석기를 거치지 않고 원문 그대로 색인함.
        • elastic search 데이터를 keyword로 지정하면 elastic, search 로 검색이 불가능
      • 특정 코드, 키워드로 정형화된 콘텐츠에 사용
    • text
      • 분석기 사용. 전문검색 가능.
      • 정렬/집계 연산을 사용하기 위해 Text/Keyword 타입을 동시에 갖도록 설정할 수도 있음
  • array, date, long, double, integer, boolean, ip, range
  • object, nested: JSON 형식
  • geo_point, ge_shape

분석기

루씬을 기반으로 한 텍스트 기반 검색엔진

  • 문서를 색인하기 전에 해당 문서의 필드 타입이 무엇인지 확인하고 텍스트 타입이면 분석기를 이용해 분석
  • 텍스트가 분석되면 개별 텀으로 나뉘어 형태소 형태로 분석
  • 형태소는 특정 원칙에 의해 필터링되어 단어 삭제,추가,수정되어 최종적으로 역색인

텍스트에 분석기를 따로 설정하지 않으면 Standard Analyzer가 사용됨


분석기는 데이터의 특성에 따라 원하는 분석 결과를 미리 예상해보고 해당 결과를 얻기 위한 옵션을 적용해 설정해야 함.

  • 다양한 경우를 대상으로 직접 테스트해보고 특성을 파악해야 함.

REST API 제공함

  • _analyze API를 통해 직접 테스트 해볼 수 있다.

역색인 구조

책 뒤 단어로 문서 페이지를 찾을 수 있는 구조를 의미.

색인한다는 것이 역색인 파일을 만든다는 것.

  • 원문 자체를 변경한다는 것이 아님
  • 색인 파일에 들어갈 토큰만 변경되어 저장하고 실제 문서의 내용은 변함없이 저장함.

분석기의 구조

  1. 문장을 특정한 규칙에 의해 수정함
  2. 수정한 문장을 개별 토큰으로 분리
  3. 개별 토큰을 특정한 규칙에 의해 변경

기본적으로 제공하는 분석기(Analyzer)들이 있고, 아래 필터들을 조합해 Custom 분석기를 정의해 활용할 수도 있다.


필터 구조

image


Char filter (전처리 필터)

  • 문장을 분석하기 이전에 입력 텍스트에 대해 단어 변경/HTML 제거 등을 수행 (전처리 과정)
  • 토크나이저 내부에서도 일종의 전처리가 가능해서 사실 활용도가 크지 않다.
  • ex) Html strip char filter

Tokenizer filter

  • 텍스트를 어떻게 나눌지를 정의
    • 즉, 언어마다 토크나이저 필터는 다르게 사용할 것.
  • 하나만 사용 가능
  • ex) Standard tokenizer, Whitespace tokenizer, Ngram tokenizer, Keyword tokenizer

Token filter

  • 토큰화된 단어를 필터링해 원하는 토큰으로 변환
  • 불필요한 단어 제거, 동의어 처리, 소문자 변환 등의 작업을 수행
  • ex) Ascii Foliding token filter, Lowercase/Uppercase token filter, Stop token filter, Synonym token filter

색인, 검색 시 분석기를 각각 설정

색인하는 것과 검색하는 것 과정에 대해 분석기를 따로 설정하고 싶을 수 있다.

  • 엘라스틱서치에서는 이 둘을 따로 분리해 설정할 수 있다.
    • 색인 시에 사용하는 Index Analyzer (analyzer)
    • 검색 시에 사용하는 Search Analyzer (search_analyzer)

동의어 사전

색인 데이터를 토큰화 해 저장할 때, 동의어/유의어에 해당하는 단어를 함께 저장해 검색이 가능해지게 할 수 있다.


추가하는 방식

  1. 동의어를 매핑 설정 정보에 미리 파라미터로 등록하는 방식
  2. 특정 파일을 별도로 생성해 관리하는 방식 (추천)

아래와 같은 형태로 파일을 생성하고(txt), synonyms_path 에 해당 파일을 보도록 설정해 적용

Elasticsearch, 엘라스틱서치 // 동의어 추가
Harry => 해리 // 동의어 치환
  • 동의어 추가: 해당 문자열이 토큰에 있으면, 동의어를 토큰에 추가한다.
  • 동의어 치환: 해당 문자열이 토큰에 있으면, 제거하고 치환 값을 토큰에 추가한다.

색인 파일을 변경한다고 자동 reload 되지 않는다.

  • 기존 색인을 삭제하고, 색인을 다시 생성해야만 변경된 내용이 적용된다.

인덱스 Reload

POST {index_name}/_close

POST {index_name}/_open


Document API

아래와 같은 API들이 제공된다.

  • 생성(Index), 조회(Get), 삭제(Delete), 변경(Update), 대량처리(Bulk), 복사(Reindex)

문서 파라미터

문서 ID (_id)

생성 시, Id를 따로 설정하지 않으면 자동으로 문서 ID가 생성된다.

  • 고유한 값으로 UUID 형태의 값이다.

버전관리 (_version)

색인된 모든 문서는 버전 값을 가짐

  • 최초 1을 갖게 되고, 문서에 변경이 일어날 때마다 버전 값이 증가함

이 version 값으로 낙관적 락을 지원할 수 있는 것으로 보인다.

  • POST movie_dynamic/_doc/1?version=1
    • 해당 문서가 version 1이 아니면 오류 발생

오퍼레이션 타입 (op_type)

일반적으로 ID가 이미 존재하는 경우에는 update 작업이 일어나고, 없으면 create 작업이 일어난다. (upsert)

데이터가 존재할 경우, update 하지않고 색인이 실패하길 원한다면 op_type 을 사용하면 된다.

  • create만 강제하고 싶은 경우, op_type=create 를 주면 된다.

타임아웃(timeout)


API

  • 특정 조건에 해당하는 문서만 제거하고 싶은 경우, _delete_by_query 를 이용.
  • Update API가 호출되면 Index에서 문서를 가져와 스크립트를 실행한 후 이를 다시 재색인(reindex) 함
  • Bulk API는 여러 건의 데이터를 한 번에 처리할 수 있지만, 트랜잭션이 없음. 갱신/수정된 결과가 롤백되지 않음에 주의.
  • Reindex API는 한 인덱스에서 다른 인덱스로 문서를 복사할 때 활용됨. 기본적으로 1,000건 단위로 스크롤 하는데, 많은 양을 복사해야 한다면 size 항목을 늘려 수행할 것.

4장 - 데이터 검색

검색 API

색인시점과 검색시점의 동작과정

색인시점

  • Analyzer를 통해 분석된 텀을 Term, 출현빈도, 문서번호와 같이 역색인 구조를 만들어 내부 저장

검색시점

  • Keyword 타입과 같은 분석이 불가능한 데이터와 Text 타입과 같이 분석이 가능한 데이터 구분
  • 분석이 가능한 경우, 분석기를 ㅌ오해 분석을 수행해 Term을 구함.
  • 역색인 구조의 문서에서 이를 찾고, Score를 계산해 결과로 제공



검색 방법

검색 방법은 2가지가 제공됨

  • URI 검색
  • RESTful API 검색 - POST, Request Body, Query DSL 문법 사용



URI 검색은 _search path에 q 파라미터 등을 통해 검색(153p)할 수 있는데, 복잡한 질의는 불가능하며 의미를 파악하기 어렵다.

  • ex) POST movie_search/_search?q=movieNmEn:* AND prdtYear:2017&analyze_wildcard=true&from=0&size=5&sort=_score:desc,movieCd:asc&_source_includes=movieCd, movieNm,mvoieNmEn,typeNm&pretty

그렇기에 간단한 질의, 테스트 용도가 아니라면 RESTful API를 이용하는 것이 낫다.



RESTful API (Query DSL)

위 예제를 아래와 같이 작성해 요청할 수 있다.

POST movie_search/_search
{
  "query": {
    "query_string": {
      "default_field": "movieNmEn",
      "query": "movieNmEn:* OR prdtYear:2017"
    }
  },
  "from": 0,
  "size": 5,
  "sort": [
    {
      "_score": {
        "order": "desc"
      },
      "movieCd": {
        "order": "asc"
      }
    }
  ],
  "_source": [
    "movieCd",
    "movieNm",
    "mvoieNmEn",
    "typeNm"
  ]
}
  • 각 필드별 정확히 어떤 녀석인지 모르더라도 대충 이해가 간다.



Query DSL 이해하기

Search your data | Elasticsearch Guide [8.10] | Elastic

Request body 필드

  • size: 리턴받는 결과의 개수 (default: 10)
  • from: 몇 번째 문서부터 가져올지 지정 (default: 0)
  • timeout: 요청해 결과를 받는 데까지 걸리는 시간을 나타냄 (default: -1 (no timeout))
  • _source: 검색 시 필요한 필드만 출력하고 싶을 때 사용
  • query: 검색 조건문
  • aggs: 통계 및 집계 사용
  • sort: 결과 정렬조건 지정



Response body 필드

  • took: 쿼리를 실행한 시간
  • timed_out: 타임아웃 시, true
  • _shards: 쿼리를 요청한 전체 샤드의 개수
    • 하위 필드: total, successful, failed
  • hits: 결과 문서 개수 및 스코어 정보
    • 하위 필드: total, max_score, hits



Query와 Filter

Query context: Analyzer에 의한 전문 분석이 필요한 경우

  • 분석기에 의해 분석이 수행됨. 즉, Score가 계산됨.
  • 루씬 레벨에서 분석과정을 거침으로 상대적으로 느림.
    • 결과가 캐싱되지 않음. 디스크 연산이여서 더 느린 듯.
  • match 필드를 이용
  • ex) Harry Potter 문장 분석
POST  movie_search/_search
{
  "query": {
    "match": {
      "movieNm": "기묘한 가족"
    }
  }
}



Filter context: Yes/No로 판단할 수 있는 조건 검색의 경우

  • Yes/No로 판단 가능. Score를 계산하지 않음.
  • 엘라스틱서치 레벨에서 처리가 가능해 상대적으로 빠름.
    • 결과가 엘라스틱서치 내부적으로 캐싱됨.
  • filter 필드를 이용
  • ex) create_year 필드의 값이 2018인지 확인
POST movie_search/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match_all": {}
        }
      ],
      "filter": {
        "term": {
          "repGenreNm": "다큐멘터리"
        }
      }
    }
  }
}
  • match_all 검색을 하기 전 filter 조건에 맞는지 필터링 과정을 수행하게 된다.



주요 파라미터

Multi index 검색

여러 인덱스를 한 번에 조회할 수 있음

  1. , 을 이용해 다수의 인덱스 명을 입력하면 된다.
POST movie_search,movie_auto/_search
{
   "from": 0,
   "size": 5,
   "query": {
     "term": {"repNationNm": "한국"}
   }
 }

  1. * 와일드카드를 이용해 다수의 인덱스를 대상으로 패턴에 맞는 인덱스만 골라 조회 요청을 할 수도 있다.
POST /log-2023-*/_search

중요한건 다수의 인덱스가 다른 스키마를 가지고 있어도 검색이 가능하다는 것.

  • 다수의 비정형 데이터를 가지고 있는 경우에도 한 번에 검색할 수 있다.



쿼리 결과 페이징

from , size 파라미터를 이용해 Pagination를 지원

  • 첫 페이지 - 5개 데이터를 조회

    POST movie_search/_search
    {
       "from": 0, // 생략 가능
       "size": 5,
       "query": {
            "term": {"repNationNm": "한국"}
        }
    }
  • 두 번째 페이지 - 5개 데이터를 조회

    POST movie_search/_search
    {
       "from": 5,
       "size": 5,
       "query": {
            "term": {"repNationNm": "한국"}
        }
    }



주의할 점은 Offset pagination 특징을 가진다는 것이다.

  • 즉, 특정 페이지의 데이터만 읽어 가져오는 구조가 아닌 선택된 페이지까지의 모든 데이터를 조회하고 필터링하게 된다.
  • 위 두 번째 페이지 조회를 예로 들어보자.
    • 첫 5개의 데이터를 띄어넘고 그 후 5개만 조회한다면 좋지만, 10개의 문서를 가져와 사이즈만큼 필터링하게 된다.
    • 그러므로 페이지 번호가 높아질수록 쿼리 비용이 높아지는 구조이다.
  • ref: https://binux.tistory.com/148#comment14337039



쿼리 결과 정렬

sort 파라미터를 통해 결과를 정렬할 수 있다.

기본적으로는 계산한 유사도 스코어(score)로 정렬이 되는데, 어떠한 이유로 인해 다른 조건으로 정렬하고 싶은 경우 이를 활용하면 된다.

여러 정렬 조건을 활용할 수도 있다.

POST movie_search/_search
{
   "query": {
     "term": {
       "repNationNm" : "한국"
     }
   },
   "sort" :{
     "prdtYear": {
        "order": "asc"
      },
			"_score": {
        "order": "desc"
      }
   }
}



_source 필드 필터링

결과 문서에 대해 특정 필드만 가져오고 싶다면 _source 를 이용하면 된다.

  • 네트워크 사용량을 줄여 응답 속도를 빠르게 받을 수 있으므로 왠만한 상황에서 이를 활용하는게 좋을 것 같다.



operator 설정

  • 일반적으로 엘라스틱서치는 검색 시 OR 연산으로 동작함.

  • Query DSL에서는 operator를 통해 명시적으로 설정이 가능하다.

    "query": {
      "match": {
        "movieNm": {
          "query": "자전차왕 엄복동",
          "operator": "and"
        }
      }
    }



**minimum_should_match 설정**

OR 조건으로 검색하는 경우, 결과가 엄청 많을 수 있다.

이를 방지하기 위해 Term의 개수가 최소 몇 개 이상 매치되는 경우만 결과로 나오도록 설정이 필요할 수 있다.

이 때, minimum_should_match 를 이용하면 된다.

"query": {
  "match": {
    "movieNm": {
      "query": "자전차왕 엄복동",
      "minimum_should_match": 2
    }
  }
}



Fuzzy query

기본적으론 단순한 값을 찾는 Match query를 이용하는데, 유사한 값도 포함시키고 싶은 경우 fuzziness 옵션을 이용해 Fuzzy Query를 사용할 수 있다.

Common options | Elasticsearch Guide [8.10] | Elastic

Fuzzy query | Elasticsearch Guide [8.10] | Elastic

레벤슈타인 편집 거리 알고리즘을 기반으로 문서의 필드 값을 여러번 변경하고, 이를 허용범위의 텀으로 변경해가며 문서를 찾는다.



Query DSL 주요 쿼리

다양한 쿼리 검색을 제공한다. 사용하기 위해서는 query 필드 하위에 해당 조건을 사용하면 된다.

  • Match all query(match_all): 모든 문서를 검색
  • Match query(match): 문장에 대해 형태소 분석을 통해 분리한 후, 결과 Term을 이용해 검색
  • Multi match query(multi_match): 여러 개의 필드를 대상으로 검색
  • Term query(term): 형태소 분석을 하지 않고 입력된 텍스트가 존재하는 문서를 검색 (keyword 타입)
  • Bool query(bool): 하나 또는 여러 개의 쿼리를 조합해 활용할 때 사용
    • must: [queries]: 모든 조건 만족하는 문서만 검색
    • must_not: [queries]: 모든 조건을 만족하지 않는 문서만 검색
    • should: [queries]: 여러 조건 중 하나 이상을 만족하는 문서가 검색
    • filter: [queries]: 조건을 포함하고 있는 문서만 검색
  • Wildcard Query(wildcard): *, ? 옵션을 이용해 일치하는 구문을 문서에서 검색 (형태소 분석기 X)
  • Nested Query(nested): 문서 내부에 다른 문서가 존재할 때(부모/자식 관계)에서 조건 이용해 검색



엘라스틱서치는 성능 상의 이유로 Parent 문서와 Child 문서를 모두 동일한 샤드에 저장함.

  • 이를 통해 네트워크 비용을 대폭 줄일 수 있음.
  • 특정 Parent 문서에 포함된 Child 문서가 비정상적으로 커지는 경우, 샤드의 크기가 일정하게 분배되지 못하는 문제가 발생할 수 있음.
  • redis cluster에서 Hashtag와 유사한 듯.



버전도 많이 올라갔고, 책에서는 모든 쿼리를 다루지 않고 있기에 더 자세한 내용은 공식문서를 보는 것이 좋다.

Query DSL | Elasticsearch Guide [8.10] | Elastic

  • geo 기반, span 기반 등 다양한 쿼리가 제공된다.



부가적인 검색 API

내부 동작방식

  • 검색 요청 발생
  • 모든 샤드에 검색 요청 브로드캐스트, 기다림.
  • 각 샤드는 가지고 있는 데이터를 기준으로 검색 수행, 결과 리턴
  • 모든 샤드로부터 검색 결과 도착하면 모든 결과 조합해 최종 결과 리턴



동적분배 방식

  • 검색을 수행할 때, 동일 데이터를 가지고 있는 샤드 중 하나만 선택해 검색을 수행
    • 모든 레플리카 샤드에 대해 검색 요청이 가는 것이 아니다.
  • 특별히 설정하지 않으면 검색 요청의 적절한 분배를 위해 라운드로빈 방식 알고리즘을 사용함
    • 동적 분배 방식(adaptive replica selection)을 이용할 수도 있음.
    • 응답시간, 검색요청 스레드풀 크기 등을 고려해 최적의 샤드를 동적으로 결정하는 방식.
PUT _cluster/settings
{
	"transient": {
    	"cluster.routing.use_adaptive_replica_selection": true
	}
}

현재 버전인 8.10에서는 이를 기본 값으로 사용하는 것으로 보인다.

By default, Elasticsearch uses adaptive replica selection to route search requests. This method selects an eligible node using shard allocation awareness and the following criteria:

  • Response time of prior requests between the coordinating node and the eligible node
  • How long the eligible node took to run previous searches
  • Queue size of the eligible node’s search threadpool



글로벌 타임아웃 설정

앞서 검색 API에서 타임아웃을 다뤘는데, 기본 값이 무제한(-1)이였다.

다수의 무거운 쿼리가 실행된다면, 전체 시스템에 영향을 줄 수 있으므로 전체 정책으로 타임아웃을 설정할 수 있다.

PUT _cluster/settings
{
	"transient": {
    	"search.default_search_timeout": "1s"
	}
}



다양한 부가 쿼리들

  • Search shards API: 검색이 수행되는 노드 및 샤드에 대한 정보 확인

    • POST movie_search/_search_shards
  • Count API: 검색된 문서의 개수가 몇 개인지만 궁금할 때 사용

    • POST movie_search/_count?q=prdtYear:2017

      POST movie_search/_count
      {
        "query": {
          "query_string": {
            "default_field": "prdtYear",
            "query": "2017"
          }
        }
      }
  • Validate API: 쿼리가 유효하게 작성됬는지 검증

    • POST movie_search/_validate/query?q=prdtYear:2017

      POST movie_search/_validate/query
      {
        "query" : {
          "match": {
            "prdtYear": 2017
          }
        }
      }
    • 왜 실패했는지 더 자세한 정보가 필요한 경우, rewrite=true query parameter를 추가하면 된다.

  • Explain API: 요청한 쿼리가 어떻게 스코어가 계산되는지 확인할 때 사용

    • **‘내 질의 결과에 대한 스코어가 어떻게 계산됬는가?’**에 대해 답변할 수 있다
    POST  movie_search/_doc/fzzJqmkBjjM-ebDb8PsR/_explain
    {
      "query" : {
        "term": {
          "prdtYear": 2017
        }
      }
    }
  • Profile API: 쿼리에 대한 상세 수행계획과 수행 계획 별 수행된 시간을 확인할 때 사용

    • **‘내 질의를 실행하는 과정에서 각 샤드별로 얼마나 많은 시간이 소요됬는가?’**에 대해 답변할 수 있다.

    • 성능 튜닝, 디버깅에서 유용하게 활용된다.

    • 매우 상세하게 설명해 결과가 매우 방대하므로 주의하자.

      POST  movie_search/_search
      {
        "profile": true,
        "query": {
          "match_all": {}
        }
      }

5장 - 데이터 집계

집계

보통 통계 프로그램은 배치 방식으로 데이터를 처리한다.

  • 대용량 데이터를 하둡, RDBMS에 적재하고 배치로 처리하는 식.

반면 엘라스틱서치는 많은 양의 데이터를 조각내어 관리함.

  • 덕분에 문서의 수가 늘어나도 배치 방식보다 좀 더 실시간에 가깝게 문서를 처리할 수 있음.


엘라스틱서치가 집계에 사용하는 기술

캐시



Aggregation API

서비스를 운영하다보면 검색 쿼리로 반환된 데이터를 집계하는 경우가 많다.

검색 쿼리의 집계는 다음과 같이 기존 검색 쿼리에 집계 구문을 추가하는 방식으로 수행한다.

{
	"query": { ... },
	"aggs": { ... }
}

질의가 생략되면 match_all 쿼리로 수행되어 전체문서에 대한 집계가 수행됨.



집계 연산

Aggregations | Elasticsearch Guide [8.10] | Elastic

  • 버킷 집계: 쿼리 결과로 도출된 도큐먼트 집합에 대해 특정 기준으로 나눈 버킷을 구성. 현재 문서가 해당 버킷에 속하는지 등 확인해 산술연산 수행 (특정 버킷에 몇 개의 문서가 있는가를 계산).
  • 메트릭 집계: 쿼리 결과로 도출된 도큐먼트 집합에서 필드의 값을 더하거나 평균을 내는 등 산술연산 수행.
  • 파이프라인 집계: 다른 집계 또는 관련 메트릭 연산의 결과를 집계.
  • 행렬 집계: 버킷 대상이 되는 도큐먼트의 여러 필드에서 추출한 값으로 행렬연산을 수행. 최근 문서를 보면 행렬 집계가 보이지 않는 것을 보아 없어진 것으로 보인다.


메트릭 집계

다양한 메트릭 집계를 제공.

이름에서 알 수 있듯 정수/실수 같은 숫자 연산 값 집계를 수행한다.

  • 특정 필드에 대해 합이나 평균을 계산
  • 다른 집계와 중첩해 결과에 대해 특정 필드의 _score 값에 따라 정렬을 수행
  • 지리 정보를 통해 범위 계산 등


2가지 집계로 나뉜다.

  1. 단일 숫자 메트릭 집계 (single-value numeric metrics aggregation)
    • 수행한 결과 값이 하나인 경우
    • sum, avg 등
  2. 다중 숫자 메트릭 집계 (multi-value numeric metrics aggregation)
    • 수행한 결과 값이 여러개인 경우
    • stats, geo_bounds 등


집계 구조

요청

GET /apache-web-log/_search?size=0 // test를 위해 Size를 0으로 설정
{
  "aggs": {
    "집계 이름": {
      "집계 타입": {
        "field": "필드명"
      }
    }
  }
}

응답

{
  "took": 78, // 걸린 시간
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": { // 검색 결과
    "total": {
      "value": 5,
      "relation": "eq"
    },
    "max_score": 1.0,
    "hits": [...]
  },
  "aggregations": {
    "my-agg-name": {                           
      "doc_count_error_upper_bound": 0,
      "sum_other_doc_count": 0,
      "buckets": []
    }
  }
}


집계 종류

  • 합산 집계 (sum)
  • 평균 집계 (avg)
  • 최솟값 집계 (min)
  • 최대값 집계 (max)
  • 개수 집계 (value_count)
  • 통계 집계 (stats)
    • 합/평균/최대/최소/개수를 한 번의 쿼리로 알 수 있음
  • 확장 통계 집계 (extended_stats)
    • 통계 집계 + 표준편차 등
  • 카디널리티 집계 (cardinality)
    • 키워드 집계 (terms)는 중복된 키워드도 다 집계함.
    • 중복된 값들을 제외하고 집계하고 싶은 경우, Cardinality 집계사용
  • 백분위 수 집계 (percentiles)
    • 백분위 별 데이터 값 집계.
    • 기본적으로 1, 5, 25, 50, 75, 95, 99 백분위에 대해 집계.
  • 백분위 랭크 집계 (percentile_ranks)
    • 데이터 값의 백분위 퍼센트 값 집계 (percentiles와 반대 개념)
  • 지형 경계 집계 (geo_bounds)
    • geo_point type만 사용가능.
    • 수집된 데이터의 범위 중 가장 끝 부분에 위치한 정보로 경계가 정해져 집계됨.
    • 맨 위 왼쪽, 맨 아래 오른쪽의 위도/경도가 반환됨.
  • 지형 중심 경계 (geo_centroid)
    • geo_point type만 사용가능.
    • 데이터들의 지형 정 가운데의 위치를 반환함.


알아둘 것

결과에 대해 변화를 시키고 싶은 경우, script 를 사용할 수 있다.

How to write scripts | Elasticsearch Guide [8.10] | Elastic

  • painless 언어를 사용한다.
    • script.lang 옵션을 작성하지 않아도 기본적으로 painless 언어를 사용함.
GET my-index-000001/_search
{
  "script_fields": {
    "my_doubled_field": {
      "script": { 
        "source": "doc['my_field'].value * params['multiplier']", 
        "params": {
          "multiplier": 2
        }
      }
    }
  }
}

Cardinality 집계의 경우, 모든 문서에 대해 중복된 값을 집계하는 것은 성능에 큰 영향을 줄 수 있다.

  • 그래서 근사치를 통해 집계를 수행한다. 이 근사치 계산에는 HyperLogLog++ 알고리즘을 사용함.

Cardinality aggregation | Elasticsearch Guide [8.10] | Elastic



버킷 집계

  • 메트릭을 계산하지 않고 버킷을 생성. 버킷은 쿼리와 함께 수행되어 쿼리 결과에 따른 집계가 이루어짐.
  • 버킷을 생성한다? ⇒ 집계된 결과 데이터 집합을 메모리에 저장한다는 의미
    • 중첩된 집계의 경우, 메모리 사용량이 계속 증가해서 성능에 악영향 줄 수 있음.
    • 이를 위해 최대 버킷 수가 정해져 있음 (search.max_buckets)


집계 종류

  • 범위 집계 (range) - ranges.from ≤ data < ranges.to
    • ranges 개수에 따라 여러 버킷이 만들어지고 결과를 반환
  • 날짜 범위 집계 (date_range)
  • 히스토그램 집계 (histogram)
    • 특정 Interval마다의 count 집계
  • 날짜 히스토그램 집계 (date_histogram)
  • 텀즈 집계 (terms)
    • 지정한 필드에 대해 빈도수가 높은 텀의 순위로 결과가 반환


알아 둘 것

집계를 수행할 땐 각 샤드에 집계 요청을 전달하고, 각 샤드는 집계 결과에 대해 정렬을 수행한 자체 뷰를 갖는다.

그리고 이것들을 병합해 최종 뷰를 구성하기에 포함되지 않은 문서가 존재하는 경우, 집계가 정확하지 않을 수 있다.

  • 샤드 별로 사이즈 개수로 문서를 나누고, 이를 합쳐 결과로 보여준다. 그렇기에 사이즈가 작을수록 결과가 정확하지 않을 가능성이 크다.
  • 이 문제를 해결하기 위해서(정확도를 높이기 위해서)는 사이즈를 키우면 되지만, 버킷에 더 많은 양의 데이터를 담아야하기 때문에 메모리 사용량과 결과 처리비용 또한 증가한다 (Trade-off).


파이프라인 집계

다른 집계로 생성된 버킷을 참조해 집계를 수행하는 집계 (aggs 내 aggs)

  • 집계를 통해 생성된 버킷을 사용해 추가적으로 계산을 수행
  • 부모, 형제라는 두 가지 유형
  • 집계 수행 시, buckets_path 파라미터를 사용해 참조할 집계의 경로를 설정


형제 집계

동일 선상의 위치에서 수행되는 새 집계.

  • 평균/최대/최소/합계/통계/확장통계/백분위/이동평균 집계

ex) 분 단위 데이터량 합산과 가장 큰 데이터량 구하기

GET /apache-web-log/_search?size=0
{
  "aggs": {
    "histo": {
      "date_histogram": {
        "field": "timestamp",
        "interval": "minute"
      },
      "aggs": {
        "bytes_sum": {
          "sum": {
            "field": "bytes"
          }
        }
      }
    },
    "max_bytes": {
	      "max_bucket": { // 버킷 중 가장 큰 값(Max) 계산
        "buckets_path": "histo>bytes_sum" // histo 버킷의 bytes_sum 버킷 참조
      }
    }
  }
}


부모 집계

생성된 버킷을 사용해 집계를 수행하고, 그 결과를 기존 집계에 반영.

  • 파생 집계
  • 누적 집계
  • 버킷 스크립트 집계
  • 버킷 셀렉터 집계
  • 시계열 차분 집계


  • 실제 데이터를 다루다보면 종종 노이즈가 포함되기도 하고, 필요한 필드에 값이 존재하지 않을 수 있다.
  • 즉, 올바른 데이터가 존재하지 않는 부분을 의미.
  • 여러 이유로 발생할 수 있다.
    • 어느 하나 버킷 안으로 들어오는 문서들 중 요청된 필드가 존재하지 않는 경우
    • 하나 이상의 버킷에 대해 쿼리와 일치하는 문서가 존재하지 않는 경우
    • 다른 종속된 버킷에 값이 누락되 계산된 메트릭이 값을 생성할 수 없는 경우
  • 이러한 갭을 어떻게 다룰 것인가? 옵션 (gap_policy)
    • skip: 누락된 값을 스킵한다. 버킷을 건너뛰고 음 으로 사용가능한 값으로 계산을 계속 수행.
    • insert_zeros: 누락된 값을 0으로 대체한다. 파이프라인 집계계산은 정상적으로 수행.


예제

GET /apache-web-log/_search?size=0
{
  "aggs": {
    "histo": {
      "date_histogram": {
        "field": "timestamp",
        "interval": "day"
      },
      "aggs": {
        "bytes_sum": {
          "sum": {
            "field": "bytes"
          }
        },
        "sum_deriv": {
          "derivative": { // 현재 값과 이전 값을 비교하는 집계 수행
            "buckets_path": "bytes_sum"
          }
        }
      }
    }
  }
}


근삿값으로 제공되는 집계 연산

다시 한번 집계 정리

  • 버킷 집계: 특정 기준을 충족하는 문서들을 버킷으로 분류
  • 메트릭 집계: 버킷에 존재하는 문서들을 이용해 각종 통계지표 생성
  • 파이프라인 집계: 서로 다른 메트릭 집계의 출력을 연결

이 가운데 실제로 수학적인 계산을 하는 것은 메트릭 집계 뿐이다.

  • 메트릭 집계는 또 3가지로 세분화 할 수 있다.
    1. 일반적인 계산을 위한 집계 (avg, cardinality, max, min, …)
    2. 고차원 계산을 위한 집계 (geo bound, geo centroid, precentile …)
    3. 특수한 목적을 위한 집계 (stats, extended_stats, …)
  • 이 중 아래 3가지만 근사치를 이용한다.
    • Cardinality
    • Percentile
    • Percentile-rank
  • 근사치 중 Cardinality는 자주 사용된다. 해당 값이 정확하지 않을 수 있음을 주의해야한다.


왜 Cardinality 집계는 근사치를 이용할까?

  • 집계 프로세스는 앞서 이야기했듯 Coordinating node가 Data node에게 집계를 명령하고, 그 결과를 집계해 Coordinating node가 최종 집계해 결과를 반환한다.
    Untitled

  • Min, Max 같은 한 값에 대한 집계의 경우, Data node에서 Coordinating node로 문서 전체를 넘길 필요가 없다. 계산한 값만 넘기고, Coordination node에서 이를 모아 한번 더 연산하면 끝이다.

  • 문제는 Cardinality 집계이다. 이를 정확히 계산하기 위해서는 Data node에서 중복을 제거한 모든 문서를 Coordinating node로 전달하고, 이를 다 받고나서 다시 한번 중복제거를 해야만 최종 결과를 만들어낼 수 있다.


문제

  • 데이터들이 모두 네트워크를 타기 때문에 네트워크 트래픽 사용량이 크다.
  • Coordinating node는 빠른 결과 제공을 위해 중간 계산 결과를 메모리에 올리는데, Cardinality 연산은 많은 문서가 메모리에 올라감으로 메모리 사용량도 증가한다.
  • 트래픽 사용량/메모리 사용량이 이론 상 무한대로 증가할 수 있기에 이를 제어하는 것이 필요하다.

제안

  • 이를 해결하기 위해서는 3가지 중 하나를 포기하면 된다.
  • 분산환경을 포기
    • 단일 서버로 서비스 구성하고, 모든 데이터를 메모리에서 처리
    • 복잡한 연산 수행 시 빠르게 처리가 가능함
    • 처리 가능한 데이터 크기 제한, SPOF 위험
  • 실시간성 포기
    • 분산환경에서 정확한 데이터를 제공
    • 데이터 크기 제한없고, 대용량 처리할 수 있음
    • 처리 시간이 오래걸려 실시간성 서비스에 문제
  • 정확도 포기
    • 분산환경에서 데이터를 실시간으로 제공
    • 데이터 크기 제한없고, 대용량 처리할 수 있음
    • 근사치 사용해서 빠르게 처리 가능하나, 정확도가 필요한 프로그램에서는 문제가 될 수 있음

해결

엘라스틱서치는 위 제안 중 정확도 포기를 선택했다.

  • 일부 복잡한 연산(Cardinality 등)에서 근삿값을 이용해 처리한다.
  • 대부분의 경우 대용량 처리가 가능하면서 연산 결과를 빠르게 제공하는 것이 가능해졌다.
  • 근삿값을 이용하는 연산들이 있으므로 항상 이 점을 명확히 인지하고 의도에 맞게 사용해야 한다.

9장 - 엘라스틱서치와 루씬 이야기

Untitled

클러스터

데이터를 실제로 가지고 있는 노드의 모음

  • 엘라스틱서치에서 관련된 모든 노드들을 논리적으로 확장해 모드 클러스터라고 부름.
  • 클러스터 내부의 데이터만 서로 공유가 가능함.
    • 연관된 노드는 하나의 클러스터 구성원으로 연결해야 하는 것이 중요.
    • 같은 클러스터 이름으로 설정해 하나의 클러스터로 구성할 수 있음.

여러 클러스터 간 한번에 검색해야하는 경우도 있다.


노드

물리적으로 실행된 런타임 상태의 엘라스틱서치 (엘라스틱서치 인스턴스)

  • 클러스터를 이루는 구성원의 일부이며, 실제 데이터를 물리적으로 가지고 있는 단일 서버.
  • 노드 내부에 다수의 인덱스를 가지고 있으며, 그 인덱스는 다수의 문서를 가지고 있음.

같은 클러스터 내 모든 노드는 서로 다른 노드와 수시로 정보를 주고받음.

  • 노드는 각 역할에 맞게 하는 것이 다름.
  • 그 역할 가운데 한 노드가 죽거나 이럴 때를 대비해서 계속 소통하는 것이 아닐까 싶음.
    • 리더선출/Raft algorithm 같은거 사용해서 하지 않으려나 (추측)
    • 인 줄 알았는데 자체적인 알고리즘(Zen Discovery)을 사용하나 보다. 자세한 건 이 글 참조
  • 노드의 역할의 종류
    • 마스터 노드
    • 데이터 노드
    • 인제스트 노드
    • 코디네이팅 노드
  • 한 노드가 여러 역할을 할 수도 있지만, 안정적인 운영을 위해 각 역할에 맞게 분리해 운영하는 것을 권고함.
    • 특히 마스터 노드는 클러스터 전체 동작을 구성 및 제어하는 역할이기에 물리적 분리해 독립운영하는 것을 권장함.

인덱스

유사한 특성을 가지고 있는 문서들을 모아둔 컬렉션

  • 클러스터 내에서 유일한 인덱스명을 가져야 함.

원하는 만큼의 많은 문서를 저장할 수 있으며, 물리적인 샤드형태로 나눠서 다수의 노드에 저장됨.


문서

검색 대상이 되는 실제 물리적인 데이터

  • 인덱스 생성의 기본 정보단위이며 JSON으로 표시됨.

샤드

데이터를 분산해 저장하는 방식, 개념, 단위.

  • 엘라스틱서치에서 기본적으로 1개의 샤드로 데이터가 분산저장됨. 설정 가능함.
    • 인덱스 생성 시 설정하며, 한 번 샤드 수를 설정하면 변경할 수 없음
  • 복사본의 개념과 다름. 샤드의 개수만큼 인덱스 내 문서를 샤드별로 나눠 저장함.
    • 즉, 인덱스의 부분집합 개념

인덱스에 쿼리하면, 다음과 같은 과정을 거침.

  • 모든 샤드로 검색 요청이 전달되고, 각 샤드에서 1차적으로 검색함.
  • 모든 샤드의 결과를 취합해 최종 결과로 제공함.

이로써 2가지 좋은 점이 있음

  • 데이터가 수평적으로 분할되어 하드웨어 한계를 극복할 수 있다.
  • 여러 노드에서 여러 샤드를 통해 분산처리되므로 성능 및 처리량을 향상시킬 수 있다.

레플리카

샤드의 복제본.

  • 엘라스틱서치에서 기본적으로 1개의 레플리카가 생성됨. 설정 가능함.
    • 인덱스 생성 시 설정하며, 샤드와 달리 설정 후에도 자유롭게 변경이 가능
  • 복제본이라고 해서 사용안하는 것이 아님. 검색 시에 적극적으로 활용됨. (읽기 분산)

특정 노드가 문제가 생긴 경우, 페일오버 메커니즘을 통해 레플리카를 이용해 안정적인 클러스터 운영을 보장함.

  • 레플리카는 기본적으로 원본 샤드가 있는 노드가 아닌 다른 노드에서 생성되기 때문.

세그먼트

문서가 저장되는 데이터 자료구조.

  • 루씬 라이브러리를 통해 데이터가 최소단위인 토큰으로 분리됨.
  • 이 토큰은 역색인 구조로 디스크에 저장되는데, 이 저장되는 자료구조를 세그먼트라고 함.

엘라스틱 서치와 루씬

루씬 라이브러리가 핵심이며, 크게 2가지 기능을 가지고 있음.

  • IndexWriter: 데이터 색인하는 녀석
  • IndexSearch: 색인된 데이터를 검색 결과로 제공하는 녀석

루씬 인덱스: 이 2가지 기능을 가지고 색인 및 검색을 제공

  • 하나의 엘라스틱서치 샤드는 하나의 인덱스라고 말할 수 있음.
  • 즉, 하나의 샤드는 자체적으로 데이터를 색인 및 검색할 수 있는 가장 작은크기의 단일 검색 엔진.

엘라스틱서치는 이 독립적인 루씬 인덱스를 엘라스틱서치 샤드라는 형태로 확장해 서비스하는 것

  • 모든 샤드가 가지고 있는 세그먼트를 논리적으로 통합해 검색하도록 만듦.
  • 이렇게 해서 다수의 데이터를 분산저장할 수 있게 되고, 분산 클러스터를 구축하는 것.

세그먼트 기본 동작 방식

하나의 루씬 인덱스는 내부적으로 다수의 세그먼트로 구성되어 있음.

  • 검색 요청을 받으면 다수의 작은 세그먼트 조각들이 각각 검색결과 조각을 내고 이를 통해바 하나의 결과로 합쳐 응답한다.
    • 샤드 → 엘라스틱서치 개념과 비슷함
    • 이를 세그먼트 단위 검색(Per-Segment Search)이라고 함
  • 세그먼트는 실제로 색인된 데이터가 역색인 구조로 저장되어 있음

루씬에서는 세그먼트를 관리하기 위한 용도로 커밋 포인트(Commit Point) 자료구조를 사용

  • IndexSearcher커밋 포인트를 이용해 가장 오래된 세그먼트부터 차례대로 검색 후 결과를 합쳐 제공
  • 색인작업이 들어오면 IndexWriter 에 의해 색인작업이 이뤄지고, 결과물로 하나의 세그먼트가 생성됨.
    • 색인 작업마다 하나의 새로운 세그먼트가 생성되는 것. 생성된 후 커밋 포인트에 기록된다.

즉, 시간이 흐를수록 세그먼트들의 개수는 늘어나는 것인데, 너무 많이 생성되면 읽기 성능저하가 일어남.

  • 세그먼트 파일이 많아지고, 읽어봐야 하는 파일들이 많아지는 것이므로 성능이 저하될 수 있음.
  • 이를 해결하기 위해 루씬에서는 백그라운드에서 주기적으로 세그먼트 파일을 하나의 파일로 병합(Merge)함.
  • 이러한 일련의 과정이 지속적으로 반복되면 결과적으로 하나의 커다란 세그먼트만 남음.

왜이리 복잡하게 할까? 그냥 기존 세그먼트 파일에 덮어씌우면 되지 않나?

  • 세그먼트가 수정 불가능(불변)하도록 관리되기 때문.
  • 왜 불변으로 관리되는가?
    • 동시성 문제를 해결: Lock을 사용하지 않아도 됨
    • 시스템 캐시를 적극적으로 활용, 높은 캐시 적중률: OS 커널에서 캐시할 때 불변성이면 캐시를 삭제할 필요가 없음. Merge 할 땐 캐시가 업데이트되어야 하지 않으려나.
    • 리소스 절감: 쓰기작업에서 역색인을 새로 구성해야 하는데, 많은 시스템 리소스가 소모됨, 실시간 반영에 딜레이.

수정/삭제는 어떻게 일어나는가?

  • 수정
    • 세그먼트 불변성을 유지하기 위해 데이터를 삭제 후 다시 추가하는 방식으로 진행됨
    • 기존 데이터는 삭제되며 검색 대상에서 제외되고, 새로운 세그먼트가 추가되어 검색 대상에 포함됨.
    • Q) 새로운 세그먼트를 추가한다? 아예 전체적인 세그먼트에 대하여 재색인 과정?
  • 삭제
    • 문서를 바로 삭제하지 않음. 삭제 플래그를 이용해 삭제처리 함.
    • Merge 작업이 일어날 때, 삭제 플래그가 작성된 파일들을 한 번에 제거
      • 제거하려면 전체 역색인 구조 뒤져서 Term 별로 제거 과정을 수행해야하는데, 너무 Overhead가 큼.
      • Merge 작업할 때 어차피 재구조 과정이 있으므로, 이때 제거.

루씬 Flush, Commit, Merge

Flush: 읽기 가능하게 만드는 작업

  • 인 메모리 버퍼 → 버퍼 차면 세그먼트 형태로 생성
  • fsync 대신 write 방식을 통해 쓰기 과정 수행
    • 디스크 대신 시스템 캐시에 쓰는 것

Commit: 쓰기 작업 ⇒ Flush

  • 시스템 캐시에 작성해놓은 내용을 실제 디스크에 쓰는 것 (fsync)
  • 캐시는 날라갈 수 있으므로, 일정 주기로 실행해 물리적인 디스크로 기록 작업을 해야 함.
  • 직접 디스크 기록단계이므로 많은 리소스가 필요함

Merge: 세그먼트 통합 작업 ⇒ Optimize API

  • 만들어진 여러 세그먼트를 하나의 세그먼트로 통합하는 작업
  • 이 과정에서 삭제처리된 데이터가 실제로 제거됨
  • 검색할 세그먼트 개수가 줄어들어 검색 성능도 좋아짐

엘라스틱서치 Refresh, Flush, Optimize API

엘라스틱서치 Refresh (루씬 Flush)

  • 샤드 별로 Refresh 작업 수행
  • 기본적으로 1초마다 한 번씩 Refresh 작업 수행
    • 이를 통해 실시간 검색에 가깝게 동작할 수 있음
  • refresh_interval 필드를 통해 설정이 가능
    • 대량의 데이터 색인 시, 블로킹 될 수 있어 색인 기능이 다운될 수 있음.
    • 이에 따라 -1 로 설정하여 잠시 비활성화 시켜놓을 수 있다.
  • Translog를 작성하는 것도 수반됨.
    • Translog는 샤드의 장애복구를 위해 제공되는 특수한 파일
    • 샤드에 일어나는 모든 변경사항을 Translog에 먼저 기록한 후 내부에 존재하는 루씬을 호출 (WAL과 유사)
    • Commit 하지 않았을 때 장애가 나면, 이 Translog를 읽어 복구할 수 있는 것.

엘라스틱서치 Flush (루씬 Commit)

  • 루씬의 Commit을 작업하고 새로운 Translog를 시작
  • Commit을 통해 디스크에 지금까지의 변경사항이 기록됨.
  • 즉, 지금까지의 Translog는 디스크에 기록되었기에 이를 정리함.
  • 엘라스틱서치에서는 기본적으로 5초에 한 번씩 Flush 작업 수행.

엘라스틱서치 Optimize API (루씬 Merge)

  • 세그먼트를 하나로 합치는 일명, forced merge API를 제공함.
    • 루씬 Merge를 강제로 일으킬 수 있다는 것.

엘라스틱서치 샤드 최적화

운영 중에 샤드의 개수를 수정하지 못하는 이유?

  • 샤드의 개수를 조정한다는 것 == 프라이머리 샤드의 개수를 변경한다는 것
  • 프라이머리 샤드의 개수를 조정한다는 것 == 독립적인 루씬이 가지고 있는 데이터를 모두 재조정한다는 것
  • 이 과정에서 여러 세그먼트를 가져와 합치는 작업을 수행하는 등 작업을 해야지만 확장이 가능할텐데, 세그먼트는 불변함.
  • 이런 이유들로 인해 변경할 방법이 없다. (제공하지 않는다)

레플리카 샤드의 복제본 수는 얼마가 적당할까?

  • 데이터가 추가될 때, 프라이머리 샤드/레플리카 샤드 모두 동일하게 세그먼트 생성 과정을 거치게 된다.
    • 즉, 레플리카가 많아질수록 색인 성능은 이에 비례해 떨어질 수 밖에 없음.
  • 읽기 분산이 중요하다? ⇒ 색인 성능 포기하고, 레플리카 세트 수 증가시키자.
  • 빠른 색인이 중요하다? ⇒ 읽기 분산을 포기하고, 레플리카 세트 수를 최소화하자.

레플리카 수는 앞서 이야기했듯 운영 중 언제든 변경이 가능하다.

  • 그러므로 서비스 오픈 시, 레플리카 수를 최소화해서 시작하는 것이 좋다.

클러스터 내 운영 가능한 최대 샤드 수는?

  • 기본적으로 1개의 프라이머리 샤드로 설정되는데, 최대 수는 현재 1024개로 제한하고 있다.
  • ES_JAVA_OPTS="-Des.index.max_number_of_shards 환경변수로 이 제한을 따로 설정할 수도 있다.

클러스터에 많은 수의 샤드가 존재할 경우?

클러스터 내 모든 샤드는 마스터 노드에서 관리된다.

  • 즉, 샤드가 많아질수록 마스터 노드의 부하도 증가함.
    • 빠른 처리를 위해 샤드 정보와 같은 데이터를 모두 메모리에 올려 제공하기 때문.
    • 참고) 마스터 노드의 역할
      • 모든 노드/샤드 관리
      • 노드 상태 모니터링하면서, 색인/검색 요청 라우팅 처리
      • 장애 발생 시, 레플리카 이용해 샤드 복구
  • 그러므로 너무 많은 샤드로 인해 마스터 노드의 메모리가 부족해지지 않도록 주의해야 함

샤드의 물리적인 크기와 복구시간

마스터 노드 입장에서는 샤드가 가지고 있는 데이터 수보다 데이터의 물리적인 크기가 더 중요하다.

  • 장애가 발생할 경우, 샤드 단위로 복구를 수행하기 때문.

일단 장애가 발생하면,

  • 프라이머리 샤드와 동일한 데이터를 가지고 있는 레플리카 샤드를 순간적으로 프라이머리 샤드로 전환
  • 기존에 레플리카 샤드(전환된 샤드)와 물리적으로 다른 장비에 레플리카 샤드로 새롭게 생성
  • 즉, 샤드 단위로 데이터가 이동하기 때문에 샤드의 크기가 클수록 복구 작업에 부정적인 영향을 끼칠 수 있음.

하나의 인덱스에 생성 가능한 최대 문서 수는?

  • number_of_shards 옵션에 따라 인덱스당 생성할 수 있는 최대 샤드는 기본적으로 1024개

  • 개별 샤드가 가질 수 있는 최대 문서 수

    • 세그먼트가 문서를 몇 개까지 저장할 수 있는가? 를 보면 된다.
    • java.lang.Integer.MAX_VALUE - 127 . 즉, 20억개 정도를 저장할 수 있음.
  • 즉, 1024 * 20억 = 약 2조.

    • 이론적으로 엘라스틱서치에서 생성할 개별 인덱스가 가질 수 있는 최대 문서는 약 2조개다.
    • 당연히 이론 상의 숫자 일 뿐 적절한 문서 수를 계산해 유지하는 것이 좋음.

안정적인 클러스터 운영을 위해서는 자신의 환경에 맞게 다방면에 걸친 충분한 테스트가 필요.

  • 최적의 인덱스 수나 최적의 샤드 수를 구하는 공식 같은 것은 현실에 존재하지 않는다.
  • No silver bullet.

7장 - 한글 검색 확장 기능

Suggest API가 키워드 자동완성을 지원하지만 한글 키워드 대상에서는 제대로 동작하지 않는다.

  • 한글은 유니코드 상 자모가 융합되며 새로운 유니코드로 변하기 때문에 동작하지 않을 것으로 보인다.
  • 고로 직접 자동완성을 구현해야 한다.

Suggest API는 4가지 방식을 제공한다.

  1. Term Suggest API: 추천 단어 제안 (유사한 단어)
  2. Completion Suggest API: 자동완성 제안 (검색어 예측)
  3. Phrase Suggest API: 추천 문장 제안
  4. Context Suggest API: 추천 문맥 제안

Term Suggest API

Edit distance(편집거리)를 이용해 비슷한 단어를 제안

  • 두 문자열 사이의 편집 횟수를 계산해 편집거리를 계산
  • 한글의 경우, 입력하며 유니코드가 계속 변하기에 분석하기 위해서는 자소를 분리해 계산해야 한다.

ICU 분석기를 통해 한글 오타 교정 가능

  • ICU는 국제화 처리를 위해 개발된 분석기로, 내부에 한국어 자소 분해/융합 기능을 가지고 있음.

  • 간단한 한글 처리는 가능하나, 한글 기반으로 정교한 오타 교정 등 처리하기 위해선 전문적인 플러그인 개발이 요구됨.

  • 다나와에서는 이를 이용해 오타교정을 한 사례가 있음.

    엘라스틱서치 오타교정 API 만들어보기


Completion Suggest API

검색을 효율적으로 돕기 위해 자동완성 기능을 제공

  • 응답속도가 매우 중요해서, 내부적으로 FST(Finite State Transducer)를 사용함. 메모리에 올려 서비스하는 구조.
  • 검색 시 로드해 처리하기엔 비용이 커서, 성능 최적화를 위해 색인 중에 FST를 작성함.

Example

PUT movie_term_completion
{
	"mappings": {
		"_doc": {
			"properties": {
				"movieNmEnComple": {
					"type": "completion"
				}
			}
		}
	}
}

POST movie_term_completion/_search
{
	"suggest": {
		"movie_completion": {
			"prefix": "l",
			"completion": {
				"field": "movieNmEnComple",
				"size": 5
			}
		}
	}
}

일반적으로 전방일치(prefix)만 가능함. 실 환경과 맞지 않음.

부분일치를 해야한다면, 부분일치 하고자 하는 부분을 분리해 색인 데이터를 구성해야 함.


  • 배열 형태로 데이터를 분리해 저장

    PUT movie_term_completion/_doc/1
    {
    	"movieNmEnComple": {
    		"input": ["After", "Love"]
    	}
    }

맞춤법 검사기

오타 교정, 한영/영한 오타 교정 등을 Analyzer를 직접 구현해 사용할 수 있다.

책에서는 직접 구현하진 않고, 구현해놓은 소스를 이용하여 테스트 한다.


PUT /company_spellchecker
{
  "settings": {
    "index": {
      "analysis": {
        "analyzer": {
          "korean_spell_analyzer": {
            "type": "custom",
            "tokenizer": "standard",
            "filter": [
              "trim",
              "lowercase",
              "javacafe_spell"
            ]
          }
        }
      }
    }
  }
}

PUT /company_spellchecker/_doc/_mappings
{
    "properties": {
      "name": {
        "type": "keyword",
        "copy_to":["suggest"]
      },
      "suggest": {
        "type": "completion",
        "analyzer": "korean_spell_analyzer"
      }
    }
}

--- 검색 ---

POST /company_spellchecker/_doc/_search
{
	"suggest": {
		"my-suggestion": {
			"text": "샴성전자",
			"term": {
				"field": "suggest"
			}
		}
	}
}

프로세스

  • 오타 교정 API를 실행하고, 결과를 가지고 실제 인덱스를 검색한다.
  • 즉, 먼저 교정 인덱스를 통해 교정 값(교정됬든/안됬든)을 만들어서, 이를 실제 인덱스에 쿼리하는 식으로 구성된다.

교정 과정에서 중요한 것

  • 키워드 자체가 존재하지 않는다면, 오차교정이 불가능하다.
  • 즉, 검색 결과가 0인 경우를 늘 모니터링하고 이를 수정해나가야 검색 품질을 높일 수 있다.

직접 구현해보기

Completion Suggest API를 이용하면 간단하게 자동완성을 이용할 수 있다.

  • Completion Suggest API로 생성된 인덱스는 메모리에 올려 캐시로 생성/연산 수행하므로 빠르다.

하지만 앞서 이야기했듯 한글은 유니코드 변함의 이슈로 인해 제대로 지원되지 않는다.

  • 단순히 자동완성 뿐 아니라 초성검색/한영오타 감지 등을 제대로 지원할 수 없다.
  • 또한, Completion Suggest API는 Prefix 매칭만 지원하기에 더 많은 기능을 지원하는데 어려움이 있다.

이를 해결하기 위해 루씬에서 제공하는 다양한 분석기능(API)을 활용할 수 있다.

  • 하지만 루씬에서 제공하는 것은 메모리 연산은 아니기에 속도 측면에서 손해를 보는 것은 감당해야 한다.

부분일치 구현하기

  • 부분일치를 구현하는 제일 쉬운 방식은 Ngram을 이용하는 것이다.
  • 한글자 한글자 단위로 잘라내어 토큰화 하기에 누락 없는 부분일치를 구현하기 좋다.

대표적인 Ngram 3가지

  • (일반) Ngram 분석기 - 음절 단위로 토큰을 생성. 재현율은 높으나 정확도는 떨어짐.
    • 아, 아버, 아버지, 아버지가, 버, 버지, 버지가, …
  • Edge Ngram 분석기 - 문장의 한 글자부터 더해가는 형태
    • 아, 아버, 아버지, 아버지가
  • Edge Ngram Back Analyzer - Edge Ngram의 반대
    • 아, 버, 아버, 지, 아버지, 가, 지가, 버지가, 아버지가


💡 복습
Analyzer
Tokenizer + Token Filter. 데이터가 들어오면 검색 가능하도록 정제/가공

Tokenizer
정해진 Separator로 토큰을 분리하는 역할.
Analyzer에는 하나 이상의 Tokenizer가 있어야만 함
****ex) Whitespace Tokenizer

Token Filter
Tokenizer로 분리된 토큰을 가공하는 역할.
ex) Lowercase filter



초성 검색을 지원하고 싶다?

  • 초성만 추출한 다음 추출된 내용을 ngram으로 분해하면 된다.

https://techblog.woowahan.com/7425/

IMG_5225

nori_tokenizer 실습

https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-nori-tokenizer.html

image

user_dictionary는 수정해도 바로 반영되지 않음.

  • 인덱스가 정의될 때, user_dictionary를 읽음. 로딩된 이후로는 다시 읽어들이지 않는다.
  • 고로, 업데이트를 위해서는 해당 인덱스의 node를 재시작하거나, index를 새로 생성하거나, _close/_open 하면 된다.

하지만 이 방식을 적용해도 이미 인덱싱 된 문서에는 적용되지 않는다.

  • 왜? 문서가 추가될 때 analyzer를 통해 인덱싱 되기 때문이다. 데이터가 들어갈 때 있던 analyzer는 업데이트 이전의 user_dictionary를 이용해 토큰을 생성해두었기 때문.
  • 만약 기존의 문서에도 적용하고 싶다면 문서를 재생성(업데이트)해줘야 한다. update_by_query api 활용.

10장 - 대용량 처리를 위한 시스템 최적화

노드 실행환경과 JVM 옵션

OpenJDK 기준

지원 매트릭스

  • ~ 7.17까지 Java 8 지원
  • 6.5 ~ 7.17까지 Java 11 지원
  • 7.15 부터 Java 17 지원
  • 8.10 부터 Java 21 지원
  • 즉, 최근 ES 버전을 사용한다면 Java 17, 21 지원이 필요하다.

근데 사실 최근에 와서는 Container를 사용하고, 이에 따라 따로 Java를 따로 설치해야할 필요가 있을까 싶다.

  • 8.10.2 Elasticsearch container image는 어떤 Java/Version을 사용할까?

    • Oracle 20.0.2 사용
    IMPLEMENTOR="Oracle Corporation"
    JAVA_RUNTIME_VERSION="20.0.2+9-78"
    JAVA_VERSION="20.0.2"
    JAVA_VERSION_DATE="2023-07-18"

현재 버전 유지보수는 해줄까? EOL이 언제까지인가?

Elastic 제품 단종일

  • 6 버전 만료
  • 7 점대는 9 버전 나오면 만료.
  • 8점대는 9점대가 나온 이후에 단종될 예정

ES/Lucene도 어찌됬든 Java로 만들어진 프로젝트들.

  • 그렇기에 JVM 옵션/최적화가 필요하다.
  • GC도 관리해야 한다… 흑…

ES는 다년간의 경험으로 적합한 JVM 옵션을 사용하고 있다.

Advanced configuration | Elasticsearch Guide [8.11] | Elastic

  • 만약 업데이트 하고 싶다면,

    • Docker의 경우, Bind mount custom JVM options files into /usr/share/elasticsearch/config/jvm.options.d/.
    • ES_JAVA_OPTS 환경변수 이용
  • root의 jvm.options 파일은 건드리지 말고 환경 별 jvm.options.d/ 를 사용하라고 가이드하고 있다.

  • 8.10.2 docker file 기준으로 아래와 같이 jvm.options 가 설정되어 있다.

    -XX:+UseG1GC
    
    ## JVM temporary directory
    -Djava.io.tmpdir=${ES_TMPDIR}
    
    # Leverages accelerated vector hardware instructions; removing this may
    # result in less optimal vector performance
    20:--add-modules=jdk.incubator.vector
    
    ## heap dumps
    
    # generate a heap dump when an allocation from the Java heap fails; heap dumps
    # are created in the working directory of the JVM unless an alternative path is
    # specified
    -XX:+HeapDumpOnOutOfMemoryError
    
    # exit right after heap dump on out of memory error
    -XX:+ExitOnOutOfMemoryError
    
    # specify an alternative path for heap dumps; ensure the directory exists and
    # has sufficient space
    -XX:HeapDumpPath=data
    
    # specify an alternative path for JVM fatal error logs
    -XX:ErrorFile=logs/hs_err_pid%p.log
    
    ## GC logging
    -Xlog:gc*,gc+age=trace,safepoint:file=logs/gc.log:utctime,level,pid,tags:filecount=32,filesize=64m
  • 전체 옵션을 보고 싶다면, GET _nodes/_all/jvm 으로 확인해볼 수 있다.

    {
      "_nodes": {
        "total": 1,
        "successful": 1,
        "failed": 0
      },
      "cluster_name": "docker-cluster",
      "nodes": {
        "JqgoQyD3QwidYsJXMonygw": {
          "name": "es",
          "transport_address": "192.168.228.2:9300",
          "host": "192.168.228.2",
          "ip": "192.168.228.2",
          "version": "8.10.2",
          "transport_version": 8500061,
          "build_flavor": "default",
          "build_type": "docker",
          "build_hash": "6d20dd8ce62365be9b1aca96427de4622e970e9e",
          "roles": [
            "data",
            "data_cold",
            "data_content",
            "data_frozen",
            "data_hot",
            "data_warm",
            "ingest",
            "master",
            "ml",
            "remote_cluster_client",
            "transform"
          ],
          "attributes": {
            "ml.max_jvm_size": "536870912",
            "ml.config_version": "10.0.0",
            "xpack.installed": "true",
            "transform.config_version": "10.0.0",
            "ml.machine_memory": "1073741824",
            "ml.allocated_processors": "10",
            "ml.allocated_processors_double": "10.0"
          },
          "jvm": {
            "pid": 121,
            "version": "20.0.2",
            "vm_name": "OpenJDK 64-Bit Server VM",
            "vm_version": "20.0.2+9-78",
            "vm_vendor": "Oracle Corporation",
            "using_bundled_jdk": true,
            "start_time_in_millis": 1699961395643,
            "mem": {
              "heap_init_in_bytes": 536870912,
              "heap_max_in_bytes": 536870912,
              "non_heap_init_in_bytes": 7667712,
              "non_heap_max_in_bytes": 0,
              "direct_max_in_bytes": 0
            },
            "gc_collectors": [
              "G1 Young Generation",
              "G1 Concurrent GC",
              "G1 Old Generation"
            ],
            "memory_pools": [
              "CodeHeap 'non-nmethods'",
              "Metaspace",
              "CodeHeap 'profiled nmethods'",
              "Compressed Class Space",
              "G1 Eden Space",
              "G1 Old Gen",
              "G1 Survivor Space",
              "CodeHeap 'non-profiled nmethods'"
            ],
            "using_compressed_ordinary_object_pointers": "true",
            "input_arguments": [
              "-Des.networkaddress.cache.ttl=60",
              "-Des.networkaddress.cache.negative.ttl=10",
              "-Djava.security.manager=allow",
              "-XX:+AlwaysPreTouch",
              "-Xss1m",
              "-Djava.awt.headless=true",
              "-Dfile.encoding=UTF-8",
              "-Djna.nosys=true",
              "-XX:-OmitStackTraceInFastThrow",
              "-Dio.netty.noUnsafe=true",
              "-Dio.netty.noKeySetOptimization=true",
              "-Dio.netty.recycler.maxCapacityPerThread=0",
              "-Dlog4j.shutdownHookEnabled=false",
              "-Dlog4j2.disable.jmx=true",
              "-Dlog4j2.formatMsgNoLookups=true",
              "-Djava.locale.providers=SPI,COMPAT",
              "--add-opens=java.base/java.io=org.elasticsearch.preallocate",
              "-Des.cgroups.hierarchy.override=/",
              "-XX:+UseG1GC",
              "-Djava.io.tmpdir=/tmp/elasticsearch-15691817446927015418",
              "--add-modules=jdk.incubator.vector",
              "-XX:+HeapDumpOnOutOfMemoryError",
              "-XX:+ExitOnOutOfMemoryError",
              "-XX:HeapDumpPath=data",
              "-XX:ErrorFile=logs/hs_err_pid%p.log",
              "-Xlog:gc*,gc+age=trace,safepoint:file=logs/gc.log:utctime,level,pid,tags:filecount=32,filesize=64m",
              "-Xms512m",
              "-Xmx512m",
              "-XX:MaxDirectMemorySize=268435456",
              "-XX:G1HeapRegionSize=4m",
              "-XX:InitiatingHeapOccupancyPercent=30",
              "-XX:G1ReservePercent=15",
              "-Des.distribution.type=docker",
              "--module-path=/usr/share/elasticsearch/lib",
              "--add-modules=jdk.net",
              "--add-modules=org.elasticsearch.preallocate",
              "-Djdk.module.main=org.elasticsearch.server"
            ]
          }
        }
      }
    }

힙 크기를 32GB 이하로 유지해야하는 이유

JVM heap size

  • 책에서는 JVM heap 크기가 1GB로 설정되어있으니, 좀 더 높게 설정하라고 설명되어있다. 아마 근데 옛날 버전을 기반으로 한 책이다보니 오래된 사항으로 보인다.
  • jvm.options 파일도 그렇고, 공식문서에도 현재 사용가능한 메모리를 기반으로 자동으로 설정되니 업데이트 하지 않아도 됨을 명시하고 있다.

By default, Elasticsearch automatically sets the JVM heap size based on a node’s roles and total memory. Using the default sizing is recommended for most production environments.


**-Xms, -Xmx 의 크기를 같게 설정해야 한다.**


운영체제의 50% 메모리 공간을 보장하자.

  • 즉, 현재 전체 메모리(Container 환경이라면 할당된 전체 메모리)의 50% 이하로 Heap size를 잡으라는 것.
  • 내부 Lucene에서 시스템 캐시를 이용하는데, 이를 지원할 수 있도록 남은 메모리 공간을 확보해두어야 한다. 그래서 적어도 50% 정도는 보장할 수 있어야 한다.

Heap 사이즈를 Compressed ordinary object pointers(oops) threshold보다 높게 설정하지 말자.

Object Pointer는 개체의 메모리 번지를 표현하는 주소값.

  • Java에서는 객체를 Heap 영역에서 관리하기에 이 객체를 가리킬 포인터를 가지고 있다. 이 포인터의 주소를 Ordinary Object Pointer(OOP)라는 특수한 자료구조로 만들어 관리한다.
  • 초기 64비트 시스템에서 하나의 포인터를 사용하기 위해 64비트가 필요했는데, 많은 메모리 공간 낭비가 발생했으며, 캐시 간 주소 값들을 이동시킬 때도 64비트를 이용해야했기에 큰 대역폭 소모가 발생했다.
  • 그래서 Java의 경우, 64비트 머신에서의 성능향상과 효율적인 메모리 사용을 위해 Compressed OOP 개념을 도입했다.
    • Object의 주소 대신 주소의 오프셋을 가리킨다. 8의 n배수로 계산되어 기존의 OOP보다 8배 더 많은 주소를 표현할 수 있다.

Untitled

  • 32비트 Compressed OOP에서 최대 32GB 메모리 공간을 가리킬 수 있게 되었고, 64비트 시스템에서 Compressed OOP를 사용할 경우, 32비트 포인터를 사용해 동작한다.
    • 하지만 JVM 힙 크기가 32GB를 넘어가는 순간 64비트 OOP로 자동 변환한다. 그래서 기존에 Compressed OOP에서 얻는 이점을 모두 잃어버린다.
      • 만약 일반적인 OOP 방식으로 같은 효율을 내려면 대략적으로 40~50GB의 추가적인 메모리가 필요하다.
    • 또한, 힙 크기가 크면 실시간성이 중요한 서비스인데도 FullGC를 수행하는 시간이 늘어나서 STW 시간도 늘어나기에 문제가 될 수 있다.
      • FullGC는 현재 사용중인 모든 힙 영역을 수집 대상으로 삼기에 메모리키가 커지면 커질수록 FullGC 걸리는 시간도 늘어난다. 또한 FullGC는 STW 방식으로 동작하기에 해당 시간동안 모든 스레드는 정지되어 더 큰 문제를 초래할 수 있다.

[Java] Compressed Class Space와 Compressed Object Pointer

  • 그래서 수백GB의 메모리를 가진 서버가 있다고 해도, 32GB 이상 설정하는 것은 좋지 않고, 오히려 2개의 ES를 띄우는 방식이 더 좋을 수 있다.

  • Compressed ordinary object pointers threshold가 항상 정확하진 않으나 26GB 정도가 안전하며, 특정 시스템에서는 30GB정도 되는 경우도 있다.

    Set Xms and Xmx to no more than the threshold for compressed ordinary object pointers (oops). The exact threshold varies but 26GB is safe on most systems and can be as large as 30GB on some systems.

  • 사용하고 있는지 알고 싶다면, 엘라스틱로그에서 아래 로그를 확인하거나 위에서 언급한 jvm option 조회에서 jvm.using_compressed_ordinary_object_pointers 를 확인하면 된다.

    heap size [1.9gb], compressed ordinary object pointers [true]

Zero based Compressed OOP

  • Compressed OOP를 사용해야하는 상황이 오면, Heap 영역 시작주소를 0에서 부터 시작하도록 요청함.
  • 0부터 시작하면 Compressed OOP에서 shift 외의 시작 번지수 주소 더하기 연산이 필요없어져서 더 효율적으로/빠른 성능으로 계산해낼 수 있는 것.

엘라스틱서치와 가상메모리, 메모리 스와핑

가상 메모리

  • 물리적인 메모리보다 많은 양의 메모리를 사용할 수 있도록 운영체제가 제공하는 대표적인 메모리 관리 기술

메모리 스와핑

  • 운영체제에서는 효율적인 메모리 관리를 위해 메모리와 디스크 간에 데이터를 교환하는 작업을 수행함. 이게 메모리 스와핑.
  • 스와핑하는 순간에는 시스템 성능이 떨어진다. 그러므로 리소스가 충분한 상황에선 되도록 스와핑이 일어나지 않도록 설정하는 것이 안전함.
  • 반복적인 Swap in/out은 시스템의 성능을 급격하게 나쁘게 만들 수 있다.

적은 메모리로도 프로그램이 돌아가고 있다?

  • 작은 메모리로도 충분히 실행될 수 있기 때문.
  • 가상 메모리의 데이터를 나눠 반드시 필요한 부분은 물리 메모리에 로드하고, 나머지 데이터들은 디스크에 임시로 저장한다.
  • 동작하며 메모리 상 필요한 부분들이 변경되고, 이에따라 메모리와 디스크 간에 데이터 교환(스와핑)이 반복적으로 일어난다.

Lucene은 내부적으로 Java에서 제공하는 NIO를 이용하여 운영체제 커널에서 제공하는 mmap 시스템 콜을 직접 호출함.

  • 이에 VM을 거치지 않고 직접 커널 모드로 진입할 수 있어 높은 성능을 낼 수 있으며, 파일 시스템 캐시(커널 러벨 메모리)의 이점을 볼 수 있다.
  • 엘라스틱서치는 Java heap 메모리도 사용할 수 있고, 운영체제에 할당된 물리 메모리도 사용할 수 있는 일석이조 결과를 누리게 됨
  • mmap 개수가 부족하면 운영중 메모리부족으로 예외가 발생할 수 있음. 262,114 이상으로 설정해 안정적으로 운영되도록 설정 필요할 수 있음.

ES에서 스와핑은 노드 안정성에 치명적임. 그래서 최대한 피해야 함.

Disable swapping | Elasticsearch Guide [8.11] | Elastic

  • 강제로 죽어서 제외되는 편이 낫다. 계속 연결됨과 끊어짐이 반복되는 현상이 발생하는 것은 막아야 함.
  • 시스템이 노드 전용이라면 swapoff -a 등을 통해 스와핑을 비활성화하는게 제일 좋음.

시스템 튜닝 포인트

ulimit 명령어

[Linux] 5분이면 가능! ulimit 확인 및 설정 방법(feat. open files)

  • 어플리케이션이 얼마만큼의 리소스를 할당받을 수 있는지 리소스 상황 지켜볼 수 있음.
  • ulimit에 설정된 값 이상의 리소스를 사용할 수 없음.

sysctl 명령어

sysctl 명령어

  • 리눅스 내부에 존재하는 커널의 파라미터 조절

11장 - 장애 방지를 위한 실시간 모니터링

11.1 클러스터 Health 체크

GET /_cluster/health 를 통해 Health 체크를 지원함.

Cluster health API | Elasticsearch Guide [8.11] | Elastic

{
  "cluster_name" : "testcluster",
  "status" : "yellow",
  "timed_out" : false,
  "number_of_nodes" : 1,
  "number_of_data_nodes" : 1,
  "active_primary_shards" : 1,
  "active_shards" : 1,
  "relocating_shards" : 0,
  "initializing_shards" : 0,
  "unassigned_shards" : 1,
  "delayed_unassigned_shards": 0,
  "number_of_pending_tasks" : 0,
  "number_of_in_flight_fetch": 0,
  "task_max_waiting_in_queue_millis": 0,
  "active_shards_percent_as_number": 50.0
}
  • 각종 노드/샤드 개수들이 출력되고, 제일 중요한 현재 클러스터 상태가 출력된다.
  • status 는 클러스터의 상태를 나타내며 green, yellow, red 값을 가진다.
    • green: 클러스터의 모든 샤드가 정상임
    • yellow: 프라이머리 샤드는 정상 할당됬으나, 일부 레플리카 샤드가 정상적으로 할당되지 못함
    • red: 일부 프라이머리 샤드가 정상적으로 할당되지 못함. 클러스터가 시작하며 프라이머리가 아직 할당되지 않았을 때 잠시 red 상태가 될 수 있음
  • 여러 파라미터를 지원한다.
    • 인덱스 레벨의 Health check를 하고 싶다면, level=indices 를 붙이면 된다.
    • 샤드 레벨의 Health check를 하고 싶다면, level=shard 를 붙이면 된다.
  • 아예 특정 인덱스만을 대상으로 Health check를 하고 싶다면, GET /_cluster/health/{index_name} 으로 하면 된다.
    • 특정 인덱스만을 대상으로 Health check/정보가 주어지기 때문에 멀티테넌시 구조로 구축할 때 활용할 수 있다.

11.2 물리적인 클러스터 상태 정보 조회

엘라스틱서치는 실행 시 config 디렉터리에 설정된 환경설정 정보를 바탕으로 노드를 인스턴스화한다.

이 때, 사용 가능한 리소스를 적절히 분산해 각 모듈에서 나눠 사용할 수 있도록 자동으로 리소스를 분배한다.

클러스터가 구성되면 물리적인 노드가 실제로 어떤 설정을 가지고 있으며, 사용 중인 리소스가 어떤지 상태정보 API를 통해 확인할 수 있다.


클러스터 레벨 물리상태 조회

  • GET /_cluster/state
  • 클러스터 구성의 기본이 되는 metadata 정보, routing_table 정보, Restore/Snapshot 정보 등을 제공

노드 레벨 물리상태 조회

  • GET /_nodes
  • 노드가 실행 될 때, 실제로 어떤 설정을 가지고 인스턴스가 생성됬는지 살펴볼 수 있음
    • 상태정보를 조회하면 전체노드를 대상으로 정보조회가 되기 때문에 엄청나게 많은 정보가 제공된다.
  • 그래서 특정 노드의 정보만을 조회할 수 있는 API가 제공된다.
    • GET /_nodes/{node ip}
    • GET /_nodes/{node id}
    • GET /_nodes/_local : 실제로 API 요청을 받았었던 노드만을 대상으로 정보를 조회할 수 있음
    • 더 많은 노드 선택법은 문서 참고

제공되는 정보는 다음과 같다.

  • Settings 정보 (settings): elasticsearch.yml 에 설정된 설정사항을 보여준다.
    • 수정사항 반영이 잘 되었는지 확인하려면 해당 정보를 확인하면 된다.
  • OS 정보 (os): 인스턴스가 실행된 운영체제 정보
  • Process 정보 (process): 인스턴스 생성 시, Memory lock이 수행됬는지 알 수 있음
  • JVM 정보 (jvm): 설정된 JVM 옵션들
  • 스레드풀 정보 (thread_pool): 설정된 스레드풀 관련 정책. 적절한 스레드를 생성하도록 CPU 코어 개수 기반으로 자동 설정
  • Transport 정보 (transport): 클러스터 내부의 노드 간의 통신을 위해 transport 모듈 이용. 어떤 포트 바인딩 되었는지 확인 가능.
  • HTTP 정보 (http): 클러스터 외부 통신을 위해 http 모듈 이용. 어떤 포트 바인딩 되었는지 확인 가능.
  • 플러그인 정보 (plugins): 설치된 플러그인 목록 확인가능
    • 한글 Nori 모듈과 초성검색 등 한글 검색을 위한 필터 모듈을 설치한 경우, 아래와 같이 출력.
    • Untitled
  • 모듈 정보 (modules): 어떤 모듈 동작하고 있는지 확인 가능

11.3 클러스터에 대한 실시간 모니터링

elasticsearch에서는 실시간 모니터링을 위해 여러 기준별로 상세한 정보를 API로 제공한다.

  • 클러스터 레벨의 실시간 모니터링: GET /_cluster/stats
  • 노드 레벨의 실시간 모니터링: GET /_nodes/stats
  • 인덱스 레벨의 실시간 모니터링: GET /_stats

상세해서 레벨마다 많은 양의 데이터를 제공한다. 필요할 때마다 찾아보는게 좋을 듯.

대충 아래와 같은 정보들을 원한다면 찾아보면 된다.

  • 클러스터/노드 내 총 인덱스 개수, 디스크 용량
  • 루씬 세그먼트가 사용 중인 메모리 정보
  • 클러스터 노드 CPU/JVM/OS/Network 등 정보
  • 노드 별 인덱스, Get/Search/Merge/Refresh/Flush 등 수행 통계 정보
  • 루씬 segment 작업 수행정보, Translog 작업 수행정보, 캐시/메모리/가상메모리 관련 정보
  • JVM thread/gc/buffer_pools 등 정보
  • 스레드풀 및 파일 시스템 상세정보
  • 서킷브레이커 설정정보

11.4 Cat API를 이용해 콘솔에서 모니터링

Compact and aligned text (CAT) APIs | Elasticsearch Guide [8.11] | Elastic

cat? 리눅스 cat에서 영감받아 만들어진 API로 콘솔에서 모니터링 하기 위한 목적으로 만들어졌음.

  • REST API랑은 무엇이 다른가? ⇒ JSON이 아닌 콘솔 친화적인 형태로 응답이 온다.

결과를 보면 대충 다음과 같다.

Untitled 1

  • 다양한 종류의 API를 제공하므로 위 문서를 참고하자.

여러 파라미터를 지원한다.

  • help: API에 대한 정보
  • v: 헤더 정보 포함여부. v=true 시, 헤더가 추가되어 전달된다.
  • h: 특정 헤더만 포함시키고 싶은 경우
  • format: text, yaml 등 다양한 결과 포멧을 지원한다.
    • yaml format으로 요청한 경우

Untitled 2