Elasticsearch에 대해 검색하다가 ebay에 퍼포먼스 튜닝방법에 대해 좋은 글이 있어서 간단하게 정리해봤다. 새롭게 알게된 사실이 많아서 좋았다. 

정리 잘된 기술 블로그를 보는것은 책을 읽는거보다 훨씬 유익한 경우가 많은 것 같다.

 

Elasticsearch


엘라스틱 서치는 아파치 루씬을 기반으로한 검색과 분석 엔진으로 데이터를 실시간에 가깝게 보여주고 분석해 준다. 실시간성으로 분석과 검색을 위해서 많이 사용되는 엘라스틱 서치의 퍼포먼스는 무엇보다 중요한데 이를 위한 퍼포먼스 튜닝방법을 정리해보자.

높은 엘라스틱서치의 퍼포먼스를 위해서는 많은 처리량, 낮은 검색 지연시간등이 요구된다.

 

고효율성 Elasticsearch를 위한 솔루션


- 효율 적인 인덱스 디자인
인덱스를 설계하다보면 하나의 인덱스에 모든 데이터를 넣고 쿼리로 찾을것인지 아니면 여러 인덱스로 나눌것인지 고민된다. 어느것이 효율적이냐는 정답은 내가 사용하는 쿼리에 달려있다. 유형별로 사례를 보면서 확인해보자.

1. 쿼리에 filter가 들어가고 그 값이 Enumerable할 때는 인덱스를 나눠서 설계하라
만약 인덱스 내부에 데이터에서 지역별로 나눠서 데이터를 찾아야 한다면 다음과 같이 쿼리를 실행시킬 것이다. 이럴경우 지역별로 인덱스를 구분하여 만들면 더욱 효율적인 퍼포먼스를 기대할 수 있다.

{
    "query": {
        "bool": {
            "must": {
                "match": {
                    "title": "${title}"
                }
            },
            "filter": {
                "term": {
                    "region": "US"
                }
            }
        }
    }
}

 

2. 값이 enumerable하지 않다면 routing key를 사용하라.
만약 수백만개의 주문 데이터가 구매자별로 쌓인다고 가졍하였을때 구매자별로 인덱스를 구분한다면 너무 많은 인덱스를 구분해야한다. filter 사용되는 필드를 routing key로 사용하여 인덱스를 여러 shard로 쪼갤 수 있다. 그래서 이럴 경우 동일한 buyerId를 가지고 있는 데이터를 동일한 shard에 모여서 저장하면 모든 쿼리가 buyerId라는 routing key를 사용하여 한번에 조회되게 하면 퍼포먼스가 향상된다.

3. 기간이 정해져 있는 데이터들의 경우 기간별로 인덱스를 구성하여 사용하라.
로깅, 모니터링을 하는 데이터의 경우 일, 주, 월별로 데이터를 모을 수 있기 때문에 이를 사용하여 날짜별로 데이터를 모으면 더 빠르게 데이터에 접근할 수 있다.

4. mapping을 효율적으로 지정하라
인덱스에서 필드별로 mapping을 지정해줘야 해당 필드에 대한 검색이 가능하다. 기본적으로 데이터가 인덱스에 들어가면 기본적으로 자동 매핑을 해주지만 효율적이지는 않다. 그래서 정상적으로 인덱스에 맞게 효율적으로 인덱스를 설계하고 index생성시 dynamic옵션을 false로 지정하여 자동 매핑을 방지하는게 좋다. 자동매핑에 경우 문자열을 text, keyword모두 지정해주는데 이는 그닥 효율적이지 못한 대표적인 예이다.

5. 사용자 정의 id를 사용할 경우 불균형 샤딩에 대해 조심하라.
엘라스틱 서치는 자동으로 id를 생성해주는데 이를 호ㄹ용하여 적절하게 document들에 대하여 샤딩을 수행한다. 만약 사용자 지정 id를 사용하거나 별도의 routing key를 충분히 랜덤하지 않게 부여한다면 다른 shard들에 비해 특정 shard가 데이터가 많이 적재되는 문제를 초래할 수 있다. 이럴 경우 일기/쓰기가 굉장히 느려지게 되기 때문에 조심해야한다.

6. 여러 노드에 고르게 shard를 만들어라.
엘라스틱서치에는 coordinator node와 여러 마스터 노드들로 구성되어 있는데 coordinator 노드는 분배 역할을 주로 담당하고 master node들이 데이터 적재를 담당하는데 각 특정노드에 shard가 몰려있을 경우 전체 시스템의 bottle neck 현상이 발생할 수 있다.


