Elasticsearch Master node scale in

Elasticsearch를 관리 운영을 하다 보면 안정성에 너무 신경 쓴 나머지 실제 부하보다 너무 많은 마스터 노드를 구성하게 되어 나중에 마스터 노드를 불 필요하게 많이 설정을 했다는 것을 깨닫게 되는 순간이 올 수 있습니다. 물론 필요한 만큼 마스터 노드를 구축했지만 회사의 자금 사정으로 노드의 개수를 줄여야 되는 순간도 발생할 수 있고요 (현재 저의 상황입니다.) ...  이럴 때 아무 생각 없이 마스터 노드의 개수를 줄이면 다음과 같은 에러 master not discovered or elected yet, an election requires at least 3 nodes with ids from 를 출력하면서 Elasticsearch의 클러스터에 문제가 발생할 수 있는데요. 이번 문서에서는 뜻하지 않게 마스터 노드의 개수를 줄여야 될 때 해당 에러를 해결하여 마스터 노드의 개수를 줄이는 방법에 대하여 정리하였습니다.

 

이번 실습의 목표!!

이번 테스트의 목적은 극단적으로 테스트를 하기 위해 마스터 노드 5대로 구성된 클러스터를 마스터 노드 1대로 줄이는 것을 목표로 하였습니다.

 

이런 에러가 발생하는 이유는 무엇일까?

Elasticsearch 공식 문서를 참고하면 클러스터의 마스터 노드가 한 번에 절반이 넘는 개수의 마스터 노드가 드랍되면 Elasticsearch의 클러스터를 운영할 수 없게 됩니다. 예를 들어 7개인 경우 4대, 5대인 경우 3대, 3대인 경우 2대의 노드가 드랍되면 문제가 발생하게 됩니다. 절반 이상의 노드가 한번에 드랍되면 클러스터를 유지할 수 없기 때문에 해당 에러가 발생하게 됩니다.

When removing master-eligible nodes, it is important not to remove too many all at the same time. For instance, if there are currently seven master-eligible nodes and you wish to reduce this to three, it is not possible simply to stop four of the nodes at once: to do so would leave only three nodes remaining, which is less than half of the voting configuration, which means the cluster cannot take any further actions.
More precisely, if you shut down half or more of the master-eligible nodes all at the same time then the cluster will normally become unavailable. If this happens then you can bring the cluster back online by starting the removed nodes again.
As long as there are at least three master-eligible nodes in the cluster, as a general rule it is best to remove nodes one-at-a-time, allowing enough time for the cluster to automatically adjust the voting configuration and adapt the fault tolerance level to the new set of nodes.

 

실제 출력된 Error 메시지

아무런 설정을 하지 않고 마스터 노드의 개수를 줄이면(Scale in) 에러를 출력하면서 클러스터가 동작하지 않습니다. 먼저 에러의 메시지에서 지시한 데로 일단 3개 이상의 마스터 노드로 Scale out을 수행하여 일단 먼저 클러스터부터 다시 정상화해줍니다.저 같은 경우 원래대로 5대로 돌려놓았습니다.

{"@timestamp":"2023-11-26T05:37:28.578Z", "log.level": "WARN", "message":"master not discovered or elected yet, an election requires at least 3 nodes with ids from [ZppHhQQ9TBOt9BKVvqasng, 98xtbt4RRVyDEYUzidEqZw, aezJdnjdRc6XSlSCoY99CA, yTKgyqsqTc-kV7EyOkbRPQ, A86U5_pGQ9O_s2UP16sbOA], have only discovered non-quorum [{elasticsearch-master-0}{aezJdnjdRc6XSlSCoY99CA}{RdbsY5IJT-6DxLbE-tTXEA}{elasticsearch-master-0}{10.1.0.205}{10.1.0.205:9300}{m}]; discovery will continue using [10.1.0.208:9300, 10.1.0.204:9300, 10.1.0.207:9300, 10.1.0.206:9300] from hosts providers and [{elasticsearch-master-1}{yTKgyqsqTc-kV7EyOkbRPQ}{NUvkKaw7Ts-sKun6xLxVeQ}{elasticsearch-master-1}{10.1.0.207}{10.1.0.207:9300}{m}, {elasticsearch-master-3}{A86U5_pGQ9O_s2UP16sbOA}{iowYGyv2S6yV3o35KyV5ZQ}{elasticsearch-master-3}{10.1.0.208}{10.1.0.208:9300}{m}, {elasticsearch-master-2}{98xtbt4RRVyDEYUzidEqZw}{llyII7k3QsSjLrYuxaWe7g}{elasticsearch-master-2}{10.1.0.204}{10.1.0.204:9300}{m}, {elasticsearch-master-0}{aezJdnjdRc6XSlSCoY99CA}{RdbsY5IJT-6DxLbE-tTXEA}{elasticsearch-master-0}{10.1.0.205}{10.1.0.205:9300}{m}] from last-known cluster state; node term 2, last-accepted version 38 in term 2", "ecs.version": "1.2.0","service.name":"ES_ECS","event.dataset":"elasticsearch.server","process.thread.name":"elasticsearch[elasticsearch-master-0][cluster_coordination][T#1]","log.logger":"org.elasticsearch.cluster.coordination.ClusterFormationFailureHelper","elasticsearch.cluster.uuid":"MtfvN5pDQHmSCjaFEQpmPA","elasticsearch.node.id":"aezJdnjdRc6XSlSCoY99CA","elasticsearch.node.name":"elasticsearch-master-0","elasticsearch.cluster.name":"elasticsearch"}

 

마스터 노드 권한 제외 시키기

현재 마스터 노드의 권한을 가진 노드들이 elasticsearch-master-0, elasticsearch-master-1, elasticsearch-master-2, elasticsearch-master-3, elasticsearch-master-4로 구성되어 있습니다. 먼저 마스터 노드의 개수를 줄이기 위해 1개의 마스터 노드를 제외하고 다른 마스터 노드의 권한을 제외시켜 줍니다. 저는 elasticsearch-master-0만 마스터 노드의 권한을 유지하고 그 이외의 노드는 마스터 노드의 권한을 제외하였습니다. (솔직히 이번 실습에는 이게 전부입니다.)

### 마스터 투표권 제외
POST /_cluster/voting_config_exclusions?node_names=elasticsearch-master-1,elasticsearch-master-2,elasticsearch-master-3,elasticsearch-master-4

 

마스터 노드 줄이기

다른 노드들의 권한을 제외했다면 이제 모든 준비는 끝났습니다. 이제 그냥 줄여주면 됩니다. 한 번에 너무 많은 노드를 줄이는 것은 권장하지 않기 때문에 만약 서비스 중인 클러스터라면 안전하게 한 대씩 줄여주세요.

저 같은 경우 Elasticsearch를 Kubernetes에 구성하여 사용하기 때문에 아래 명령어로 마스터 노드의 개수를 줄였습니다.

$ kubectl scale sts elasticsearch-master --replicas=1

 

마지막으로

혹시나 나중에 다시 마스터 노드를 늘릴 수 있기 때문에 마스터 노드 권한을 제외한 목록을 제거해 줍니다. (제거해주지 않으면 나중에 스캐일 아웃해도 마스터의 권한을 가질 수 없습니다.)

DELETE /_cluster/voting_config_exclusions

 

저는 마스터 노드를 줄이는 업무를 진행할 때 이 방법을 몰라서 많이 당황을 했었는데요... 다행히 Elasticsearch 공식 문서에 잘 설명이 되어 있어 무사히 업무를 잘 마무리 할 수 있었습니다. 이런 경험 덕분에 보다 더 안정적인 클러스터를 운영할 수 있는 엔지니어가 된 거 같습니다.

 

참조

https://www.elastic.co/guide/en/elasticsearch/reference/current/add-elasticsearch-nodes.html

 

Add and remove nodes in your cluster | Elasticsearch Guide [8.11] | Elastic

Voting exclusions are only required when removing at least half of the master-eligible nodes from a cluster in a short time period. They are not required when removing master-ineligible nodes, nor are they required when removing fewer than half of the mast

www.elastic.co

 

'Elasticsearch > Error 처리' 카테고리의 다른 글

[Error] Error bulk 429 Too many requests  (0) 2023.06.30

Elasticsearch rack 설정을 통한 샤드 배치

보다 더 안정적인 Elasticsearch cluster를 운영하기 위해서는 각각의 데이터 노드가 별도의 서버 Rack에서 동작하도록 설정하는 것을 권장하고 있습니다. 그 이유는 서버의 하드웨어의 이슈가 발생하거나 서버실에서의 화재 또는 네트워크의 문제로 인하여 해당 서버랙이 문제가 발생하게 되면 해당 서버랙에 가동 중인 모든 노드에 영향을 미칠 수 있습니다. 만약 문제가 발생한 서버랙에 여러 대의 데이터 노드가 동작하고 있었다면 치명적인 데이터 유실이 발생하게 됩니다. 그렇기 때문에 각각의 서버랙을 구분하여 인덱스의 Primary shard와 Replica shard를 동일한 서버랙에 배치하지 못하도록 설정을 한다면 서버랙에 문제가 발생하여도 데이터의 유실을 방지할 수 있습니다. 이번 문서에서는 Elasticsearch data node에 rack_id를 부여하여 Primary shard와 Replica shard를 rack_id로 구분하여 배치할 수 있는 방법에 대하여 정리하였습니다.

 

극단적인 예시로 모든 데이터 노드를 하나의 랙에 동작하고 있다면 해당 랙에 문제가 발생 시 모든 데이터를 유실하여 서비스 운영에 치명적인 문제가 될 수 있습니다. 그렇기 때문에 각각의 서버 랙에 노드를 균등하게 배치하고 각 서버랙이 설치된 데이터 노드에 rack_id를 부여하면 하나의 서버랙에 문제가 발생하여도 데이터는 Replica shard를 통하여 데이터를 보존할 수 있습니다.

 

데이터 노드 서버 rack_id 설정

"rack one"에 설치되는 데이터 노드의 이름을 elasticsearch-one-0, elasticsearch-one-1, elasticsearch-one-2라고 지정하고 "rack two"에 설치되는 데이터 노드의 이름을 elasticsearch-two-0, elasticsearch-two-1, elasticsearch-two-2라고 설정하도록 하겠습니다.

 

rack one에 설치 되는 노드 elasticsearch.yml 파일에 값 추가하고 실행하기

node.attr.rack_id: rack_one

 

rack two에 설치 되는 노드 elasticsearch.yml 파일에 값 추가하고 실행하기

node.attr.rack_id: rack_two

 

master node에 rack_id를 지정하여 rack_id를 통해 샤드를 할당할 때 replica shard가 primary shard와 rack_id가 같지 않도록 elasticsearch.yml 파일에 다음과 같이 값을 추가합니다.

cluster.routing.allocation.awareness.attributes: rack_id

 

인덱스에 데이터를 인덱싱하고 결과 확인

인덱스에 number_of_shards의 값을 4로 설정하고 number_of_replicas의 값을 1로 설정하여 데이터를 인덱싱 하고 테스트를 진행하였습니다.

위 결과로 확인해보면 rack one에 설치된 노드에 primary shard가 생성되면 해당 샤드의 replica shard는 반듯이 rack two에 배치가 된 것을 확인할 수 있습니다. 이렇게 샤드의 배치를 서버랙 기준으로 설정하여 보다 안정적인 서비스를 운영할 수 있습니다.

 

마지막으로

서버랙을 기준으로 샤드할당을 지정할 수 있지만 zone을 기준으로 샤드할당을 설정할 수도 있습니다. 그러나 AWS Zone을 기준으로 설정할 경우 primary shard와 replica shard가 서로 데이터 sync를 하는 과정에서 너무 많은 네트워크 비용이 발생할 수 있습니다. 회사에서 많은 비용을 투자할 수 있다면 AWS Zone까지 설정하는 것이 가장 최선이지만 저의 경우 네트워크 비용 발생이 부담스러워 비용 발생을 최소화하기 위해 rack_id를 기준으로 샤드를 배치하는 것까지만 설정하고 운영하는 것이 비용 대비 좋은 선택이라고 생각됩니다.

 

참조

https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-cluster.html

 

Cluster-level shard allocation and routing settings | Elasticsearch Guide [8.10] | Elastic

You cannot mix the usage of percentage/ratio values and byte values across the cluster.routing.allocation.disk.watermark.low, cluster.routing.allocation.disk.watermark.high, and cluster.routing.allocation.disk.watermark.flood_stage settings. Either all val

www.elastic.co

 

 

 

Data streams

Data Stream은 시계열 데이터를 관리하기 위한 기능으로 하나 이상의 시계열 인덱스를 논리적으로 묶은 그룹으로, 여러 시계열 데이터 소스를 하나의 개체로 인지하여 관리하기 때문에 로그, 이벤트, 지표 및 기타 지속적으로 생성되는 데이터에 적합한 기능입니다. Elasticsearch의 경우 역색인 형태를 가지고 있기 때문에 Document를 삭제하거나 업데이트를 하는데 많이 비율적인 모습을 보이고 있습니다. 그렇기 때문에 데이터를 Document 단위로 관리를 하는 것보다는 인덱스 단위로 관리하는 것이 더 효율적입니다. 이번 문서에서는 시계열 데이터를 data stream을 통해 관리하는 방법에 대하여 정리하였습니다.

 

Data stream의 인덱싱과 검색 동작 방식

data stream을 통해 데이터를 인덱싱 할 경우 마지막으로 생성된 인덱스에 데이터를 인덱싱을 수행합니다.

그러나 검색은 인덱싱과 다르게 data stream으로 생성된 모든 인덱스에 검색 요청을 수행합니다.

사전 작업(클러스터 구성 및 설정)

먼저 이번 실습을 위해 저는 Master Node 3대, Hot Data 2대, Warm Data 2대로 클러스터를 구성하였으며, 빠른 테스트를 위해 클러스터가 인덱스를 롤오버 검사를 하는 주기를 5초로 설정하였습니다. 설정 방법은 아래와 같습니다.

### 인덱스 롤오버 체크 주기 변경
PUT http://localhost:9200/_cluster/settings
Content-Type: application/json

{
  "persistent": {
    "indices.lifecycle.poll_interval": "5s"
  }
}

 

ILM(Index Lifecycle Management) 설정

data stream은 ILM(Index Lifecycle Management)를 고려하여 추가된 기능으로 data stream을 사용할 때 ILM 기능과 같이 사용하는 것을 권장합니다. ILM 설정으로는 빠른 테스트를 위해 인덱스가 롤오버 되면 2분 뒤에 Warm 노드로 이동하고 10분 뒤에 Delete 되도록 설정하였습니다. 실제로는 이렇게 짧은 시간으로 설정하지는 않고 최고 일단위로 설정합니다.

### Create ILM policy
PUT http://localhost:9200/_ilm/policy/my-ilm-policy
Content-Type: application/json

{
  "policy": {
    "phases": {
      "hot": {
        "min_age": "0ms",
        "actions": {
          "rollover": {
            "max_size": "1gb",
            "max_docs": 10
          },
          "set_priority": {
            "priority": 100
          }
        }
      },
      "warm": {
        "min_age": "2m",
        "actions": {
          "forcemerge": {
            "max_num_segments": 1
          },
          "shrink": {
            "number_of_shards": 1
          },
          "allocate": {
            "number_of_replicas": 1
          },
          "set_priority": {
            "priority": 50
          }
        }
      },
      "delete": {
        "min_age": "10m",
        "actions": {
          "delete": {}
        }
      }
    }
  }
}

데이터가 Warm노드로 이동할 때 shrink 옵션을 통하여 샤드의 개수가 1개로 설정되도록 하였습니다.

 

Component template 생성

component template은 template의 기능을 여러개로 분리하여 관리하는 기능으로 한번 정의해 놓으면 다른 인덱스 template에서도 재사용할 수 있게 하는 기능입니다. 이번 실습에서는 settings에 관련된 template과 mapping에 관련된 template을 분리하여 생성하였습니다.

데이터 스트림에 인덱싱된 모든 문서에는 또는 필드 유형 @timestamp으로 매핑된 필드가 포함되어야 합니다. 인덱스 템플릿이 필드에 대한 매핑을 지정하지 않으면 Elasticsearch는 기본 옵션을 사용하여 필드로 매핑합니다

### create component template
PUT http://localhost:9200/_component_template/my-settings
Content-Type: application/json

{
  "template": {
    "settings": {
      "index.lifecycle.name": "my-ilm-policy",
      "number_of_shards": "2",
      "number_of_replicas": "1",
      "refresh_interval": "5s"
    }
  }
}

### create component template
PUT http://localhost:9200/_component_template/my-mappings
Content-Type: application/json

{
  "template": {
    "mappings": {
      "properties": {
        "@timestamp": {
          "type": "date",
          "format": "date_optional_time||epoch_millis"
        },
        "message": {
          "type": "text"
        },
        "level": {
          "type": "keyword"
        }
      }
    }
  }
}

 

Index template 생성

위에서 생성한 component template을 이용하여 index template을 구성하였습니다.

### create index template
PUT http://localhost:9200/_index_template/my-index-template
Content-Type: application/json

{
  "index_patterns": ["my-index*"],
  "priority": 500,
  "data_stream": {},
  "composed_of": ["my-mappings", "my-settings"]
}

 

Data stream 생성 및 데이터 테스트

위에서 설정한 것을 기반으로 data stream을 생성합니다.

### create data stream
PUT http://localhost:9200/_data_stream/my-index

### data stream 정보 가져오기
GET http://localhost:9200/_data_stream

 

이제 data stream에 실제로 데이터를 넣으면서 확인해보겠습니다.

curl -XPOST http://localhost:9200/my-index/_bulk -H 'Content-Type: application/json' -d '
{ "create": {} }
{ "@timestamp": "1698068232000", "level": "debug", "message": "test message" }
{ "create": {} }
{ "@timestamp": "1698068422000", "level": "debug", "message": "test message" }
{ "create": {} }
{ "@timestamp": "1698068232000", "level": "info", "message": "success test message" }
{ "create": {} }
{ "@timestamp": "1698068432000", "level": "info", "message": "success test message" }
{ "create": {} }
{ "@timestamp": "1698068242000", "level": "error", "message": "failed test message" }
{ "create": {} }
{ "@timestamp": "1698068452000", "level": "error", "message": "failed test message" }
{ "create": {} }
{ "@timestamp": "1698068232000", "level": "debug", "message": "test message" }
{ "create": {} }
{ "@timestamp": "1698068422000", "level": "debug", "message": "test message" }
{ "create": {} }
{ "@timestamp": "1698068232000", "level": "info", "message": "success test message" }
{ "create": {} }
{ "@timestamp": "1698068432000", "level": "info", "message": "success test message" }
{ "create": {} }
{ "@timestamp": "1698068242000", "level": "error", "message": "failed test message" }
{ "create": {} }
{ "@timestamp": "1698068452000", "level": "error", "message": "failed test message" }

'

 

데이터 확인

data stream으로 생성된 인덱스는. ds의 접두사가 붙어서 인덱스가 생성되었습니다. 이제 생성된 인덱스는 ILM에 설정한 것과 같이 2분 뒤에 Warm 노드로 이동하고 10분 뒤에 삭제됩니다.

2분뒤 .ds-my-index-2023.10.24-000001 인덱스가 shrink-f-pw-.ds-my-index-2023.10.24-000001 인덱스로 변하면서 샤드의 개수가 1개로 줄었습니다.

다시 한번 데이터 인덱싱

curl -XPOST http://localhost:9200/my-index/_bulk -H 'Content-Type: application/json' -d '
{ "create": {} }
{ "@timestamp": "1698068232000", "level": "debug", "message": "test message" }
{ "create": {} }
{ "@timestamp": "1698068422000", "level": "debug", "message": "test message" }
{ "create": {} }
{ "@timestamp": "1698068232000", "level": "info", "message": "success test message" }
{ "create": {} }
{ "@timestamp": "1698068432000", "level": "info", "message": "success test message" }
{ "create": {} }
{ "@timestamp": "1698068242000", "level": "error", "message": "failed test message" }
{ "create": {} }
{ "@timestamp": "1698068452000", "level": "error", "message": "failed test message" }
{ "create": {} }
{ "@timestamp": "1698068232000", "level": "debug", "message": "test message" }
{ "create": {} }
{ "@timestamp": "1698068422000", "level": "debug", "message": "test message" }
{ "create": {} }
{ "@timestamp": "1698068232000", "level": "info", "message": "success test message" }
{ "create": {} }
{ "@timestamp": "1698068432000", "level": "info", "message": "success test message" }
{ "create": {} }
{ "@timestamp": "1698068242000", "level": "error", "message": "failed test message" }
{ "create": {} }
{ "@timestamp": "1698068452000", "level": "error", "message": "failed test message" }

'

 

인덱스의 상태를 확인해 보면 shrink-f-pw-.ds-my-index-2023.10.24-000001에 document가 12개 있고 ds-my-index-2023.10.24-000002에 document가 12개 있는 것을 확인할 수 있습니다.

이제 data stream으로 생성될 수 있는 인덱스는 종류별로 생성되었습니다. data stream으로 다음과 같이 검색을 진행하면 data stream으로 생성된 모든 인덱스에서 검색이 된 것을 확인할 수 있습니다.

### search index
POST http://localhost:9200/my-index/_search
Content-Type: application/json

{
  "size": 1000
}

 

참고

https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-component-template.html

 

Create or update component template API | Elasticsearch Guide [8.10] | Elastic

Create or update component template APIedit Creates or updates a component template. Component templates are building blocks for constructing index templates that specify index mappings, settings, and aliases. response = client.cluster.put_component_templa

www.elastic.co

