본문 바로가기
문제 해결, 기술 비교/개인프로젝트(북클럽)

Elastic Search 연동 및 테스트하기

코동이 2022. 5. 27.

Elastic search란?


Elasticsearch는 분산 검색과 분석 도구입니다.

 

Elasticsearch로 정형화, 비정형화, 숫자 데이터, 지역 데이터 등 다양한 데이터를 빠르게 검색할 수 있습니다. 이를 인덱싱(indexing)이라고 합니다. 단순한 데이터 검색과 데이터에서 패턴을 발견하기 위해 정보를 수집할 수 있습니다. 데이터와 쿼리의 크기가 증가하면서, Elasticsearch의 분산 환경은 배포가 그에따라 원활하게 확장하도록 합니다.

 

indexing : 원본 데이터를 변환하여 저장하는 과정으로 데이터가 효율적으로 검색 될 수 있는 구조로 변경됩니다.

 

indices, index : indexing 과정을 통해 생성된 결과물이자 저장소입니다.

 

역색인(Inverted Index)


아래는 일반적인 인덱스와 역색인 구조입니다.

 

https://www.skyer9.pe.kr/wordpress/?p=1002

 

일반 Index 구조는, 정해진 순서에 따라서 순서대로 저장됩니다. 따라서, 특정 단어를 검색하기 위해서는 모든 데이터를 검사해야 합니다. 하지만, Inverted Index는 단어 단위로 짤라서, 어디에 위치하는지 기록합니다. 이 방식으로 특정 단어를 검색 할 때 모든 데이터를 확인 할 필요가 없습니다. (영어와 달리 한국어, 예를 들어, "재미", "재미있다" 라는 단어는 다르게 인식하는데, 이를 같은 단어로 인식하게 하려면 형태소 분석이 필요하며 "노리" 플러그인을 참고 합니다.)

 

Shard


 document의 집합체인 인덱스는 기본적으로 한 개 혹은 여러개의 샤드(shard)라는 단위로 분리됩니다. 샤드는 내부적으로는 루씬(Lucene)의 Index와 대응됩니다. 이 인덱스들은 한 개 혹은 여러 개가 모여서 노드를 구성합니다.  노드들이 모여 전체 클러스터를 구축합니다.

 

https://www.elastic.co/kr/blog/how-many-shards-should-i-have-in-my-elasticsearch-cluster

 

 

운영중인 샤드 갯수를 변경 할 수 없는데, 그 이유는 내부적으로 루씬의 Index와 대응되기 때문입니다.

 

 루씬은 단일 머신에서 동작하는 Stand Alone으로, 샤드 내부에 이러한 독립적인 루씬 라이브러리를 각각 가지고 있습니다. 따라서 내부의 루씬은 외부의 엘라스틱서치가 다른 샤드들과 더 큰 데이터셋을 가지고 인덱스를 구성한다는 사실을 전혀 모릅니다.

 

 이러한 특징 때문에 엘라스틱서치에서는 샤드의 개수 변경은 불가능하며, 샤드의 변경이 필요한 경우 아예 새로운 인덱스를 생성할 수 있도록 ReIndex API를 사용해야 합니다. 일반적으로 샤드의 갯수가 많을 수록 검색 성능이 좋아집니다. 검색은 각 샤드가 독립적으로 검색을 수행하고 나서 하나의 결과를 합쳐서 제공되므로, 다수의 샤드로 분산될 수록 검색 속도도 비례해서 빨라집니다.

 

(후에 테스트를 위해 4개의 서버에 각각 노드를 생성하고, 1개의 노드에는 2개의 프라이머리 샤드와 1개의 레플리카 샤드를 구성 할 예정입니다. 만약 1개의 노드가 다운이 된 경우 timeout 시간을 기다렸다가 복구가 되지 않는다고 판단이 되면, 해당 노드가 가지고 있는 샤드들이 다른 노드들에게 복사되어 작업을 수행하게 됩니다.)

 

1개의 서버에 2개의 노드를 구성할 수도 있는데, 이 경우 일반적으로 9200, 9201 포트를 사용합니다.

 

 

