Elasticsearch의 노드 

Elasticsearch의 인스턴스를 시작하는 동시에 노드도 같이 시작된다. 노드들을 연결해놓은 것을 클러스터라고 한다.

만약 하나의 엘라스틱 서치 노드만을 실행시킨 경우도 하나의 노드를 가진 클러스터라고 한다. 

클러스터안에서 모든 노드는 HTTP와 Transport 트래픽을 기본적으로 다룬다. Transport 레이어는 오로지 노드들과 Java TransportClient와의 통신에만 사용된다. Http 레이어는 오직 외부 Rest Cliente들과 통신할 때 사용된다.

모든 노드는 클러스터 안에서 서로 다른 노드들에 대하여 알고 있고 client에 요청을 적적한 노드로 향하게 조절해준다. 기본적으로 노드는 master-eligible, data, ingest, machine learning이 존대한다.

 

Elasticsearch의 노드 종류

Master-eligible 노드

- node.master를 true로 지정하며 클러스터의 컨트롤을 통해 마스터 노드로 선택될 자격을 가지게 된다.

- 마스터 노드는 클러스터에서 인덱스를 만들고 지우는 행위, 클러스터에서 노드들을 트래킹하고 각각의 노드를 샤드를 할당할건지 결정한다.

- Masger Eligible Node등에서 마스터 노드는 마스터 설출 프로세스를 통해 선출된다.

(https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-discovery.html)

- 마스터 노드는 데이터 노드처럼 데이터와 폴더에 접근 권한이 있어야 한다. 노드가 재시작 하는 사이에도 클러스터 상태가 유지되어야 하기 때문에 접근이 가능해야 한다.

- 데이터를 인덱싱하고 찾고 하는 작업은 CPU, Memory, I/O 자원을 많이 사용하기 때문에 큰 규모에서는 data node와 master node를 구별한다.

- 마스터 노드도 Coordinating Node 처럼 데이터를 routing하고 모으고 하는 작업이 가능하지만 이는 마스터 노드가 하는 주 목적이 아니다. 안정적인 마스터 노드를 운영하기 위해서는 자기 일만 딱 하게 해주는 것이 좋다.

- 마스터 노드로 노드를 지정하기 위한 기본 설정 값은 다음과 같다.

node.master: true 
node.data: false 
node.ingest: false 
node.ml: false 
xpack.ml.enabled: true 
cluster.remote.connect: false

 

Data 노드

- node.data가 true로 지정된 노드는 데이터를 가지고 있을 수 있고 CRUD, 검색, aggregation 등의 데이터와 관련된 작업이 가능하다.

- 데이터 노드는 인덱싱 된 Document를 포함하고 있는 샤드를 관리한다.

- 데이터 노드는 데이터를 직접적으로 다루기 때문에 리소스 자원이 많이 필요하다.

- 데이터 노드로 노드를 지정하기 위한 기본 설정 값은 다음과 같다.

node.master: false 
node.data: true 
node.ingest: false 
node.ml: false 
cluster.remote.connect: false

 

Ingest 노드 

- node.ingest가 true로 지정된 노드가 Document가 인덱싱 되기 전에 변형되고 풍성하게 하기 위해서 Document를 ingest pipeline으로 적용할 수 있다.

- ingetst node는 pre processing 파이프라인을 실행하고 하나 또는 하나 이상의 ingest processor들을 모으는 작업을 한다.

- ingest를 로드하는건 무겁기 때문에 데이터나 마스터 노드에서는 node.ingest를 false로 지정하는 것이 좋다.

- 많은 리소스를 잡아먹기 때문에 ingest node는 별도로 지정하는 것이 좋다.

- ingest 노드로 노드를 지정하기 위한 기본 설정 값은 다음과 같다.

node.master: false 
node.data: false 
node.ingest: true 
node.ml: false 
cluster.remote.connect: false

 

Machine Learning 노드

- xpack.ml이 true로 지정되어 있고 node.ml이 true로 설정되어 있는 노드는 기본적으로 엘라스틱서치에서 분배하는 행위를 한다.