https://www.elastic.co/guide/en/elasticsearch/reference/current/data-streams.html

 

Data streams | Elasticsearch Guide [8.10] | Elastic

A data stream lets you store append-only time series data across multiple indices while giving you a single named resource for requests. Data streams are well-suited for logs, events, metrics, and other continuously generated data. You can submit indexing

www.elastic.co

 

Elasticsaerch 설정 변경 및 버전 업그레이드

https://stdhsw.tistory.com/entry/Elasticsearch-version-upgrade-rolling-%EB%B0%A9%EC%8B%9D-in-kubernetes

 

Elasticsearch version upgrade rolling 방식 (in Kubernetes)

Elasticsaerch 설정 변경 및 버전 업그레이드 Elasticsearch를 운영하다 보면 처음에 생각하지 못하거나 점점 Elasticsearch에 대한 지식이 늘면서 운영하는 Elasticsearch의 설정을 변경하고 싶은 순간이 있습

stdhsw.tistory.com

이전 블로그에서는 Rolling방식으로 Elasticsearch의 버전을 업그레이드하는 방법에 대해서 정리하였습니다. 이번에는 Swap 방식을 통하여 Elasticsearch의 Version을 Upgrade 하는 방법에 대해서 정리하였습니다.

클러스터 구성

이번 실습 Elasticsearch의 클러스터 구성으로는 Master node 3대, Data node 4대로 구성하여 진행하였습니다.

Elasticsearch의 버전은 8.5.1을 사용하였습니다.

 

Master node helm values (value-master.yaml)

---
createCert: false
clusterName: "elasticsearch"
nodeGroup: "master"
replicas: 3
roles:
  - master

image: "docker.elastic.co/elasticsearch/elasticsearch"
imageTag: "8.5.1"
imagePullPolicy: "IfNotPresent"

protocol: http
service:
  type: NodePort
  nodePort: "30000"

 

Data node helm values (value-data.yaml)

---
createCert: false
clusterName: "elasticsearch"
nodeGroup: "data"
replicas: 4

image: "docker.elastic.co/elasticsearch/elasticsearch"
imageTag: "8.5.1"
imagePullPolicy: "IfNotPresent"

roles:
  - data
  - data_content
  - data_hot
  - data_warm
  - data_cold
  - ingest
  - ml
  - remote_cluster_client
  - transform
  
protocol: http
service:
  type: NodePort
  nodePort: "30001"

 

Makefile 구성

PREFIX := elasticsearch
TIMEOUT := 1200s

install:
	helm upgrade --wait --timeout=$(TIMEOUT) --install --values value-master.yaml $(PREFIX)-master ./elasticsearch
	helm upgrade --wait --timeout=$(TIMEOUT) --install --values value-data.yaml $(PREFIX)-data ./elasticsearch

 

설치 및 결과 확인

# elasticsearch 설치
> make install

# 파드 확인
> kubectl get po                                                                                                                                               ─╯
NAME                     READY   STATUS    RESTARTS   AGE
elasticsearch-data-0     1/1     Running   0          25m
elasticsearch-data-1     1/1     Running   0          25m
elasticsearch-data-2     1/1     Running   0          25m
elasticsearch-data-3     1/1     Running   0          25m
elasticsearch-master-0   1/1     Running   0          26m
elasticsearch-master-1   1/1     Running   0          26m
elasticsearch-master-2   1/1     Running   0          26m

# 버전 확인
GET http://localhost:30001/_nodes/elasticsearch-data-0

{
  "_nodes": {
    "total": 1,
    "successful": 1,
    "failed": 0
  },
  "cluster_name": "elasticsearch",
  "nodes": {
    "4312Dy1YSS6tnBs28Pn4tg": {
      "name": "elasticsearch-data-0",
      "transport_address": "10.1.0.63:9300",
      "host": "10.1.0.63",
      "ip": "10.1.0.63",
      "version": "8.5.1",
      생략 ....

정상적으로 클러스터가 구성되었습니다. 이제 해당 클러스터의 버전을 업그레이드 작업을 진행하겠습니다.

새로운 노드 구성

rolling upgrade 방식과는 다르게 이번에는 새로운 노드 그룹을 생성합니다.

New Master node helm values (value-newmaster.yaml)

---
createCert: false
clusterName: "elasticsearch"
nodeGroup: "new-master"
replicas: 3
roles:
  - master

image: "docker.elastic.co/elasticsearch/elasticsearch"
imageTag: "8.6.0"
imagePullPolicy: "IfNotPresent"

protocol: http
service:
  type: NodePort
  nodePort: "30000"

 

New Data node helm values (value-newdata.yaml)

---
createCert: false
clusterName: "elasticsearch"
nodeGroup: "new-data"
replicas: 4

image: "docker.elastic.co/elasticsearch/elasticsearch"
imageTag: "8.6.0"
imagePullPolicy: "IfNotPresent"

roles:
  - data
  - data_content
  - data_hot
  - data_warm
  - data_cold
  - ingest
  - ml
  - remote_cluster_client
  - transform
  
protocol: http
service:
  type: NodePort
  nodePort: "30001"

 

Makefile 구성

PREFIX := elasticsearch
TIMEOUT := 1200s

install:
	helm upgrade --wait --timeout=$(TIMEOUT) --install --values value-newmaster.yaml $(PREFIX)-new-master ./elasticsearch
	helm upgrade --wait --timeout=$(TIMEOUT) --install --values value-newdata.yaml $(PREFIX)-new-data ./elasticsearch

 

설치

> make install

현재 클러스터 상태

위와 같이 구성하였다면 클러스터의 상태는 다음과 같을 것입니다.

기존 노드들 하나씩 Scale In

기존의 마스터, 데이터 노드의 Statfulset의 Scale을 하나씩 줄여 줍니다.

> kubectl scale sts elasticsearch-master --replicas=2
# 위 작업 완료 시 
> kubectl scale sts elasticsearch-master --replicas=1
# 위 작업 완료 시 
> kubectl scale sts elasticsearch-master --replicas=0

# 위 작업 완료 시 
> kubectl scale sts elasticsearch-data --replicas=3
# 위 작업 완료 시 
> kubectl scale sts elasticsearch-data --replicas=2
# 위 작업 완료 시 
> kubectl scale sts elasticsearch-data --replicas=1
# 위 작업 완료 시 
> kubectl scale sts elasticsearch-data --replicas=0

# 모든 스케일이 정상적으로 내려가면 기존 자원 반납
> helm delete elasticsearch-master
> helm delete elasticsearch-data

작업양은 많지만 보다 더 빠른 방법

Snapshot을 이용

일시적으로 오늘 이전의 데이터는 잠시 출력이 되지 않아도 된다면 사용하는 방법입니다.

  • 상태 복구를 위한 스냅샷 생성
  • 오늘 날짜의 데이터를 제외한 모든 인덱스를 삭제
  • 오늘 날짜의 인덱스 샤드를 Reroute를 이용하여 원하는 노드로 배치
  • 샤드들이 이동이 전부 정상적으로 이루어지면 기존 노드 Scale In
  • 오늘 이전의 날짜의 데이터를 스냅샷으로 복구

reroute 사용법은 아래 블로그에서 확인해 주세요

https://stdhsw.tistory.com/entry/Elasticsearch-reroute-%EC%83%A4%EB%93%9C%EB%A5%BC-%EB%8B%A4%EB%A5%B8-%EB%85%B8%EB%93%9C%EB%A1%9C-%EC%9D%B4%EB%8F%99%EC%8B%9C%ED%82%A4%EA%B8%B0

 

Elasticsearch reroute 샤드를 다른 노드로 이동시키기

reroute Elasticsearch에서는 기본적으로 각각의 노드에 샤드가 일정 비율로 균등하게 배치되도록 하고 있습니다. 그래도 뜻하지 않게 특정 노드에 샤드가 많이 배치될 수도 있고 어떤 인덱스는 샤드

stdhsw.tistory.com

 

Elasticsaerch 설정 변경 및 버전 업그레이드

Elasticsearch를 운영하다 보면 처음에 생각하지 못하거나 점점 Elasticsearch에 대한 지식이 늘면서 운영하는 Elasticsearch의 설정을 변경하고 싶은 순간이 있습니다. 뿐만 아니라 보안적인 이슈로 인하여 해당 이슈가 해결된 Version으로 업그레이드를 해야 하는 순간도 겪을 수 있습니다. Elasticsearch의 버전 및 설정을 바꾸는 방법이 여러 가지 있지만 이번 문서에서는 그중에 rolling 방식으로 변경하는 방법에 대해서 정리하였습니다.

주의사항

이번 실습 환경은 Kubernetes 환경에서 실습을 진행하였습니다.

  • Shard의 Replication이 존재하지 않으면 데이터가 손실될 수 있습니다.
  • Replication partition의 개수보다 작은 수의 노드가 한 번에 rolling update 되어야 한다는 것입니다. (이해하기 어려우면 그냥 노드 하나씩 업그레이드가 진행되면 됩니다.)
  • Kubernetes에서는 기본적으로 파드 순서의 역순으로 파드가 업데이트를 진행합니다. 그렇기 때문에 한 번에 드랍되는 문제는 크게 걱정하지 않아도 되지만 억지로 파드를 드랍하는 행위는 해서는 안됩니다.
  • 마스터 노드 또한 업그레이드를 진행해야 되기 때문에 마스터 Role을 가진 노드의 개수가 2대 이상 유지되어야 합니다.
  • elasticsearch의 helm으로 구성하였습니다. (https://artifacthub.io/packages/helm/elastic/elasticsearch)

클러스터 구성

이번 실습 Elasticsearch의 클러스터 구성으로는 Master node 3대, Data node 4대로 구성하여 진행하였습니다.

Elasticsearch의 버전은 8.5.1을 사용하였습니다.

 

Master node helm values (value-master.yaml)

---
createCert: false
clusterName: "elasticsearch"
nodeGroup: "master"
replicas: 3
roles:
  - master

image: "docker.elastic.co/elasticsearch/elasticsearch"
imageTag: "8.5.1"
imagePullPolicy: "IfNotPresent"

protocol: http
service:
  type: NodePort
  nodePort: "30000"

 

Data node helm values (value-data.yaml)

---
createCert: false
clusterName: "elasticsearch"
nodeGroup: "data"
replicas: 4

image: "docker.elastic.co/elasticsearch/elasticsearch"
imageTag: "8.5.1"
imagePullPolicy: "IfNotPresent"

roles:
  - data
  - data_content
  - data_hot
  - data_warm
  - data_cold
  - ingest
  - ml
  - remote_cluster_client
  - transform
  
protocol: http
service:
  type: NodePort
  nodePort: "30001"

 

Makefile 구성

PREFIX := elasticsearch
TIMEOUT := 1200s

install:
	helm upgrade --wait --timeout=$(TIMEOUT) --install --values value-master.yaml $(PREFIX)-master ./elasticsearch
	helm upgrade --wait --timeout=$(TIMEOUT) --install --values value-data.yaml $(PREFIX)-data ./elasticsearch

 

설치 및 결과 확인

# elasticsearch 설치
> make install

# 파드 확인
> kubectl get po                                                                                                                                               ─╯
NAME                     READY   STATUS    RESTARTS   AGE
elasticsearch-data-0     1/1     Running   0          25m
elasticsearch-data-1     1/1     Running   0          25m
elasticsearch-data-2     1/1     Running   0          25m
elasticsearch-data-3     1/1     Running   0          25m
elasticsearch-master-0   1/1     Running   0          26m
elasticsearch-master-1   1/1     Running   0          26m
elasticsearch-master-2   1/1     Running   0          26m

# 버전 확인
GET http://localhost:30001/_nodes/elasticsearch-data-0

{
  "_nodes": {
    "total": 1,
    "successful": 1,
    "failed": 0
  },
  "cluster_name": "elasticsearch",
  "nodes": {
    "4312Dy1YSS6tnBs28Pn4tg": {
      "name": "elasticsearch-data-0",
      "transport_address": "10.1.0.63:9300",
      "host": "10.1.0.63",
      "ip": "10.1.0.63",
      "version": "8.5.1",
      생략 ....

정상적으로 클러스터가 구성되었습니다. 이제 해당 클러스터의 버전을 업그레이드 작업을 진행하겠습니다.

Elasticsearch version upgrade

기존 8.5.1 버전을 8.6.0 버전으로 업그레이 작업을 진행하겠습니다.

 

Master node helm values (value-master.yaml)

---
createCert: false
clusterName: "elasticsearch"
nodeGroup: "master"
replicas: 3
roles:
  - master

image: "docker.elastic.co/elasticsearch/elasticsearch"
imageTag: "8.6.0"
imagePullPolicy: "IfNotPresent"

protocol: http
service:
  type: NodePort
  nodePort: "30000"

 

Data node helm values (value-data.yaml)

---
createCert: false
clusterName: "elasticsearch"
nodeGroup: "data"
replicas: 4

image: "docker.elastic.co/elasticsearch/elasticsearch"
imageTag: "8.6.0"
imagePullPolicy: "IfNotPresent"

roles:
  - data
  - data_content
  - data_hot
  - data_warm
  - data_cold
  - ingest
  - ml
  - remote_cluster_client
  - transform
  
protocol: http
service:
  type: NodePort
  nodePort: "30001"

 

업그레이드 및 결과 확인

helm 설치 방법을 upgrade 형식으로 Makefile을 만들었기 때문에 make install 명령어를 통하여 업그레이드가 가능합니다.

# 버전 업그레이드
> make install

> GET http://localhost:30001/_nodes/elasticsearch-data-0
{
  "_nodes": {
    "total": 1,
    "successful": 1,
    "failed": 0
  },
  "cluster_name": "elasticsearch",
  "nodes": {
    "4312Dy1YSS6tnBs28Pn4tg": {
      "name": "elasticsearch-data-0",
      "transport_address": "10.1.0.72:9300",
      "host": "10.1.0.72",
      "ip": "10.1.0.72",
      "version": "8.6.0",
      생략 ....

업그레이드 순서로는 Statefulset의 가장 뒷 번호부터 업그레이드가 진행되며 데이터가 많으면 많을 수록 오랜 시간이 걸리게 됩니다.

 

클러스터 업그레이 동작 방식

현재 클러스터의 구성으로는 마스터 노드는 데이터를 가지지 않습니다. 마스터 노드의 롤링 업데이트 진행 방법은 현재 마스터 역할을 하고 있는 노드가 드랍될 때 다른 마스터 권한을 가진 노드에게 마스터 권한을 넘겨주면서 롤링 업데이트가 진행됩니다.

데이터 노드들은 마스터 노드와 다르게 데이터를 가지고 있는데요 데이터 노드가 순차적으로 드랍될 때 드랍된 샤드의 데이터를 Replica shard를 통하여 다른 노드에 샤드를 새롭게 구성하면서 롤링 업데이트를 진행합니다.

파란색은 현재 Primary shard를 뜻하고 흰색은 Replica shard를 뜻합니다. 그리고 보라색은 복구된 Shard를 의미합니다.

시작하기 앞서 Ingest Pipeline이란

Elasticsearch에서 Ingest Pipeline은 데이터를 색인하기 전에 전처리 및 변환 작업을 수행하는 개념입니다. Ingest Pipeline은 데이터를 가져와서 필요한 형식으로 변환하거나 데이터를 필터링하고 파싱 하는 등의 작업 및 데이터의 일관성을 유지하는데 매우 유용하여 효율적인 데이터 처리를 가능하게 하는 중요한 기능입니다.

그림에 보이는 것과 같이 데이터가 들어오면 Ingest pipeline을 통하여 순차적으로 프로세서가 실행되고 결과물은 해당 인덱스에 저장됩니다. Ingest Pipeline이 동작할 수 있도록 해당 노드에는 ingest role이 등록되어야 하지만 일반적으로 데이터 노드에 많이 사용합니다. 허나 프로세서가 많아 부하를 많이 받을 경우 노드를 별도로 분리하는 방법을 고려해 볼 필요성이 있습니다.

이번 문서에서는 여러 유용한 파이프라인 프로세서 중 json에 대해서 실습해봤습니다.

만약 JSON Pipeline을 사용하지 않았다면

만약 JSON Pipeline을 사용하지 않았다면 데이터가 Text형식으로 문자열 그대로 저장되어 통계를 내거나 보다 자세한 데이터 처리를 하는데 많은 제약이 있을 수 있습니다. 아래는 JSON Pipeline을 사용하지 않았을 때의 데이터 형식입니다.

### 데이터 검색
GET http://localhost:9200/my-manifests/_search

### 결과
{
  "took": 4,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 1,
      "relation": "eq"
    },
    "max_score": 1.0,
    "hits": [
      {
        "_index": "my-manifests",
        "_id": "1",
        "_score": 1.0,
        "_source": {
          "cluster": "my-cluster",
          "manifest": "{\"cluster\":\"my-cluster\",\"namespace\":\"my-namespace\",\"name\":\"my-name\",\"version\":\"1.0.0\"}"
        }
      }
    ]
  }
}

 

JSON Pipeline

개발을 할 때 엔지니어가 json 데이터를 Elasticsearch 쿼리로 변경하여 데이터를 넣는 형식으로 개발을 진행한다면 JSON Pipeline은 굳이 필요 없는 기능으로 여길 수 있지만 개발을 하다 보면 앞으로 들어오는 json 데이터의 형식을 모르거나 json 문자열 데이터를 변경할 수 없는 환경에서 개발을 진행해야 되는 경우도 많습니다. 저 같은 경우 Kubernetes의 Resource Manifest 데이터를 가공 처리하는데 Manifest데이터는 yaml 또는 json 데이터로 수집되는데 어떠한 key와 value가 들어오는지 모든 것을 파악하기 어려운 상황을 겪었습니다. 이런 경우 JSON Pipeline을 사용하면 유용하게 데이터를 처리할 수 있을 것 같아 실습 내용을 정리하였습니다.

JSON Pipeline 생성

### 파이프라인 생성
PUT http://localhost:9200/_ingest/pipeline/index-json
Content-Type: application/json

{
  "description" : "ingest pipeline for json",
  "processors" : [
    {
      "json" : {
        "field" : "manifest",
        "target_field": "manifest_json"
      }
    }
  ]
}

파이프라인 생성 파라미터

  • json : 파이프라인 프로세서 중 JSON 프로세서를 사용합니다.
  • json.field : 문자열 형식으로 들어오는 json 필드를 명시합니다.
  • json.target_field : 문자열 필드 형식으로 들어온 데이터를 어떤 필드에 저장할지 지정합니다.

JSON Pipeline 테스트

파이프라인은 simulate라는 기능을 이용하여 내 생각대로 결과물이 나오는지 확인할 수 있습니다.

### 파이프라인 시뮬레이터
POST http://localhost:9200/_ingest/pipeline/_simulate
Content-Type: application/json

{
  "pipeline": {
    "description" : "ingest pipeline for json",
    "processors" : [
      {
        "json" : {
          "field" : "manifest",
          "target_field": "manifest_json"
        }
      }
    ]
  },
  "docs": [
    {
      "_source": {
        "cluster": "my-cluster",
        "manifest": "{\"cluster\":\"my-cluster\",\"namespace\":\"my-namespace\",\"name\":\"my-name\",\"version\":\"1.0.0\"}"
      }
    }
  ]
}

실제 데이터로 JSON Pipeline 적용되었는지 확인

먼저 테스트를 하기 위한 인덱스부터 정의하도록 하겠습니다. JSON Pipeline으로 생성되는 모든 필드들을 지정하기는 매우 번거롭기 때문에 dynamic_template을 통하여 JSON Pipeline이 생성하는 필드를 지정하였고 데이터를 넣을 때는 pipeline=index-json을 통해서 파이프라인을 지정해 줍니다.

### 인덱스 생성
PUT http://localhost:9200/my-manifests
Content-Type: application/json

{
  "mappings": {
    "dynamic_templates": [
      {
        "default_strings": {
          "match_mapping_type": "string",
          "mapping": {
            "type": "keyword"
          }
        }
      }
    ],
    "properties": {
      "cluster": {
        "type": "keyword"
      },
      "manifest": {
        "type": "text"
      }
    }
  }
}

### 인덱스 데이터 삽입1
POST http://localhost:9200/my-manifests/_doc/1?pipeline=index-json
Content-Type: application/json

{
  "cluster": "my-cluster",
  "manifest": "{\"cluster\":\"my-cluster\",\"namespace\":\"my-namespace\",\"name\":\"my-name\",\"version\":\"1.0.0\"}"
}

### 인덱스 데이터 삽입2
POST http://localhost:9200/my-manifests/_doc/2?pipeline=index-json
Content-Type: application/json

{
  "cluster": "my-cluster",
  "manifest": "{\"cluster\":\"my-cluster\",\"namespace\":\"my-namespace\",\"name\":\"test\",\"version\":\"2.0.0\"}"
}

결과 확인

manifest_json 필드를 통하여 json의 데이터가 dynamic_template으로 지정한 타입으로 필드가 생성된 것을 확인할 수 있습니다.  확실한 차이점을 보기 위해 위에서 정의한 "만약 JSON Pipeline을 사용하지 않았다면"의 결과와 비교해 보세요.

### 데이터 검색
GET http://localhost:9200/my-manifests/_search

### 결과 확인
{
  "took": 6,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 2,
      "relation": "eq"
    },
    "max_score": 1.0,
    "hits": [
      {
        "_index": "my-manifests",
        "_id": "1",
        "_score": 1.0,
        "_source": {
          "cluster": "my-cluster",
          "manifest": "{\"cluster\":\"my-cluster\",\"namespace\":\"my-namespace\",\"name\":\"my-name\",\"version\":\"1.0.0\"}",
          "manifest_json": {
            "cluster": "my-cluster",
            "namespace": "my-namespace",
            "name": "my-name",
            "version": "1.0.0"
          }
        }
      },
      {
        "_index": "my-manifests",
        "_id": "2",
        "_score": 1.0,
        "_source": {
          "cluster": "my-cluster",
          "manifest": "{\"cluster\":\"my-cluster\",\"namespace\":\"my-namespace\",\"name\":\"test\",\"version\":\"2.0.0\"}",
          "manifest_json": {
            "cluster": "my-cluster",
            "namespace": "my-namespace",
            "name": "test",
            "version": "2.0.0"
          }
        }
      }
    ]
  }
}

시작하기 앞서 Ingest Pipeline이란

Elasticsearch에서 Ingest Pipeline은 데이터를 색인하기 전에 전처리 및 변환 작업을 수행하는 개념입니다. Ingest Pipeline은 데이터를 가져와서 필요한 형식으로 변환하거나 데이터를 필터링하고 파싱 하는 등의 작업 및 데이터의 일관성을 유지하는데 매우 유용하여 효율적인 데이터 처리를 가능하게 하는 중요한 기능입니다.

그림에 보이는 것과 같이 데이터가 들어오면 Ingest pipeline을 통하여 순차적으로 프로세서가 실행되고 결과물은 해당 인덱스에 저장됩니다. Ingest Pipeline이 동작할 수 있도록 해당 노드에는 ingest role이 등록되어야 하지만 일반적으로 데이터 노드에 많이 사용합니다. 허나 프로세서가 많아 부하를 많이 받을 경우 노드를 별도로 분리하는 방법을 고려해 볼 필요성이 있습니다.

이번 문서에서는 여러 유용한 파이프라인 프로세서 중 Split에 대해서 실습해봤습니다.

Split processor란

Elasticsearch Ingest Pipeline에서 Split Processor는 텍스트 필드를 분할하거나 분리하는 데 사용되는 프로세서입니다. Split Processor를 사용하면 텍스트 필드에서 구분자를 기반으로 하여 여러 하위 텍스트 필드로 분리하거나 특정 패턴을 기반으로 필드를 분할할 수 있습니다. 이는 로그 데이터 또는 다양한 유형의 텍스트 데이터를 구조화하고 필요한 정보를 추출하는 데 유용하게 사용될 수 있습니다.

파이프라인 Split processor 생성하기

### 파이프라인 생성
PUT http://localhost:9200/_ingest/pipeline/user-split
Content-Type: application/json

{
  "description" : "Split user field",
  "processors" : [
    {
      "split" : {
        "field" : "users",
        "separator": ","
      }
    }
  ]
}

user-split 파이프라인 파라미터

  • split : 파이프라인에는 여러 프로세서를 등록할 수 있는데 이번 실습에서는 split 프로세서만 선언하였습니다.
  • split.field : 데이터를 분할할 필드명을 정의합니다.
  • split.separator: 데이터를 분할하는 기준이 되는 문자 캐릭터를 정의합니다.

파이프라인 정상동작 테스트

Elasticsearch에서는 simulate라는 기능을 제공하여 해당 파이프라인의 동작을 엔지니어의 생각대로 동작하는지 테스트를 해볼 수 있습니다.

### 파이프라인 시뮬레이터
POST http://localhost:9200/_ingest/pipeline/_simulate
Content-Type: application/json

{
  "pipeline": {
    "description" : "Split user field",
    "processors" : [
      {
        "split" : {
          "field" : "users",
          "separator": ","
        }
      }
    ]
  },
  "docs": [
    {
      "_source": {
        "users": "user1,user2,user3"
      }
    }
  ]
}

실제 데이터 인덱싱을 통한 결과 확인

파이프라인 테스트가 생각대로 정상적이었다면 이제 실제 데이터를 인덱싱하여 파이프라인이 정상적으로 동작하는지 실습해 보겠습니다.

### 데이터 삽입1
POST http://localhost:30001/user-connected-log/_doc/1?pipeline=user-split
Content-Type: application/json

{
  "level": "info",
  "users": "user1,user2,user3",
  "message": "user connected"
}

### 데이터 삽입2
POST http://localhost:30001/user-connected-log/_doc/2?pipeline=user-split
Content-Type: application/json

{
  "level": "info",
  "users": "user3,user4,user5",
  "message": "user connected"
}

결과 확인

doc_id 1에서는 user1, user2, user3의 데이터가 인덱싱 되었고 doc_id 2에서는 user3, user4, user5의 데이터가 인덱싱 되었습니다. 여기서 users에 user1로만 검색하면 doc_id 1의 데이터만 결과로 추력 되는 것을 확인할 수 있습니다.

### 인덱스 검색
GET http://localhost:30001/user-connected-log/_search
Content-Type: application/json

{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "users": "user1"
          }
        }
      ]
    }
  }
}


### 결과
{
  "took": 5,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 1,
      "relation": "eq"
    },
    "max_score": 0.6931471,
    "hits": [
      {
        "_index": "user-connected-log",
        "_id": "1",
        "_score": 0.6931471,
        "_source": {
          "message": "user connected",
          "level": "info",
          "users": [
            "user1",
            "user2",
            "user3"
          ]
        }
      }
    ]
  }
}

 

