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/

api의 성능 테스트를 위해서 네이버에서 만든 ngrinder 설치하고 테스트를 진행해봤다.


ngrinder는 controller와 agent로 구성이 되어 있는데 이에 대한 내용은 https://naver.github.io/ngrinder/ 해당 내용을 체크하자.


1. Controller 설치
- 톰캣을 설치하고 아래 주소에서 war를 다운받아서 실행시킨다.
https://github.com/naver/ngrinder/releases
단, 3.4.2는 테스트 스크립트 실행 시 unexpected token에러가 발생한다. 그래서 3.4.1을 사용하는걸 추천한다.

설치 완료되면 아래 url로 접근 해서 확인 (초기 계정은 admin/admin)
- 뒤에 root path는 편의를 위해서 war 파일을 ngrinder-controller-3.4.1.war => ngrinder.war로 변경해서 ngrinder로 사용

http://localhost:8080/ngrinder

 

2. Agent 설치
Agent는 테스트에서 필요한 worker process를 실행시켜주고 관리하는 역할을 한다.
- agent를 다운받고 내부에 ./run_agent.sh를 실행시킨다.

- 실행이 완료되면 Agent Management에 들어가면 정상적으로 동작하는걸 확인할 수 있다.

주의사항
먼저 자바 1.9이상의 버전에서는 Agent을 지원을 하지 않는다. 1.9에서 agent 실행 시 다음과 같은 오류가 난다.

1
java.lang.ClassCastException: class jdk.internal.loader.ClassLoaders$AppClassLoader cannot be cast to class java.net.URLClassLoader (jdk.internal.loader.ClassLoaders$AppClassLoader and java.net.URLClassLoader are in module java.base of loader 'bootstrap')
cs
이는 1.9에서 URLClassLoader를 사용하는 방식이 바뀌었으나 ngrinder agent가 아직 지원하지 못해서 발생하는 오류인거 같다. 1.8을 사용하면 괜찮다.



테스트 진행

각 옵션을 설정하고 테스트를 진행하면 아래와 같이 TPS결과가 나온다. 각 설정 옵션에 대해서는 인터넷이나 메인 git에 가면 자세히 나와있다.


Agent, VUser를 조절해가면서 api의 성능을 tps를 확인하면서 조절해서 테스트하면 된다.



'IT 지식 > ngrinder' 카테고리의 다른 글

ngrinder Mac os 간단 설치 및 테스트 방법  (0) 2019.03.11

Nginx과 Apache 비교


 

 Nginx

Apache 

특징 

- Nginx는 싱글 스레드 Event driven 방식

- 미리 설정된 worker 프로세스 안에서 요청이 들어올 때 마다 요청을 분배하여 worker에게 역할을 분배

- 기존에 정해놓은 리소스를 사용하기 때문에 CPU, Memory 등의 자원 사용률이 낮음

- 요청이 올 때마다 쓰레드를 생성하여 할당한다. 작업이 많아질 경우 많은 쓰레드할당이 필요하다. 그리고 쓰레드들이 작업을 진행 할 때마다  CPU를 사용하려 하기 때문에 문맥교환이 자주 발생된다. 

차이점 

 - Apache에 경우 Blocking 방식으로 Network, DB 등 별도의 동작이 진행 될 때 Block되지만 Nginx는 Non-blocking 방식을 지원함


Non-blocking, Event Loop 방식의 Node.js


- Event Loop 방식은 요청이 오면 무조건 이벤트 핸들러에게 요청을 넘김. 그 것이 처리되면 완료되었다고 연락이온다. 그래서 Single Thread Non-blocking 방식이 가능

- Single Thread이기 때문에 Multi thread의 자원에 대한 크리티컬 섹션에 대한 문제가 발생하지 않음.

- Node.js는 Single Thread를 사용하기 때문에 CPU에 문맥교환이 발생하지 않아서 Multi Thread를 지원하는 프레임워크보다 무조건 빠를다고 하지만 그렇지는 않다. Node.js에서 요청은 비동기를 지원하지만 내부적으로 Event를 처리할 때는 libio의 Thread Pool에 의해 동작하기 때문에 I/O 작업이 많을 때는 Multi Thread 방식이 유리하다.(Node.js는 메시지 처리와 같이 I/O가 적은 작업이 어울림.) 

- Single Thread이기 때문에 CPU를 많이 잡아먹는 연산일 경우 다른 작업도 모두 느려지기 때문에 연산이 오래걸리는 작업에는 어울리지 않음.