Replica


 Replica는 특정 노드에 문제가 생길 때, 백업 데이터를 지원하는 복사본 입니다. 만약에, 특정 노드가 죽어버린다면, 해당 노드와 동일한 데이터를 가지고 있는 Replica를 이용해 서비스에 차질없도록 대응합니다. 프라이머리 샤드와 달리 레플리카 샤드의 수는 운영중에 변경이 가능합니다. 일반적으로 장애 대응을 위해 최소 한개 이상의 레플리카 샤드를 두는 것이 좋습니다.  당연히도, 샤드와 복제본 샤드는 서로 다른 노드에 저장이 됩니다.

 

레플리카 수에 따라서 다음과 같은 장단점이 있습니다.

 

  • 레플리카가 많아질 수록 색인 성능은 떨어지고, 읽기 성능은 좋아진다.
  • 반대로 레플리카가 적으면 색인 성능은 좋지만, 읽기 성능은 떨어진다.

 

 위와 같은 관계가 발생하는 이유는 레플리카 샤드가 생성될 때도 프라이머리 샤드와 마찬가지로 분산 읽기를 제공하기 위해 내부의 루씬이 세그먼트를 생성하고 색인 데이터를 전송하는 과정을 거쳐야 하기 때문입니다.

 

 

Windows에서 구성하기


https://www.elastic.co/kr/downloads/elasticsearch

 

위의 홈페이지에서 Windows 버전의 Elasticsearch를 다운받습니다.

 

 

elasticsearch 폴더의 bin에서 elasticsearch를 관리자 권한으로 실행합니다.

 

 

started라는 문구와 함께 정상적으로 실행된 것을 확인할 수 있습니다.

 

 

linux 환경에서는 관리자 권한을 사용하기 위해서 sudo를 사용합니다. 하지만, window에는 그러한 기능이 없다. 따라서 관리자 권한으로 실행하기를 통해서 해결해야 합니다.

 

chrome의 elastic-head를 설치하고, 내가 만든 elasticsearch 도메인 주소와 기본 포트번호 9200로 접속합니다.

 

 

Indices로 이동해서 shard 1개, replica가 0개를 먼저 생성합니다.

 

 

 

초록색 숫자0이 나타나는데, shard의 번호입니다.

 

 

만약에 여러개의 shard를 만들면, 0부터 시작해서 여러개의 숫자가 나타납니다.

 

* CentOS에서 구성하기

 

linux 환경에서의 경우 다음과 같이 실행합니다. (centOS 기준)

 

sudo yum install -y docker

sudo systemctl start docker

sudo chmod 666 /var/run/docker.sock

sudo sysctl -w vm.max_map_count=262144

 

도커를 이용하기 때문에 도커를 설치하고 실행합니다. ES는 가상메모리가 많이 필요하므로 가상메모리를 확대합니다.

 

# 1. 1번 노드에서 실행시키는 명령어
docker network create somenetwork
docker run -d --name elasticsearch --net somenetwork -p 9200:9200 -p 9300:9300 \
-e "discovery.seed_hosts=10.178.0.3,10.178.0.4,10.178.0.5" \
-e "node.name=es01" \
-e "cluster.initial_master_nodes=es01,es02,es03,es04" \
-e "network.publish_host=10.178.0.2" \
elasticsearch:7.10.1

# 2. 2번 노드에서 실행시키는 명령어
docker run -d --name elasticsearch -p 9200:9200 -p 9300:9300 \
-e "discovery.seed_hosts=10.178.0.2,10.178.0.4,10.178.0.5" \
-e "node.name=es02" \
-e "cluster.initial_master_nodes=es01,es02,es03,es04" \
-e "network.publish_host=10.178.0.3" \
elasticsearch:7.10.1

# 3. 3번 노드에서 실행시키는 명령어
docker run -d --name elasticsearch -p 9200:9200 -p 9300:9300 \
-e "discovery.seed_hosts=10.178.0.2,10.178.0.3,10.178.0.5" \
-e "node.name=es03" \
-e "cluster.initial_master_nodes=es01,es02,es03,es04" \
-e "network.publish_host=10.178.0.4" \
elasticsearch:7.10.1