'Elasticsearch > pipeline' 카테고리의 다른 글

Elasticsearch Ingest pipeline json  (0) 2023.10.20
Elasticsearch Ingest pipeline enrich processor  (0) 2023.10.18

시작하기 앞서 Ingest Pipeline이란

Elasticsearch에서 Ingest Pipeline은 데이터를 색인하기 전에 전처리 및 변환 작업을 수행하는 개념입니다. Ingest Pipeline은 데이터를 가져와서 필요한 형식으로 변환하거나 데이터를 필터링하고 파싱 하는 등의 작업 및 데이터의 일관성을 유지하는데 매우 유용하여 효율적인 데이터 처리를 가능하게 하는 중요한 기능입니다.

그림에 보이는 것과 같이 데이터가 들어오면 Ingest pipeline을 통하여 순차적으로 프로세서가 실행되고 결과물은 해당 인덱스에 저장됩니다. Ingest Pipeline이 동작할 수 있도록 해당 노드에는 ingest role이 등록되어야 하지만 일반적으로 데이터 노드에 많이 사용합니다. 허나 프로세서가 많아 부하를 많이 받을 경우 노드를 별도로 분리하는 방법을 고려해 볼 필요성이 있습니다.

이번 문서에서는 여러 유용한 파이프라인 프로세서 중 Enrichment에 대해서 실습해봤습니다.

Enrich Processor

RDB에서 간단한 기능인 JOIN이라는 기능을 Elasticsearch에서 역색인 구조를 가지고 있는 Elasticsearch에서는 구현하는데는 아주 많은 리소스와 페널티가 있습니다. 그렇기 때문에 JOIN을 수행하는 행위는 Elasticsearch에서 권장하지 않고 있습니다.

Elasticsearch의 Enrich Processor는 Ingest pipeline의 기능중 하나로 외부 데이터 소스를 활용하여 기존 인덱스 데이터를 이용하여 추가 정보를 추출하거나 결합할 수 있는 기능을 제공하여 RDB에 JOIN과 비슷한 기능을 가능하게 되었습니다.

이러한 Enrich processor를 이용하여 데이터를 추가하는 방법을 실습해봤습니다.

작업 내용

미리 저장된 my-service 인덱스의 데이터를 enrich.field를 기반으로 추출하여 my-log 인덱스가 인덱싱 될 때 같이 데이터가 기록되도록 구현하였습니다.

소스 인덱스 생성하기

먼저 Enrich policy를 생성하기 전에 데이터를 추가하기 위한 소스 인덱스를 생성하고 데이터를 미리 삽입하는 작업을 수행하겠습니다.

### 서비스 인덱스 생성
PUT http://localhost:9200/my-service
Content-Type: application/json

{
  "settings": {
    "index": {
      "number_of_shards": 4,
      "number_of_replicas": 1,
      "refresh_interval": "5s"
    }
  },
  "mappings": {
    "properties": {
      "service": { "type": "keyword" },
      "endpoint": { "type": "keyword" },
      "description": { "type": "text" }
    }
  }
}

### 서비스 데이터 삽입1
PUT http://localhost:9200/my-service/_doc/1?refresh=wait_for
Content-Type: application/json

{
  "service": "payment",
  "endpoint": "localhot:32000",
  "description": "This service is for selling products"
}

### 서비스 데이터 삽입2
PUT http://localhost:9200/my-service/_doc/2?refresh=wait_for
Content-Type: application/json

{
  "service": "login",
  "endpoint": "localhot:31000",
  "description": "This service is for users to login"
}

Enrich processor 생성 및 실행

### 엔리치 생성
PUT http://localhost:9200/_enrich/policy/my-enrich
Content-Type: application/json

{
  "match": {
    "indices": "my-service",
    "match_field": "service",
    "enrich_fields": ["endpoint", "description"]
  }
}

### 엔리치 실행
POST http://localhost:9200/_enrich/policy/my-enrich/_execute

enrich policy 생성 파라미터

  • match.indices : 데이터를 추출할 소스 인덱스를 지정합니다.
  • match.match_field : 소스 인덱스에 서로 매칭할 필드를 지정합니다.
  • match.enrich_fields : 매칭 후 추출될 데이터 필드를 지정합니다.

파이프라인 생성

파이프라인을 생성할 때는 processor를 리스트 형식으로 생성하게 되는데 리스트의 순서대로 processor가 실행됩니다. 

### 파이프라인 생성
PUT http://localhost:9200/_ingest/pipeline/service_join
Content-Type: application/json

{
  "description" : "Service information using enrich",
  "processors" : [
    {
      "enrich" : {
        "policy_name": "my-enrich",
        "field" : "service",
        "target_field": "service_info",
        "max_matches": "1"
      }
    }
  ]
}

파이프라인 생성 파라미터

  • enrich.policy_name : 위에서 생성한 enrich policy
  • enrich.field : my-log인덱스에서 매칭할 필드를 지정합니다.
  • enrich.target_field : enrich policy에서 추출된 (endpoint, description) 데이터를 넣을 필드를 설정합니다.
  • enrich.max_matches : 최대로 매칭할 수 있는 개수를 설정합니다. 간단한 테스트이므로 1로 설정하였습니다.

my-log 인덱스 생성 및 데이터 삽입

데이터가 추가될 때 pipeline을 명시하여 위에서 설정한 Ingest pipeline이 동작할 수 있도록 합니다.

PUT http://localhost:9200/my-log
Content-Type: application/json

{
  "settings": {
    "index": {
      "number_of_shards": 4,
      "number_of_replicas": 1,
      "refresh_interval": "5s"
    }
  },
  "mappings": {
    "properties": {
      "service": { "type": "keyword" },
      "level": { "type": "keyword" },
      "message": {"type": "text" }
    }
  }
}

### 로그 데이터 삽입1
PUT http://localhost:9200/my-log/_doc/1?pipeline=service_join
Content-Type: application/json

{
  "service": "login",
  "level": "debug",
  "message": "user A logged in successfully"
}

### 로그 데이터 삽입2
PUT http://localhost:9200/my-log/_doc/2?pipeline=service_join
Content-Type: application/json

{
"service": "login",
"level": "error",
"message": "user B login failed"
}

### 로그 데이터 삽입3
PUT http://localhost:9200/my-log/_doc/3?pipeline=service_join
Content-Type: application/json

{
  "service": "payment",
  "level": "debug",
  "message": "user a successfully paid for the product"
}

실제로 우리는 my-log 인덱스에 데이터를 넣을 때 service, level, message 데이터만 인덱싱하였습니다. 이제 결과물을 확인하면 enrich를 통하여 새롭게 정의된 service_info 필드를 통해 service의 데이터가 추가된 것을 확인할 수 있습니다.

GET http://localhost:9200/my-log/_search

### 결과
{
  "took": 8,
  "timed_out": false,
  "_shards": {
    "total": 4,
    "successful": 4,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 3,
      "relation": "eq"
    },
    "max_score": 1.0,
    "hits": [
      {
        "_index": "my-log",
        "_id": "1",
        "_score": 1.0,
        "_source": {
          "service_info": {
            "description": "This service is for users to login",
            "endpoint": "localhot:30000",
            "service": "login"
          },
          "message": "user A logged in successfully",
          "level": "debug",
          "service": "login"
        }
      },
      {
        "_index": "my-log",
        "_id": "2",
        "_score": 1.0,
        "_source": {
          "service_info": {
            "description": "This service is for users to login",
            "endpoint": "localhot:30000",
            "service": "login"
          },
          "message": "user B login failed",
          "level": "error",
          "service": "login"
        }
      },
      {
        "_index": "my-log",
        "_id": "3",
        "_score": 1.0,
        "_source": {
          "service_info": {
            "description": "This service is for selling products",
            "endpoint": "localhot:30001",
            "service": "payment"
          },
          "message": "user a successfully paid for the product",
          "level": "debug",
          "service": "payment"
        }
      }
    ]
  }
}

 

참조

https://www.elastic.co/guide/en/elasticsearch/reference/current/ingest.html

 

Ingest pipelines | Elasticsearch Guide [8.10] | Elastic

Ingest pipelines let you perform common transformations on your data before indexing. For example, you can use pipelines to remove fields, extract values from text, and enrich your data. A pipeline consists of a series of configurable tasks called processo

www.elastic.co

https://www.elastic.co/guide/en/elasticsearch/reference/current/enrich-setup.html

 

Set up an enrich processor | Elasticsearch Guide [8.10] | Elastic

The enrich processor performs several operations and may impact the speed of your ingest pipeline. We strongly recommend testing and benchmarking your enrich processors before deploying them in production. We do not recommend using the enrich processor to

www.elastic.co

https://www.elastic.co/guide/en/elasticsearch/reference/current/put-enrich-policy-api.html

 

Create enrich policy API | Elasticsearch Guide [8.10] | Elastic

Once created, you can’t update or change an enrich policy. Instead, you can: Create and execute a new enrich policy. Replace the previous enrich policy with the new enrich policy in any in-use enrich processors. Use the delete enrich policy API to delete

www.elastic.co

 

'Elasticsearch > pipeline' 카테고리의 다른 글

Elasticsearch Ingest pipeline json  (0) 2023.10.20
Elasticsearch Ingest pipeline split processor  (0) 2023.10.19

ILM policy로 data phases 설정

data phases를 구성한다는 것은 성능을 올린다기보다는 자원을 효율적으로 사용하는 방법입니다. Elasticsearch 인덱스의 document를 수정하거나 삭제하는 것은 역색인 방식에서는 많은 자원을 소모하게 되기 때문에 데이터를 document 단위로 관리를 하는 것보다는 주로 인덱스 단위로 삭제하여 효율적으로 스토리지 사용량을 유지할 수 있습니다. 그래서 Elasticsearch에서는 ILM(Index Lifecycle Management)를 이용하여 인덱스의 생명주기를 자동으로 관리해 주는 기능을 제공하고 있습니다. 이번 문서에서는 ILM을 이용하여 자원을 효율적으로 관리하는 방법에 대하여 기록하였습니다.

 

Data phases 수행을 위한 노드 설정

많은 사용자가 로그를 분석하기 위해서 Elasticsearch를 사용합니다. 로그의 데이터를 효율적으로 관리하기 위해 많은 사용자들이 로그 인덱스를 날짜 별로 생성하여 사용하고 당일 로그를 분석하기 위해 해당 인덱스에서 많은 조회를 수행합니다. 그리고 다음날이 되면 어제 생성되었던 인덱스에서는 데이터가 들어가지 않고 오늘 날짜로 된 로그 인덱스가 새롭게 생성되어 오늘 날짜의 인덱스에 데이터가 들어가게 됩니다. 이러한 특징을 이용하여 Data phases 설정을 통하여 각각의 노드에 역할을 부여하고 해당 역할에 어울리는 자원을 할당하여 자원을 효율적으로 사용할 수 있습니다. 이번 실습을 위해 저는 Master 3대, Hot 2대, Warm 2대, Cold 2대를 구성하여 테스트를 진행하였습니다.

 

Hot node

hot 노드에는 데이터가 현재도 인덱싱 되며 자주 사용되는 데이터를 보관합니다. 데이터를 읽고 쓰는 작업이 빈번하게 이루어지기 때문에 SSD 스토리지를 사용하는 것을 권장합니다.

- roles : data, data_content, data_hot

 

Warm node

데이터가 거의 인덱싱 되지 않으며, 데이터를 읽는데 주로 사용하는 노드입니다.

- roles : data_warm

 

Cold node

데이터도 인덱싱 되지 않으며, 데이터를 읽는 쿼리 또한 거의 사용하지 않는 노드입니다. 주로 Searchable snapshot을 cold node를 통하여 사용하지만 Searchable snapshot은 Enterpise license에서 제공하기 때문에 이번 실습에서는 Searchable snapshot을 사용하지 못하였습니다.

- roles : data_cold

 

ILM Policy 생성

ILM Policy 생성

PUT http://localhost:9200/_ilm/policy/my_log_ilm
Content-Type: application/json

{
  "policy": {
    "phases": {
      "hot": {
        "min_age": "0ms",
        "actions": {
          "rollover": {
            "max_age": "1d",
            "max_size": "10gb",
            "max_docs": 10
          },
          "set_priority": {
            "priority": 100
          }
        }
      },
      "warm": {
        "min_age": "2m",
        "actions": {
          "forcemerge": {
            "max_num_segments": 1
          },
          "shrink": {
            "number_of_shards": 2
          },
          "allocate": {
            "number_of_replicas": 1
          },
          "set_priority": {
            "priority": 50
          }
        }
      },
      "cold": {
        "min_age": "4m",
        "actions": {
          "allocate": {
            "number_of_replicas": 0
          },
          "set_priority": {
            "priority": 10
          },
          "freeze": {}
        }
      },
      "delete": {
        "min_age": "6m",
        "actions": {
          "delete": {}
        }
      }
    }
  }
}

 

Hot tier 설정 파라미터 설명

  • min_age: "0ms" 인덱스가 생성되자마자 hot tier로 지정됩니다.
  • actions.rollover.max_age: rollover가 발생되는 조건으로 인덱스가 생성되고 하루(1d)가 지나면 인덱스가 rollover 됩니다.
  • actions.rollover.max_size: rollover가 발생되는 조건으로 인덱스의 크기가 10gb를 넘어가면 인덱스가 rollover 됩니다.
  • actions.rollover.max_docs: rollover가 발생되는 조건으로 인덱스의 document의 개수가 10개 넘어가면 rollover 됩니다. 이 설정은 테스트를 위해 아주 작은 값을 넣었습니다. 실제로는 입력하지 않다도 됩니다.
  • actions.set_priority.priority: 노드의 문제가 생길 경우 hot 데이터를 복구하는 것은 우선순위를 가장 높게 설정합니다.

Warm tier 설정 파라미터 설명

  • min_age: 인덱스가 rollover된지 2분이 지나면 해당 인덱스는 Warm tier로 설정됩니다.
  • actions.forcemerge.max_num_segments: 이 설정은 hot tier에서 여러 루씬 세그먼트를 사용한 것을 하나로 합치는 것을 의미합니다. 더 이상 데이터가 해당 인덱스에 써지지 않을 경우에 이 설정을 넣는 것을 권장합니다.
  • actions.shrink.number_of_shards: 여러 샤드로 나누어진 것을 2개로 합칩니다. 샤드의 수가 줄어 마스터 노드의 부하를 줄여줍니다.
  • actions.allocate.number_of_replicas: 복제 샤드를 1개로 설정합니다.
  • set_priority.priority: 인덱스 복구 우선순위를 hot tier보다 낮게 설정합니다.

Cold tier 설정 파라미터 설명

  • min_age: 인덱스가 rollover된지 4분이 지나면 해당 인덱스는 Cold tier로 설정합니다.
  • actions.allocate.number_of_replicas: 인덱스의 복제 샤드를 제거하여 디스크 사용량을 줄여줍니다.
  • set_priority.priority: 인덱스 복구 우선순위를 Warm tier보다 낮게 설정합니다.
  • freeze: 해당 인덱스를 읽기 전용으로 설정합니다.

Delete 설정 파라미터 설명

  • min_age: 인덱스가 rollover된지 6분이 지나면 해당 인덱스를 삭제합니다.

ILM Policy 설정을 통하여 데이터의 Tier가 변경되기 위해서는 먼저 Rollover가 발생하여야 합니다. 이번 테스트를 위해 doc의 개수가 10개 이상이면 rollover가 발생하도록 설정을 하였고 warm은 2분 뒤 cold는 4분 뒤 delete는 6분 뒤로 아주 짧게 시간을 설정하였습니다.

 

Index Tempalte 설정

ILM Policy 설정을 적용한 Index template을 구성합니다. 인덱스 template 설정에 대한 설명은 다음 링크를 통해 확인해 주세요

https://stdhsw.tistory.com/entry/Elasticsearch%EC%9D%98-Index-template-%EC%84%A4%EC%A0%95

PUT http://localhost:9200/_template/my_log_template
Content-Type: application/json

{
  "index_patterns": ["my_log-*"],
  "settings": {
    "index": {
      "number_of_shards": "4",
      "number_of_replicas": "1",
      "refresh_interval": "5s",
      "lifecycle": {
        "name": "my_log_ilm",
        "rollover_alias": "my_log"
      }
    }
  },
  "mappings": {
    "properties": {
      "app": {
        "type": "keyword"
      },
      "level": {
        "type": "keyword"
      },
      "message": {
        "type": "text"
      }
    }
  }
}

 

빠른 테스트를 위한 클러스터 설정(무시해도 됩니다.)