- 만약 머신러닝 특징을 사용하고 싶으면 적어도 클러스터 내에 하나의 머신러닝 노드가 있어야 한다.

 

Coordinating 노드

- 검색 요청과 bulk indexing과 같은 요청들은 다른 노드들의 있는 데이터를 많이 다룬다.

- 데이터가 흩어져 있는 경우 데이터가 있는 노드로 향하게 조정해준다. 각각의 데이터 노드는 요청을 자체적으로 처리하고 그것의 값을 Coordinating 노드에 전달해준다. 그럼 Coordinating 노드는 이를 모아서 하나의 데이터 형태로 정제하여 반환한다.

- 각각의 노드는 Coordinating node가 될 수 있다. 대신 node.master, node.data, node.integer가 false로 되어 있어야 한다.

- 또한 데이터를 모으고 조작하고 하는 작업이 많기 때문에 Coordinating 노드는 메모리랑 CPU에 대한 자원이 많아야 한다. 그렇기 때문에 오직 요청을 라우팅하고 검색 구절을 조절하고, bulk indexing 분배작업을 하는 노드로만 사용하는 게 좋다.

- Coordinating node는 본질적으로 로드 밸런싱 같은 역할을 한다.

- Coordinating node로 지정하기 위한 설정은 다음과 같다.

node.master: false 
node.data: false 
node.ingest: false 
node.ml: false 
cluster.remote.connect: false

 

Node Data Path Setting

path.data

- 모든 데이터와 master-eligible 노드는 샤드 그리고 인덱스, 클러스터 메타데이터가 저장되어 있는 데이터 디렉토리를 접근한다.

- path.data는 기본적으로 $ES_HOME/data로 지정되어 있지만 elasticsearch.yml을 통해서 바꿀 수 있다.

redis에 만약 200 ~ 300개가 넘는 캐시 정보를 계속 request를 날리면 레이턴시가 발생할 가능성이 크기 때문에 이런경우에 mget, mset, pipeline 등 멀티키 명령어를 사용한다.

하지만 redis가 single mode일 때는 아무 상관이 없지만 cluster mode인경우에는 다음과 같은 오류를 발생 시킨다.

"CROSSSLOT Keys in request don't hash to the same slot"

무슨 오류일까?? 처음에 redis가 싱글모드로 돌고 있던 stage에서 테스트를 해서 정상적으로 멀티키 명령어가 잘 되는줄 알고 배포 했다가 라이브에서 위와 같은 오류가 발생했다...

너무 당황해서 바로 수정했다.

무엇이 문제 였을까? 하면서 문서를 찾아보니 redis에는 16384의 키 슬롯이 있는데 클러스터 모드인 경우 이 키 슬롯들이 각 node에 분할되기 때문에 멀티키 명령어를 사용할 수 없다고 한다.

만약 멀티키 명령어를 사용하고 싶다면 그 키들은 모두 같은 키 슬롯에 들어있어야 한다.

방법은 다음과 같다.

 

같은 키 슬롯에 데이터 넣기


동일한 키 슬롯에 저장하고 싶으면 강제로 지정하는 해야한다. 이때 저장하고자 하는 키들 앞에 {..}의 값을 붙여주고 저장하는 것이다. 예를 들어 저장하고자 하는 데이터에 {product}.products1, {product}.products2로 저장하면 이 두개는 동일한 키 슬롯에 포함되게 된다.

이는 aws elasticcache페이지에 상세히 나와있으니 참고하면 된다.

https://aws.amazon.com/ko/premiumsupport/knowledge-center/elasticache-crossslot-keys-error-redis/

 

ElastiCache 오류 "CROSSSLOT Keys in request don't hash to the same slot" 해결

이 오류는 키가 동일한 노드가 아니라, 동일한 해시 슬롯에 있어야 하기 때문에 발생합니다. 샤딩된 Redis ElastiCache 클러스터(클러스터 모드 활성화됨)에서 다중 키 작업을 구현하려면 키는 동일한 해시 슬롯으로 해시되어야 합니다. 해시 태그를 사용하여 키를 동일한 해시 슬롯에 강제로 배치할 수 있습니다. 이 예제에서는 "myset2" 및 "myset" 세트가 동일한 노드에 있습니다. 이 오류를 해결하려면 해시 태그를 사용하여 키를 동일한 해시

