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를 많이 설정하는건 안좋다.

묵시적 형변환
조건절의 데이터 타입이 다를 때 우선순위가 높은 타입으로 형이 내부적으로 변환 되는 것. 
정수 > 문자열 순이며 만약 정수와 문자열이 비교가 되는 경우에는 둘중에 우선순위가 낮은 것이 변경된다. 

우리는 이렇게 자동으로 형변환 해주는 경우에 익숙해져 있다. 자바에서도 Integer와 int 두 개의 변수의 값을 묵시적으로 형변환 시켜주지만 이는 이펙티브 자바 책에서도 볼 수 있지만 성능저하의 원인이 된다고 한다.

Mysql도 예외가 아닌 것 같다. 

예를 들어 보자 아래와 같은 테이블을 생성 후 데이터를 삽입한다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 테이블 생성 
create table chagne_data (
    id int unsigned not null auto_increment,
    sub_id int unsigned not null,
    val varchar(64not null,
    date_d datetime not null,
    primary key(id)
);
 
# 랜덤 데이터 삽
insert INTO 
chagne_data (
        sub_id,
        val,
        date_d
    )
values
    (
        crc32(rand()),
        crc32(rand()) * 12345,
        date_add(now(), interval - crc32(rand()) / 5 second)
    );
INSERT INTO test.chagne_data(sub_id, val, date_d) SELECT sub_id, val, date_d FROM test.chagne_data;
cs

인덱스를 생성하고 
정수형 컬럼에 문자열 조건을 주어서 실행계획을 확인해보자.

1
2
3
4
5
# 인덱스 생성     
CREATE INDEX int_index ON test.chagne_data(sub_id);    
 
# 정수형에 문자열형 조건으로 추가 (정수가 더 우선순위가 높으므로 문제 없음)     
SELECT * FROM test.chagne_data where sub_id = '3689107608';
cs


별 문제 없다. 왜냐면 정수형 데이터가 우선순위가 더 높기 때문에 우측의 문자열 데이터가 변경되었기 때문에 인덱스를 정상적으로 사용했기 때문이다.


그렇다면 문자열 컬럼을 정수형 데이터로 조건을 주어서 데이터를 추출한다면 어떨까?

우선 정상적인 경우의 실행계획을 살펴보자.

1
2
3
4
5
# 인덱스 생성 
CREATE INDEX int_index ON test.chagne_data(val);    
 
# 문자열에 문자열로 조건을 주고 실행계획 확인 
SELECT * FROM test.chagne_data WHERE val = '10227816402120';
cs

이번에는 문자열 컬럼에 정수 데이터를 넣고 조회해보자. 

1
2
# 문자열에 정수형 조건 추가 (묵시적 형변환 발생)
SELECT * FROM test.chagne_data WHERE val = 10227816402120;
cs


인덱스 사용을 못하고 문제가 되는 것을 확인 할 수 있다.

특히 이런 문제가 발생하는 대표적인 부분이 mybatis에서 데이터를 #{}형태로 넣어서 사용할 때 문제 없이 실행되기 때문에 잘 몰라서 문제소지를 일으킬 수 있다.

항상 조심하자.


인덱스가 Id,  ch_date, ch_order 순으로 생성되어 있을 경우 MIN 값을 구해도 별도의 정렬연산을 수행하지 않는다. 수직적 탐색을 통해서 가장 왼쪽지점에서 보는 최소 값이 바로 구하고자 하는 값이기 때문이다.


1
SELECT MIN(ch_date) FROM scott.SORT_TEST WHERE ID = ‘C’;
cs

MAX의 경우도 마찬가지이다. MIN과 다른 점은 왼쪽에서 찾는게 아니라 가장 오른쪽에 있는 데이터를 찾는다는 점이다.

1
SELECT MAX(ch_date) FROM scott.SORT_TEST WHERE ID = ‘C’;
cs

그래서 두 개의 실행계획을 살펴보면 인덱스 리프 블록의 왼쪽(MIN) 또는 오른쪽 (MAX)에서 레코드 하나(FIRST ROW)만 읽고 멈춘다.

1
SELECT MAX(TO_DATE(ch_date)) FROM scott.SORT_TEST WHERE ID = 'C';
cs

이 경우에는 MAX일지라도 ch_date가 내부적으로 변경을 한 뒤에 최대값을 찾기 때문에 정렬을 한뒤에 진행이 가능하다.


하지만 이걸 반대로 바꿔서 진행하면 최대 값을 찾고 그 값을 TO_DATE()로 변경하기 때문에 큰 문제 없이 FIRST 항목만 찾게된다.

출처 : 친절한 SQL 튜닝

  1. 2019.03.30 14:54

    비밀댓글입니다

인덱스 Range Scan이 되기 위한 선행 조건


학교이름, 나이, 이름, 주소로 구성된 테이블이 있다고 가정해보자.



빠른 검색을 위해서 인덱스를 학교 이름, 나이, 이름으로 구성해서 만들었다고 가정해보자.


CREATE  INDEX SCOTT.student_idx

ON SCOTT.STUDENT_TEST ("SCHOOL_NAME" ASC,"AGE" ASC,"NAME" ASC);


인덱스 구성의 순서로 인해 학교순으로 정렬하고, 나이로 정렬하고, 이름으로 정렬해서 데이터를 찾는다.


그렇기 때문에 이름을 조건으로 데이터를 검색하였을 때 결국 모든 리프노드를 다 검색해야한다.





그렇기 때문에 인덱스를 Range Scan  하기 위한 가장 첫 번째 조건은 인덱스 선두 컬럼이 조건절에 있어야한다.



그렇다면 만약 인덱스에 사용된 컬럼이 가공 되었으면 인덱스 Range Scan이 지정이 되지 않는거가??


다음 예를 살펴보자.


SELECT * FROM student_test WHERE name = '정철' and substr(SCHOOL_NAME, 0, 1) = :SCHOOL_NAME;


위와 같이 쿼리를 수행하려고 할 때 아래와 같이 인덱스를 구성해보자.


CREATE  INDEX SCOTT.student_idx

ON SCOTT.STUDENT_TEST ("NAME" ASC,"AGE" ASC,"SCHOOL_NAME" ASC);


그리고 실행쿼리에 대해 실행계획을 확인해 보면 인덱스 Range Scan이 가능한 것을 알 수 있다.


인덱스에 사용되는 컬럼이 조작되면 인덱스 Range Scan이 되지 않는다고 알고 있었는데 의아할 수도 있다.





인덱스 Range Scan이 가능한 이유는 인덱스를 구성하는 첫 번째 컬럼이 가공되지 않았기 때문이다.


인덱스 Range Scan을 사용하기 위해서는 인덱스를 사용하는 첫 번째 컬럼이 가공되지 않으면 사용이 가능하다.





인덱스를 타기만 하면 튜닝이 종료되는건가??


대부분의 개발자가 실행계획 확인 없이 SQL 작성한다. 그리고 인덱스 Range Scan이 지정된 것만 확인하면 추가적으로 확인하지 않는다.


위의 테이블에서 인덱스를 다음과 같이 지정해보자.


CREATE  INDEX SCOTT.student_idx

ON SCOTT.STUDENT_TEST ("NAME" ASC,"SCHOOL_NAME" ASC);


그리고 학생 검색을 위해 다음 쿼리 두 개를 살펴보자.


SELECT * FROM student_test WHERE name = '정철' and substr(SCHOOL_NAME, 0, 1) = :SCHOOL_NAME;

 SELECT * FROM student_test WHERE name = '정철' and SCHOOL_NAME LIKE :SCHOOL_NAME;

두 개의 쿼리 모두 인덱스 Range 스캔을 사용하지만 조건에 사용된 컬럼이 가공되었기 때문에 성능에 문제가 있다.


이를 해결하는 방법은 추후에 공부해보자.

  1. 동구 2018.07.08 18:41

    실행계획을 믿었다가 프로덕트 DB에서 속도가 안나오는거 보고 예전엔 많이 의아했는데....
    통계작업으로 인한 수치와 옵티마이저에 의해 언제든지 달라질 수 있다는 것 ㅜ

우리가 색인을 통해 단어를 찾는 순간을 생각해보자.

ㄱ.

가나

가방 장식

가시 방석

ㄴ.

나방

나방 나무

누에고치

나무 장식

누나


여기서 누에고치라는 단어를 찾을 때, 위에서 순차적으로 진행한다고 가정하였을 때 큰 어려움 없이 발견할 수있다. 이 방식을 Index Range Scan이라고 한다.

반대로 장식이 포함된 단어를 찾아보자. 찾기 어려운 건 아니여도 모든 색인을 전부 확인해봐야한다. 이렇게 모든 색인을 다 확인하고 나서 찾을 수 있는 방식을 Index Full Scan 방식이라고 한다.

그렇기 때문에 인덱스의 기준이되는 데이터 즉 컬럼을 가공하게되면 Range Scan이 불가능해진다. 정리하면 인덱스 기준이 가공되면 인덱스 스캔의 시작점을 찾는 수직적 탐색이 불가능해지기 때문이다.


몇 가지 쿼리를 예로 들어보자.


1
2
3
4
create table student (
name varchar2(255),
birth date);
cs


학생 테이블이 있을 때 생일이 1월로 시작하는 사용자를 찾기위해 다음과 같은 쿼리를 사용한다고 가정해보자.


1
select * from student where substr(birth, 52= '01';
cs



어디서 부터 스캔을 멈춰야할지 인덱스 스캔을 할 수가 없다. 또 다른 예를 들어서 확인해보자.



1
2
3
select * from student where nvl(birth, CURRENT_DATE) < '2018-08-12';
 
select * from student where name like '%edu%';
cs



어디서 인덱스 스캔을 멈추어야하는지 알 수없기 때문에 마찬가지로 인덱스 스캔을 진행할 수가 없다.





OR이나 IN절의 경우에는 내부적으로 UNION ALL으로 내부적으로 나뉘어서 각자 자신의 쿼리를 사용하여 인덱스를 사용할 수 있다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
where 전화번호 in ( tel_no1, tel_no2 )
 
 
 
                |
// 아래와 같이 변경
                ▽
 
 
 
select * from 고객 where 전화번호 = tel_no1
 
union all 
 
select * from 고객 where 전화번호 = tel_no2

cs


+ Recent posts