테스트를 진행하는데 너무 오랜 시간을 투자할 수 없기 때문에 Elasticsearch에서 Index를 롤오버를 검사하고 수행하는 주기를 10초로 줄여줍니다. 만약 이 설정을 추가하지 않을 경우 기본값으로 10분의 시간이 소요됩니다. 테스트를 위해 설정을 해주었지만 상용서버에서는 설정을 변경하지 않는 것을 권장드립니다.

PUT http://localhost:9200/_cluster/settings
Content-Type: application/json

{
  "persistent": {
    "indices.lifecycle.poll_interval": "10s"
  }
}

 

인덱스 생성하기

인덱스를 수동적으로 먼저 생성해 주는 이유는 is_write_index 옵션을 사용하기 위해서입니다.

PUT http://localhost:9200/my_log-000001
Content-Type: application/json

{
  "aliases": {
    "my_log": {
      "is_write_index": true
    }
  }
}

 

인덱스에 데이터 넣기

테스트를 위한 데이터를 벌크를 통하여 넣어줍니다. 한번에 rollover가 발생할 수 있도록 10개의 document를 넣습니다.

curl -XPOST http://localhost:9200/my_log/_bulk -H 'Content-Type: application/json' -d '
{ "index": {} }
{ "app": "my-app", "level": "debug", "message": "successes hello world" }
{ "index": {} }
{ "app": "my-app", "level": "info", "message": "hello world" }
{ "index": {} }
{ "app": "my-app", "level": "error", "message": "failed hello world" }
{ "index": {} }
{ "app": "my-app", "level": "warn", "message": "warning hello world" }
{ "index": {} }
{ "app": "my-app", "level": "debug", "message": "successes hello world" }
{ "index": {} }
{ "app": "my-app", "level": "info", "message": "hello world" }
{ "index": {} }
{ "app": "my-app", "level": "error", "message": "failed hello world" }
{ "index": {} }
{ "app": "my-app", "level": "warn", "message": "warning hello world" }
{ "index": {} }
{ "app": "my-app", "level": "error", "message": "failed hello world" }
{ "index": {} }
{ "app": "my-app", "level": "warn", "message": "warning hello world" }

'

인덱스 생성 결과

인덱스 생성된 결과를 보면 my_log-000001은 hot node에만 생성된 것을 확인할 수 있습니다.

 

Hot node에서 Warm node로 인덱스 이동

인덱스가 생성되고 롤오버 된 지 2분이 지나면 해당 데이터는 warm노드로 이동합니다. 이때 ILM Policy에서 값을 설정한 것처럼 인덱스의 샤드가 2개로 합쳐지면서 해당 인덱스의 이름 앞에 shrink이라는 단어가 추가되었습니다. 인덱스의 명칭이 변경되었지만 원래의 인덱스 이름으로 alias가 생성되었기 때문에 원래 인덱스 이름 그대로 검색을 하여도 전혀 문제가 발생하지 않습니다.

 

Warm node에서 Cold node로 인덱스 이동

인덱스가 롤오버된지 4분이 지나면 해당 인덱스는 Cold 노드로 이동하게 됩니다. 이것 또한 ILM Policy에 값을 설정한 것처럼 복제 샤드를 제거하였고 해당 인덱스는 읽기 모드로 전환된 상태입니다. 여전히 원래 인덱스 이름으로 alias를 가지고 있기 때문에 원래 인덱스명으로 검색이 가능합니다. 주로 Cold 데이터는 Searchable snapshot을 이용하지만 위에서 언급한 것처럼 Searchable snapshot은 Enterpise license에서만 제공하기 때문에 이번 실습에서는 그냥 warm노드처럼 사용하였습니다.

 

Cold node에서 인덱스 삭제까지

인덱스가 롤오버 된지 6분이 지나면 해당 인덱스는 삭제됩니다. 아래 이미지를 보면 my_log-000001 인덱스가 삭제된 것을 확인할 수 있습니다.

 

마지막으로

이번에 빠르게 실습을 하기 위해 data tier의 변경을 아주 짧게 2분 단위로 변경될 수 있도록 하여 테스트를 진행하였습니다. 그러나 실제로는 이렇게 사용하지 않고 일 단위로 tier가 변경되도록 설정을 합니다. 예시로 다음과 같이 설정할 경우 시간 흐름도에 따라 data tier가 변경되는 모습은 다음과 같습니다.

  • hot의 min_age: "0ms"
  • warm의 min_age: "1d"
  • cold의 min_age: "3d"
  • delete의 min_age: "7d"

 

참조

https://www.elastic.co/guide/en/elasticsearch/reference/current/data-tiers.html

 

Data tiers | Elasticsearch Guide [8.10] | Elastic

Elasticsearch generally expects nodes within a data tier to share the same hardware profile. Variations not following this recommendation should be carefully architected to avoid hot spotting.

www.elastic.co

https://www.elastic.co/guide/en/elasticsearch/reference/current/set-up-a-data-stream.html

 

Set up a data stream | Elasticsearch Guide [8.10] | Elastic

If you use Fleet or Elastic Agent, skip this tutorial. Fleet and Elastic Agent set up data streams for you. See Fleet’s data streams documentation.

www.elastic.co

https://www.elastic.co/guide/en/elasticsearch/reference/current/ilm-actions.html

 

Index lifecycle actions | Elasticsearch Guide [8.10] | Elastic

Index lifecycle actionsedit Allocate Move shards to nodes with different performance characteristics and reduce the number of replicas. Delete Permanently remove the index. Force merge Reduce the number of index segments and purge deleted documents. Migrat

www.elastic.co

 

 

JVM 설정

elasticsearch JVM이라고 검색을 해보면 많은 글들이 JVM은 전체 메모리에 50%로 설정하라고 설명하는 곳이 많습니다. 그렇지만 단순히 50%라고만 하기에는 사용자의 환경이 다르기 때문에 설명이 부족합니다. elasticsearch 공식 문서에 따르면 전체 메모리의 50% 이하로 설정하는 것을 권장하고 있으며, 환경에 따라 다르지만 대중적으로 26GB 이하로 설정해야 된다고 설명을 하고 있지만 사용자의 환경에 따라 30GB까지도 설정을 할 수 있다고 합니다. 그 이유는 Zero base comporessed OOP 때문인데요 Compressed OOP는 아래에서 다루도록 하겠습니다. 그리고 JVM 설정에 유의해야 되는 것은 전체 메모리의 50% 이하를 JVM에 줬다면 나머지 메모리는 시스템과 Lucene이 사용하게 됩니다. 그렇기 때문에 통계 및 분석이 많은 쿼리를 자주 사용한다면 JVM에 메모리를 높게 주고(50%에 가깝게) 검색만 주로 하는 쿼리를 자주 사용한다면 JVM메모리를 보다 적게 주어 Lucene이 더 많은 메모리를 사용할 수 있게 하는 것이 좋습니다.

 

Compressed OOP

포인터의 공간 낭비를 줄이고 좀 더 빠른 연산을 위해 포인터를 압축해서 표현하는 메모리 최적화 기법 중 하나입니다. 이 기법의 원리는 포인터가 메모리 주소를 가리키는게 아닌 Object Offset을 가리키도록 변형해서 동작시키는 것입니다. 만약 8비트 포인터를 사용한다면 256바이트의 물리적인 주소를 표현하는게 아니라 256개의 객체를 가리킬 수 있게 되는 것입니다. 만약 JVM의 크기가 32GB를 넘어가게 된다면 포인터 구조가 Compressed OOP가 아닌 일반 OOP 방식으로 전환되어 Compressed OOP의 이점을 살릴 수가 없습니다. 그러므로 메모리의 여유가 있다면 차라리 노드를 더 늘려 주는 것이 더 현명한 선택이 될 것 같습니다.

 

Compressed OOP 메모리 주소

  • 32비트 OOP : 2의 32승 까지의 주소를 가리킬 수 있다. 4GB
  • 64비트 OOP : 2의 64승 까지의 주소를 가리킬 수 있다. 18EB
  • 32비트 Compressed OOP : 2의 32승 오브젝트를 가리킬 수 있습니다.
  • 메모리 공간으로는 2의 32승 곱하기 8만큼 표현이 가능합니다.

 

Zero base Compressed OOP 확인

Zero base Compressed OOP는 JVM의 힙 메모리가 0번지부터 시작하게 하는 설정이다. JVM의 크기가 32GB로 설정하여 Zero base Compressed OOP가 깨진 것과 30GB 이하로 설정하여 Zero base Compressed OOP가 활성화된 것의 차이를 확인해 보겠습니다.

 

JVM 32GB

$ java -Xmx32768m -XX:+PrintFlagsFinal -XX:+UnlockDiagnosticVMOptions -XX:+PrintComressedOopsMode 2>/dev/null | grep Compressed | grep Oops

bool PrintCompressedOopsMode = true
bool UseCompressedOops = false

 

JVM 30GB

$ java -Xmx30720m -XX:+PrintFlagsFinal -XX:+UnlockDiagnosticVMOptions -XX:+PrintComressedOopsMode 2>/dev/null | grep Compressed | grep Oops

heep address : 0x0000000...
bool PrintCompressedOopsMode = true
bool UseCompressedOops = true

 

JVM 여러가지 옵션

Elasticsearch 공식 문서에 따르면 Xms, Xmx의 값은 동일해야 하며 전체 메모리의 50% 이하로 설정하며 root jvm.options 파일을 수정하지 않는 것을 권장합니다. 저는 Xms, Xmx만 수정하고 나머지는 그냥 이런 것들이 있다는 것만 알고 넘어갔습니다.

  • -Xms : 기본 heep 사이즈 (기본값 : 64MB)
  • -Xmx : 최대 heep 사이즈 (기본값 : 256MB)
  • -XX:PermSize : 기본 Perm 사이즈
  • -XX:MaxPermSize : 최대 Perm 사이즈
  • -XX:NewSize : 최소 New 영역 사이즈
  • -XX:MaxNewSize : 최대 New 영역 사이즈
  • -XX:SurvivorRatio : New 영역 / Survivor 영역 비율 설정
  • -XX:NewRatio : Young Gen, Old Gen의 비율 설정
  • -XX:+DisableExplicitGC : System.gc() 무시 설정
  • -XX:+CMSPermGenSweepingEnabled : Perm Gen 영역도 GC의 대상이 되도록 설정
  • -XX:+CMSClassUnloadingEnabled : Class 데이터도 GC의 대상이 되도록 설정

요약

  • JVM 설정은 전체 메모리의 50%이하로 설정
  • 사용자 환경에 따라 다르지만 최대 26GB이하로 설정하는 것을 권장합니다. 물론 환경에 따라 30GB까지 가능합니다.
  • 26GB 또는 30GB가 넘으면 Zero base compressed OOP가 깨짐
  • 통계 및 분석을 많이 사용하면 JVM을 50%에 가깝게 설정

 

참고

https://www.elastic.co/guide/en/elasticsearch/reference/current/advanced-configuration.html#set-jvm-options

 

Advanced configuration | Elasticsearch Guide [8.10] | Elastic

This functionality is in technical preview and may be changed or removed in a future release. Elastic will apply best effort to fix any issues, but features in technical preview are not subject to the support SLA of official GA features.

www.elastic.co


노드 역할 분리 및 노드 개수

소규모 프로젝트 및 개발 환경에서는 리소스를 절약하게 위해 단일 노드로 사용해도 무방합니다. 그러나 프로젝트의 규모가 거대해지고 데이터의 양이 많아지면 그때는 단일 노드로는 불가능한 시점이 오게 되고 Elasticsaerch의 클러스터가 깨지는 위험이 발생할 수 있습니다. 이럴 때 자원을 효율적(효율적이라는 표현은 자원을 적게 쓰는 것이 아닌 역할에 맞게 잘 활용한다는 의미)으로 사용하기 위해서는 노드의 역할을 분배하는 것을 권장하고 있습니다. 노드의 종류는 여러 가지가 있지만 대표적으로 마스터 노드와 데이터 노드를 분리하는 것만으로도 클러스터를 안정적으로 운영할 수 있습니다. 마스터 노드는 클러스터 운영관리 및 노드, 샤드를 관리하는 역할을 수행하고 데이터 노드는 데이터를 저장하고 검색하는 역할을 수행합니다. 마스터와 데이터의 역할을 분리하게 되면 마스터는 온전히 마스터의 역할만 수행하게 되어 마스터 노드에 문제가 발생할 문제가 줄어들게 됩니다. 마스터 노드를 설정할 때는 마스터 선출을 위한 Vorting이 이루어 지게 되므로 홀수개로 설정하는 것을 권장합니다. 마스터 노드를 여러 개로 설정할 경우 실제로 마스터의 역할을 수행하는 것은 1개이며 나머지는 현재 마스터 노드의 문제가 발생할 경우 마스터로 승격되어 마스터 노드의 역할을 수행할 수 있도록 Standby형태로 대기하고 있습니다.

 

Scale In시 주의사항 (in kubernetes)

인덱스의 데이터는 샤드로 분할하여 데이터가 저장됩니다. 이때 Replica Shard를 지정하지 않았다면 데이터 노드의 Scale In을 수행할 때 데이터의 손실이 발생할 수 있습니다. 물론 Replica Shard를 구성하더라도 무턱대로 Scale In을 수행하면 데이터의 손실일 발생할 수 있습니다. 아래 그림은 Scale In 수행시 데이터 손실이 발생하는 경우입니다.

그림에서 볼 수 있듯이 Replica Shard가 존재하더라도 Data Node 2번과 3번이 같이 드랍되면 Shard2번의 데이터는 손실되게 됩니다. 그렇기 때문에 Scale In을 수행할 때는 1개씩 줄여가면서 데이터의 복사가 완전히 이루어지면 다음 노드를 줄이는 것을 권장합니다.

 

Scale Up시 주의사항 (in kubernetes)

저 같은 경우 Elasticsearch를 Kubernetes 환경에서 사용하고 있습니다. 만약 마스터 노드가 1개밖에 없는 상황에서 마스터 노드를 스케일업을 수행하게 되면 클러스터가 깨질 수 있습니다. 그 이유는 해당 마스터 노드가 드랍될 때 마스터의 역할을 수행할 수 있는 노드가 없기 때문입니다. 그렇기 때문에 Kubernetes환경에서 마스터 노드가 1개 일 경우 스케일업을 수행할 때는 반듯이 먼저 스케일 아웃을 하여 해당 마스터 노드가 드랍되더라도 마스터의 역할을 대신할 수 있는 노드가 있기 때문에 문제없이 클러스터를 운영할 수 있습니다.

 

노드는 무조건 많으면 좋을까?

노드의 개수가 많으면 데이터를 분할 저장하고 부하를 나눠 받기 때문에 무조건 좋게 느껴질 수 있습니다. 그러나 각각의 노드들은 서로 통신을 하며 클러스터를 유지하기 때문에 노드가 너무 많게 되면 서로 통신하느라 클러스터의 문제가 발생할 수 있습니다. 그래서 저는 스토리지가 부족하면 스케일아웃을 수행하고 CPU, Memory 리소스가 부족하면 스케일업을 수행하고 있습니다. 무엇이 부족한지 측정하기 위해서는 Elasticsearch Exporter를 설치하여 Prometheus, Grafana를 통하여 측정할 수 있습니다.

vm.max_map_count

Elasticsearch는 파일을 메모리에 매핑하고 해당 Lucene MMapDirectory을 통하여 인덱스 샤드가 저장됩니다. 이러한 동작 과정에서 많은 데이터를 처리하기 위해 어느 정도 크기의 가상 주소 공간이 필요합니다. 그러나 기본적으로 대부분의 운영체제에서는 65,630 만큼의 mmap 개수로 설정되어 있는데 이 수치는 elasticsearch를 운영하는데 매우 작은 양입니다. 그래서 별도로 설정을 하지 않고 Elasticsearch를 실행시키면 bootstrap에 의해 해당 vm.map_map_count의 값이 매우 작다는 Error가 발생하면서 Elasticsearch가 종료합니다. 이러한 문제를 해결하기 위해 vm.max_map_count의 값을 262,144 값으로 수정하여 사용하시면 됩니다.

수정 방법

# 명령어로 변경하면 재부팅 시 유지되지 않는다.
$ sudo sysctl -w vm.max_map_count = 262144

# 파일 수정으로 영구적으로 값 유지 vm.max_map_count 수정
$ vim /etc/sysctl.conf

# max_map_count 값 확인
$ sysctl vm.max_map_count

Kubernetes 환경에서 설치된 Elasticsearch는?

kubernetes 환경에서 helm을 통하여 elasticsearch를 설치하셨다면 해당 설정을 굳이 하실 필요가 없는데요 그 이유는 Elasticsearch StatefulSet의 initcontainer의 "configure-sysctl"이라는 컨테이너가 Elasticsearch가 구동되기 전에 해당 설정을 적용하고 Elasticsearch가 동작하기 때문입니다.

 

elasticsearch/template/statefulset.yaml

      - name: configure-sysctl
        securityContext:
          runAsUser: 0
          privileged: true
        image: "{{ .Values.image }}:{{ .Values.imageTag }}"
        imagePullPolicy: "{{ .Values.imagePullPolicy }}"
        command: ["sysctl", "-w", "vm.max_map_count={{ .Values.sysctlVmMaxMapCount}}"]

참고

https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules-store.html

 

Store | Elasticsearch Guide [8.10] | Elastic

This is a low-level setting. Some store implementations have poor concurrency or disable optimizations for heap memory usage. We recommend sticking to the defaults.

www.elastic.co

https://www.elastic.co/guide/en/elasticsearch/reference/current/vm-max-map-count.html

 

Virtual memory | Elasticsearch Guide [8.10] | Elastic

Elasticsearch uses a mmapfs directory by default to store its indices. The default operating system limits on mmap counts is likely to be too low, which may result in out of memory exceptions. On Linux, you can increase the limits by running the following

www.elastic.co


메모리 스와핑 비활성화

대부분의 운영체제에서는 메모리 관리를 효율적으로 하기 위해서 메모리 스와핑이라는 기술을 사용합니다. 메모리 스와핑은 메모리가 부족하거나 현재 사용되지 않거나 우선순위가 낮은 프로세스의 데이터를 메모리 스와핑을 통하여 디스크에 이동시켜 메모리를 효율적으로 사용하는 방법인데 Elasticsearch에서는 이러한 메모리 스와핑이 치명적인 성능 저하의 원인이 될 수 있습니다.

Elasticsearch는 메모리를 많이 사용하는 특성을 가지고 있는데 메모리 스와핑 작업에 의해 가비지 컬렉션이 비정상적으로 느려지며 노드가 데이터를 디스크로 쓰이고 읽어지는 과정에서 데이터를 처리하는데 응답속도가 매우 느려지고 이러한 문제로 인하여 클러스터와의 연결이 끊어지는 문제가 발생하게 됩니다. 그래서 Elasticsearch 공식 문서에서는 메모리 스와핑을 비활성화하는 것을 권장하고 있습니다.

메모리 스왑핑 비활성

# 명령어로 일시적으로 스와핑 비활성화
$ sudo swapoff -a

# 파일 수정으로 영구적으로 비활성화
$ vi /etc/fstab


# 스왑 상태 확인 명령어
$ free
# 스왑 디렉토리 확인
$ cat /proc/swaps
# 스왑 통계 확인
$ sar -B 2 5

메모리 스와핑을 비활성화하기 때문에 Elasticsearch가 동작하는 노드에 다른 애플리케이션이 같이 동작하는 것은 권장하지 않습니다.

메모리 스와핑 비활성을 할 수 없는 상황이라면?

환경 특성상 메모리 스와핑을 비활성화할 수 없는 상황이라면 최대한 스와핑 주기를 낮춰 발생 빈도를 줄이는 방법이 있습니다.

# 명령어로 설정
$ sudo sysctl vm.swappiness=1

# 설정 확인
$ cat /proc/sys/vm/swappiness

memory_lock 설정

만약에 현제 root 권한이 없어 메모리 스와핑 설정이 불가능한 경우에는 bootstrap.memory_lock 설정을 통하여 메모리 스와핑을 최소화할 수 있습니다. 커널 수준에서 제공되는 함수 중에 mlockall()가 있는데 이 함수는 프로세스의 페이징을 금지하고 모든 메모리가 램에 상주하는 것을 보장해 줍니다. memory_lock을 이용하여 애플리케이션에서 메모리를 강제로 스와핑 하지 못하도록 설정합니다.

memory_lock 설정

# memory_lock 설정하기
$ vi ./elasticsearch.yml
bootstrap.memory_lock: true

# Request를 보내 memory_lock 설정 여부 확인하기
curl -XGET http://localhost:9200/_nodes?filter_path=**.mlockall

Kubernetes 환경에서 설치된 Elasticsearch는?