- Node.js에 특성상 오류가 발생하면 프로그램이 죽어버림

- Single Thread Async를 지원하고 있지만 네트워크, DB와 같은 I/O가 발생할 때는 libio의 Thread Pool에 작업이 전달되고 완료되면 EventQueue에 CallBack 함수에 전달된다.


=> 결론 

Single Thread를 사용. 비동기지만 I/O 발생 시 내부적으로 비동기처럼 동작하기 위해 쓰레드 풀을 사용하여 진행한다. 그래서 요청이 많을 경우 문맥교환이 일어나는 Multi Thread보다 오히려 성능이 나쁠 수 있다.


Nginx와 Node.js

Nginx와 Node.js를 사용할 때 성능적으로 도움이 되는 설정을 정리했다.


Port Range

- Nginx에서 Node.js에 요청을 보낼 때는 2개의 TCP 소켓이 필요하다.(2개의 포트가 필요) 그래서 가용 가능한 포트가 적을 경우에 요청을 받지 못하는 문제가 발생한다. 그래서 상황에 따라서 sysctl의 net.ipv4.ip_local_port_range를 사용하여 설정한다.


Time Wait

- Time wait는 커넥션이 종료 되었으나 할당된 연결이 아직 release가 되지 않은 자원이다. 그래서 요청이 와도 자원을 사용할 수 없어서 문제가 발생할 수 있어서 이를 사용할 수있게 해줘야 한다. sysctl의 net.ipv4.tcp_tw_reuse값을 1로 변경해줘야 한다.


Time Wait 시간

- Time Wait 자원을 사용할 수 있는 자원으로 변경되는 시간을 줄여서 빠르게 사용할 수 있게 해주는 것이 좋다. sysctl의 net.ipv4.tcp_fin.timeout값을 줄이면 된다.


Context Switch 

Node.js에서 사용중인 CPU 코어가 다른 프로세스에서 사용되지 않도록하면 성능 향상에 도움이 된다. 여러 프로세스에서 하나의 코어를 같이 사용할 수 있다. 각각의 프로세스는 돌아가면서 코어 스케줄대로 이용한다. 이 경우 문맥교환이 많이 발생하여 CPU 부하가 증가한다. 그렇기 때문에 node에서 사용하는 CPU 코어는 다른 업무를 못하게하여 서로 코어를 잡아먹으려 하는 문맥교환을 줄여야한다.

※ Context switch

- 문맥 교환(Context Switch)이란 하나의 프로세스가 CPU를 사용 중인 상태에서 다른 프로세스가 CPU를 사용하도록 하기 위해, 이전의 프로세스의 상태(문맥)를 보관하고 새로운 프로세스의 상태를 적재하는 작업을 말한다.


현재 Connection 상태를 확인하는 명령어

netstat -tan | awk '{print $6}' | sort | uniq -c


Nginx에서 Upstream이란?

Nginx 설정에서 Upstream이 있다. 여기서 Upstream은 순차적으로 서비스를 처리하기 위해 사용되는 서버를 의미한다. Nginx에서 내장된 Upstream 모듈은 설정된 서버들의 부하분산, 속도 개선을 담당한다. 일반적으로 설정된 서버의 Upstream은 라운드 로빈 방식으로 진행된다.


※ 라운드 로빈

라운드 로빈 스케줄링(Round Robin Scheduling, RR)은 시분할 시스템을 위해 설계된 선점형 스케줄링의 하나로서, 프로세스들 사이에 우선순위를 두지 않고, 순서대로 시간단위(Time Quantum)로 CPU를 할당하는 방식의 CPU 스케줄링 알고리즘이다.


Mysql에는 두 가지 형태의 엔진이 존재한다. 

아래 그림에서 보면 하단에 길게 표시된 Pluggable 스토리지 엔진을 제외하고 위에 모든 부분이 서버엔진이다.


엔진별 특징 정리

서버엔진 (SQL Interface, Parser, Optimizer, Cache & Buffer)
- 클라이언트의 요청을 받아 SQL을 처리하는 DB 자체의 기능적인 역할을 수행 
- DB가 SQL을 이해할 수 있도록 쿼리를 파싱하고 메모리, 물리적 저장장치와 통신하는 기능을 수행
- 디스크와 직접적인 접근을 제외한 대부분의 역할 수행