aws.amazon.com

하지만 여기서 중요한건, 우리가 클러스터 모드를 사용하는 이유는 한군데이 집중시키지 않고 값을 분산시켜서 부하를 막기 위해서인데 이를 하나의 키 슬롯에 데이터를 모두 집어 넣을 경우 의미가 없어진다.

그럼 키를 각 노드마다 분산시키면서 멀티키 명령어를 사용할 수 있는 방법이 있을까?

 

클러스터 모드에서 각 노드별 key slot에 데이터 삽입하여 멀티 명령어 사용하기


생각을 바꿔보았다. 싱글모드에서는 키 슬롯 16384개가 존재하고 mset, mget, pipeline등의 명령어가 사용 가능했다. 

그럼 각 노드를 하나의 싱글모드의 레디스라고 생각하면 어떨까? 하는 생각으로 전략을 바꿔봤다.

처음에는 가능할지 모르고 작업을 진행했는데 정상적으로 동작해서 기분이 좋았다.

#작업 순서 (명령어는 ioredis를 기준으로 작성하였다.)

1. 클러스터 모드에서 동작중인 master node에 대한 정보를 가져온다. (cluster mode에는 master 노드와 slave노드가 있는데 slave node는 단순 readonly이고 마스터 노드 값을 가지고 있기 때문에 가져올 필요 없다.)

const masterNodes = redis.nodes('master')

2. 각 노드별 key slot 현황을 확인하고 masterNode와 매핑한다. key slot 현황을 확인하기 위해서는 cluster slots 사용한다. ioredis에서는 redis.cluster('slots')

127.0.0.1:7001> cluster slots
1) 1) (integer) 0
   2) (integer) 4095
   3) 1) "127.0.0.1"
      2) (integer) 7000
   4) 1) "127.0.0.1"
      2) (integer) 7004
2) 1) (integer) 12288
   2) (integer) 16383
   3) 1) "127.0.0.1"
      2) (integer) 7003
   4) 1) "127.0.0.1"
      2) (integer) 7007
3) 1) (integer) 4096
   2) (integer) 8191
   3) 1) "127.0.0.1"
      2) (integer) 7001
   4) 1) "127.0.0.1"
      2) (integer) 7005
4) 1) (integer) 8192
   2) (integer) 12287
   3) 1) "127.0.0.1"
      2) (integer) 7002
   4) 1) "127.0.0.1"
      2) (integer) 7006
const slots = await redis.cluster('slots');

// 각 슬롯정보에 있는 ip, port정보를 확인하여 각 master와 슬롯의 범위를 정리한다.
{
 'ip' : ,
 'port' : ,
 'node' : masterNode[0],
 'slotStart' : 0
 'slotEnd': 4460
},
{
 'ip' : ,
 'port' : ,
 'node' : masterNode[1],
 'slotStart' : 4461
 'slotEnd': 8845
},
.
..
...
....