kubelet에서는 메모리 스왑이 기본적으로 off된 환경이라고 합니다. 그렇기 때문에 메모리 스왑핑에 대해서는 kubernetes 환경에서는 굳이 고민을 안 해도 될 것 같습니다. (https://github.com/elastic/helm-charts/issues/7)

참고

https://www.elastic.co/guide/en/elasticsearch/reference/current/setup-configuration-memory.html

 

Disable swapping | Elasticsearch Guide [8.10] | Elastic

mlockall might cause the JVM or shell session to exit if it tries to allocate more memory than is available!

www.elastic.co


Max open file 설정

Elasticsearch에서는 클러스터의 노드끼리 소켓을 사용해 통신을 하며, 루씬 인덱스에서 데이터를 색인 처리하는데 많은 수의 파일을 사용하고 있습니다. 그렇기 때문에 파일을 열람할 수 있는 크기가 매우 작으면 문제가 될 수 있기 때문에 설정을 통하여 max open file의 수를 지정해 주어야 합니다.

max open file 설정

# 명령어를 통하여 일시적인 적용
$ sudo su
$ ulimit -n 81920

# 파일 수정을 통하여 영구적인 적용
$ vim /etc/security/limits.conf
irterm  softnofile  81920
irterm  hardnofile  81920

max file 설정 값 확인

curl -XGET http://localhost:9200/_nodes/stats/process

위 Request를 통해 open_file_descriptors로는 현재 열려 있는 파일 디스크립트의 개수를 알 수 있으며, max_file_descriptors를 통하여 최대 사용 가능한 파일 디스크립터의 개수를 확인할 수 있습니다.

참고

https://www.elastic.co/guide/en/elasticsearch/reference/current/setting-system-settings.html

 

Configuring system settings | Elasticsearch Guide [8.10] | Elastic

Ubuntu and limits.conf Ubuntu ignores the limits.conf file for processes started by init.d. To enable the limits.conf file, edit /etc/pam.d/su and uncomment the following line: # session required pam_limits.so

www.elastic.co

 

reroute

Elasticsearch에서는 기본적으로 각각의 노드에 샤드가 일정 비율로 균등하게 배치되도록 하고 있습니다. 그래도 뜻하지 않게 특정 노드에 샤드가 많이 배치될 수도 있고 어떤 인덱스는 샤드의 크기가 매우 커서 노드의 스토리지 사용량이 불균형이 발생할 수도 있습니다. 이런 경우 Elasticsearch에서 reroute를 이용하여 해당 문제를 해결할 수 있습니다.

샤드 재배치

해당 인덱스의 특정 샤드를 reroute를 이용하여 "elasticsearch-data-1"에서 "elasticsearch-data-3"노드로 이동하도록 하겠습니다.

curl -X POST http://localhost:9200/_cluster/reroute -H 'Content-Type: application/json' -d '
{
  "commands": [
    {
      "move": {
        "index": "my-log",
        "shard": 0,
        "from_node": "elasticsearch-data-1",
        "to_node": "elasticsearch-data-3"
      }
    }
  ]
}
'

성공했을 때의 결과는 다음과 같이 출력됩니다.

{
  "acknowledged": true,
  "state": {
    "cluster_uuid": "kvqSEAWcRiKL1Riklcqi_w",
    "version": 93,
    "state_uuid": "HnEEoQ3YRVSEzzme0i_5hg",
    "master_node": "iWgI2xYvRYyhfhdV_TFKKQ",
    "blocks": {},
    "nodes": {...생략 ...},
    "routing_table": {...생략 ...},
    "routing_nodes": {...생략 ...},
    "health": {
      "disk": {
        "high_watermark": "90%",
        "high_max_headroom": "150gb",
        "flood_stage_watermark": "95%",
        "flood_stage_max_headroom": "100gb",
        "frozen_flood_stage_watermark": "95%",
        "frozen_flood_stage_max_headroom": "20gb"
      }
    }
  }
}

해당 request를 처리하는데 관여한 노드의 정보와 인덱스 샤드의 대한 정보가 출력되며 disk의 설정 값까지 출력됩니다.

 

request를 수행하기 전과 수행한 후의 샤드의 배치 모습을 아래와 같이 그림으로 그렸습니다. 여기서 중요한 것은 "Shard 0"의 Primary shard와 Replica shard가 고가용성(HA)을 유지하기 위해서 같은 노드에 존재하게 하면 안 됩니다.

 

마지막으로

위에서 설명한 것처럼 샤드는 인덱스 별로 크기가 모두 다르기 때문에 스토리지 사용량을 균일하게 분배할 때 용이하게 사용할 수 있을 것 같습니다. 또한 reroute는 샤드의 재배치뿐만 아니라 특정 노드가 드롭되었을 때 샤드를 복구하는 과정에서 해당 샤드를 어디에 배치하는지에 대한 설정도 가능합니다. 자세한 내용은 아래 elasticsearch 공식 문서를 참고해 주세요

참고

https://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-reroute.html#cluster-reroute-api-prereqs

 

Cluster reroute API | Elasticsearch Guide [8.10] | Elastic

move Move a started shard from one node to another node. Accepts index and shard for index name and shard number, from_node for the node to move the shard from, and to_node for the node to move the shard to. cancel Cancel allocation of a shard (or recovery

www.elastic.co

 

copy_to란?

Elasitcsearch의 index에는 여러 필드들이 존재합니다. copy_to는 여러 필드의 값을 그룹으로 묶어 하나의 필드로 검색할 수 있는 기능을 제공하는 것입니다. Elasticsearch 공식 문서에서는 first name과 last name을 예시를 들어 두 필드를 하나의 full name이라는 필드로 copy_to 하여 full name 하나의 필드만으로 first name과 last name을 모두 검색이 가능하도록 하였습니다. 이렇게 copy_to를 이용하면 검색을 할 때 "피트"라는 사람이 "피트"가 first name인지 last name인지 명확하게 알지 못할 때 검색이 유용할 뿐만 아니라 여러 필드를 자주 검색할 때 쿼리에 여러 필드를 검색하는 것보다 하나의 copy_to 필드를 검색하는 것이 검색 속도 향상에도 많은 도움이 된다고 합니다.

 

copy_to 사용해보기

Elasticsearch 공식 문서에서는 first name, last name을 예시로 들었지만 저 같은 경우 Elasticsearch를 로그를 저장하고 분석하는데 많이 사용하기 때문에 App name, Service name을 예시로 사용해 보겠습니다.

 

my-log 인덱스 필드 타입

필드명 타입
app keyword
service keyword
app_service keyword
level keyword
message text

 

my-log 인덱스 생성

curl -X PUT http://localhost:9200/my-log -H 'Content-Type: application/json' -d '
{
  "mappings": {
    "properties": {
      "app": {
        "type": "keyword",
        "copy_to": "app_service"
      },
      "service": {
        "type": "keyword",
        "copy_to": "app_service"
      },
      "app_service": {
        "type": "keyword"
      },
      "level": {
        "type": "keyword"
      },
      "message": {
        "type": "text"
      }
    }
  }
}
'

 

데이터는 아래와 같이 넣어줬습니다.

{"app":"payment", "service":"api_server", "level": "debug", "message": "A payment was successful"}
{"app":"payment", "service":"api_server", "level": "debug", "message": "B payment was successful"}
{"app":"client", "service":"server", "level": "debug", "message": "client connected"}

 

이제 app_service라는 필드 하나만으로 payment를 검색하여 2개의 데이터가 출력되는지 확인합니다.

curl -X POST http://localhost:9200/my-log/_search -H 'Content-Type: application/json' -d '
{
  "query": {
    "match": {
      "app_service": {
        "query": "payment"
      }
    }
  }
}
'

 

출력결과

{
  "took": 43,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 2,
      "relation": "eq"
    },
    "max_score": 0.5908618,
    "hits": [
      {
        "_index": "my-log",
        "_id": "1",
        "_score": 0.5908618,
        "_source": {
          "app": "payment",
          "service": "api_server",
          "level": "debug",
          "message": "A payment was successful"
        }
      },
      {
        "_index": "my-log",
        "_id": "2",
        "_score": 0.5908618,
        "_source": {
          "app": "payment",
          "service": "api_server",
          "level": "debug",
          "message": "B payment was successful"
        }
      }
    ]
  }
}

위에서 설명했던 것과 같이 여러 필드를 검색 조건에 전부 기입하는 것 보다 이렇게 copy_to를 이용하면 개발자 입장에서 보다 간편하고 검색 성능을 높을 수 있습니다. copy_to를 잘 활용만 한다면 상황에 따라 개발하는데 많은 도움이 될 것 같습니다.

 

참고

https://www.elastic.co/guide/en/elasticsearch/reference/current/copy-to.html

 

copy_to | Elasticsearch Guide [8.10] | Elastic

copy_to is not supported for field types where values take the form of objects, e.g. date_range

www.elastic.co

Elasticsearch cluster

Elasticsearch의 Cluster는 물리적으로 나뉜 하나 이상의 노드를 논리적으로 하나의 서버 그룹으로 묶어 관리하는 것입니다. 하나의 노드로 모든 역할을 수행할 수 있도록 설정할 수 있지만 그렇게 설정할 경우 하나의 노드가 모든 부하를 받기 때문에 클러스터가 깨질 수 있는 위험이 있습니다. 물론 개인 테스트 환경 및 개발환경에서는 리소스를 아끼기 위해 이렇게 설정할 수 있지만 상용으로 사용할 경우 Cluster를 구성할 때는 각각의 노드가 역할을 분리하여 한 노드의 문제가 발생하더라도 클러스터를 유지하는데 문제가 없게 클러스터를 구성하는 것이 좋습니다.

각 노드들의 역할

위에서 설명한 것처럼 Elasticsearch에서는 모든 역할을 수행하는 하나의 노드로 클러스터를 구성할 수도 있지만 안정적인 클러스터를 운영하기 위해서 각각의 노드의 역할을 분리하여 관리할 수 있습니다. 뿐만 아니라 규모가 커질 경우 자신에 상황에 맞게 해당 노드에 리소스를 할당하여 리소스를 보다 효율적으로 사용할 수 있습니다. Elasticsearch 노드의 종류와 기능은 다음과 같습니다.

 

마스터 노드 (Master node)

마스터 노드는 Elasticsearch의 클러스터 전체를 관리하며 각 노드들의 연결 상태 및 인덱스 및 샤드의 상태 등을 관리하는 역할을 수행합니다. 마스터 노드의 개수를 여러 개 설정하여 현재 마스터는 하나만 수행하고 나머지 마스터 노드는 현재 마스터 노드에 문제가 발생하면 마스터로 승격될 수 있도록 스탠바이 형식으로 구성하여 안정적인 클러스터 운영을 할 수 있습니다. 마스터 노드의 개수는 보팅 알로리즘으로 인한 스플릿 브레인 현상이 발생하지 않도록 1개 이상의 홀수개로 생성하는 것을 권장합니다.

 

데이터 노드 (Data node)

데이터 노드는 이름 그대로 데이터를 처리하는 노드입니다. 데이터를 저장하고 색인처리를 수행하며 저장된 데이터를 검색하는 역할을 수행할 수 있습니다.  데이터 노드를 여러 개 생성하여 데이터를 분산 저장하여 하나의 노드에 부하가 몰리는 것을 방지할 수 있습니다. 데이터 노드는 hot, warm, cold 등으로 여러 개로 분리하여 리소스를 보다 효율적으로 사용할 수 있도록 설정할 수 있습니다.

 

코디네이팅 노드 (Coordinating Node)

코디네이팅 노드는 요청을 받아서 해당 요청을 처리하는 역할을 수행합니다. 코디네이팅 노드는 별도로 설정하는 것이 아닌 요청을 받은 노드를 코디네이팅 노드라고 합니다. 그렇기 때문에 일반적으로 데이터 노드에서 코디네이팅 역할을 같이 수행하도록 하지만 대규모 클러스터에서는 데이터를 저장하지 않고 요청만 받아서 처리하는 코디네이팅 노드를 별도로 구성하여 요청에 대한 부하를 분리하기도 합니다.

 

Elasticsearch cluster 구성도

 

저는 별도로 사용해 본 적은 없지만 그밖에 ingest, ml, client, transform 등 여러 형식의 노드 또한 존재합니다.

index, shard, document

도큐먼트 (Document)

도큐먼트는 Elasticsearch의 가장 작은 데이터 단위를 나타냅니다. 도큐먼트는 JSON 형식으로 표현되며 인덱스로 색인되어 저장됩니다. 데이터가 인덱싱 될 때 자동으로 고유한 도큐먼트 아이디가 생성되어 저장됩니다.

 

인덱스 (Index)

인덱스는 Elasticsearch의 핵심적인 기능으로 데이터를 그룹화하여 데이터를 저장하고 검색 및 통계를 할 수 있도록 하는 기능을 수행합니다. 인덱스는 고유한 이름을 가지며 하나 이상의 샤드로 구성되어 여러 노드에 분산되어 데이터가 저장됩니다. index는 template을 구성하여 데이터를 보다 효율적으로 관리할 수 있으며 ilm(Index Lifecycle Manager)를 통해 인덱스의 생명주기를 관리할 수 있습니다.

 

인덱스 구성 방법

  • Long term retention index : 인덱스의 크기가 크지 않고 변화가 많지 않아 지속적으로 데이터를 읽기 위해 사용하는 방식으로 단일 인덱스로 구성된 방식입니다.
  • Rolling update : 인덱스를 my-index-2023-09-29, my-ingdex-2023-09-30 등과 같이 날짜별로 구성하여 관리하는 방식입니다. 날짜별로 데이터가 분리되어 저장되었기 때문에 시계열 데이터 및 로그와 같은 데이터인 경우 날짜별로 인덱스를 관리하고 검색하여 보다 효율적으로 사용할 수 있는 방식입니다. 뿐만 아니라 날짜 별로 데이터를 스냅샷을 찍을 수 있으며 복구할 수 있어 데이터를 날짜별로 관리해야 되는 경우 매우 유용한 방식입니다.
  • Rollover : 인덱스의 크기를 예측하기 어려운 경우 사용하는 방식입니다. 하나의 인덱스가 너무 크면 해당 인덱스의 검색에 많은 부하를 받을 수 있기 때문에 인덱스가 일정 개수의 도큐먼트 또는 일정 크기에 도달했을 경우 인덱스를 분리하여 저장하는 방식입니다. 예시로 my-index-0001, my-index-0002 형식으로 데이터가 저장되며 검색할 때는 모든 같은 형식의 이름의 인덱스를 Alias를 통해 모두 검색합니다.

샤드 (Shard)

인덱스를 구성하는 단위로 인덱스는 하나 이상의 샤드로 구성됩니다. Elasticsearch에서는 각 노드에 부하를 분산하기 위해 샤드를 기반으로 한 인덱스의 데이터가 분산 저장하게 됩니다. 또한 데이터를 검색하는 데 있어 병렬로 데이터를 검색하여 빠른 검색 및 색인이 가능해집니다. 샤드의 수는 인덱스가 생성되고 나서 수정할 수 없어 새롭게 생성되는 인덱스에만 샤드를 설정할 수 있습니다. 또한 샤드는 Primary 샤드와 Replica 샤드로 구분되며 Primary 샤드는 원본 데이터를 저장하고 Replica 샤드에서는 Primary 샤드를 복제하여 고가용성(HA)을 보장해 주는 역할을 수행합니다.

샤드는 Elasticsearch 7 버전부터 기본값으로 인덱스당 1개가 생성되게 되어 있습니다. 그렇기 때문에 인덱스를 생성할 때 샤드의 개수를 지정해야 됩니다. 만약 너무 많은 양의 샤드를 구성할 경우 Master 노드에서 클러스터를 운영하는데 부하를 받기 때문에 샤드의 크기는 하나의 샤드당 30GB 이하의 크기를 가지도록 설정하여 적당한 개수를 가지게 하는 것을 권장합니다. 인덱스당 샤드를 최대 1024개까지 구성할 수 있습니다.

Lucene

Shard는 루씬 인덱스로 구성되며 루씬 인덱스는 여러개의 segment로 구성되어 있습니다. 다수의 세그먼트로 분산되어 있기 때문에 병렬로 데이터를 검색을 수행하며 역색인 형식으로 데이터를 저장하여 검색에 최적화되어 있습니다. IndexWriter를 통하여 새로운 세그먼트가 생성되고 세그먼트의 개수가 늘어나면 주기적으로 각각의 세그먼트를 병합(merge)하여 일정 크기의 세그먼트를 유지하게 됩니다.

세그먼트는 읽기 전용이기 때문에 한번 기록되면 변경이 불가능합니다. 데이터가 변경되면 새로운 세그먼트가 생성되고 변경된 내용이 반영되기 위해서는 Commit이라는 작업을 통해 이루어집니다. 세그먼트가 인덱스 샤드에 구성되는 모습은 다음과 같습니다.

segment

검색 동작 과정

Request를 요청하게 되면 Request를 수신한 노드(코디네이팅 노드)는 해당 인덱스의 샤드를 가지고 있는 데이터 노드에게 검색을 요청하게 됩니다. 그럼 해당 샤드에서 조건에 맞는 데이터를 검색하여 리턴하게 되고 사용자 요청을 수신한 노드에서 결과를 통합하여 사용자에게 전달합니다.

 

 

kubernetes 환경에서 elasticsearch를 설치하는 방법을 정리하였습니다. 비록 테스트 환경이지만 실제 운영환경처럼 구성하기 위해 3대의 마스터 노드와 2대의 데이터 노드로 구성하였습니다(현업에서 직접 사용할 경우 보다 더 여유 있는 구성을 추천드립니다) 3대의 마스터 노드 중 실제로 마스터의 역할을 수행하는 것은 1대이며 나머지 2대의 마스터 노드는 현재 마스터 노드의 문제가 발생할 경우 마스터로 승격할 수 있도록 Standby 형식으로 구성하였습니다.

 

1. Helm chart 다운로드

저는 kubernets 환경에서 elasticsearch를 설치하기 위해 helm을 사용하였습니다. 아래 링크의 helm으로 구성하였습니다.
https://artifacthub.io/packages/helm/elastic/elasticsearch

 

elasticsearch 8.5.1 · elastic/elastic

Official Elastic helm chart for Elasticsearch

artifacthub.io

 

helm 명령어를 통하여 repo를 추가하고 다운로드합니다.

# helm repo 추가
$ helm repo add elastic https://helm.elastic.co

# helm 파일 다운로드
$ helm pull elastic/elasticsearch
$ ls -al
drwxr-xr-x@  6 hsw  staff    192  6 25 09:45 .
drwxr-xr-x@  9 hsw  staff    288  6 25 09:44 ..
-rw-r--r--@  1 hsw  staff  28898  6 25 09:45 elasticsearch-8.5.1.tgz

# helm 압축 출기
$ tar -zxvf elasticsearch-8.5.1.tgz
$ ls -al
drwxr-xr-x@  6 hsw  staff    192  6 25 09:45 .
drwxr-xr-x@  9 hsw  staff    288  6 25 09:44 ..
drwxr-xr-x@ 12 hsw  staff    384  6 25 10:26 elasticsearch
-rw-r--r--@  1 hsw  staff  28898  6 25 09:45 elasticsearch-8.5.1.tgz

 

2. values.yaml 수정

elasticsearch 디렉토리로 이동하여 values 값을 설정합니다. 저의 경우 values.yaml 파일을 직접적으로 수정하는 것보다는 새로운 yaml을 구성하여 values.yaml 파일을 override 하여 사용하는 것을 선호하기 때문에 마스터 노드와 데이터 노드 별로 설정파일을 구성하여 작업하였습니다.

master.yaml 파일 구성

마스터 노드는 3대로 구성되어 있고 클러스터 유지를 위해 CPU는 4 코어, Memory는 16GB로 여유 있게 구성하였습니다. 그리고 Lucene의 메모리 사용을 위해 16GB의 절반인 8GB만 JVM옵션으로 설정을 하였습니다.

tolerations, nodeAffinity의 경우 사용자 환경에 맞게 설정하여 사용해 주세요 사용법은 링크를 달아놨습니다.
tolerations : https://kubernetes.io/ko/docs/concepts/scheduling-eviction/taint-and-toleration/
nodeAffinity : https://kubernetes.io/ko/docs/concepts/scheduling-eviction/assign-pod-node/

##################################
# cluster info
createCert: false
clusterName: "elasticsearch"
nodeGroup: "master"
replicas: 3
imageTag: "7.10.2"

roles:
- master

##################################
# nodeAffinity
tolerations:
- key: "develop/elasticsearch-master"
  operator: "Exists"

nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
  nodeSelectorTerms:
    - matchExpressions:
      - key: develop/group
        operator: In
        values:
        - elasticsearch-master

##################################
# cluster config
# 테스트 환경이라 보안 설정을 끔
esConfig:
  elasticsearch.yml: |
    xpack.security.enabled: false

# AWS S3 스냅샷 테스트를 위한 설정
esJvmOptions:
  jvm.options: |
    -Des.allow_insecure_settings=true

##################################
# resources
esJavaOpts: "-Xmx8g -Xms8g"

resources:
  requests:
    cpu: 4
    memory: "16Gi"
  limits:
    cpu: 4
    memory: "16Gi"

volumeClaimTemplate:
  resources:
    requests:
      storage: 10Gi
  storageClassName: openebs

##################################
# service
protocol: http
service:
  type: NodePort
  nodePort: "30090"

data.yaml 파일 구성

데이터 노드의 경우 마스터 노드보다 더 많은 요청을 처리하고 데이터 관리를 수행하기 때문에 마스터 노드보다 더 여유 있는 스펙으로 리소스를 구성하였습니다. 데이터 노드 또한 마스터 노드처럼 사용자의 환경에 맞는 값으로 구성해 주세요

##################################
# cluster info
createCert: false
clusterName: "elasticsearch"
nodeGroup: "data"
replicas: 2
imageTag: "7.10.2"

roles:
- data
- ingest

##################################
# nodeAffinity
tolerations:
- key: "develop/elasticsearch-data"
  operator: "Exists"

nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
  nodeSelectorTerms:
    - matchExpressions:
      - key: develop/group
        operator: In
        values:
        - elasticsearch-data

##################################
# cluster config
# 테스트 환경이라 보안 설정을 끔
esConfig:
  elasticsearch.yml: |
    xpack.security.enabled: false

# AWS S3 스냅샷 테스트를 위한 설정
esJvmOptions:
  jvm.options: |
    -Des.allow_insecure_settings=true

##################################
# resources
esJavaOpts: "-Xmx16g -Xms16g"