스토리지 엔진
- 서버 엔진이 필요한 데이터를 물리적 장치에서 가지고 오는 역할을 수행
- 물리적 저장장치에서 데이터를 읽어오는 역할을 수행하고 플러그인 형식으로 여러 스토리지 엔진을 필요에 따라 추가 삭제 할 수 있다.
- 대표적 스토리지 엔진으로 MyISAM, InnoDB, CSV 등등이 있다. (8버전부터 MyISAM을 지원하지 않는것으로 알고 있다.)
- 다양한 사용 DBMS와의 호완성있게 동작하기 위해서 DB엔진에 최적화 되어있지는 않지만 범용성은 다른 DBMS보다 좋다. (카산드라나 스핑크스 등과도 연동 가능)


그럼 다른 DBMS와 다르게 유동적으로 플러그인처럼 변경이 가능한 Storage Engine에는 어떤 특징이 있을까 조금 더 알아보자.

대표적으로 존재하는 스토리지 엔진은 MyIsam과 InnoDB 스토리지 엔진이 존재한다. 
MyIsam 엔진
- 데이터는 디스크에서 인덱스와 키만 메모리에 적재해서 사용한다. 
- 트랜잭션을 지원하지 않고 테이블 단위 Lock 을 지원한다. (특정 테이블의 여러 세션에서 데이터를 변경하려 하면 성능저하. -> Lock 기준에 테이블이기 때문에 무조건 대기)
- 저사양 서버를 위해 고안된 방법으로 데이터 사이즈와 키, 인덱스를 압축해서 사용한다. 그렇기 때문에 인덱스가 필요한 검색기능이 추가된 테이블을 사용하기에 부적절

InnoDB 스토리지 엔진
- 트랜잭선이 지원, Concurrency control이 가능하고 행 단위 잠금으로 데이터 변경 작업 시 다른 사용자가 테이블에 접근할 수 있다. (MyIsam에 한계를 넘어선다.)
- 메모리에 인덱스와 데이터가 모두 적재되기 때문에 메모리 버퍼 커기가 DB 성능에 많은 영향을 끼치기 때문에 MyIsam보다 더 고사양의 서버를 요구한다.



다음에는 MySQL 성능 높이기 위한 프로파일링에 대해 공부해보자.



모든 프로그래머가 알아둬야 하는 최적화에 관련된 격언이 있다.

1. 맹목적인 어리석음을 비롯한 다른 어떤 이유보다도, 효율성이라는 이름으로 저질러지는 죄악이 더 많다.
2. 97%는 효율성을 잊어버려라.  섣부른 최적화는 모든 악의 근원이다.

그리고 프로그램을 작성하면서 기준을 삼아야 할 내용에 대해 소개한다.

[기준]
빠른 프로그램을 만들려고 처음부터 노력하지말고, 좋은 프로그램을 만들려 노력하라.
-> 좋은 구조를 가진 프로그램은 빠른게 변경하는데 어렵지 않다.
-> 정보은닉의 원칙을 지키는 것이 좋은 구조를 갖는것에 첫 번째 항목이다.

설계를 할 떄는 성능을 제약할 가능성이 있는 결정들을 피하라.
-> 특히 통신 API, 프로토콜 정의서는 변경하기 어렵기 때문에, 신중하게 코딩해야한다.

API를 설계할 떄 내리는 결정들이 성능에 어떤 영향을 끼칠지를 생각하라.
-> public 자료형을 무분별하게 사용하면 잘못된 객체 생성등으로 인해 성능에 이슈가 생길 수 있다.


코드를 최적화한 이후에 코드가 성능이 좋아졌는지 확인을 해야하는데, 전통적인 정적 컴파일 언어들에 비해, 프로그래머가 작성한 코드와 CPU가 실행하는 코드 사이의 "의미론적 차이"가 훨씬 크기 때문에 최적화 결과로 성능이 얼마나 좋아질지 안정적으로 예측하기 어렵다.
-> 특히 자바의 경우 JVM 구현마다, 릴리즈마다, 프로세스 마다 다르다. 
-> JVM 구현이나 하드웨어 플랫폼마다 다른 성능을 내기 때문에 타협적 결정을 내려야 할 때가 있다.




결론을 내면, 자잘한 성능을 고치려고 노력하지말고 구조적 알고리즘 문제를 먼저 생각하라. 잘못된 알고리즘 선택으로 인해 잘못 설계된 프로그램은 자잘한 성능개선으로 문제를 잡을 수 없기 때문이다.


출처 : 조슈아 블로크, 『 Effective Java 2/E』, 이병준 옮김, 인사이트(2014.9.1), 규칙55 인용.

+ Recent posts