7. bulk request를 높이고 refresh 기간을 올려라.
이벤트가 발생할 때마다 엘라스틱서치는 새로운 lucene segment를 만들고 그것을 추후에 합치는 작업을 진행한다. 이런 작업이 발생하는 주기가 refresh가 발생할 때 진행되는데 이런 문제를 줄이기 위해서는 refresh interval을 길게 잡아 놓으면 좋다. 하지만 refresh interval이 길면 변경된 데이터가 검색이 되지 않는다. 왜냐면 index가 refresh가 종료되어야 변경되거나 추가된 document가 검색에 잡히기 때문이다.


8. replica 수를 줄여라.
엘라스틱서치 노드는 주요 노드에 기록되고 모든 replica shard들에 인덱싱 요청이 발생된다. 그렇기 때문에 replica shard수가 많을수록 document의 인덱싱 속도가 현저하게 느려진다.


9. 가능하면 자동으로 생성되는 ID를 사용하라.
엘라스틱서치에서 만들어주는 ID는 고유값을 보증한다. 만약 사용자 고유 ID를 진행하고 싶은 경우 루신에 친화적인 zero-paddeded sequaltial 아이디인 UUID-1 또는 Nono time을 사용해야 한다. 이 아이디들은 일관되고 연속적인 패턴이 잘 압축되어 있는 형식들이다. 대조적으로 UUID-4는 완전하게 랜덤한값을 만들어내기 때문에 추천하지 않는다.


10. 가능하면 query context에 filter를 사용해라.
쿼리에서 찾고자 하는 값과 일치하는 걸 찾을 때 대부분인 yes or no로 찾을 수 있다. match등을 통해서 데이터를 찾으면 scoring이 들어가기 때문에 더 성능적으로 힘들어진다. 하지만 filter를 사용하면 결과가 캐시가 되는것뿐만 아니라 단순 yes or no이기 때문에 score 계산이 빠져서 더 효율적이다.


11. shard 노드를 효율적으로 관리하라
인덱스에 어느정도에 shard를 설정해야할까가 인덱스 구축시 고민을 많이 하게되는 요소이다. 불행하게도 모든 시나리오에 정확한 정답은 없다. 모든것은 어떻게 설계하냐에 따라 달려있다. 만약 너무 작은 수의 shard를 설정하게 되면 scale out이 되지 않는다. 예를들어 오직 하나의 shard만 있는 경우 모든 도큐먼트는 한곳에 저장되서 문제가 발생한다. 반대로 너무 많은 shard를 설정하게 되면 퍼포먼스에 문제가 발생한다. 왜냐하면 엘라스틱서치는 모든 shard에 쿼리를 실행시키기 때문에 request안에 routing key가 있다고 하더라도 fetch 그리고 merge가 모든 shard에서 나온 결과에서 발생하기 때문이다. 여러 관례로 봤을 때, 만약 index가 1G가보다 작으면 shard는 1개가 적당하다. 대부분에 경험상 shard를 5섯개를 기본으로 놓고 운영하는게 적당했다. 하지만 shard사이즈가 30GB가 넘으면 더 쪼개서 사용해야한다. 한번 생성된 index에 shard수를 조정하려면 reindex를 해야하므로 신중하길 바란다.


12. stop word를 사용한 검색을 자제하라
stop word는 a, the와 같이 검색 결과를 증가시키는 단어들이다. 만약 fox라고 검색하면 10건나올 데이터가 the fox라고 하면 the와 가까운 데이터가 출력되면서 거의 대부분의 document가 출력될 것이다. 엘라스틱서치는 모든결과를 score별로 정렬하기 때문에 the fox와 같이 stop word로 검색하면 전체 시스템을 느리게 만든다. 미리 token filter를 만들어서 stop word를 제거하고 검색되게 만드는게 효율적이다. 만약 당장 어렵다면 the ADN fox라고 검색해서 중요한 결과만을 뽑아낼 수 있다.

 

오랜만에 좋은글 잘 읽어서 기분이 좋다. 

출처 : https://www.ebayinc.com/stories/blogs/tech/elasticsearch-performance-tuning-practice-at-ebay/

  1. wedul 2019.08.13 18:11

    shard는 데이터 자체를 의미하고 replica shard는 백업을 위한 shard가 아니라 failover를 위한 shard이기 때문에 replica shard를 많이 설정하는건 안좋다.

JPA에서 사용되는 기본적인 어노테이션 몇개를 정리해보자.