resources:
  requests:
    cpu: 8
    memory: "32Gi"
  limits:
    cpu: 8
    memory: "32Gi"

volumeClaimTemplate:
  resources:
    requests:
      storage: 200Gi
  storageClassName: openebs

##################################
# service
# UI 사용을 위해 NodePort로 설정
protocol: http
service:
  type: NodePort
  nodePort: "30091"

Makefile 구성

저는 지속적으로 지웠다 설치했다는 반복 하는데 편리하기 위해 Makefile을 구성하였습니다.

PREFIX := es
TIMEOUT := 1200s
NAMESPACE := elasticsearch

install:
helm upgrade --wait --timeout=$(TIMEOUT) --install -n $(NAMESPACE) --create-namespace --values override-master.yaml $(PREFIX)-master .
helm upgrade --wait --timeout=$(TIMEOUT) --install -n $(NAMESPACE) --create-namespace --values override-data.yaml $(PREFIX)-data .

purge:
helm del $(PREFIX)-master -n $(NAMESPACE)
helm del $(PREFIX)-data -n $(NAMESPACE)

 

3. 설치 진행

Makefile을 이용하여 설치를 진행합니다.

# elasticsearch 설치
$ make install

 

설치 작업이 완료 되면 Elasticsearch는 다음과 같이 구성됩니다.

pod/elasticsearch-data-0             1/1     Running     0       1d
pod/elasticsearch-data-1             1/1     Running     0       1d
pod/elasticsearch-master-0           1/1     Running     0       1d
pod/elasticsearch-master-1           1/1     Running     0       1d
pod/elasticsearch-master-2           1/1     Running     0       1d

service/elasticsearch-data               NodePort    10.233.47.200  <none>   9200:30091/TCP,9300:31095/TCP   1d
service/elasticsearch-data-headless      ClusterIP   None           <none>   9200/TCP,9300/TCP               1d
service/elasticsearch-master             NodePort    10.233.56.28   <none>   9200:30090/TCP,9300:32277/TCP   1d
service/elasticsearch-master-headless    ClusterIP   None           <none>   9200/TCP,9300/TCP               1d

statefulset.apps/elasticsearch-data         2/2     1d
statefulset.apps/elasticsearch-master       3/3     1d

 

4. Elasticsearch 삭제

테스트가 끝나고 elasticsearch를 지우기 위해서는 다음과 같이 진행하면 됩니다.

# Elasticsearch 제거
$ make purge

 

본 문서는 2023년 6월에 작성된 문서입니다.

Elasticsearch 라이센스 문제로 현재 7.10 버전을 사용하고 있습니다. 그러나 7.10 버전에서는 AWS S3에 저장하는 기능이 기본적으로 제공하고 있지 않습니다. 그래서 7.10 버전을 AWS S3에 스냅샷 저장하는 방법에 대하여 문서를 작성하였습니다. (참고로 8.0 버전 이상에서는 기본적으로 AWS S3 Snapshot을 제공하고 있습니다. 8.0 이상의 버전을 사용하시는 분들은 큰 제목 3번부터 진행하셔도 됩니다.)

1. 현재 나의 환경은?

저의 경우 Elasticsearch를 kubernetes에 설치하여 사용하고 있습니다. 그렇기 때문에 Kubernetes 환경 위주로 설명을 진행하지만 플러그인 설치하는 방법은 비슷하므로 일반적인 VM 방법과 Kubernetes 및 Docker에서 사용하는 방법에 대하여 정리하였습니다.

VM Elasticsearch

Elasticsearch는 공식적으로 제공하지 않는 부분에 있어 Elasticsearch Plugin을 통하여 확장 기능을 설치할 수 있도록 제공하고 있습니다. 이런 기능을 사용하여 Elasticsearch가 설치된 VM으로 접속하여 아래 명령어를 통하여 모든 노드에 AWS S3 Plugin을 설치하여 줍니다.

bin/elasticsearch-plugin install --batch repository-s3

모든 노드에 플러그인이 설치가 되었다면 모든 노드들을 하나씩 재시작하여 줍니다.

Kubernetes 및 Docker 환경

kubernetes, docker 환경에서는 플러그인을 설치하여 재시작을 하게 되면 해당 파드가 초기화가 되기 때문에 위 방법을 사용하지 않고 Elasticsearch 이미지를 다시 생성하여 사용해야 합니다. 

FROM docker.elastic.co/elasticsearch/elasticsearch:7.10.2
RUN bin/elasticsearch-plugin install --batch repository-s3

 

다음과 같이 Dockerfile을 생성하여 이미지를 빌드하고 빌드된 이미지를 사용합니다.

docker build -t (이미지) (도커파일경로)

2. Helm을 이용하여 Elasticsearch 설치 진행

저는 아래의 ArtifactHub를 통해 Elasticsearch 설치 작업을 진행하였습니다. 해당 문서는 AWS S3 스냅샷에 대한 내용이므로 Helm 사용법에 대해서는 추후 문서를 작성하여 링크를 붙이도록 하겠습니다. values.yaml 파일을 다음과 같이 수정하여 Elasticsearch 설치를 진행합니다. https://artifacthub.io/packages/helm/elastic/elasticsearch

image: "위에서 생성한 이미지"
imageTag: "이미지 태그"

esJvmOptions:
  jvm.options: |
    -Des.allow_insecure_settings=true

3. Elasticsearch Snapshot S3 Repo 등록

Elasticsearch가 정상적으로 설치가 되었다면 스냅샷을 저장할 Repo를 생성합니다. 한글로 작성된 부분을 사용자 환경에 맞는 값을 넣어주세요.

curl -XPUT 'localhost:9200/_snapshot/my_repository?pretty' -H 'Content-Type: application/json' -d '{
  "type": "s3",
  "settings": {
    "bucket": "버킷",
    "region": "리전",
    "base_path": "백업경로",
    "access_key": "엑세스키",
    "secret_key": "스크릿키",
    "compress": true
  }
}'

# 성공 결과
{
  "acknowledged" : true
}

4. 정상적으로 Repo가 생성되었는지 확인

curl -XGET localhost:9200/_snapshot/_all?pretty

5. Snapshot 생성

SLM(Snapshot Lifecycle Management) 를 진행하실 분은 해당 부분을 스킵하여도 됩니다.
해당 Request를 통하여 스냅샷 생성이 성공적으로 진행이 되었다면 스냅샷 정보와 스냅샷에 성공한 샤드와 실패한 샤드에 대한 개수를 알려줍니다. 종종 네트워크의 문제로 특정 노드의 샤드가 실패할 수 있는데 이런 경우 다시 시작하여 주시면 됩니다.

curl -XPUT "http://localhost:9200/_snapshot/my_repository/backup_20230613?wait_for_completion=true?pretty" -H "Content-Type: application/json" -d '
{
  "indices": "*",
  "ignore_unavailable": true,
  "include_global_state": false
}'

# 결과
생략 ...
{"total":50,"failed":0,"successful":50}

6. SLM 생성

SLM(Snapshot Lifecycle Management) 이란 Snapshot의 생명주기를 관리해 주는 기능입니다. 일정 시간에 자동으로 스냅샷을 생성하고 저장 기간을 관리하여 줍니다.

curl -X PUT 'http://localhost:9200/_slm/policy/my-snapshot' -H 'Content-Type: application/json' -d '
{
  "schedule": "0 30 1 * * ?", 
  "name": "<backup-{now/d{yyyy-MM-dd}}>",
  "repository": "my_repository", 
  "config": { 
    "indices": ["*"],
    "ignore_unavailable": true,
    "include_global_state": false
  },
  "retention": { 
    "expire_after": "30d", 
    "min_count": 5, 
    "max_count": 50 
  }
}
'

# 결과
{
  "acknowledged" : true
}
  • schedule : 스냅샷을 생성할 시간을 입력합니다.
  • name : 생성된 스냅샷을 이름 규칙을 입력합니다.
  • repository : 큰제목 3번에서 생성한 Repo를 입력합니다.
  • config
    • indices : 스냅샷 생성할 인덱스를 입력합니다. (* 이면 모든 인덱스를 스냅샷으로 저장합니다.)
    • ignore_unavailable : 유효하지 않는 값은 무시합니다.
    • include_global_state : 글로벌 설정 값을 저장할지 결정합니다.
  • retention
    • expire_after : 스냅샷 보존 기간을 설정합니다.
    • min_count : 스냅샷 최소 보존 개수를 설정합니다.
    • max_count : 스냅샷 최대 보존 개수를 설정합니다.

위 작업이 완료되었다면 매일 새벽 1시 30분마다 모든 인덱스의 스냅샷을 생성하고 30일 동안 보존되며 최대 50개 최소 5개의 스냅샷을 관리합니다.

7. SLM을 이용하여 바로 스냅샷 생성해 보기

SLM이 정상적으로 잘 작동하는지 우리는 새벽 1시 30분까지 기다릴 수 없습니다. 그렇기 때문에 다음 API를 통하여 방금 설정한 SLM을 통해 스냅샷을 지금 바로 생성하는 방법을 알아보겠습니다.

curl -X POST 'http://localhost:9200/_slm/policy/my-snapshot/_execute?pretty'

# 결과
{
  "snapshot_name" : "backup-2023-06-13-hci5ctcoq9wjfiwiuy2osg"
}

생성된 스냅샷 결과 확인

curl -XGET 'http://localhost:9200/_snapshot/my_repository/_all?pretty'

# 결과
    {
      "snapshot" : "backup-2023-06-13-hci5ctcoq9wjfiwiuy2osg",
      "uuid" : "wXXFF223FTt-rLTKIOFQk2A",
      "version_id" : 7100299,
      "version" : "7.10.2",
      "indices" : [
        "인덱스-0",
        "인덱스-1",
        "인덱스-2",
        "인덱스-3",
        "인덱스-4",
        "인덱스-5"
      ],
      (생략 ...)
      "state" : "SUCCESS",
      "start_time" : "2023-06-13T06:38:37.211Z",
      "start_time_in_millis" : 1686638317211,
      "end_time" : "2023-06-13T06:38:50.620Z",
      "end_time_in_millis" : 1686638330620,
      "duration_in_millis" : 13409,
      "failures" : [ ],
      "shards" : {
        "total" : 50,
        "failed" : 0,
        "successful" : 50
      }
    }

8. 스냅샷으로 복구하기

위에서 생성한 스냅샷으로 복구하는 방법은 다음과 같습니다. 그러나 여기서 명심해야 되는 것은 이미 같은 이름의 인덱스가 Elasticsearch에 존재하면 안 됩니다. 스냅샷은 특정 시점으로 돌아가는 것이기 때문에 같은 이름의 인덱스가 존재하면 해당 인덱스를 삭제해야 합니다.

curl -X POST "http://localhost:9200/_snapshot/my_repository/backup-2023-06-13-hci5ctcoq9wjfiwiuy2osg/_restore?pretty" -H "Content-Type: application/json" -d '
{
  "indices": "*"
}'

# 결과
{"accepted":true}
  • indices : 복구하려는 인덱스를 명시합니다.

 


Snapshot 삭제하기

curl -XDELETE 'http://localhost:9200/_snapshot/my_repository/backup-2023-06-13-hci5ctcoq9wjfiwiuy2osg?pretty'

Snapshot Repo 삭제하기

curl -XDELETE 'http://localhost:9200/_snapshot/my_repository?pretty'

 

elasticdump는 Elasticsearch 인덱스와 데이터를 내보내거나 가져오기 위한 도구입니다. elasticdump를 사용하면 Elasticsearch 클러스터의 index, document, template 등을 백업하거나 다른 클러스터로 마이그레이션하는 작업을 수행할 수 있습니다. 본 문서에서 elasticdump의 기본 적인 사용법에 대해서 정리하였습니다.

elasticdump install

$ sudo apt update
$ sudo apt install npm

$ npm install elasticdump -g

$ elasticdump --version
6.103.0
(node:16397) NOTE: We are formalizing our plans to enter AWS SDK for JavaScript (v2) into maintenance mode in 2023.
(생략...)

Elasticsearch Index의 데이터를 파일로 가져오기

Index의 documents데이터를 디렉토리에 파일로 가져올 수 있습니다. 현재 Index에 저장된 데이터는 다음과 같습니다.

 

이제 elasticdump를 이용하여 파일로 가져오겠습니다.

$ elasticdump --input="http://localhost:9200/my-index" --output="./my-index.txt"

 

위 명령어를 통하여 현재 디렉토리에 my-index.txt 파일로 데이터를 가져왔습니다. 가져온 데이터는 다음과 같습니다.

{"_index":"my-index","_type":"_doc","_id":"RO_7BokBQKytg6noxeXW","_score":1,"_source":{"uid":"1","message":"aaaa","type":"APP","timestamp":1688038455}}
{"_index":"my-index","_type":"_doc","_id":"Ru_7BokBQKytg6noxeXW","_score":1,"_source":{"uid":"3","message":"cccc","type":"DB","timestamp":1688038457}}
{"_index":"my-index","_type":"_doc","_id":"Re_7BokBQKytg6noxeXW","_score":1,"_source":{"uid":"2","message":"bbbb","type":"APP","timestamp":1688038456}}
{"_index":"my-index","_type":"_doc","_id":"R-_7BokBQKytg6noxeXW","_score":1,"_source":{"uid":"4","message":"dddd","type":"APP","timestamp":1688038459}}
{"_index":"my-index","_type":"_doc","_id":"SO_7BokBQKytg6noxeXW","_score":1,"_source":{"uid":"5","message":"eeee","type":"DB","timestamp":1688038459}}
{"_index":"my-index","_type":"_doc","_id":"Se_7BokBQKytg6noxeXW","_score":1,"_source":{"uid":"6","message":"ffff","type":"DB","timestamp":1688038459}}

파일로 가져온 데이터 Elasticsearch로 보내기

앞에서는 elasticsearch에서 데이터를 가져오는 방법을 알아봤습니다. 파일에서 elasticsearch로 전송하는 방법은 input과 output를 반대로 설정해 주면 파일에서 elasticsearch로 데이터를 전송합니다.

elasticdump --input="./my-index.txt" --output="http://localhost:9200/my-index"

A Cluster에서 B Cluster로 데이터 전송하기

이번에는 파일로 가져오지 않고 다른 클러스터로 바로 데이터를 전송하겠습니다.

A Cluster가 "localhost:9200"이고 B Cluster는 "localhost:9400" 일 경우 A Cluster에서 B Cluster로 데이터를 전송해보겠습니다.

elasticdump --input="http://localhost:9200/my-index" --output="http://localhost:9400/my-index"

그밖에 elasticdump 옵션

  • --type=analyzer: settings를 내보낸다.
  • --type=mapping: mapping을 내보낸다.
  • --type=data: documents를 내보낸다 (Default).
  • --overwirte: export 파일이 이미 존재한다면 덮어쓰기 한다.
  • --limit: 지정한 limit 개수만큼씩 끊어 가져온다. (Default: 100)
  • --output-index=${index명}: 백업해 놓은 파일로부터 가져오기 할 때 원래 인덱스가 아닌 원하는 인덱스로 지정해 줄 수 있다.

마지막으로

elasticdump는 적은 데이터를 옮길 때는 편하게 사용할 수 있지만 속도가 매우 느리기 때문에 큰 인덱스의 데이터를 가져오는 것은 추천하지 않습니다. 큰 데이터를 마이그레이션 할 때는 snapshot을 이용하여 특정 인덱스만 가져오는 것을 추천드립니다.

 

더 자세한 사용방법은 elasticdump github에서 확인할 수 있습니다.

https://github.com/elasticsearch-dump/elasticsearch-dump

 

GitHub - elasticsearch-dump/elasticsearch-dump: Import and export tools for elasticsearch

Import and export tools for elasticsearch. Contribute to elasticsearch-dump/elasticsearch-dump development by creating an account on GitHub.

github.com

 

Elasticsearch의 성능을 위해 데이터를 한 건 한 건 처리하는 것이 아닌 bulk를 이용하여 한 번에 처리하는 경우가 많습니다.
bulk를 사용하여 많은 양의 데이터를 Elasticsearch에 저장할 경우 분명 HTTP 요청은 정상적으로 끝이 났는데 데이터가 저장이 안 되는 경우가 발생할 수 있습니다. 여기서 중요한 것은 Elasticsearch로 보낸 HTTP의 Response는 200이라는 정상적인 값을 리턴을 하기 때문에 문제를 찾기가 어려운데요. 이럴 때는 Response의 state code를 보는 것이 아라 Response의 body의 내용을 볼 필요가 있습니다.

문제 해결방법

해당 문제를 해결하는 방법은 2가지 방법이 있습니다.

  1. 한번에 요청하는 bulk의 사이즈를 줄인다 : 만약 자원의 부족으로 thread 설정이 불가능한 경우 한번에 처리하는 bulk의 개수를 줄여서 해당 문제를 해결할 수 있습니다.
  2. thread_pool의 wrtie queue size를 늘린다 : http request connect를 통한 부하를 줄이는 것이 더 중요한 경우 1번 방법을 사용할 수 없습니다. 그런 경우 thread가 처리할 수 있는 자원이 있을 때 2번 방법을 통하여 한번에 처리할 수 있는 bulk의 양을 늘려서 문제를 해결할 수 있습니다.

모든 상황에 만족하는 문제 해결은 없습니다. 본인의 환경에 맞는 문제 해결 방법을 찾고 해당 방법을 적용하는 것을 추천드립니다.

아래는 2번 방법을 통해 한번에 처리할 수 있는 bulk의 양을 늘리는 방법에 대해 정리하였습니다.

설정 변경하기

Elasticsearch 디렉토리 위치에 config/elasticsearch.yml파일에 다음과 같은 설정 값을 추가하고 Elasticsearch를 재시작합니다.

thread_pool.write.queue_size: 2000
thread_pool.write.size: 16

설정을 진행하자가 문제가 발생한 경우

만약 다음과 같은 메시지가 발생한 다면 thread_pool.write.size의 값을 13보다 작은 값으로 설정하세요.

java.lang.IllegalArgumentException: Failed to parse value [16] for setting [thread_pool.write.size] must be <= 13

설정값의 의미

  • thread_pool.write.size : write를 수행하는 스레드 풀의 스레드 수
  • thread_pool.write.queue_size : write를 할 때 사용되는 스레드 풀의 큐 크기
  • thread_pool.search.size : search를 수행하는 스레드 풀의 스레드 수
  • thread_pool.search.queue_size : earch를 할 때 사용되는 스레드 풀의 큐 크기

마지막으로

thread에 대한 설정은 노드의 리소스 스펙에 한계가 있기 때문에 어느 정도 설정 값 튜닝을 했음에도 불구하고 더 이상의 성능 향상이 없을 경우 노드의 Scale up을 고려하는 것을 추천드립니다.

 

Aggregations을 수행할 때 사용할 데이터

 

Sub Aggregations

aggregation을 통해 집계를 내고 그 결과에서 세부적인 집계를 위해 Sub Aggregation을 지원하고 있습니다.

 

level 필드별 duration의 평균 시간 구하기

우리는 데이터를 가져오는 것이 목적이 아닌 Aggregations을 수행하는 것이 목적이기 때문에 "size"를 0으로 설정하였습니다.

api POST http://localhost:9200/test-log-index-2021-09-12/_search
header content-type: application/json
body {
    "size": 0,
    "query": {
        "match_all": {}
    },
    "aggs": {
        "byLevel": {
            "terms": {
                "field": "level"
            },
            "aggs": {
                "avgDuration": {
                    "avg": {
                        "field": "duration"
                    }   
                }
            }
        }
    }
}

결과 보기

level의 "info"는 9개가 있으며 9개의 duration 평균은 167.777777입니다.

level의 "warn"는 6개가 있으며 6개의 duration 평균은 333.33333입니다.

level의 "debug"는 5개가 있으며 5개의 duration 평균은 258입니다.

 

level별 action 종류 및 개수 측정하기

우리는 데이터를 가져오는 것이 목적이 아닌 Aggregations을 수행하는 것이 목적이기 때문에 "size"를 0으로 설정하였습니다.

api POST http://localhost:9200/test-log-index-2021-09-12/_search
header content-type: application/json
body {
    "size": 0,
    "query": {
        "match_all": {}
    },
    "aggs": {
        "byLevel": {
            "terms": {
                "field": "level"
            },
            "aggs": {
                "byAction": {
                    "terms": {
                        "field": "action"
                    }   
                }
            }
        }
    }
}

결과 보기

{
    "took": 4,
    "timed_out": false,
    "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": {
            "value": 20,
            "relation": "eq"
        },
        "max_score": null,
        "hits": []
    },
    "aggregations": {
        "byLevel": {
            "doc_count_error_upper_bound": 0,
            "sum_other_doc_count": 0,
            "buckets": [
                {
                    "key": "info",
                    "doc_count": 9,
                    "byAction": {
                        "doc_count_error_upper_bound": 0,
                        "sum_other_doc_count": 0,
                        "buckets": [
                            {
                                "key": "close_file",
                                "doc_count": 4
                            },
                            {
                                "key": "open_file",
                                "doc_count": 4
                            },
                            {
                                "key": "open_socket",
                                "doc_count": 1
                            }
                        ]
                    }
                },
                {
                    "key": "warn",
                    "doc_count": 6,
                    "byAction": {
                        "doc_count_error_upper_bound": 0,
                        "sum_other_doc_count": 0,
                        "buckets": [
                            {
                                "key": "close_file",
                                "doc_count": 2
                            },
                            {
                                "key": "open_file",
                                "doc_count": 2
                            },
                            {
                                "key": "close_socket",
                                "doc_count": 1
                            },
                            {
                                "key": "open_socket",
                                "doc_count": 1
                            }
                        ]
                    }
                },
                {
                    "key": "debug",
                    "doc_count": 5,
                    "byAction": {
                        "doc_count_error_upper_bound": 0,
                        "sum_other_doc_count": 0,
                        "buckets": [
                            {
                                "key": "close_file",
                                "doc_count": 2
                            },
                            {
                                "key": "open_file",
                                "doc_count": 2
                            },
                            {
                                "key": "close_socket",
                                "doc_count": 1
                            }
                        ]
                    }
                }
            ]
        }
    }
}