3. 찾고자하는 키들의 slot 정보를 정리하고 slot 범위에 맞는 오브젝트에 키 값을 배열에 정리한다. slot 정보를 뽑는 알고리즘이 CRC16이기 때문에 이를 이용해서 구할수도 있고 npm에 모듈로 나와있는게 있으니 활용해도 된다. (https://www.npmjs.com/package/cluster-key-slot)

{
 'ip' : ,
 'port' : ,
 'node' : masterNode[0],
 'slotStart' : 0
 'slotEnd': 4460,
 'keys': ['product::1', 'product::2', 'product::5'] // 범위가 0 ~ 4460에 속하고 masterNode[0]에 존재하는 키 슬롯에 저장되는 키들
},
{
 'ip' : ,
 'port' : ,
 'node' : masterNode[1],
 'slotStart' : 4461
 'slotEnd': 8845,
 'keys': ['product::4', 'product::3', 'product::6'] // 범위가 4461 ~ 8845에 속하고 masterNode[1]에 존재하는 키 슬롯에 저장되는 키들
},
.
..
...
....

4. 그리고 정리된 Object를 순회하면서 각 키에 있는 데이터를 조회하기 위한 pipeline을 생성하고 get명령어를 매핑하고 실행시킨다. 없는 데이터는 pipeline을 set으로 설정하고 exec 실행시킨다.

const objs = [{
 'ip' : ,
 'port' : ,
 'node' : masterNode[0],
 'slotStart' : 0
 'slotEnd': 4460,
 'keys': ['product::1', 'product::2', 'product::5']
},
{
 'ip' : ,
 'port' : ,
 'node' : masterNode[1],
 'slotStart' : 4461
 'slotEnd': 8845,
 'keys': ['product::4', 'product::3', 'product::6']
}];

for(const obj of objs) {
 const pipeline = obj.node.pipeline();
 
 obj.keys.forEach(keySlot => pipeline.get(key => pipeline.get);
 
 // 결과
 const ret = await keys.exec();
 
 //..... 없으면 없는 것들은 set로 pipeline 열어서 설정(예를 들어서 keys 배열에 1, 2번째 값이 없다고 가정)
 const setPipeline = obj.node.pipeline();
 
 pipeline.set(obj.keys[1]);
 pipeline.set(obj.keys[2]);
 
 await setPipeline.exec();
}

 

소스를 모두 다 적기에는 양도 많기 때문에 간단하게 내가 진행한 방식에 대한 설명만 적었다.

이 글을 보고 방법을 찾고자 하는 사람들에게 도움이 되지 않을수도 있지만  클러스터 모드에서 레디스의 멀티키 사용법에 대해 자료가 없어서 고민을 많이 했었기 때문에 작은 힌트라도 되었으면 좋겠다.

redis를 사용하면서 cluste로 구성해봐야하는 일이 있었다.

그래서 찾아보던 중 redis문서에서 방법을 찾았다. https://redis.io/topics/cluster-tutorial

 

Redis cluster tutorial – Redis

*Redis cluster tutorial This document is a gentle introduction to Redis Cluster, that does not use complex to understand distributed systems concepts. It provides instructions about how to setup a cluster, test, and operate it, without going into the detai

redis.io

따라서 구성해보자.

 

redis 다운로드

https://redis.io/download

 

다운 받고 압축을 풀고 make, make install 명령어를 사용해서 빌드한다.

tar xvfz redis-5.0.4
cd redis-5.0.4
make
make install

 

redis 클러스터 생성

redis 클러스터 구성을 위해서는 다음과 같이 6개의 노드가 필요하다. 이유는 아래의 내용과 같다.

** ERROR: Invalid configuration for cluster creation.** Redis Cluster requires at least 3 master nodes.
** This is not possible with 2 nodes and 1 replicas per node.** At least 6 nodes are required.

 

그럼 각 노드 생성을 위해서 필요한 설정을 담을 cluster-test 폴더를 먼저 만들자.

그리고 6700 ~ 6705까지의 폴더를 만든다.

jeongcheol-ui-MacBookPro:redis-5.0.4 wedul$ mkdir cluster-test
jeongcheol-ui-MacBookPro:redis-5.0.4 wedul$ cd cluster-test
jeongcheol-ui-MacBookPro:cluster-test wedul$ mkdir $(seq 6700 6705)
jeongcheol-ui-MacBookPro:cluster-test wedul$ ls
6700	6701	6702	6703	6704	6705

그리고 src 폴더에 make를 통해 생성된 redis-server 파일을 모든 폴더에 넣어주고 다음 설정이 들어간 redis.conf 파일까지 같이 넣어준다.

port 6700
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes

그럼 모든 서버를 실행시켜보자.

./redis-server redis.conf &

이제 6개의 인스턴스가 실행중인것을 확인 할 수 있다. 그럼 이 인스턴스를 특별한 설정을 통해 cluster를 구성해야한다.

cluster설정은 redis 3, 4에서는 redis-trib.rb를 사용했지만 5부터는 redis-cli에 다 포함되어 있다.

그럼 아래 명령어를 사용해서 cluster를 구성해보자.

redis-cli --cluster create 127.0.0.1:6700 127.0.0.1:6701 127.0.0.1:6702 127.0.0.1:6703 127.0.0.1:6704 127.0.0.1:6705 --cluster-replicas 1

여기서 사용된 cluster-replicas는 각 인스턴스의 replica 노드를 하나씩 구성하겠다는 뜻으로 마스터노드 3개 slave노드 3개가 만들어진다.

그러면 아래 보는것과 같이 서버가 3대 3대로 마스터와 슬레이브가 구성이되고 yes를 누르면 그에 맞게 클러스터가 구성된다.

 

그럼 이렇게 구성된 클러스터를 사용해서 redis에 접속해보자.

cluster모드에 redis 접속을 위해서는 -c 옵션을 달아서 실행시킨다.

 

이렇게 레디스의 클러스터를 구성해봤다.

이를 사용해서 이제 업무에 적용해보자.

파티셔닝 공부를 위해 아래 페이지의 내용을 번역하며 정리해봤다.

https://redis.io/topics/partitioning


Redis Partitioning: 여러 레디스 인스턴스로 데이터 분배하기


파티셔닝은 데이터를 여러 레디스 인스턴스로 분할하여 모든 인스턴스가 자기가 소유한 키의 집합들만 소유하도록 하는 프로세스이다. 먼저 파티셔닝 개념에 대해 설명하고 레디스 파티셔닝에 대한 대안을 소개한다.

파티셔닝이 효율적인 이유

레디스에서 파티셔닝을 하기는 다음 두개의 이점이 있다.
1. 하나의 컴퓨터로 메모리의 양이 제한되는 경우에 파티셔닝을 사용하여 더 큰 데이터베이스와 메모리를 가질 수 있다.
2. 여러 개의 코어와 여러 대의 컴퓨터에 연산 능력을 확장하고 네트워크 대역폭을 여러 대의 컴퓨터와 네트워크 어댑터로 확장할 수 있다.

기본 파티셔닝 방법 (range partitioning)

파티셔닝에는 여러 기준이 있다. Redis 인스턴스 R0, R1, R2, R3 그리고 많은 사용자를 대표하는 키인 user:1, user:2와 같은 존재한다고 가정해보자. 이때 해당 키들을 어느 인스턴스에 어떻게 넣어야 하는지에 대한 여러 방법을 가지고 있다. 다른말로 말하면 주어진 키들을 주어진 인스턴스에 어떻게 매핑 할것인지에 대한 여러 방법이 있다.

가장 간단한 방법으로 range partitoning이 있다. 이 방법은 특정 범위에 있는 데이터는 특정 인스턴스에 매핑시켜서 데이터를 분배한다. 예를 들면 1 ~ 10000 까지의 데이터는 R0, 10001 ~ 20000 까지는 R1 식으로 저장 할 수 있다. 이 방식은 어떤 범위에 키를 어느 인스턴스로 매핑할지에 대한 정리가 되어있는 테이블이 필요하다.

이 테이블은 관리가 필요하고 모든 번위에대한 정리가 되어있어야 한다. 그래서 매우 불편하여 다른 파티셔닝 기법을 사용하여 이 번거로움을 대체한다.

해시 파티셔닝 Hash Partitioning

이 방식은 키와 함께 동작하고 object_name:<id>형식으로 키를 만들어서 사용하지 않아도 된다. 동작방식은 간단하다. 우선 키 이름을 crc32 해시 함수를 이용해서 숫자로 변경한다. 예를 들면 foobar라는 키는 93024922로 변경한다. 그리고 인스터스 개수 만큼 % 연산을 진행한다. 만약 인스턴스가 4개라면 93024922 % 4는 2이기 때문에 2번째 인스턴스에 들어간다.

다른 파티셔닝 종류

몇몇의 레디스 클라이언트와 프록시로 부터 hash function을 향상시켜서 만든 파티셔닝으로 consistent hashing라고 불린다.

Client side 파티셔닝
- 클라이언트에서 직접적으로 키를 가지고 읽고 기록할 노드를 선택한다. 많은 레디스 클라이언트는 이 파티셔닝을 구현한다.

Proxy assisted 파티셔닝
- 레디스 클라이언트가 바로 레디스 인스턴스에 요청을 보내지 않고 프록시에게 전송한다.
이 프록시는 적절하게 설정된 파티셔닝 스키마 대로 레디스 인스턴스에 저장하고 클라이언트에게 응답한다. 레디스와 Memcached에 대표적으로 Twemproxy가 존재한다.

Query 라우팅
- 임의의 인스턴스로 전달된 쿼리가 올바른 노드로 리다이렉션 되는 것을 말한다. redis cluster는 클라이언트에 도움을 받아서 하이브리드 형태의 쿼리 라우팅을 구현한다.

여러 파티셔닝이 있지만 기본 베이스는 기폰 파티셔닝과 해시 파티셔닝에서 구현된것이기 때문에 이 두가지가 기본이다.

Data store or Cache?

레디스에서 파티셔닝은 개념적으로 데이터 스토어와 캐시로 사용할 때 동일하지만 사실 데이터스토어로써 파티셔닝을 사용할 때는 약간의 제약이 존재한다. 레디스가 데이터 스토어로 사용될 때 키는 항상 같은 레디스 인스턴스에 있어야한다. 하지만 레디스가 캐시로 사용될 때 주어진 노드를 사용할 수 없을 때 다른 노드를 사용한다고 해서 큰문제가 되지 않는다. 이 경우에는 인스턴스 맵을 변경하여 수정할 수 있다. 위에서 제시되었던 파티셔닝에서 기존에 가야할 노드가 사용불가능할 경우 다른 노드로 저장될 수 있다. 비슷하게 만약 새로운 노드가 추가되면 새로운 키의 일부는 새로운 노드에 저장될 수 있다.

정리된 컨셉은 다음과 같다.
- 레디스를 캐시로 사용할 경우 scaling up and down이 자유롭다.
- 레디스를 데이터 ㅈ장소로 써 사용할 경우에는 고정된 키-인스턴스 맵이 존재해야하고 인스턴수의 개수는 그렇게 크지 않게 고정되어 있어야 한다. 그렇지 않으면 인스턴스가 추가되거나 제거 될 때 인스턴스간에 키를 리밸런싱 할 수 있는 시스템이 필요하다. 현재는 redis cluster만 이 기능을 제공한다.


PreSharding

위에 본거와 같이 레디스를 캐시로써 사용하지 않는이상 파티셔닝에 단점이 있느 것을 확인 할 수 있다.

하지만 데이터 스토어는 매일 많이 사용된다. 오늘 10개의 레디스 인스턴스 노드를 사용한다고 해도 다음날 50개가 필요할 수도 있다. 그렇기 때문에 고정된 인스턴스로 키-인스턴스 맵으로 관리하는 방식으로는 데이터 스토어로써 레디스를 사용하는데 어려움이 있다.

레디스가 필요 리소스가 적기 때문에 이 문제에 대한 간단한 접근방법은 애초에 많이 생성하는 것 입니다. 만약 하나의 서버로 서비스를 시작한다면 하나의 서버안에서 파티셔닝을 통해 여러 레디스를 구동할 수 있다. 그래서 처음부터 32개 또는 64개의 인스턴스를 만들어서 충분하게 사용자들이 사용할 수 있도록 설계할 수 있다. 이러한 방식으로 인스턴스를 크게 늘리고 만약 데이터 저장소가 더 필요하고 레디스 서버가 더 필요하다면 간단하게 인스턴스를 다른 서버로 이동 시킬 수 있따. 만약 부가적인 서버가 추가된다면 레디스 인스턴스 반을 추가된 서버로 이동 시킬 수 있다.
Redis 복제를 사용하면 사용자를위한 중단 시간이 거의 없거나 전혀 없을 때 이동을 수행 할 수 있다.


레디스 파티셔닝 사용

이론을 공부했다. 이제 어떻게 사용해야하는지 보자.

Redis Cluster
redis cluster는 자동으로 샤딩을 하고 높은 가용성을 가지는것을 선호한다. 2015년 4월 1일 부터 redis cluster를 사용할 수 있다. redis cluster는 query routing과 client side 파티셔닝을 섞어놓은 방식으로 진행된다.

Twemproxy
Twemproxy는 memcached ASCII와 redis 프로토콜을 위해서 트위터에서 개발된 프록시 이다. 싱글스레드이고 C로 개발되어 전적으로 빠르다. 여러 레디스 인스턴스에서 자동으로 샤딩이 되는 것을 지원하며 하나의 인스턴스가 사용이 불가능하면 다른 인스턴스로 전환되는 것을 지원한다.


주의사항
http://www.zdnet.co.kr/view/?no=20131119174125
여기에 보면 주의사항이 나오는데 핵심은 redis의 경우 싱글 스레드로 돌아가기 때문에 작업이 오래 발생되는 keys나 flushall은 사용하지 말아라. 1만건 이하에 데이터를 조작하는 경우에는 사용해도 되는데 그 이상 사용하는 경우에는 주의하라는 뜻.

Elasticsearch를 Spring Boot에서 작업을 하는 간단한 정리를 해보자.


1. Library 추가

Elasticsearch를 사용하기 위해서는 spring-data-elasticsearch 라이브러리가 추가되어야 한다. 

gradle에 추가해보자.

1
2
3
4
5
6
7
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    compileOnly "org.projectlombok:lombok:1.16.16"
}
 
cs


spring-data-elasticsearch 버전별로 호환되는 elasticsearch가 상이하니 참고

spring data elasticsearchelasticsearch
3.2.x6.5.0
3.1.x6.2.2
3.0.x5.5.0
2.1.x2.4.0
2.0.x2.2.0
1.3.x1.5.2


2. Configuration

Elasticsearch에 접속하기 위한 Configuration을 정의해준다.

Elasticsearch  접속을 위해서는 host, port, cluster name이 필요하다. cluster name을 알아야 하는데 docker에 설치 한 경우 여기서 확인하면 된다.

우선 docker exec -it elastic bash로 콘솔에 접속한 후에 elasticsearch.yml에 적혀있는 cluster name을 확인한다.

그리고 application.properties에 설정 내용을 적어준다.

1
2
3
4
elasticsearch.host=127.0.0.1
elasticsearch.port=9300
elasticsearch.cluster_name=docker-cluster
spring.main.allow-bean-definition-overriding=true
cs

그리고 EnableElasticsearchRepositories 애노테이션을 설정한 Configuration 클래스를 만들어준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package com.elasticsearch.study.configuration;
 
import org.springframework.beans.factory.annotation.Value;
import org.elasticsearch.client.Client;
import org.elasticsearch.client.transport.TransportClient;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.transport.TransportAddress;
import org.elasticsearch.transport.client.PreBuiltTransportClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories;
 
import java.net.InetAddress;
 
/**
 * Elasticsearch Configuration
 *
 * @author wedul
 * @since 2019-02-09
 **/
@EnableElasticsearchRepositories
@Configuration
public class ElasticConfiguration {
 
  @Value("${elasticsearch.host}")
  private String host;
 
  @Value("${elasticsearch.port}")
  private int port;
 
  @Value("${elasticsearch.cluster_name")
  private String clusterName;
 
  @Bean
  public Client client() throws Exception {
    Settings settings = Settings.builder().put("cluster.name", clusterName).build();
 
    TransportClient client = new PreBuiltTransportClient(settings);
    client.addTransportAddress(new TransportAddress(InetAddress.getByName(host), port));
    return client;
  }
 
  @Bean
  public ElasticsearchOperations elasticsearchTemplate() throws Exception {
    return new ElasticsearchTemplate(client());
  }
 
}
 
cs


3. DTO 생성

Elasticsearch에서 Document 내용을 담을 DTO를 만들어주고 @Document 애노테이션을 달고 index name과 type을 정의해준다.

@Id 어노테이션이 붙은 필드는 각 Doucument에 붙어있는 _id 값이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.elasticsearch.study.dto;
 
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
 
/**
 * studyFor
 *
 * @author wedul
 * @since 2019-02-09
 **/
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Document(indexName = "wedul_play", type = "story")
public class WedulPlay {
 
  @Id
  private String id;
  private String title;
  private String user;
  private long startAt;
  private long endAt;
 
}
 
cs


4. Repository

JPA를 사용하면 익숙할 패턴으로 Elasticsearch에서도 ElasticsearchRepository가 존재한다. 사용방법은 JPA와 동일하게 저장할 때는 save, 조회할 때는 find(), findByUser()등으로 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.elasticsearch.study.repository;
 
import com.elasticsearch.study.dto.WedulPlay;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;
 
/**
 * study
 *
 * @author wedul
 * @since 2019-02-09
 **/
@Repository("wedulPlayRepository")
public interface WedulPlayRepository extends ElasticsearchRepository<WedulPlay, String> {
 
  WedulPlay findByUser(String user);
  
}
 
cs


5. Service

지금 테스트 하는 부분에서는 크게 비즈니스 로직에 들어갈 소스가 없다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.elasticsearch.study.service;
 
import com.elasticsearch.study.dto.WedulPlay;
import com.elasticsearch.study.repository.WedulPlayRepository;
import com.google.common.collect.Lists;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import org.springframework.stereotype.Service;
 
import java.util.List;
 
/**
 * study
 *
 * @author wedul
 * @since 2019-02-09
 **/
@AllArgsConstructor
@NoArgsConstructor
@Service
public class WedulPlayService {
 
  private WedulPlayRepository wedulPlayRepository;
 
  public void save(WedulPlay play) {
    wedulPlayRepository.save(play);
  }
 
  public List<WedulPlay> findAll() {
    return Lists.newArrayList(wedulPlayRepository.findAll());
  }
 
  public WedulPlay findByUser(String user) {
    return wedulPlayRepository.findByUser(user);
  }
 
}
 
cs


6. Test 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
package com.elasticsearch.study.wedulplay;
 
import com.elasticsearch.study.dto.WedulPlay;
import com.elasticsearch.study.repository.WedulPlayRepository;
import com.elasticsearch.study.service.WedulPlayService;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
 
import java.util.List;
 
import org.hamcrest.core.IsNull;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
 
/**
 * wedul play document 조회
 *
 * @author wedul
 * @since 2019-02-09
 **/
@RunWith(SpringRunner.class)
@SpringBootTest
public class WedulPlayTest {
 
  WedulPlayService wedulPlayService;
 
  @Autowired
  @Qualifier("wedulPlayRepository")
  WedulPlayRepository wedulPlayRepository;
 
  @Before
  public void setup() {
    wedulPlayService = new WedulPlayService(wedulPlayRepository);
  }
 
  @Test
  public void whenValidParameter_thenSuccessFind() {
    List<WedulPlay> list = wedulPlayService.findAll();
 
    assertNotNull(list);
  }
 
  @Test
  public void whenValidParameter_thenSuccessSave() {
    Exception ex = null;
 
    try {
      wedulPlayService.save(WedulPlay.builder().title("안녕 이건 테스트야").user("위들").startAt(1242421424).endAt(23214124).build());
    } catch (Exception exception) {
      ex = exception;
    }
 
    assertTrue(null == ex);
  }
 
  @Test
  public void whenValidParameter_thenSuccessFindByUser() {
    Exception ex = null;
 
    try {
      WedulPlay play = wedulPlayService.findByUser("위들");
 
      assertThat(play, is(IsNull.notNullValue()));
    } catch (Exception exception) {
      ex = exception;
    }
 
    assertTrue(null == ex);
  }
 
 
}
 
cs


설정이 간단하다. 

나중에 이용해 먹어야지


자세한 소스코드는 여기 참조

https://github.com/weduls/spring_elastic

+ Recent posts