@Entity

- 클래스와 테이블과 매핑한다고 JPA에게 알려준다. 이렇게 @Entity가 사용될 클래스를 엔티티 클래스라고 한다.


@Table

- 엔티티 클래스에 매핑할 테이블 정보를 알려준다. (이 어노테이션을 생략하면 클래스 이름을 테이블정보로 매핑한다.)


@Id

- 엔티티 클래스의 필드를 테이블에 기본키로 매핑한다. (데이터베이스는 엔티티를 구별할때 이 키값으로 구분한다.)


@Column

- 필드를 컬럼에 매핑한다.


매핑 정보가 없는 필드

- @Column을 생략하면 필드명을 사용해서 컬럼명과 매핑하게 된다. 만약 대소문자를 데이터베이스가 구분할 경우에는 꼭 위에 @Column어노테이션을 사용해서 진행해야한다.


#Dialect(방언)

- 데이터베이스마다 서로 다른 특징(Mysql의 limit, Oracle의 Rownum....)들을 JPA에서 방언(Dialect)라고 한다. 이렇게 데이터베이스 벤더사마다 서로 다른 기능을 사용해서 JPA를 사용하면 나중에 데이터베이스를 바꿀때 문제가 발생하기 때문에 문제가 없는 방언 클래스를 사용해야한다. 


위의 어노테이션이 적용된 클래스

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
package com.wedul.springboottest.member.dto;
 
import lombok.Data;
 
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
 
/**
 * springboottest
 *
 * @author wedul
 * @since 03/10/2018
 **/
@Entity
@Data
@Table(name = "MEMBER")
public class Member {
 
  @Id
  @Column(name = "ID")
  private String id;
 
  @Column(name = "NAME")
  private String userName;
 
  // 매핑 정보가 없는 필드
  private int age;
 
}
 
cs


Spring Boot에서는 여러 설정값을 application.properties에 입력하여 사용할 수 있다.


Spring보다 편리하고 효율적이다.
하지만 DBMS 사용을 위해서 연결정보를 properties에 입력할 때 평문으로 그냥 삽입하면 정보 유출에 문제가 발생 할 수 있다. 

이를 해결하기 위해서 application.properties에서 어떻게 사용하는지 확인해보자.


1. application.properties 내용 암호화하여 입력하기

우선 application.properties에 있는 내용을 암호화 해서 삽입한다. 나는 AES256으로 암호화 하여 삽입하였다.

1
2
spring.datasource.username=K2amNtg+kL5xK23g7H3Znw==
spring.datasource.password=ivWXmtXGN289BY6q9AwRnw==
cs


2. EnvironmentPostProcessor 구현하기

EnvironmentPostProcessor 인터페이스는 application context가 올라가기전에 application을 커스터마이징 할 수 있게 지원해주는 기능을 제공한다.

postProcessEnvironment 메소드를 구현하여 원하고자 하는 속성값을 변경하여 처리할 수 있다.  이 부분에서 DBMS 연결정보를 다시 복호화 처리하도록 설정하면 된다.

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
/**
 * JDBC 접속 ID 와 Password 암복호화에 사용하기 위해 사용하는 Processor
 *
 * @author cjung
 * @date 2018. 08. 12
 *
 */
public class EncryptionEnvironmentPostProcessor implements EnvironmentPostProcessor {
 
    private final Logger logger = LoggerFactory.getLogger(EnvironmentPostProcessor.class);
 
    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
        Properties props = new Properties();
        try {
            props.put("spring.datasource.password", AES256Cipher.getInstance().AES_Decode(environment.getProperty("spring.datasource.password")));
            props.put("spring.datasource.username", AES256Cipher.getInstance().AES_Decode(environment.getProperty("spring.datasource.username")));
        } catch (Exception e) {
            logger.error("Fail decrypt datasource info", e);
        }
 
        environment.getPropertySources().addFirst(new PropertiesPropertySource("myProps", props));
    }
 
}
cs


3. META-INF/spring.factories 에 클래스 full 경로 기입

EnvironmentPostProcessor를 구현한 클래스가 정상적으로 동작하기 위해서는 classpath에 META-INF/spring.factories 파일을 만들고 해당 클래스의 풀 경로를 기입해주어야한다.


Full path 기입

1
2
# post processor 적용
org.springframework.boot.env.EnvironmentPostProcessor=com.wedul.common.config.EncryptionEnvironmentPostProcessor
cs


참고
https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/env/EnvironmentPostProcessor.html


+ Recent posts