"info"는 9개이며 그중에 "close_file"는 4개, "open_file"은 4개, "open_socket"은 1개 입니다.

"warn"는 6개이며 그중에 "close_file"는 2개, "open_file"은 2개, "open_socket"은 1개, "close_socket"은 1개입니다.

"info"는 9개이며 그중에 "close_file"는 2개, "open_file"은 2개, "close_socket"은 1개입니다.

 

 

Aggregation을 수행할 때 사용할 데이터

terms

terms는 데이터의 keyword별 종류 및 개수를 집계하는데 사용합니다.

 

모든 도큐먼트의 level의 종류와 개수 구하기

우리는 데이터를 가져오는 것이 목적이 아닌 Aggregation을 수행하는 것이 목적이기 때문에 "size"를 0으로 설정하였습니다.

level 필드의 데이터 종류와 각 종류별로 도큐먼트가 몇개 있는지 확인합니다.

api POST http://localhost:9200/test-log-index-2021-09-12/_search
header content-type: application/json
body {
    "size": 0,
    "query": {
        "match_all": {}
    },
    "aggs": {
        "byLevel": {
            "terms": {
                "field": "level"
            }
        }
    }
}

결과 보기

"byLevel" 내부의 buckets를 보면 "key"에는 level필드의 값이 "doc_count"에는 개수를 표현하고 있습니다.

즉 "info"는 9개가 있고, "warn"은 6개, "debug"는 5개가 있는 것을 확인할 수 있습니다.

 

range

값의 범위 별로 도큐먼트의 개수를 측정하는데 사용할 수 있습니다.

 

모든 도큐먼트의 duration의 범위별 개수 측정하기

우리는 데이터를 가져오는 것이 목적이 아닌 Aggregations을 수행하는 것이 목적이기 때문에 "size"를 0으로 설정하였습니다.

duration의 범위 100이하, 100부터 200까지, 200부터 300까지, 300부터 400까지, 400이상 으로 범위를 지정하여 검색하겠습니다.

api POST http://localhost:9200/test-log-index-2021-09-12/_search
header content-type: application/json
body {
    "size": 0,
    "query": {
        "match_all": {}
    },
    "aggs": {
        "byDuration": {
            "range": {
                "field": "duration",
                "ranges": [
                    {
                        "to": 100
                    },
                    {
                        "from": 100,
                        "to": 200
                    },
                    {
                        "from": 200,
                        "to": 300
                    },
                    {
                        "from": 300,
                        "to": 400
                    },
                    {
                        "from": 400
                    }
                ]
            }
        }
    }
}

결과 보기

 

histogram

range의 경우 from, to를 통하여 범위를 지정했다면 histogram은 range와 다르게 interval옵션을 통하여 interval단위 별로 필드의 개수를 측정하는 Aggregations입니다.

 

모든 도큐먼트의 duration을 100 단위로 개수 측정하기

우리는 데이터를 가져오는 것이 목적이 아닌 Aggregations을 수행하는 것이 목적이기 때문에 "size"를 0으로 설정하였습니다.

api POST http://localhost:9200/test-log-index-2021-09-12/_search
header content-type: application/json
body {
    "size": 0,
    "query": {
        "match_all": {}
    },
    "aggs": {
        "byDuration": {
            "histogram": {
                "field": "duration",
                "interval": 100
            }
        }
    }
}

결과 보기

100 단위 별로 도큐먼트의 개수를 확인할 수 있습니다.

 

date_range

range처럼 date필드 또한 범위를 지정하여 집계를 할 수 있습니다.

 

모든 도큐먼트의 start_time 필드를 범위 별로 측정하기

우리는 데이터를 가져오는 것이 목적이 아닌 Aggregations을 수행하는 것이 목적이기 때문에 "size"를 0으로 설정하였습니다.

"start_time"필드의 값을 범위 별로 측정하도록 하겠습니다.

api POST http://localhost:9200/test-log-index-2021-09-12/_search
header content-type: application/json
body {
    "size": 0,
    "query": {
        "match_all": {}
    },
    "aggs": {
        "date_range": {
            "date_range": {
                "field": "start_time",
                "ranges": [
                    {
                        "from": "2021-09-12 10:10:10",
                        "to": "2021-09-12 10:10:15"
                    },
                    {
                        "from": "2021-09-12 10:10:15",
                        "to": "2021-09-12 10:10:20"
                    },
                    {
                        "from": "2021-09-12 10:10:20"
                    }
                ]
            }
        }
    }
}

결과 보기

각 시간대별로 도큐먼트의 개수를 확인할 수 있습니다.

 

date_histogram

histogram과 같이 date_histogram도 interval 옵션을 넣어 각 interval별 집계를 측정할 수 있습니다.

interval옵션에 second, minute, hour, day, month, week 등을 넣을 수 있습니다.

 

모든 도큐먼트의 start_time 필드를 1분 별로 측정하기

우리는 데이터를 가져오는 것이 목적이 아닌 Aggregations을 수행하는 것이 목적이기 때문에 "size"를 0으로 설정하였습니다.

api POST http://localhost:9200/test-log-index-2021-09-12/_search
header content-type: application/json
body {
    "size": 0,
    "query": {
        "match_all": {}
    },
    "aggs": {
        "date_his": {
            "date_histogram": {
                "field": "start_time",
                "interval": "minute"
            }
        }
    }
}

결과 보기

 

 

 

 

Aggregation을 수행할 때 사용할 데이터

 

cardinality

cardinality는 데이터의 종류가 몇 가지 있는지 확인하는 데 사용됩니다.

 

user_id가 1인 도큐먼트에 action이 몇가지 종류가 있는지 확인하기

우리는 데이터를 가져오는 것이 목적이 아닌 Aggregations을 수행하는 것이 목적이기 때문에 "size"를 0으로 설정하였습니다.

api POST http://localhost:9200/test-log-index-2021-09-12/_search
header content-type: application/json
body {
    "size": 0,
    "query": {
        "bool": {
            "must": [
                {
                    "match": {
                        "user_id": "1"
                    }
                }
            ]
        }
    },
    "aggs": {
        "cardinality_by_action": {
            "cardinality": {
                "field": "action"
            }
        }
    }
}

결과 보기

"cardinality_by_action"를 통해 user_id가 1인 도큐먼트에서는 action의 종류가 4개가 있다는 것을 확인할 수 있습니다.

실제로 "open_file", "close_file", "open_socket", "close_socket" 이렇게 4개입니다.

 

percentiles

각각의 값을 백분율로 보는 Aggregations입니다. 퍼센트 옵션을 설정하지 않으면 디폴트로 [1, 5, 25, 50, 75, 95, 99]로 설정됩니다.

 

user_id가 1인 도큐먼트에 duration의 백분율 확인하기

우리는 데이터를 가져오는 것이 목적이 아닌 Aggregations을 수행하는 것이 목적이기 때문에 "size"를 0으로 설정하였습니다.

api POST http://localhost:9200/test-log-index-2021-09-12/_search
header content-type: application/json
body {
    "size": 0,
    "query": {
        "bool": {
            "must": [
                {
                    "match": {
                        "user_id": "1"
                    }
                }
            ]
        }
    },
    "aggs": {
        "percentiles_by_duration": {
            "percentiles": {
                "field": "duration"
            }
        }
    }
}

결과 보기

하위 1%의 값은 100이며, 상위 99%의 값은 430인 것을 확인할 수 있습니다.

 

퍼센트를 설정하여 확인하기

이제는 디폴트 퍼센트값이 아닌 퍼센트를 사용자가 [25, 50, 75]로 지정하여 검색하겠습니다.

api POST http://localhost:9200/test-log-index-2021-09-12/_search
header content-type: application/json
body {
    "size": 0,
    "query": {
        "bool": {
            "must": [
                {
                    "match": {
                        "user_id": "1"
                    }
                }
            ]
        }
    },
    "aggs": {
        "percentiles_by_duration": {
            "percentiles": {
                "field": "duration",
                "percents": [ 25, 50, 75 ]
            }
        }
    }
}

결과 보기

 

percentile_ranks

지정한 값이 백분율로 몇 퍼센트 안에 속하는지 확인할 수 있습니다.

values라는 옵션으로 값을 지정할 수 있습니다.

 

user_id가 1인 도큐먼트에 필드 duration에 100, 200, 300의 값이 있다면 백분율 몇 퍼센트인지 확인하기

우리는 데이터를 가져오는 것이 목적이 아닌 Aggregations을 수행하는 것이 목적이기 때문에 "size"를 0으로 설정하였습니다.

api POST http://localhost:9200/test-log-index-2021-09-12/_search
header content-type: application/json
body {
    "size": 0,
    "query": {
        "bool": {
            "must": [
                {
                    "match": {
                        "user_id": "1"
                    }
                }
            ]
        }
    },
    "aggs": {
        "rank_by_duration": {
            "percentile_ranks": {
                "field": "duration",
                "values": [ 100, 200, 300 ]
            }
        }
    }
}

결과 보기

 

Aggregation을 수행할 때 사용할 데이터

Aggregations이란

저장된 데이터를 수치화시키는 여러 가지의 연산을 수행하는 역할을 합니다. 이러한 기능을 통하여 데이터를 분석하여 사용자가 보기 좋게 시각화를 할 수 있습니다.

 

이번 페이지에서는 min, max, sum, avg, stats에 대하여 알아보겠습니다.

min

query의 검색 결과에 특정 필드의 값이 가장 작은 값을 나타냅니다.

 

user_id가 1인 도큐먼트의 duration 필드의 최솟값 구하기

"size"를 0으로 설정한 이유는 우리는 데이터를 가져오는 것이 목적이 아닌 user_id가 1인 도큐먼트에서 duration이 가장 작은 값만 도출하는 것이 목적이기 때문에 불필요하게 데이터를 가져오는 것을 생략하기 위함입니다.

"aggs"를 통하여 쿼리에 Aggregation을 선언하고 "minDuration"이라는 Aggregation의 이름을 사용자가 지정합니다. 그리고 "min"을 통하여 duration이라는 필드의 최솟값을 측정하도록 합니다.

api POST http://localhost:9200/test-log-index-2021-09-12/_search
header content-type: application/json
body {
    "size": 0,
    "query": {
        "bool": {
            "must": [
                {
                    "match": {
                        "user_id": "1"
                    }
                }
            ]
        }
    },
    "aggs": {
        "minDuration": {
            "min": {
                "field": "duration"
            }
        }
    }
}

결과 보기

"hits"는 검색 결과를 나타내지만 우리는 "size"를 0으로 설정하였기 때문에 결과값은 나오지 않습니다.

"aggregations"에 우리가 지정한 "minDuration"이라는 명칭으로 최소값이 100이라는 결과가 나왔습니다.

 

max

query의 검색 결과에 특정 필드의 값이 가장 큰 값을 나타냅니다.

 

user_id가 1인 도큐먼트의 duration 필드의 최대값 구하기

"maxDuration"이라는 명칭으로 duration의 값이 가장 큰 값을 구하는 쿼리입니다.

api POST http://localhost:9200/test-log-index-2021-09-12/_search
header content-type: application/json
body {
    "size": 0,
    "query": {
        "bool": {
            "must": [
                {
                    "match": {
                        "user_id": "1"
                    }
                }
            ]
        }
    },
    "aggs": {
        "maxDuration": {
            "max": {
                "field": "duration"
            }
        }
    }
}

결과 보기

maxDuration을 통하여 user_id가 1인 도큐먼트에서 duration이 가장 큰 값은 430이라는 결과를 알 수 있습니다.

 

sum

query의 검색 결과에 특정 필드의 값을 전부 더한 값을 나타냅니다.

 

user_id가 1인 도큐먼트의 duration 필드의 전부 더한 값 구하기

"sumDuration"이라는 명칭으로 user_id가 1인 duration의 값을 전부 더한 값을 구하는 쿼리입니다.

api POST http://localhost:9200/test-log-index-2021-09-12/_search
header content-type: application/json
body {
    "size": 0,
    "query": {
        "bool": {
            "must": [
                {
                    "match": {
                        "user_id": "1"
                    }
                }
            ]
        }
    },
    "aggs": {
        "sumDuration": {
            "sum": {
                "field": "duration"
            }
        }
    }
}

결과 보기

sumDuration을 통하여 user_id가 1인 도큐먼트에서 duration을 전부 더한 값이 2600이라는 것을 알 수 있습니다.

 

avg

query의 검색 결과에 특정 필드의 값의 평균 값을 나타냅니다.

 

user_id가 1인 도큐먼트의 duration 필드의 전부 더한 값 구하기

"avgDuration"이라는 명칭으로 user_id가 1인 duration의 값의 평균을 구하는 쿼리입니다.

api POST http://localhost:9200/test-log-index-2021-09-12/_search
header content-type: application/json
body {
    "size": 0,
    "query": {
        "bool": {
            "must": [
                {
                    "match": {
                        "user_id": "1"
                    }
                }
            ]
        }
    },
    "aggs": {
        "avgDuration": {
            "avg": {
                "field": "duration"
            }
        }
    }
}

결과 보기

avgDuration을 통하여 user_id가 1인 도큐먼트에서 duration의 평균값이 216.66666666이라는 것을 알 수 있습니다.

 

stats

stats를 통하여 위에서 본 count, min, max, sum, avg의 모든 결과를 가져올 수 있습니다.

 

user_id가 1인 도큐먼트의 duration 필드의 stats 결과 값 확인하기

"statsDuration"이라는 명칭으로 user_id가 1인 도큐먼트의 count, min, max, sum, avg의 값을 확인하는 쿼리입니다.

api POST http://localhost:9200/test-log-index-2021-09-12/_search
header content-type: application/json
body {
    "size": 0,
    "query": {
        "bool": {
            "must": [
                {
                    "match": {
                        "user_id": "1"
                    }
                }
            ]
        }
    },
    "aggs": {
        "statDuration": {
            "stats": {
                "field": "duration"
            }
        }
    }
}

결과 보기

"statDuration"을 통해 count, min, max, avg, sum의 결과 값을 확인할 수 있습니다.

 

그렇다면 min만 필요하더라도 stats하나로 모든 결과 값을 구하면 된다고 생각할 수 있습니다. 그러나 이러한 생각은 좋지 못한 생각입니다. min만 구하기 위해서 stats를 사용하는 것은 매우 안 좋은 선택입니다. 이 이유는 Elasticsearch에게 그만큼 많은 부하를 줄 수 있기 때문입니다. 그러나 반대로 min, max, sum을 구해야 될 경우에는 min, max, sum 각각 3번의 검색을 수행하는 것보다는 stats를 한 번을 사용하여 네트워크 I/O를 줄여 주는 것이 더 현명한 선택이라고 생각됩니다.

 

삽입된 데이터는 이전 페이지를 참조해주세요

https://stdhsw.tistory.com/entry/Elasticsearch-%EA%B2%80%EC%83%89%ED%95%98%EA%B8%B01

 

match 쿼리

match는 term과 비슷해 보이지만 미묘한 차이가 있습니다. match의 경우 기본적인 검색을 수행하는데 사용을 하지만 저 같은 경우 text 필드에서 보다 상세한 검색을 수행할 경우 주로 사용됩니다.  즉 문서의 내용에서 특정 단어가 들어 갔는지 검색할때 많이 사용됩니다.

 

text 필드 검색하기

message 필드에 "aaa"라는 문자열이 포함된 모든 도큐먼트를 검색합니다.

api POST http://localhost:9200/my-log-index-2021-08-29/_search
header Content-type: application/json
body {
    "query": {
        "bool": {
            "must": [
                {
                    "match": {
                        "message": "aaa"
                    }
                }
            ]
        }
    }
}

 

term 쿼리

term의 경우 match와 비슷한 모습을 취하지만 term은 완전히 같은 데이터를 검색하는데 많이 사용됩니다. 저 같은 경우 주로 keyword타입에서 특정 단어를 검색하는데 많이 사용하고 있습니다.

 

keyword 타입 검색하기

level 필드의 값이 "info"인 값을 가진 도큐먼트를 검색합니다.

api POST http://localhost:9200/my-log-index-2021-08-29/_search
header Content-type: application/json
body {
    "query": {
        "bool": {
            "must": [
                {
                    "term": {
                        "level": "info"
                    }
                }
            ]
        }
    }
}

 

range 쿼리

range 쿼리는 값의 범위를 검색하는데 사용됩니다. 

 

range쿼리의 파라미터

파라미터명 설명
gte 크거나 같다
gt 크다
lte 작거나 같다
lt 작다

 

범위를 이용하여 검색하기

user_id가 2보다 크거나 같으면서 7보다 작은 도큐먼트 검색하기

api POST http://localhost:9200/my-log-index-2021-08-29/_search
header Content-type: application/json
body {
    "query": {
        "bool": {
            "must": [
                {
                    "range": {
                        "user_id": {
                            "gte": "2",
                            "lt": "7"
                        }
                    }
                }
            ]
        }
    }
}

 

 

데이터는 이전 페이지를 참조해주세요

https://stdhsw.tistory.com/entry/Elasticsearch-%EA%B2%80%EC%83%89%ED%95%98%EA%B8%B01

bool 쿼리

Elasticsearch에서 보다 자세한 검색을 위해 복합 쿼리가 필요합니다. 이럴 때 bool을 사용하게 됩니다. bool 쿼리에는 must, must_not, should가 있고 이러한 쿼리를 이용하여 score에 점수를 매길 수 있습니다. score의 점수가 높을수록 보다 정확한 결괏값을 도출할 수 있습니다.

must

must는 검색 조건에 반듯이 포함한 도큐먼트를 가져옵니다.

 

keyword타입 검색하기

level필드에 값이 "info"값을 가지는 모든 도큐먼트를 검색합니다.

참고로 level필드의 타입은 keyword입니다.

api POST http://localhost:9200/my-log-index-2021-08-29/_search
header Content-type: application/json
body {
    "query": {
        "bool": {
            "must": {
                "match": {
                    "level": "info"
                }
            }
        }
    }
}

 

복합 쿼리 검색하기

level필드의 값이 "info"이며, message에 "aaa"값을 포함하는 도큐먼트를 검색합니다.

level필드는 keyword이며, message는 text입니다.

api POST http://localhost:9200/my-log-index-2021-08-29/_search
header Content-type: application/json
body {
    "query": {
        "bool": {
            "must": [
                {
                    "match": {
                        "level": "info"
                    }
                },
                {
                    "match": {
                        "message": "aaa"
                    }
                }
            ]
        }
    }
}

 

text타입 OR 검색하기

message필드의 값이 "aaa" 또는 "open" 중 하나 이상의 값을 가지는 도큐먼트를 검색합니다.

만약 "aaa"와 "open"의 값 모두 가지고 있다면 score의 점수가 더 높습니다.

api POST http://localhost:9200/my-log-index-2021-08-29/_search
header Content-type: application/json
body {
    "query": {
        "bool": {
            "must": [
                {
                    "match": {
                        "message": "aaa open"
                    }
                }
            ]
        }
    }
}

 

text타입 AND 검색하기

message필드의 값이 "aaa"와 "open" 모든 값을 가지는 도큐먼트를 검색합니다.

api POST http://localhost:9200/my-log-index-2021-08-29/_search
header Content-type: application/json
body {
    "query": {
        "bool": {
            "must": [
                {
                    "match": {
                        "message": "aaa"
                    }
                },
                {
                    "match": {
                        "message": "open"
                    }
                }
            ]
        }
    }
}

must_not

must_not은 검색 조건에 반듯이 포함하지 않은 도큐먼트를 가져옵니다.

 

must_not 쿼리 검색하기

level필드에 값이 "info"가 아닌 모든 도큐먼트를 검색합니다.

api POST http://localhost:9200/my-log-index-2021-08-29/_search
header Content-type: application/json
body {
    "query": {
        "bool": {
            "must_not": [
                {
                    "match": {
                        "level": "info"
                    }
                }
            ]
        }
    }
}

 

must_not 복합 쿼리 검색하기

message 필드의 값이 "open"을 가지며, level 필드의 값이 "info"가 아닌 도큐먼트를 검색합니다.

level은 keyword타입이며, message는 text타입입니다.

api POST http://localhost:9200/my-log-index-2021-08-29/_search
header Content-type: application/json
body {
    "query": {
        "bool": {
            "must": [
                {
                    "match": {
                        "message": "open"
                    }
                }
            ],
            "must_not": [
                {
                    "match": {
                        "level": "info"
                    }
                }
            ]
        }
    }
}

should

should의 경우 반듯이 필요한 것은 아니지만 만약 있다면 score의 점수를 높여 줍니다.

keyword 타입의 경우 OR 검색을 지원하지 않습니다. 그러나 정상적인 방법은 아니지만 저 같은 경우 keyword 타입인 경우 should를 이용하여 OR 검색처럼 사용합니다.

 

should 쿼리 검색하기

level 필드의 값이 "debug"일 경우 보다 높은 점수를 부여하고 검색합니다.

api POST http://localhost:9200/my-log-index-2021-08-29/_search
header Content-type: application/json
body {
    "query": {
        "bool": {
            "should": [
                {
                    "match": {
                        "level": "debug"
                    }
                }
            ]
        }
    }
}

 