# 4. 4번 노드에서 실행시키는 명령어
docker run -d --name elasticsearch -p 9200:9200 -p 9300:9300 \
-e "discovery.seed_hosts=10.178.0.2,10.178.0.3,10.178.0.4" \
-e "node.name=es04" \
-e "cluster.initial_master_nodes=es01,es02,es03,es04" \
-e "network.publish_host=10.178.0.5" \
elasticsearch:7.10.1

 

4개의 instance에 각각 docker를 이용해서 ES를 설치합니다. seed_hosts에는 자신을 제외한 다른 노드들의 ip를, publish_host에는 자신의 ip를 입력합니다.

 

그와중에 1번 instance의 외부 ip로 ES head에 접속합니다.

 

 

Shards는 1, Replicas는 0으로 생성합니다. Shards의 갯수에 따라서 0번부터 시작이 되고, es04에 만들어졌음을 알 수 있습니다.

 

 

그렇다면 ES가 항상 DB보다 좋을까를 생각해보면 단점이 3가지 있습니다.

 

1. 실시간 처리가 불가능합니다

 

2. 트랜잭션과 롤백을 제공하지 않습니다. DB에서는 트랜잭션 단위로 롤백이 가능합니다. 하지만, ES 여러 노드에 분산해서 저장하기 때문에 힘듭니다.

 

3. 직접 수정(업데이트)를 할 수 없습니다. 데이터를 삭제했다가 다시 저장을 합니다. 따라서, 이 과정에서 전체 클러스터에 영향을 줄 수도 있습니다.

 

elasticsearch를 사용하기 위해 @Document를 만들어야 합니다. 게시글을 쓰고 조회하는 것은 모두 elastic search로 해결할 예정이므로 기존의 @Entity로 된 Post를 @Document로 바꿉니다.

 

@Document(indexName = "post")
@Getter
@NoArgsConstructor
@ToString
public class Post {
	@Id
	private String id;
	private String content;

	public Post(String id, String content) {
		this.id = id;
		this.content = content;
	}
}

 

Elasticsearch 설정을 하는데, host와 port를 정하고 통신을 정의합니다.

 

@Configuration
public class ElasticsearchConfig {
	@Value("#{'${spring.data.elasticsearch.hosts}'.split(',')}")
	private List<String> hosts;

	@Value("${spring.data.elasticsearch.port}")
	private int port;

	@Bean
	public RestHighLevelClient getRestClient() {

		List<HttpHost> hostList = new ArrayList<>();
		for (String host : hosts) {
			hostList.add(new HttpHost(host, port, "http"));
		}

		RestClientBuilder builder = RestClient.builder(hostList.toArray(new HttpHost[0]));
		return new RestHighLevelClient(builder);
	}
}

 

Controller에서는 여전히 rabbitMQ를 이용해서 Queue에서 처리하도록 합니다. 

 

@RestController
@RequestMapping("/api")
public class PostApiController {
	private final JpaPostRepository postRepository;
	private final Producer producer;
	private final ObjectMapper objectMapper;

	public PostApiController(JpaPostRepository postRepository, Producer producer, ObjectMapper objectMapper) {
		this.postRepository = postRepository;
		this.producer = producer;
		this.objectMapper = objectMapper;
	}

	@PostMapping("/post")
	@ResponseStatus(HttpStatus.CREATED)
	public Post create(@RequestBody Post post) throws JsonProcessingException {
		String jsonPost = objectMapper.writeValueAsString(post);
		producer.sendTo(jsonPost);
		return post;
	}

	@GetMapping("/search")
	public List<Post> findPostsByContent(@RequestParam String content) {
		return postRepository.findByContent(content);
	}
}

 

저장소 구현은 다음과 같이 할 수 있습니다.  Data JPA에서도 했던 방식으로 interface 수준에서는 like 로 검색이 안되기 떄문에 쿼리문을 작성합니다.

 

@Repository
public interface JpaPostRepository extends ElasticsearchRepository<Post, String> {
	List<Post> findByContent(String content);

	Post save(Post post);
}

 

자세한 사용 방법은 다음을 참고합니다

 

https://docs.spring.io/spring-data/elasticsearch/docs/current/reference/html/#elasticsearch.operations

 

 

api/post 경로에 새로운 content를 추가합니다

 

 

api/search로 검색하면 post를 like로 조회합니다

 

 

 