should 복합 쿼리로 검색하기

message필드에는 "open"의 값을 가진 도큐먼트 중에 level필드에는 "info"의 값을 가지면 보다 높은 score 점수를 부여하여 검색합니다.

(level필드에는 info 이외의 값도 같이 검색됩니다. 그러나 info의 값을 가진 도큐먼트가 더 높은 score 점수를 가집니다.)

api POST http://localhost:9200/my-log-index-2021-08-29/_search
header Content-type: application/json
body {
    "query": {
        "bool": {
            "must": [
                {
                    "match": {
                        "message": "open"
                    }
                }
            ],
            "should": [
                {
                    "match": {
                        "level": "info"
                    }
                }
            ]
        }
    }
}

 

 

참조

https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-bool-query.html

 

Boolean query | Elasticsearch Guide [7.14] | Elastic

A query that matches documents matching boolean combinations of other queries. The bool query maps to Lucene BooleanQuery. It is built using one or more boolean clauses, each clause with a typed occurrence. The occurrence types are: Occur Description must

www.elastic.co

 

 

 

데이터 넣기

지금 넣는 데이터로 앞으로 검색하는 데 사용하겠습니다.

(http request client를 사용하신다면 body에 반듯이 enter 한번 더 입력하세요)

api PUT http://localhost:9200/_bulk
header Content-type: application/json
body {"index": {"_index": "my-log-index-2021-08-29", "_id": 1}}
{"user_id":1, "level":"info", "timestamp": 1630230119, "action": "open_file", "message": "user1 aaa file open"}
{"index": {"_index": "my-log-index-2021-08-29", "_id": 2}}
{"user_id":2, "level":"debug", "timestamp": 1630230120, "action": "open_file", "message": "user2 bbb file open"}
{"index": {"_index": "my-log-index-2021-08-29", "_id": 3}}
{"user_id":3, "level":"info", "timestamp": 1630230121, "action": "open_socket", "message": "user3 socket open"}
{"index": {"_index": "my-log-index-2021-08-29", "_id": 4}}
{"user_id":4, "level":"debug", "timestamp": 1630230121, "action": "open_socket", "message": "user4 ddd socket open"}
{"index": {"_index": "my-log-index-2021-08-29", "_id": 5}}
{"user_id":5, "level":"info", "timestamp": 1630230122, "action": "open_file", "message": "user5 ccc file open"}
{"index": {"_index": "my-log-index-2021-08-29", "_id": 6}}
{"user_id":6, "level":"info", "timestamp": 1630230123, "action": "close_file", "message": "user6 aaa close open"}
{"index": {"_index": "my-log-index-2021-08-29", "_id": 7}}
{"user_id":7, "level":"info", "timestamp": 1630230124, "action": "close_socket", "message": "user7 ccc close socket"}
{"index": {"_index": "my-log-index-2021-08-29", "_id": 8}}
{"user_id":8, "level":"debug", "timestamp": 1630230125, "action": "open_file", "message": "user8 aaa file open"}
{"index": {"_index": "my-log-index-2021-08-29", "_id": 9}}
{"user_id":9, "level":"info", "timestamp": 1630230126, "action": "open_file", "message": "user9 bbb file open"}
{"index": {"_index": "my-log-index-2021-08-29", "_id": 10}}
{"user_id":10, "level":"info", "timestamp": 1630230127, "action": "open_file", "message": "user10 aaa file open"}
{"index": {"_index": "my-log-index-2021-08-29", "_id": 11}}
{"user_id":11, "level":"warn", "timestamp": 1630230128, "action": "open_file", "message": "user11 bbb file open"}
{"index": {"_index": "my-log-index-2021-08-29", "_id": 12}}
{"user_id":12, "level":"info", "timestamp": 1630230128, "action": "open_file", "message": "user12 aaa file open"}
{"index": {"_index": "my-log-index-2021-08-29", "_id": 13}}
{"user_id":13, "level":"info", "timestamp": 1630230129, "action": "open_file", "message": "user13 bbb file open"}


GET으로 데이터 가져오기

일상적으로 많이 사용하였던 방법입니다. GET으로는 도큐먼트 아이디를 통하여 해당 데이터를 가져올 수 있습니다.

 

도큐먼트 아이디 1 데이터 가져오기

api GET http://localhost:9200/my-log-index-2021-08-29/_doc/1

출력 결과

query를 이용하여 가져오기

이번에는 도큐먼트 아이디가 아닌 query를 통하여 값을 가져오는 방법을 알아보겠습니다.

 

인덱스의 모든 데이터 가져오기

size는 가져올 수 있는 최대 개수입니다. 이번 결과에서는 size가 없어도 무방합니다.

api POST http://localhost:9200/my-log-index-2021-08-29/_search
header Content-type: application/json
body {
    "size": 1000,
    "query": {
        "match_all": {}
    }
}

 

필드 값으로 검색하여 데이터 가져오기

active필드에 open_socket 값을 가지는 도큐먼트 가져오기

api POST http://localhost:9200/my-log-index-2021-08-29/_search
header Content-type: application/json
body {
    "query": {
        "match": {
            "action": "open_socket"
        }
    }
}

출력 결과

출력 결과에서 각 태그의 의미는 다음과 같습니다.

took 쿼리 수행 시간
timed_out 쿼리 수행 시간이 넘었는지 확인
_shard 쿼리를 수행한 shard의 정보
_shard.total 쿼리를 수행한 shard의 수
_shard.successful 쿼리 수행을 성공한 shard의 수
_shard.skipped 쿼리를 건너뛴 shard의 수
_shard.failed 쿼리를 실패한 shard의 수
hits 쿼리 수행 결과
hits.total 쿼리 수행에 일치하는 도큐먼트 수
hits.max_score 쿼리 수행후 가장 높은 점수를 가진 값
hits.hits 수행 결과의 도큐먼트 정보들

 

단어가 아닌 문장으로 검색하기

match_phrase를 이용하여 message필드에 "aaa file"문장을 포함한 도큐먼트를 검색합니다.

api POST http://localhost:9200/my-log-index-2021-08-29/_search
header Content-type: application/json
body {
    "query": {
        "match_phrase": {
            "message": "aaa file"
        }
    }
}

 

 

 

Keyword 

Elasticsearch에는 문자열 필드 중 text와 keyword가 존재합니다. Keyword는 키워드라는 표현 그대로 데이터 자체를 분류하는데 사용하는 타입입니다. 즉 검색 필터로 사용되며, 데이터를 정렬하고 Aggregation으로 데이터를 집계하는데 사용되는 타입입니다. Keyword 타입으로 "hello my elasticsearch"라는 데이터가 색인되면 "hello my elasticsearch" 그대로 데이터가 색인되어 검색 시 "hello my elasticsearch" 그대로 데이터를 검색해야 합니다.

 

Text

Text 필드는 지정한 분석기를 이용하여 데이터를 분석하는데 사용합니다. 만약 Analyzer를 지정하지 않으면 Standard Analyzer로 설정되는데 "hello my elasticsearch"라는 데이터가 색인되면 "hello", "my", "elasticsearch"로 데이터가 색인되어 keyword와 다르게 "hello"라고 검색을 해도 검색이 됩니다. 즉 유사한 문자열을 검사하거나 Analyzer 설정을 통해 보다 더 전문적인 검색을 수행하는데 사용하는 타입입니다.

 

정리

간단하게 정리하면 다음과 같습니다.

  Keyword Text
사용 목적 통계, 정렬, 필터링 유사성 및 전문적인 분석
검색 방식 입력된 값과 정확히 일치하는 문자를 검색 설정한 Analyzer에 따라서 검색
색인 방식 입력한 문자열 그대로 색인 토큰으로 분리되어 색인
Aggregation  색인된 데이터로 집계 가능 집계 불가능

 

Keyword와 Text 타입을 둘다 적용

Text처럼 분석도 필요하지만 Keyword처럼 Aggregation 모두 필요할 때 필드의 타입을 Keyword와 Text 둘 다 적용할 수 있습니다. 사실 별 다른 타입을 지정하지 않으면 기본 값으로 Keyword와 Text 타입 둘 다 적용됩니다. 이런 경우 문득 다방면으로 사용할 수 있을 것 같아 좋아 보일 수 있지만 보다 더 많은 리소스를 사용하기 때문에 사용 목적에 맞게 필드 타입을 지정하여 사용하는 것을 권장합니다.

 

Array 형식으로 필드에 데이터 저장하기

하나의 필드에 같은 형식의 데이터를 여러 개 넣어야 되는 경우가 많이 발생합니다. 그럴 때 대표적으로 2가지 방식이 있습니다.

1번째 방식으로는 필드와 함께 값을 같이 저장하는 방식입니다.

2번째 방식은 값만 저장하는 방식입니다.

이번에는 이 2가지 방식 모두에 대하여 알아보겠습니다.

1. 필드에 Object형식으로 데이터 리스트 저장하기

하나의 필드와 같이 값을 저장하는 방식으로는 nested를 사용하는 것 입니다.

 

nested 맵핑 만들기

api PUT http://localhost:9200/object-list-index/
header Content-type: application/json
body {
    "mappings": {
        "properties": {
            "server": {
                "type": "keyword"
            },
            "users": {
                "type": "nested"
            }
        }
    }
}

 

데이터 넣기

api PUT http://localhost:9200/object-list-index/_doc/1
header Content-type: application/json
body {
    "server": "server1",
    "users": [{"id": "user1"}, {"id": "user2"}, {"id": "user3"}, {"id": "user4"}]
}

 

데이터 출력하기

api GET http://localhost:9200/object-list-index/_doc/1

 

2. 필드에 데이터만 리스트로 넣기

이번에는 위에 방식과 다른 그냥 값만 배열 형식으로 넣는 방식입니다.

 

맵핑만들기

api PUT http://localhost:9200/value-list-index/
header Content-type: application/json
body {
    "mappings": {
        "properties": {
            "server": {
                "type": "keyword"
            },
            "users": {
                "type": "keyword",
                "fields": {
                    "keyword": {
                        "type": "keyword"
                    }
                }
            }
        }
    }
}

 

데이터 넣기

api PUT http://localhost:9200/value-list-index/_doc/1
header Content-type: application/json
body {
    "server": "server1",
    "users": ["user1","user2", "user3","user4"]
}

 

데이터 출력하기

api GET http://localhost:9200/value-list-index/_doc/1

 

Object처럼 데이터 저장하기

Elasticsearch을 사용하다 보면 데이터를 일반적인 필드 형식으로 저장하는 것이 아닌 트리 구조로 데이터를 저장하고 싶은 상황이 있습니다. 즉 object형식으로 데이터를 저장해야 되는데 이럴 때 사용하는 것이 properties입니다. 이제부터 properties 사용법에 대하여 알아보겠습니다. (참고로 해당 방식처럼 Object형식으로 데이터를 저장하게 되면 인덱스의 구조가 복잡해져 검색하는 상황에 따라 검색의 성능이 저하될 수 있습니다. 참고하시고 본인의 데이터 검색 방식에 대하여 확인하시고 적용하는 것을 추천드립니다.)

properties 사용하기

데이터의 구조는 다음과 같습니다.

  • 학과
  • 학생
    • 이름
    • 나이
    • 이메일
api PUT http://localhost:9200/student-index
header Content-type: application/json
body {
    "mappings": {
        "properties": {
            "subject": {
                "type": "keyword"
            },
            "student": {
                "properties": {
                    "name": {
                        "type": "text"
                    },
                    "age": {
                        "type": "short"
                    },
                    "email": {
                        "type": "text"
                    }
                }
            }
        }
    }
}

 

출력 결과

추가적으로 데이터를 넣고 student-index를 출력하였습니다.

Object 형식의 필드를 추천하는 상황과 추천하지 않는 상황

상황에 따라 object형식으로 데이터를 저장할 때가 좋은 경우가 있는 반면 그렇지 않은 경우도 있습니다.

 

추천하는 상황

  • object의 필드 별로 데이터를 집계 및 통계를 내야 되는 경우

추천하지 않는 상황

  • 그냥 데이터를 저장하고 읽기만 하는 경우 오히려 데이터 검색 성능에 저하가 될 수 있습니다. 만약 그냥 데이터를 검색하는 데 사용하지 않고 텍스트 형식으로 출력하는데만 사용하신다면 그냥 필드를 text로 하여 json데이터를 text로 저장하는 것을 추천합니다.

참조

https://www.elastic.co/guide/en/elasticsearch/reference/current/object.html

 

Object field type | Elasticsearch Guide [7.14] | Elastic

If you need to index arrays of objects instead of single objects, read Nested first.

www.elastic.co

 

 

bulk란

Elasticsearch를 사용하면서 여러개의 데이터를 넣을때 매번 하나씩 데이터를 넣는 일은 네트워크의 잦은 IO의 발생으로 성능을 저하 시킬 수 있는 요인입니다. 이러한 문제를 해결하는 것이 bulk입니다. bulk는 여러개의 처리를 모아 두었다가 한번에 처리하는 batch 처리 기능입니다.

 

bulk를 통해 데이터 넣기

bulk를 사용하여 여러개의 데이터를 한번의 요청으로 넣을 수 있습니다.

api POST http://localhost:9200/my-log-index-2021-08-25/_bulk
header Content-type: Application/json
body {"index": {"_index": "my-log-index-2021-08-25", "_id": 1}}
{"user_id":1, "level":"info", "timestamp": 1629893888, "action": "open_file", "message": "user1 file open"}
{"index": {"_index": "my-log-index-2021-08-25", "_id": 2}}
{"user_id":2, "level":"info", "timestamp": 1629893890, "action": "open_file", "message": "user2 file open"}
{"index": {"_index": "my-log-index-2021-08-25", "_id": 3}}
{"user_id":3, "level":"info", "timestamp": 1629893891, "action": "open_socket", "message": "user3 socket open"}
{"index": {"_index": "my-log-index-2021-08-25", "_id": 4}}
{"user_id":4, "level":"info", "timestamp": 1629893892, "action": "open_socket", "message": "user4 socket open"}
{"index": {"_index": "my-log-index-2021-08-25", "_id": 5}}
{"user_id":5, "level":"info", "timestamp": 1629893893, "action": "open_file", "message": "user5 file open"}
{"index": {"_index": "my-log-index-2021-08-25", "_id": 6}}
{"user_id":6, "level":"info", "timestamp": 1629893893, "action": "close_file", "message": "user6 close open"}
{"index": {"_index": "my-log-index-2021-08-25", "_id": 7}}
{"user_id":7, "level":"info", "timestamp": 1629893895, "action": "close_socket", "message": "user7 close socket"}

중요 ) 여기서 만약에 Postman, 크롬에 ARS와 같이 http요청을 보내는 RestClient 프로그램을 사용하신다면 body 마지막에 개행(\n) 즉 enter키를 한번 더 입력해야 됩니다.

 

데이터가 잘 들어갔는지 확인하기

api GET http://localhost:9200/my-log-index-2021-08-25/_search

 

bulk를 이용하여 데이터 입력, 수정, 삭제 한번에 하기

데이터 입력 뿐만 아니라 데이터 수정, 삭제를 한번의 bulk를 통하여 수행할 수 있습니다.

 

bulk로 입력, 수정, 삭제

api POST http://localhost:9200/my-log-index-2021-08-25/_bulk
header Content-type: Application/json
body {"index": {"_index": "my-log-index-2021-08-25", "_id": 8}}
{"user_id":8, "level":"debuf", "timestamp": 1629893993, "action": "close_file", "message": "user8 close open"}
{"index": {"_index": "my-log-index-2021-08-25", "_id": 9}}
{"user_id":9, "level":"warn", "timestamp": 1629893995, "action": "error", "message": "failed user9 connection"}
{"update": {"_index": "my-log-index-2021-08-25", "_id": 1}}
{"doc": {"level": "debug"}}
{"update": {"_index": "my-log-index-2021-08-25", "_id": 2}}
{"doc": {"level": "debug"}}
{"delete": {"_index": "my-log-index-2021-08-25", "_id": 6}}

중요 ) 여기서 만약에 Postman, 크롬에 ARS와 같이 http요청을 보내는 RestClient 프로그램을 사용하신다면 body 마지막에 개행(\n) 즉 enter키를 한번 더 입력해야 됩니다.

벌크의 내용

8번, 9번 유저의 데이터를 추가하고

1번, 2번 유저의 level필드의 값을 debug로 수정하고

6번 유저의 데이터를 삭제합니다.

 

결과 확인

크롬의 "Elasticsearch Head"라는 확장 프로그램을 사용하여 결과를 확인하겠습니다.

"Elasticsearch Head"의 설치 방법은 링크과 같습니다.

https://stdhsw.tistory.com/entry/Elasticsearch-%EC%9C%88%EB%8F%84%EC%9A%B010-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0

 

Elasticsearch 윈도우10에 설치하기

Elasticsearch 다운로드 https://www.elastic.co/kr/downloads/elasticsearch 해당 주소에서 elasticsearch를 다운로드합니다. Elasticsearch 실행하기 압축을 푼 폴더 위치에서 bin/elasticsearch.bat파일을 실..

stdhsw.tistory.com

 

인덱스의 구조는 이전 template 설정과 같습니다.
https://stdhsw.tistory.com/entry/Elasticsearch%EC%9D%98-Index-template-%EC%84%A4%EC%A0%95

현재 인덱스 구조

필드 타입
user_id long
level keyword
timestamp long
action keyword
message text

 

도큐먼트 생성하기

Elasticsearch index에 document를 생성하여 각각의 필드에 값을 설정하고 index에 데이터를 넣습니다.
user 1 추가하기

api PUT http://localhost:9200/my-log-index-2021-08-24/_doc/1
header Content-type: application/json
body {
    "user_id": 1,
    "level": "info",
    "timestamp": 1629792520,
    "action": "open_file",
    "message": "user file open"
}


user 2 추가하기

api PUT http://localhost:9200/my-log-index-2021-08-24/_doc/2
header Content-type: application/json
body {
    "user_id": 2,
    "level": "info",
    "timestamp": 1629792525,
    "action": "open_socket",
    "message": "user socket open"
}


덮어쓰기를 방지하기 위한 데이터 생성방법
기존처럼 PUT http://localhost:9200/my-log-index-2021-08-24/_doc/2 을 사용하여 데이터를 생성한다면 기존에 도큐먼트 아이디 2가 존재한다면 기존의 데이터는 지워지고 현재 데이터가 덮어써지는 문제가 발생합니다. 이러한 문제를 해결하기 위해 _create를 통하여 데이터를 생성할 수 있습니다.
만약 같은 도큐먼트 아이디가 존재 할 경우 status 409 version_conflict_engine_exception 에러를 반환합니다.

api POST http://localhost:9200/my-log-index-2021-08-24/_create/2
header Content-type: application/json
body {
    "user_id": 3,
    "level": "info",
    "timestamp": 1629792540,
    "action": "close_file",
    "message": "user close open"
}

 

도큐먼트 조회하기

지금까지 입력한 데이터를 조회하도록 하겠습니다.

인덱스에 모든 도큐먼트 조회하기

api GET http://localhost:9200/my-log-index-2021-08-24/_search

출력 결과


특정 도큐먼트 아이디를 이용하여 조회하기
이번에는 인덱스의 모든 데이터가 아닌 도큐먼트 아이디를 이용하여 해당 도큐먼트의 값을 가져오도록 하겠습니다.

api GET http://localhost:9200/my-log-index-2021-08-24/_doc/1

출력 결과


조건을 이용하여 데이터 조회하기
이번에는 특정 조건에 만족하는 데이터를 찾아 조회하도록 하겠습니다.
action 필드의 값이 "open_socket"인 것을 조회하도록 하겠습니다.

api GET http://localhost:9200/my-log-index-2021-08-24/_search?q=action:open_socket

출력 결과

 

도큐먼트 수정하기

데이터를 수정하기 전에 먼저 version의 값을 확인하면 좋습니다.

api GET http://localhost:9200/my-log-index-2021-08-24/_doc/2

출력 결과
출력 결과를 보면 알 수 있듯이 현재 수정을 하지 않은 상태에서는 version이 1입니다.


도큐먼트 아이디를 이용하여 수정하기
도큐먼트 수정하는 것도 조회와 마찬가지로 도큐먼트 아이디를 이용하여 level 필드의 값을 "debug"로 수정하도록 하겠습니다.
_update를 사용하면 기존에 도큐먼트 아이디가 존재하지 않는 다면 에러가 발생합니다.

api POST http://localhost:9200/my-log-index-2021-08-24/_update/2
header Content-type: application/json
body {
    "doc": {
        "level": "debug"
    }
}


버전 확인하기

api GET http://localhost:9200/my-log-index-2021-08-24/_doc/2

출력 결과
버전이 2로 증가하였습니다. 버전은 해당 도큐먼트 아이디의 변경된 횟수를 의미합니다. 버전을 통하여 해당 도큐먼트의 아이디가 수정된 적이 있는지 확인할 수 있습니다.

 

도큐먼트 개수 측정하기

인덱스의 모든 도큐먼트 개수 측정하기

api GET http://localhost:9200/my-log-index-2021-08-24/_count


특정 조건을 이용하여 개수 측정하기
level 필드에 값이 "debug"인 도큐먼트 개수 측정

api GET http://localhost:9200/my-log-index-2021-08-24/_count?q=level:debug

 

도큐먼트 삭제하기

특정 도큐먼트 삭제하기

api DELETE http://localhost:9200/my-log-index-2021-08-24/_doc/2


인덱스 삭제하기
인덱스를 삭제하여 인덱스의 모든 값을 삭제합니다.

api DELETE http://localhost:9200/my-log-index-2021-08-24



+ Recent posts