ES는 장점이 있지만 단점도 있습니다.

 

1. 실시간 처리 불가능

ES는 데이터를 insert하고나서 잠깐의 시간이 필요합니다. 내부적으로 데이터 처리를 하기 때문입니다. ( 매우 짧다 )

 

2. 트랜잭션과 롤백 기능 X

여러 기능을 하나의 트랜잭션으로 묶을 경우, 하나가 실패하면 모두 롤백됩니다.

하지만, ES는 DB와는 다르게 여러 노드에 분산해서 데이터를 저장하기 때문에 트랜잭션과 롤백 사용이 불가능합니다.

 

3. 업데이트 X

데이터를 삭제했다가 다시 만든다. 따라서 업데이트가 빈번할 경우 성능 이슈가 있을 수 있습니다.

 

* 샤드 여러개 사용하기

 

예를들어, 노드는 4개, 샤드는 8개를 가지는 Indicies를 생성합니다. (post_shard_8)

 

 

4개의 노드는 각각 2개씩 ( es04는 3개 ) 의 샤드를 가지고 있습니다. 초록색 박스에 써진 검은색은 샤드의 순서입니다.

각 노드는 번호가 쓰여진 샤드를 검색하기 때문에, 역색인과 기능이 합쳐서 검색 성능이 올라갑니다.

 

(참고로 여러개의 Indicies를 생성할 때마다, 노드는 고정되어 있으므로 노드에 계속 샤드가 추가된다)

 

 

이제 노드는 4개, 샤드는 8개에 더해서 레플리카를 1개 추가합니다. (post_8_1)

잘 보면, 0~7까지가 1개 더 생성되어있는 것을 알 수 있습니다.

 

 

예를 들어 2번 노드가 죽는다고 가정한다. 2번노드는 0,1,4,5를 가지고 있습니다. 하지만 죽어버린다고 해도 0,1,4는 es03이 5는 es01이 가지고 있기 때문에 지속적인 사용이 가능합니다. 따라서, 만약에 노드가 죽는다고 하더라도 서비스에 문제가 발생하지 않습니다.

 

이제 실제 노드를 죽여봅니다.

 

 

 시간이 조금 지나면, 죽은 es02 노드의 샤드들이 다른 노드로 들어간 것을 확인 할 수 있습니다. 물론 성능은 노드 4개일 때보다 조금은 떨어지지만, 여전히 정상적으로 서비스를 운영합니다.

 

 

이제, es04까지 추가적으로 하나의 노드를 더 내려봅니다.

 

 

시간이 흐른 후, 샤드들이 이제 다른 노드들에 모두 들어 간 것을 확인할 수 있습니다.

 

 

결론


평균적으로, 하나의 샤드만을 이용하면 검색 매칭이 많을수록 DB보다 성능이 낮을 수 있습니다.

 

하지만, 샤드 수를 늘리면, 당연히 성능은 전체적으로 올라갑니다.

 

레플리카를 추가하는 경우, 매칭이 많은 경우에는 레플리카가 없는경우보다 성능이 낮을수는 있으나, 매칭이 적은 문서는 성능이 동일합니다.

 

 

 

*Artillery를 이용한 테스트

 

config:
  target: "http://34.84.182.116"
  phases:
    - duration: 60
      arrivalRate: 3
      name: Warm up
    - duration: 120
      arrivalRate: 3
      rampTo: 100
      name: Ramp up load
    - duration: 600
      arrivalRate: 100
      name: Sustained load
  payload:
    path: "ratings_test_1k.csv"
    fields:
      - "content"
scenarios:
  - name: "just post content"
    flow:
      - post:
          url: "/api/diaryRaw"
          json:
            content: "{{ content }}"

 

1. JPA 직접 저장으로 NGINX 1개(ASIA), INSTANCE 3개(ASIA, ASIA, ASIA)

 

2. JPA Rabbitmq 저장으로 NGINX 1개(ASIA), INSTANCE 3개(ASIA, ASIA, ASIA)

 

 

* 참고

 

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

 

https://jaemunbro.medium.com/elastic-search-%EC%83%A4%EB%93%9C-%EC%B5%9C%EC%A0%81%ED%99%94-68062271fb64

 

반응형