오늘 포스팅할 내용은 ELK Stack의 요소중 하나인 Logstash(로그스태시)입니다. 로그스태시 설명에 앞서 로그란 시스템이나 애플리케이션 상태 및 행위와 관련된 풍부한 정보를 포함하고 있습니다. 이러한 정보를 각각 시스템마다 파일로 기록하고 있는 경우가 대다수 일겁니다. 그렇다면 과연 이러한 정보를 파일로 관리하는 것이 효율적인 것인가를 생각해볼 필요가 있습니다. 한곳에 모든 로그데이터를 시스템별로 구분하여 저장하고 하나의 뷰에서 모든 시스템의 로그데이터를 볼 수 있다면 굉장히 관리가 편해질 것입니다. 이러한 모든 로그정보를 수집하여 하나의 저장소(DB, Elasticsearch 등)에 출력해주는 시스템이 로그스태시라는 시스템입니다. 앞선 포스팅에서 다루어보았던 Filebeat와 연동을 한다면 파일에 축적되고 있는 로그데이터를 하나의 저장소로 보낼 수도 있고, 카프카의 토픽에 누적되어 있는 메시지들을 가져와 하나의 저장소에 보낼 수도 있습니다.

 

2019/06/17 - [Elasticsearch&Solr] - ELK Stack - Filebeat(파일비트)란? 간단한 사용법

 

ELK Stack - Filebeat(파일비트)란? 간단한 사용법

오늘 포스팅할 내용은 ELK Stack에서 중요한 보조 수단 중 하나인 Filebeat(파일비트)에 대해 다루어볼 것이다. 우선 Filebeat를 이용하는 사례를 간단하게 하나 들어보자면, 운영중인 애플리케이션에서 File을..

coding-start.tistory.com

다시 한번 로그스태시는 아래 그림과 같이 오픈소스 데이터 수집 엔진으로, 실시간 파이프라인 기능을 갖춘 데다 널리 사용되고 있는 시스템입니다. 로그스태시를 활용하면 다양한 입력차원에서 데이터를 수집 및 분석, 가공 및 통합해 다양한 목적지에 저장하는 파이프라인을 쉽게 구축할 수 있습니다.

 

 

게다가 로그스태시는 사용하기 쉽고, 연동하기 간단한 입력 필터와 출력 플러그인과 같이 다양한 플러그인을 제공합니다. 이처럼 로그스태시를 사용하면 대용량 데이터와 각종 데이터 형식을 통합하고 정규화하는 프로세스 구축이 가능합니다.

 

 

로그스태시 아키텍쳐

 

 

로그스태시는 위의 그림과 같이 여러 원천데이터를 가져와 필터를 통해 가공하여 하나 이상의 시스템으로 내보낼 수 있습니다. 입력은 여러 원천데이터가 될 수 있고, 필터 또한 하나 이상을 정의하여 가공할 수 있습니다. 즉 입력,필터,출력 세 가지 단계로 구성이 됩니다. 이중 입력과 출력은 필수요소, 필터는 선택요소입니다. 또한 로그스태시는 기본적으로 파이프라인 단계마다 이벤트를 버퍼에 담기 위해 인메모리 바운드 큐를 사용하는데, 로그스태시가 비정상 종료된다면 인메모리에 저장된 이벤트는 손실됩니다. 하지만 손실 방지를 위하여 영구큐를 사용해 실행하면 이벤트를 디스크에 작성하기 때문에 비정상 종료후 재시작되어도 데이터의 손실이 없습니다.

 

 

영구 큐는 LOGSTASH_HOME/config 디렉토리의 logstash.yml 설정 파일에서 queue.type: persisted 속성을 설정하면 활성화 가능하다. 또한 로그스태시 힙메모리를 늘리려면 LOGSTASH_HOME/config 디렉토리 밑에 jvm.options 파일에서 조정가능합니다.

 

입력 플러그인

일반적으로 많이 사용되는 입력 플러그인에 대해 설명한다.

 

1)File

File 플러그인은 파일에서 이벤트를 한 줄씩 가져오는데 사용한다. 리눅스와 유닉스 명령어 tail -f와 유사한 방식으로 동작한다. 각 파일의 모든 변경 사항을 추적하고 마지막으로 읽은 위치를 기반으로 해당 시점 이후의 데이터만 전송한다. 각 파일에서 현재 위치는 sincedb라는 별도로 분리한 파일에 기록하여 로그스태시를 재기동하여도 읽지 못한 데이터를 빠지지 않고 읽어 올 수 있다.

 

1
2
3
4
5
6
7
8
9
10
11
#sample.conf
 
#input plugin file
input
{
 file {
  path => ["/Users/yun-yeoseong/*","...","..."]
  start_position => "beginning"
#sincedb_path => "NULL" ~> 로그스태시를 재시작할때마다 파일의 처음부터 읽는다.
  exclude => ["*.csv"]
  discover_interval => "10s"
  type => "applogs" ~>새로운 필드를 선언한다. 추후 필터에서 유용하게 쓰이는 필드이다.
 }
}
cs

 

위의 input 설정을 설명하면 path에 설정된 경로에서 파일을 읽어오며 파일에서 로그 추출 시작점을 파일의 처음으로 잡았고 경로의 파일중 csv 확장자는 제외하고 10초마다 파일을 읽어들이며 type이라는 필드에 applogs라는 데이터를 추가한다. type 필드를 이용하여 시스템 이름등을 넣어 로그스태시가 수집한 로그데이터 구분이 가능하다. 나머지 기타 설정들을 공식 홈페이지를 확인하자.

 

2)Beat

Beat 입력 플러그인을 사용하면 로그스태시에서 엘라스틱비트 프레임워크의 이벤트를 수신할 수 있다. 엘라스틱비트란 로그스태시를 보조하는 역할이며, 다양한 시스템에서 데이터를 수집하여 전달해주는 역할이다. 로그스태시와 공통점이라면 다양한 시스템의 데이터를 수집할 수 있다는 점이며, 차이점은 수집한 데이터를 가공하지 못한다는 점이다. 즉, 보통 엘라스틱비트에서 데이터를 수집하여 로그스태시에 보내면 로그스태시는 데이터를 가공하여 출력 포인트로 가공된 데이터를 내보낸다. 

 

1
2
3
4
5
6
7
8
9
10
11
12
#input plugin beat
input
{
 beats {
  host => "192.168.10.229"
  port => 2134 =>"실행중인 비트 포트만 작성해주면 된다.
 }
 beats { =>beats를 여러개 입력하여 다중 비트 입력을 받을 수 있다.
  host => "ip"
  port => "port"
 }
}
cs

 

위의 input 설정을 설명하자면 192.168.10.229:2134로 떠있는 비트에서 이벤트를 수신한다는 설정이다. 여러개의 beat 설정을 넣어서 여러 비트프레임워크에서 이벤트를 수신할 수 있다. 만약 로그스태시와 비트가 같은 서버에 있다면 host는 생략하고 port만 명시해도 된다.

 

설명한 input 플러그인 이외에도 JDBC,Kafka 등 많은 입력 플러그인이 있다. 그 중 카프카에서 데이터를 가져오는 예제는 이 포스팅 마지막에 할 예정이라 설명을 생략하였다.

 

TIP 로그스태시 실행 시 -r 옵션을 지정하면 설정을 변경하고 저장할 때마다 자동으로 바뀐 환경설정을 다시 로드한다. 즉, 매번 설정이 변경될 때마다 로그스태시를 재시작하지 않아도 된다.

 

 

출력 플러그인

가장 일반적으로 많이 사용되는 출력 플러그인 설명이다.

 

1)Elasticsearch

로그스태시에서 일래스틱서치로 이벤트 혹은 로그 데이터를 전송하는 데 사용하며, 권장하는 방법이라고 한다. 엘라스틱서치에 데이터가 있으면 키바나로 손쉽게 시각화가 가능하기에 사용하면 여러모로 유용하다.

 

1
2
3
4
5
6
7
8
9
10
#output to elasticsearch
output {
 elasticsearch {
 index => "elasticserach-%{+YYYY.MM.dd}" => default logstash-%{+YYYY.MM.dd}
 document_type => "_doc"
 hosts => "192.162.43.30" / ["ip1:9200","ip2:9200"] => default localhost:9200
 user => "username"
 password => "password"
 }
}
cs

 

직관적인 설정방법이라 크게 설명할 것은 없다. 몇개 짚고 넘어가자면 여러 엘라스틱서치 노드가 존재한다면 위와 같이 배열형태로 설정할 수 있고, 엘라스틱서치 사용자 자격 증명이 필요하다면 user,password를 지정할 수 있다. 데이터를 보낼 인덱스명은 기본값을 가지고 있지만 사용자 정의가 가능하다. 호스트 포트를 생략하면 기본적으로 엘라스틱서치의 기본포트를 이용한다.

 

2)CSV

CSV 플러그인을 사용하면 출력을 CSV 형식으로 저장할 수 있다. 플러그인 사용 시 필요한 설정은 출력 파일 위치를 지정하는 path 매개변수와 CSV 파일에 기록할 이벤트 필드명을 지정하는 fields 매개변수다. 이벤트에 필드가 없는 경우, 빈 문자열이 기록된다. 

 

1
2
3
4
5
6
7
#output to elasticsearch
output {
 csv {
  fields => ["message","@timestamp","host"]
  path => "/home/..."
 }
}
cs

 

설명한 출력 플러그인 이외에도 많은 출력 플러그인이 존재한다. 나머지는 공식 홈페이지를 참고하자.

 

코덱 플러그인

가장 일반적으로 많이 사용되는 코덱 플러그인이다.

 

1)JSON

해당 코덱은 데이터가 json으로 구성된 경우, 입력 플러그인에서 데이터를 디코딩하고 출력 플러그인에서 데이터를 인코딩하도록 사용하는데 유용하다. 만약 \n 문자가 들어간 JSON 데이터가 있는 경우 json_lines 코덱을 사용한다. 

 

1
2
3
4
5
6
input codec exam
input {
 stdin {
  codec => "json" => \n 구분자가 있는 pretty json 일경우 json_lines codec을 이용한다.
 }
}
 
#input exam
{"question":"안녕","answer":"네, 안녕하세요"}
 
#output result
{

      "answer" => "네, 안녕하세요",

      "question" => "안녕",

      "host" => "yun-yeoseong-ui-MacBook-Pro.local",

      "@version" => "1",

    "@timestamp" => 2019-06-26T13:26:08.704Z

}

 

#input exam2

{"question":"안녕","answer":"네, \n 안녕하세요"}

#output result

{

      "message" => "{\"question\":\"안녕\",\"answer\"n 안세요\"}",

      "host" => "yun-yeoseong-ui-MacBook-Pro.local",

      "@version" => "1",

      "tags" => [

        [0] "_jsonparsefailure"

    ],

    "@timestamp" => 2019-06-26T13:28:58.986Z

}

 

=============================================================================

input codec exam

input {

 stdin {

  codec => "json_lines" => \n 구분자가 있는 pretty json 일경우 json_lines codec을 이용한다.

 }

}

 

#input exam

{"question":"안녕","answer":"네, \n 안녕하세요"}

#output result

{

      "host" => "yun-yeoseong-ui-MacBook-Pro.local",

      "question" => "안녕",

      "answer" => "네, \n 안녕하세요",

      "@version" => "1",

    "@timestamp" => 2019-06-26T13:30:12.650Z

}

cs

 

코덱플러그인은 위와 같이 입력,출력 플러그인에 설정이 들어간다.

 

2)Multiline

여러 행에 걸친 데이터를 단일 이벤트로 병합하는 데 유용한 플러그인이다.

 

1
2
3
4
5
6
7
8
9
10
11
#input codex exam2
input {
 file {
  path => "/var/log/access.log"
  codex => multiline {
   pattern => "^\s" 공백으로 시작하는 데이터를 이전 행과 결합
   negate => false
   what => "previous"
  }
 }
}
cs

 

공백으로 시작하는 모든 행을 이전 행과 결합하는 코덱설정이다.

 

이외에도 더 많은 코덱 플러그인이 존재한다. 자세한 사항을 공식 홈페이지를 참고바란다.

 

 

Kafka(카프카) + ELK Stack을 이용한 로그 분석 구현

사실 데이터의 흐름은 정의하기 나름이지만 필자 나름대로의 플로우로 로그분석을 위한 로그데이터 파이프라인을 구축해보았고, 실제 솔루션 내에도 적용한 플로우이다.

 

전체적인 플로는 아래와 같다.

 

App -> Kafka -> Logstash -> Elasticsearch

 

위와 같이 애플리케이션에서 발생하는 로그들을 DB에 저장하는 것이 아니라, 카프카를 이용해 특정 토픽에 메시지를 보낸 후에 해당 토픽을 폴링하고 있는 로그스태시가 데이터를 수집하여 엘라스틱서치에 색인한다.

 

1)App(Spring boot + Spring Cloud Stream)

애플리케이션은 스프링부트 기반의 웹프로젝트이며, 스프링 클라우드 스트림 라이브러리를 이용하여 카프카와 연동하였다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
############################<Binder Config>###############################
#broker list(cluster)
spring.cloud.stream.kafka.binder.brokers=localhost:9092,localhost:9093,localhost:9094,localhost:9095
#acks mode
spring.cloud.stream.kafka.binder.producer-properties.acks=all
#required acks num
spring.cloud.stream.kafka.binder.required-acks=3
#min partition num
#spring.cloud.stream.kafka.binder.min-partition-count=4
#auto create topic enable
spring.cloud.stream.kafka.binder.auto-create-topics=true
#auto add partitions
#spring.cloud.stream.kafka.binder.auto-add-partitions=true
#replication factor
spring.cloud.stream.kafka.binder.replication-factor=2
############################</Binder Config>###############################
############################<Producer Config>##############################
 
#Log Producer
spring.cloud.stream.bindings.logstash_producer.destination=KIVESCRIPT_CHAT_LOG
spring.cloud.stream.bindings.logstash_producer.content-type=application/json
spring.cloud.stream.bindings.logstash_producer.producer.partition-count=4
 
############################</Producer Config>#############################
cs

 

애플리케이션에서 카프카와 연동하기 위한 application.properties 설정들이다. 만약 위의 설정들을 모른다면 이전에 포스팅했던 글을 참고하길 바란다.

 

 

Kafka - Spring cloud stream kafka(스프링 클라우드 스트림 카프카)

Kafka - Spring cloud stream kafka(스프링 클라우드 스트림 카프카) 이전 포스팅까지는 카프카의 아키텍쳐, 클러스터 구성방법, 자바로 이용하는 프로듀서,컨슈머 등의 글을 작성하였다. 이번 포스팅은 이전까지..

coding-start.tistory.com

밑의 링크는 카프카 클러스터링에 관련된 포스팅이다.

 

 

Kafka - Kafka(카프카) cluster(클러스터) 구성 및 간단한 CLI사용법

Kafka - Kafka(카프카) cluster(클러스터) 구성 및 간단한 CLI사용법 ▶︎▶︎▶︎카프카란? 이전 포스팅에서는 메시징 시스템은 무엇이고, 카프카는 무엇이며 그리고 카프카의 특징과 다른 메시지 서버와의 차이..

coding-start.tistory.com

 

다음은 카프카 바인더와 연결시켜줄 채널을 명시해주는 자바 컨피그 클래스이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
 * Custom Message Processor
 * @author yun-yeoseong
 *
 */
public interface MessageProcessor {
    
    public static final String CHAT_LOG = "logstash_producer";
    
    @Output(KiveMessageProcessor.CHAT_LOG)
    MessageChannel chatLog();
    
}
cs

 

애플리케이션에서 카프카 토픽으로 메시지를 내보내는 코드이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
 * 
 * @author yun-yeoseong
 *
 */
@Slf4j
@Component
public class MessageSender {
    
    @Autowired
    private MessageProcessor messageProcessor;
    
    public void sendMessage(String log) {
                
        MessageChannel outputChannel = messageProcessor.chatLog();
        
        outputChannel.send(MessageBuilder
                           .withPayload(request)
                           .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.APPLICATION_JSON)
                           .build());
    }
    
}
cs

 

다음은 로그스태시 설명이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
input {
        kafka {
                bootstrap_servers => "127.0.0.1:9092,127.0.0.1:9093,127.0.0.1:9094,127.0.0.1:9095"
                topics => "LOGSTASH_PRODUCER"
                group_id => "APP_LOG_GROUP"
                enable_auto_commit => "true"
                auto_offset_reset => "latest"
                consumer_threads => 4
                codec => "json"
        }
}
 
output {
        elasticsearch {
                index => "chatlog-%{+YYYY.MM.dd}"
                document_type => "_doc"
                hosts => ["127.0.0.1:9200","127.0.0.1:9400"]
                template_name => "log-template"
        }
}
cs

 

위 설정은 Logstash 입력과 출력을 정의한 conf 파일이다. 별다른 설정은 없고 입력으로 카프카 클러스터의 특정 토픽을 폴링하고 있으며, 출력으로 엘라스틱서치를 바라보고 있다. 설정에 대한 자세한 사항은 공식홈페이지를 확인하길 바란다. 하나 짚고 넘어갈 것이 있다면 필자는 로그를 색인하기 위한 인덱스를 동적으로 생성하지 않고, 미리 Index Template를 선언해 놓았다. 만약 동적으로 인덱스를 생성한다면 비효율적인 필드 데이터 타입으로 생성될 것이다.(keyword성 데이터도 text타입으로 생성)

 

아래 링크는 엘라스틱서치 Rest 자바 클라이언트를 이용하여 인덱스 템플릿을 생성하는 예제이다.

 

Elasticsearch - Rest High Level Client를 이용한 Index Template 생성

오늘 간단히 다루어볼 내용은 엘라스틱서치의 REST 자바 클라이언트인 Rest High Level Client를 이용하여 Index Template을 생성해보는 예제이다. 바로 예제로 들어간다. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 1..

coding-start.tistory.com

생략된 나머지 설정(카프카,엘라스틱서치 등)들은 이전 포스팅 글들을 참조하자. 혹시나 모든 설정파일 및 소스가 필요하다면 댓글을 달아주시면 될 듯하다.

 

 

기타 명령어

 

LOGSTASH_HOME/bin logstash-plugin list -> 현재 설치된 플러그인 목록

LOGSTASH_HOME/bin logstash-plugin list --group filter -> 필터 플러그인 목록 출력(input,output,codec 등을 그룹명으로 줄 수 있음)

LOGSTASH_HOME/bin logstash-plugin list 'kafka' -> kafka라는 단어가 포함된 플러그인이 있다면 출력

LOGSTASH_HOME/bin logstash-plugin install logstash-output-email -> logstash-output-email 플러그인 설치

LOGSTASH_HOME/bin logstash-plugin update logstash-output-email -> logstash-output-email 플러그인 최신버전으로 업데이트

 

 

posted by 여성게
:

 

오늘 포스팅할 내용은 ELK Stack에서 중요한 보조 수단 중 하나인 Filebeat(파일비트)에 대해 다루어볼 것이다. 우선 Filebeat를 이용하는 사례를 간단하게 하나 들어보자면, 운영중인 애플리케이션에서 File을 통한 로그데이터를 계속 해서 쌓고 있다면 이러한 로그데이터를 단순 파일로 가지고 있는 것이 유용할까? 물론 모니터링하는 시스템이 존재 할 수 있다. 하지만 이러한 모니터링 시스템이 아닌 로그데이터를 계속해서 축적하여 통계를 내고 싶고, 데이터의 증가,하강 추이를 시각화하여 보고 싶을 수도 있다. 이렇게 특정 로그파일을 주기적으로 스캔하여 쌓이고 있는 데이터를 긁어오는 역할을 하는 것이 파일비트이다. 물론 록그스태시만 이용하여 파일에 쌓이는 행데이터를 가져올 수 있다. 하지만 이러한 로그스태시를 엔드서버에 설치하지 못하는 상황이 있을 수 있기에 가벼운 파일비트를 이용하곤 한다.

 

 

파일비트 아키텍쳐

파일비트는 Prospectors, Harvesters, Spooler라는 주요 구성 요소를 가지고 있다. Prospector는 로그를 읽을 파일 목록을 구분하는 역할을 담당한다. 여러 파일 경로를 설정하면 로그를 읽을 파일을 식별하고 각 파일에서 로그를 읽기 시작한다. 이때 파일 컨텐츠, 즉 이벤트 데이터(로그)를 읽는 역할은 Harvester가 담당한다. 파일을 행 단위로 읽고 출력으로 보낸다. 하나의 Harvester가 개별로 파일을 담당하며 파일을 열고 닫는다. 읽어올 파일 수가 여러개가 되면 그에 따라 Harvester도 여러개가 되는 것이다. 이벤트가 발생하여 읽어온 로그데이터는 Spooler에게 보낸다. 그리고 Spooler는 이벤트를 집계하고 설정할 출력으로 전달한다.

 

위의 그림처럼 파일비트는 다수의 Prospector, Harvester로 이루어진다. 파일비트가 지원하는 Input 타입은 log와 stdin이 있다. log는 Prospectors에 정의된 파일을 읽어 데이터를 수집하며, stdin은 표준 입력에서 데이터를 읽는다. 그럼 여기서 의문이 드는 것이 있다. 물리적인 파일을 읽는데 어디까지 읽었고, 출력은 어디까지 보냈고 이런 정보를 어디서 유지하고 있는 것일까? 이러한 정보는 Harvester가 offset으로 디스크에 주기적으로 기록하며 이는 레지스트리 파일에서 관리한다. 엘라스틱서치,카프카,레디스 같은 출력 부분 미들웨어 시스템에 문제가 발생하면 파일비트는 마지막으로 보낸 행을 기록하고, 문제가 해결될때까지 계속 데이터를 수집하고 있는다. 이러한 관리 덕분에 파일비트를 내렸다 다시 올려도 데이터의 위치를 기억한 상태에서 기동되게 된다. 또한 Harvester 같은 경우 출력에게 데이터를 보낸 후에 출력 부분에서 데이터를 잘 받았다는 응답을 기다리는데 해당 응답을 받지 않은 경우 다시 데이터를 보내게됨으로 반드시 한번은 데이터 손실없이 보내게 된다.

 

파일비트 사용법(filebeat-6.6.2 버전기준으로 작성됨)

여기서 파일비트 설치법은 다루지 않는다. 예제에서 다루어볼 것은 Input(file-log type) output(es,logstash) 이다. 간단한 예제이므로 주석에 설명을 달아놓았고 별도의 설명은 하지 않는다.

 

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
filebeat.yml
#Filebeat prospectors
filebeat.prospectors:
- input_type: log
  paths:
    - /Users/yun-yeoseong/rnb-chatbot-rivescript/logs/*.log
  #파일비트로 읽어오지 않을 패턴을 지정
  #exclude_lines: ["^INFO"]
  #파일비트로 읽어올 패턴을 지정
  #include_liens: ["^ERR","^WARN"]
 
  #파일비트가 파일을 output으로 내보낼때 밑의 tags도 추가해서 보낸다.(field -> tags)
  #보통 수집기 별로 혹은 애플리케이션 별로 tags 필드의 값을 다르게 주어
  #키바나와 로그스태시에서 이벤트를 필터링하는데 사용한다.
  tags: ["app_logs"]
 
  #output으로 보낼때 해당 필드를 추가해서 보낸다.(field -> fields.env)
  fields:
    env: dev
 
  #여러라인의 로그를 어떻게 처리할지 패턴을 정의한다
  #RE2 구문을 기반으로 정규식을 짠다(https://godoc.org/regexp/syntax)
  #공백으로 시작하는 연속행을 발견한다.
  multiline.pattern: '^[[:space:]]'
  #정규식패턴 무효화 여부
  multiline.negate: false
  #공백으로 시작하는 구문을 공백으로 시작하지 않는 구문 어디로 합칠것인가(after,before)
  multiline.match: after
 
  #파일을 몇 초마다 스캔할 것인가(default 10s)
  scan_frequency: 5s
  #엘라스틱서치가 아웃풋이라면 document type을 무엇으로 할지
  #document_type: doc
 
#Outputs
#Elasticsearch output
output.elasticsearch:
  #enabled로 출력을 활성/비활성 할 수 있다.
  enabled: true
  #ES클러스터 노드 리스트를 지정할 수 있다. 각 노드의 데이터 분배는
  #라운드로빈 방식을 이용한다.
  hosts: ["localhost:9200","localhost:9300"]
  #인증이 필요할 경우 username/password 지정이 가능
  username: "elasticsearch"
  password: "password"
  #인제스트노드 파이프라인에 전달해 색인 이전에 데이터 전처리가 가능
  #pipeline: "pipeline-name"
 
#logstash output, 로그스태시를 지정하려면 로그스태시에서
#비트 이벤트를 수신할 수 있는 비트 입력 플러그인을 설정해야한다.
#output.logstash:
 # enabled: true
  #출력 노드를 여러개 지정할 수 있다. 기본적으로 임의의 노드를 선택해 데이터를 보내고,
  #데이터를 보낸 호스트에서 응답을 하지 않으면 다른 노드들중 하나로 다시 데이터를 보낸다.
  #hosts: ["localhost:5044","localhost:5045"]
  #로그스태시 호스트들에서 이벤트데이터를 균등히 보내고 싶다면 로드벨런싱 설정을 넣어준다.
  #loadbalance: true
#전역옵션
#파일 정보를 유지하는 데 사용하는 레지스트리 파일 위치를 지정한다.
#마지막으로 읽은 오프셋과 읽은 행이 설정 시 지정한 출력 지점의 응답 여부 등
#filebeat.registry_file: /Users/yun-yeoseong/elasticsearch-file/filebeat/registry
#파일비트 종료 시 데이터 송신이 끝마칠 때까지 대기하는 시간
#파일비트는 기본적으로 종료시 이벤트 송신 처리를 기다리지 않고 종료하기 때문에
#이벤트 송신이 잘되었는지 수신을 종료전에 기다리도록 시간설정을 할 수 있다.
filebeat.shutdown_timeout: 10s
 
#일반옵션
#네트워크 데이터를 발송하는 수집기의 이름 기본적으로 필드에는 호스트 이름을 지정한다.
name: "app_log_harvester"
#동시에 실행할 수 있는 최대 CPU 개수를 설정. 기본값은 시스템에서 사용 가능한 논리적인 CPU 개수
#max_procs: 2
 
cs

 

./filebeat --c ./filebeat_exam.yml -> 해당 명령어로 실행시켜준다.

 

데이터는 spring boot 프로젝트를 실행시켰을 때, 올라가는 로그를 수집한 결과이다. kibana Discovery tab을 이용하여 데이터가 어떻게 들어왔는지 결과를 확인할 것이다.

 

Result

 

 

일부 예제에 들어간 설정과 다른 데이터가 들어간 것도 있다. 데이터는 고려하지 말고 어떤 필드들이 생겼고 어떤 데이터가 설정과 비교하여 들어왔는지 확인하자. 일반적으로 파일비트는 인덱스를 버전정보+날짜정보를 이용하여 일자별로 인덱스 생성을 진행한다.(매일 생긴다는 것은? 그만큼 인덱스가 많아진다는 것이기 때문에 백업 스케쥴을 잘 잡아줘야한다.)그리고 @timestamp를 찍어 데이터 색인 시간을 볼 수 있고, 모든 데이터는 message 필드에 저장된다. 또한 설정 부분에 tags, fields.env 설정이 들어간 것이 있을 것이다. 만약 성격이 다른 애플리케이션이 있고 각각 따로 통계를 내고 싶다면 해당 필드들을 이용하여 필터를 걸수도 있고 또한 Logstash를 이용하여 데이터 처리를 할때 해당 필드들을 이용하여 필터를 걸어 사용할 수도 있을 것이다.(Input의 파일 경로(- input_type)를 여러개 지정할 수 있으므로 각 패스마다 해당 필드의 값을 다르게 준다)

 

여기까지 간단히 파일비트가 뭔지 알아봤고 사용법도 알아보았다. 사실 운영환경에서는 이렇게 간단히 사용할 수 없는 환경이 대부분일 것이다. 레퍼런스를 활용하여 더 깊게 공부할 필요성이 있다.

posted by 여성게
:

개발환경 또는 테스트를 진행하기 위해서는 엘라스틱서치의 단일 노드로도 충분하다.  그래서 엘라스틱서치 노드는 기본적으로 싱글 노드에서 모든 역할을 수행할 수 있게 설정하는 것이 가능하다. 하지만 실제 운영환경에서는 대부분 다수의 노드를 클러스터링하여 구성하기 때문에 각각 목적에 맞는 노드를 적절히 설정해 운영하는 것이 유리하다.

 

엘라스틱서치 노드의 종류

elasticsearch.yml 파일에는 노드 관련 속성이 제공된다. 이 속성들을 적절히 조합해서 특정 모드로 설정하는 것이 가능하다.

 

  • node.master : 마스터 기능 활성화 여부
  • node.data : 데이터 기능 활성화 여부
  • node.ingest : Ingest 기능 활성화 여부
  • search.remote.connect : 외부 클러스터 접속 가능 여부

위의 설정들을 조합하여 아래와 같은 노드 모드로 운영가능하다.

 

  • Single Node mode
  • Master Node mode
  • Data Node mode
  • Ingest Node mode
  • Coordination Node mode

 

Single Node mode

모든 기능을 수행하는 모드다. 기본 설정으로 지정돼 있기 때문에 elasticsearch.yml 파일에 아무런 설정을 하지 않는다면 기본적으로 싱글모드로 동작한다.

 

node.master: true / node.data: true / node.ingest: true / search.remote.connect: true

 

검색 클러스터의 규모가 작을 때는 노드별로 별도의 Role을 분담하여 운영하기 보다는 모든 노드가 싱글 모드로 수행하게 하는 것이 좋다. 일반적으로 3대 이하의 소규모 클러스터를 구축한다면 모든 엘라스틱서치 노드를 싱글모드로 동작시키는 것이 좋다.

 

 

Master Node mode

클러스터의 제어를 담당하는 모드이다. 

 

node.master: true / node.data: false / node.ingest: false / search.remote.connect: false

 

마스터 모드는 기본적으로 인덱스 생성/변경/삭제 등의 역할을 담당한다. 그리고 분산코디네이터 역할을 담당하여 클러스터를 구성하는 노드의 상태를 주기적으로 점검하여 장애를 대비한다. 즉, 마스터 모드는 클러스터 전체를 관장하는 마스터 역할을 수행하게 된다. 이처럼 중요한 역할을 하는 마스터 노드는 클러스터에 다수 존재하는 것이 좋다. 그래야 장애가 발생할 경우에도 후보 마스터 노드가 역할을 위임받아 안정적으로 클러스터 운영 유지가 되기 때문이다.

 

 

Data Node mode

클러스터의 데이터를 보관하고 데이터의 CRUD, 검색, 집계 등 데이터 관련 작업을 담당하는 모드이다.

 

node.master: false / node.data: true / node.ingest: false / search.remote.connect: false

 

노드가 데이터 모드로 동작하면 내부에 색인된 데이터가 저장된다. 이말은 즉, 마스터 노드와는 달리 대용량의 저장소를 필수적으로 갖춰야한다.(물론 대용량 서비스 운영환경이라면) 또한 CRUD 작업과 검색, 집계와 같은 리소스를 제법 잡아먹는 역할도 수행하기 때문에 디스크만이 아닌 전체적인 스펙을 갖춘 서버로 운영하는 것이 좋다.

 

 

Ingest Node mode

다양한 형태의 데이터를 색인할 때 데이터의 전처리를 담당하는 모드다.

 

node.master: false / node.data: false / node.ingest: true / search.remote.connect: false

 

엘라스틱서치에서 데이터를 색인하려면 인덱스라는(RDB Schema) 틀을 생성해야한다. 비정형 데이터를 다루는 저장소로 볼 수 있지만 일정한 형태의 인덱스를 생성해주어야한다. 그리고 해당 인덱스에는 여러 포맷의 데이터 타입 필드가 존재한다. 만약 데이터를 색인할때 간단한 포맷 변경이나 유효성 검증 같은 전처리가 필요할 때 해당 모드를 이용할 수 있다.

 

Coordination Node mode

사용자 요청을 받아 처리하는 코디네이터 모드이다.

 

node.master: false / node.data: false / node.ingest: false / search.remote.connect: false

 

엘라스틱서치의 모든 노드는 기본적으로 코디네이션 모드 노드이다. 이 말은 즉, 모든 노드가 사용자의 요청을 받아 처리할 수 있다는 뜻이다. 하지만 이렇게 별도의 코디네이션 노드가 필요한 이유가 있을까? 싱글 모드로 구성된 클러스터에 사용자가 검색 요청을 보낸다면 검색요청을 받은 노드는 클러스터에 존재하는 모든 데이터 노드에게(싱글 모드는 모든 노드가 대상) 검색을 요청한다. 왜냐하면 클러스터에 존재하는 모든 데이터 노드에 샤드로 데이터가 분산되어 있기 때문이다. 그리고 각 샤드는 자신이 가지고 있는 데이터 내에서 검색을 수행하고 자신에게 요청을 보낸 노드에서 결과값을 전송한다. 그리고 모든 데이터를 취합하여 사용자에게 전달한다. 모든 데이터가 취합될때까지 요청을 다른 노드에게 보낸 코디네이션 노드역할(싱글모드에서는 코디네이션 이외의 모든일을 하는 노드가 된다.)을 하는 노드는 아무 일도 못하고 기다리고 있어야한다. 또한 데이터를 취합하는 일도 많은 양의 메모리가 필요한 작업이다. 이 상황에서 코디네이션 노드를 따로 구축하지 않았다면 이렇게 결과를 취합하는 과정에 마스터 노드로서의 역할이나 데이터 노드로서의 역할을 할 수 없게 되고 최악의 경우에는 노드에 장애가 발생할 수 있다. 이렇게 다른 노드들에게 요청을 분산하고 결과값을 취합하는 코디네이션 노드를 별도로 구축한다면 안정적인 클러스터 운영이 가능해진다.

 

 

대용량 클러스터 환경에서 전용 마스터 노드 구축이 필요한 이유

예를 들어보자. 만약 모든 노드를 싱글모드로 클러스터링을 구축한 환경에서 무거운 쿼리가 주 마스터 역할을 하는 싱글 노드에 요청되어 데이터 노드의 부하로 인해 시스템에 순간적으로 행(hang, freezing 시스템이 아무런 일도 하지 못하는 상황)이 걸리거나 노드가 다운되는 경우가 발생할 수 있다. 그렇다면 주 마스터 역할로써도 정상적으로 동작하지 못할 것이다. 이 순간 시스템 장애가 발생하면 쉽게 복구할 수 있는 상황도 복구할 수 없게되는 상황이 발생한다.

 

이러한 경우 다른 싱글 노드 중 하나가 마스터 역할로 전환되어 처리되지 않을 까라는 생각을 당연히 하게 되지만 모든 상황에서 그렇지는 않다. 주 마스터 노드가 hang 상태에 빠져있지만 시스템적으로 정상적으로 프로세스로 떠 있다 판단 될 수 있어 다른 후보 마스터에게 역할이 위임되지 않을 가능성이 있기 때문이다.

 

이렇게 마스터 노드와 데이터 노드의 분리는 대용량 클러스터 환경에서 필수이게 되는 것이다.

 

 

대용량 클러스터 환경에서의 검색엔진에서 코디네이션 노드 구축이 필요한 이유

엄청난 데이터량을 가지고 있는 클러스터를 가지고 있다고 생각해보자. 만약 이러한 클러스터에서 복잡한 집계 쿼리를 날린다고 가정하면 안그래도 리소스를 많이 잡아먹는 집계쿼리인데 데이터마저 크다면 엄청난 부하를 주게 될 것이다. 이런 상황에서 데이터 노드 모드와 코디네이션 노드 모드를 분리하여 클러스터 환경을 구성한다면 장애가 발생할 가능성이 조금은 낮아질 것이다. 왜냐 검색은 데이터노드가 담당하고 이러한 요청을 보내는 역할과 결과의 병합을 코디네이션 노드가 담당하기에 리소스 사용의 부담을 서로 나누어 갖기 때문이다.

 

 

클러스터 Split Brain 문제 방지

클러스터에는 마스터 노드 모드로 설정된 노드가 최소한 하나 이상 존재해야 장애가 발생하였을 때, 즉시 복구가 가능해진다. 다수의 마스터 노드가 존재할 경우 모든 마스터 노드들은 투표를 통해 하나의 마스터 노드만 마스터 노드로서 동작하고 나머지는 후보 마스터 노드로서 대기한다. 클러스터를 운영하는 중에 마스터 노드에 장애가 발생할 경우 대기 중인 후보 마스터 노드 중에서 투표를 통해 최종적으로 하나의 후보 마스터 노드가 주 마스터 노드로 승격하게 된다. 이후 장애가 발생한 주 마스터노드는 다시 후보 마스터 노드로 하락하게 된다. 이런식으로 마스터 노드의 부재없이 안정적인 클러스터 운영이 가능한 것이다.

 

그렇다면 전용으로 구축되는 마스터 노드는 몇개가 적당할까?

 

이런 상황을 생각해보자. 만약 주 마스터 노드에 장애가 발생하였고 후보 마스터 노드 3개 중 투표를 통해 하나의 마스터 노드를 선출하는 도중에 네트워크 환경에 단절이 발생했다 생각하자. 그렇다면 후보노드들은 모두 나 자신밖에 마스터노드 후보가 없다고 생각하고 자기 자신을 마스터 노드로 승격시킬 것이고, 각 노드가 동일하게 행동하여 하나 이상의 마스터 노드가 생겨버릴 수 있다.(Split Brain 문제) 이렇다면 클러스터는 엉망진창으로 꼬일 것이고 서비스 불능 상태가 될 수 있다. Split Brain 문제는 비단 엘라스틱서치만의 문제는 아니고 클러스터 환경에서 운영되는 애플리케이션 전반적인 문제이다. 엘라스틱서치는 이 상황을 하나의 설정만으로 해결방법을 제시한다.

 

elasticsearch.yml -> discovery.zen.minimum_master_nodes

 

이 속성은 기본 값으로 1을 가진다. 이 뜻은 마스터 노드 선출 투표를 진행할 때, 후보 마스터 노드의 최소한의 갯수를 뜻하는 것이다. 클러스터에 존재하는 마스터 노드의 개수가 1개이거나 2개일 경우는 해당 설정은 1로 설정하고 마스터 노드의 수가 3개 이상일 경우에는 다음 공식에 대입해서 적절한 값을 찾아 설정한다.

 

(마스터 후보 노드 수 / 2) + 1

 

  • 마스터 노드가 3개일 경우 : 3/2+1 = 2
  • 마스터 노드가 4개일 경우 : 4/2+1 = 3
  • 마스터 노드가 5개일 경우 : 5/2+1 = 3
  • 마스터 노드가 6개일 경우 : 6/2+1 = 4

싱글모드노드로 구성하는 간단한 클러스터링

 

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
<node1>
cluster.name: clusterName
node.name: node1
network.host: 0.0.0.0
http.port: 9200
discovery.zen.ping.unicast.hosts: ["127.0.0.1:9500"]
discovery.zen.minimum_master_nodes: 1
transport.tcp.port: 9300
#########싱글 노드로 동작 여부#########
node.master: true
node.data: true
node.ingest: true
search.remote.connect: true
 
 
<node2>
cluster.name: clusterName
node.name: node1
network.host: 0.0.0.0
http.port: 9400
discovery.zen.ping.unicast.hosts: ["127.0.0.1:9300"]
discovery.zen.minimum_master_nodes: 1
transport.tcp.port: 5300
#########싱글 노드로 동작 여부#########
node.master: true
node.data: true
node.ingest: true
search.remote.connect: true
cs

 

두개의 엘라스틱서치 디렉토리를 준비한 후에 각각의 설정 파일을 위와 같이 바꾸어준다. 우선 중요한 것은 cluster.name을 동일하게 마추어줘야한다. 그리고 포트는 적절히 할당해준다. 필자는 동일한 서버환경에서 두개의 노드를 설치한 것이라 포트가 다르지만 서로 다른 환경이라면 동일하게 포트를 맞춰놔도 무방하다. 그리고 중요한 것은 discovery 설정이다. 디스커버리 설정으로 서로다른 노드를 discovery할 수 있게 해주는 설정인 것이다. 그리고 모든 노드가 싱글모드로 동작시키게 하기 위해 싱글 노드 설정으로 세팅해주었다.

 

설정파일을 모두 변경하였으면 각각 엘라스틱서치를 실행 시킨 후에 아래의 요청을 보내 클러스터링이 잘 걸렸나 확인해보자.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
GET http://localhost:9200/_cluster/health
 
result ->
{
    "cluster_name": "clusterName",
    "status": "green",
    "timed_out": false,
    "number_of_nodes": 2,
    "number_of_data_nodes": 2,
    "active_primary_shards": 0,
    "active_shards": 0,
    "relocating_shards": 0,
    "initializing_shards": 0,
    "unassigned_shards": 0,
    "delayed_unassigned_shards": 0,
    "number_of_pending_tasks": 0,
    "number_of_in_flight_fetch": 0,
    "task_max_waiting_in_queue_millis": 0,
    "active_shards_percent_as_number": 100
}
cs

 

필자는 클러스터 설정이 모두 정상적으로 적용되어 Active한 노드가 2개인것을 볼 수 있다. 물론 완벽한 클러스터링을 위해서는 설정해야 할것이 많을 수도 있다. 그리고 지금은 개발모드로 작성되어 부트스트랩과정을 거치지 않아서 쉽게 구성되었을 것이다. 하지만 추후에 운영환경모드로 실행을 하면 14가지정도의 부트스트랩 과정을 검사하기 때문에 맞춰주어야 하는 설정들이 많이 있다. 더 자세한 클러스터링 환경설정은 추후에 다루어볼 것이다.

posted by 여성게
:

엘라스틱서치는 JVM 위에서 동작하는 자바 애플리케이션이다. 그렇기 때문에 엘라스틱서치는 JVM 튜닝옵션들을 제공한다. 하지만 수년간 엘라스틱서치의 경험으로 최적화된 JVM옵션을 거의 적용하고 있기 때문에 변경할 필요는 없다고 한다. 하지만 Heap Memory 사이즈 같은 경우는 실 운영환경에서는 기본으로 제공하는 1기가보다는 높혀서 사용할 필요성이 있다.

 

$ELASTIC_PATH/config/jvm.options 파일에 들어가면 Xms,Xmx 옵션으로 최소,최대 JVM 힙 메모리 사이즈 조정이 가능하며 기타 다른 JVM옵션 변경이 가능하다. 다시 한번 강조하자면 왠만하면 다른 옵션들은 디폴트 값으로 가져가 사용하는 것이 좋다.

 

그리고 보통 JVM에서 Xms 크기의 메모리를 사용하다가 메모리가 더 필요하면 Xmx 크기만큼 메모리를 늘려서 사용하게 된다. 하지만 이렇게 Xmx크기의 메모리를 사용하려면 그 순간 갑자기 성능이 나빠질 수 있다. 그렇기 때문에 왠만하면 Xms,Xmx의 크기를 같게 주는 것이 여러모로 유리하다. 그리고 힙사이즈를 너무 작게하면 OOM(Out Of Memory) 오류가 날 수 있고 그렇다고 힙사이즈를 너무 크게하면 FullGC 발생시 STW가(Stop The World) 발생해 애플리케이션이 프리징되는 시간이 길어지기 때문에 사용자에게 애플리케이션이 멈춰보이는 현상을 줄 수 있기에 무작정 큰 메모리 사이즈를 할당하는 것도 좋지 않다.(보통 엘라스틱서치의 힙사이즈는 데몬 서버당 32기가 이하로 설정하길 권장한다.)

 

 

운영체제가 사용할 메모리 공간을 확보

엘라스틱서치 샤드는 내부적으로 루씬을 가지고 있으며 루씬은 세그먼트 생성 및 관리를 위해 커널 시스템 캐시를 많이 사용한다. 하지만 이렇게 시스템 캐시는 운영체제가 가지고 있는 메모리 공간으로 커널 내부에 존재하게 된다. 즉, 운영체제가 사용할 메모리를 대략 전체 스펙에 50%정도를 남겨놔야 좋다.

 

자바 8 기반에서는 힙 크기를 32기가 이상 사용하지 않는 것이 좋다

예) 128기가의 물리 머신에서 64기가를 운영체제에게 나머지 64기가를 엘라스틱서치가 할당받는 다면 밑에 스펙중 무엇을 선택할것인가?

  • 1)64기가 운영체제, 64기가 엘라스틱서치 노드1개
  • 2)64기가 운영체제,32기가 엘라스틱서치 노드2개

위의 두가지중 엘라스틱서치에서 안내하는 권장사항을 따른다면 2번 스펙을 따를 것이다. 이 말은 엘라스틱서치 노드 데몬서버 하나당 힙메모리를 32기가이상 잡지 않는것이다. 엘라스틱서치에서 이러한 가이드를 제공하는 이유는 핫스팟(Hot-Spot) JVM의 Object Pointer 정책때문이다. 즉, 엘라스틱서치 뿐만 아니라 모든 자바 기반 애플리케이션에도 동일하게 32기가 이상 잡지 않는 것을 권장한다. Object Pointer는 간단히 객체의 메모리 번지를 표현하는 주소값이다. 그리고 32비트,64비트 JVM은 기본적으로 모두 32비트 주솟값을 가지고 동작한다. 이유는 기본적으로 JVM은 32비트 Object Pointer를 사용하고 있기 때문이다. 여기서 너무 자세한 내용을 설명하는 것은 주제와 맞지 않을 것같아서 간단히 이야기하면 64비트 주솟값을 사용하면 주솟값을 위해 낭비되는 메모리값이 너무 많아 진다. 그렇기 때문에 JVM은 기존 Object Pointer를 개선한 Compressed Ordinary Object Pointer를 사용하는데 이 포인터가 기본적으로 32비트 Object Pointer한다. 이렇게 64비트 환경의 서버에서 32비트의 주소값을 사용하여 메모리 낭비를 줄이며 효율적으로 사용되는데, 만약 JVM 힙메모리 옵션이 32기가 이상 넘어가게되면 COOP에서 일반적인 64비트 주소값을 사용하는 OOP로 바뀌도록 되어 있다. 이렇게 64비트 주솟값 OOP 사용하게 되면 주솟값을 위하여 낭비되는 메모리의 값이 동일하게 증가하기 때문에 효율성이 떨어지게 되는 것이다.

 

 

상황에 따른 엘라스틱서치 힙크기 설정하기

 

  • 1)적절한 성능의 서버 : 32기가 힙메모리를 할당하여 엘라스틱서치 노드를 사용한다.
  • 2)고성능 서버 : 적절히 엘라스틱서치 노드를 나누어서 32기가씩 할당하여 사용한다.
  • 3)전문(Full Text) 검색을 주목적으로 엘라스틱서치를 사용하는 경우 : 엘라스틱서치 힙에 32기가를 할당하고 나머지를 운영체제에 남겨둬서 루씬이 시스템 캐시를 통해 메모리를 최대한 사용할 수 있게 한다. 전문 검색의 경우 메모리 연산보다는 루씬의 역색인 구조를 이용하기 때문에 시스템 캐시를 많이 이용한다.
  • 4)Not Analyzed 필드의 정렬/집계 작업이 많이 수행되는 경우 : 분석되지 않은 필드들의 정렬/집계는 루씬의 DocValues(루씬 캐시,기본적으로 not analyzed한 필드들은 기본적으로 DocValues가 생성됨)를 사용하기 때문에 힙 공간을 거의 사용하지 않고 시스템캐시를 이용하기 때문에 루씬에게 메모리를 많이 할당 될 수 있게 한다.
  • 5)전문(Full Text) 필드에서 정렬/집계 작업을 많이 수행하는 경우 : 전문(analyzed fleld)같은 경우는 루씬의 DocValues를 이용하지 않기 때문에 fielddata라는 힙 기반의 캐시를 이용하기 때문에 전문 필드 정렬/집계가 많은 경우 32기가로 엘라스틱서치 노드를 나누어서 여러개 생성하는 방식이 효율적이다.

 

posted by 여성게
:
Search-Engine/Lucene 2019. 5. 25. 21:04

루씬은 색인 요청이 올때마다 새로운 세그먼트가 추가된다. 그리고 일정한 주기로 세그먼트들을 병합하는 과정을 갖는다. 만약 이러한 루씬에 인메모리버퍼가 하는 역할은 무엇일까? 우선 인메모리버퍼가 없는 루씬을 가정한다면, 만약 순간적으로 대용량의 데이터의 색인요청이 많아질 경우 세그먼트(역색인 파일)의 개수가 너무 많아져서 문제가 될 수 있다. 파일이 갑자기 많아지고 이는 당연히 색인에 지연이 생길 것이고 최종적으로 서비스 장애로 이어질 것이다. 하지만 실제적으로 루씬은 색인 작업이 요청되면 전달된 데이터는 일단 인메모리버퍼에 순서대로 쌓이고 버퍼가 일정크기 이상의 데이터가 쌓였다면 그때 한번에 모아서 색인처리를 한다. 즉, 버퍼가 일종의 큐역할을 하는 것이다. 버퍼에 모여 한번에 처리된 데이터는 즉시 세그먼트 형태로 생성되고 디스크로 동기화된다. 하지만 디스크에 물리적으로 동기화하는 일련의 과정은 운영체제 입장에서 비용이 큰 연산이기에 세그먼트가 생성될때마다 물리적인 동기화를 할 경우 성능이 급격히 나빠질 수 있다. 루씬은 이러한 문제점을 해결하기 위해 무거운 fsync 방식을 이용해 디스크 동기화를 하는 대신 상대적으로 가벼운 write 방식을 이용해 쓰기 과정을 수행한다. 이러한 방식으로 쓰기 성능을 높이고 이후 일정한 주기에 따라 물리적인 디스크 동기화 작업을 수행한다.

 

write() 함수

일반적으로 파일을 저장할 때 사용하는 함수다. 운영체제 내부 커널에는 시스템 캐시가 존재하는데 write() 함수를 이용하면 일단 시스템 캐시에만 기록되고 리턴된다. 이후 실제 데이터는 특정한 주기에 따라 물리적인 디스크로 기록된다. 물리적인 디스크 쓰기 작업을 수행하지 않기 때문에 빠른 처리가 가능한 반면 최악의 경우 시스템이 비정상 종료될 경우에는 데이터 유실이 일어날 수도 있다.

 

fsync() 함수

저수준의 파일 입출력 함수다. 내부 시스템 캐시의 데이터와 물리적인 디스크의 데이터를 동기화하기 위한 목적으로 사용된다. 실제 물리적인 디스크로 쓰는 작업을 수행하기 때문에 상대적으로 많은 리소스가 소모된다.

 

이러한 인메모리 버퍼 기반의 처리 과정을 루씬에서는 Flush라고 부른다. 데이터의 변경사항을 일단 버퍼에 모아두었다가 일정 주기에 한번씩 세그먼트를 생성하고 상대적으로 낮은 비용으로 디스크에 동기화 하는 작업까지 수행한다. 일단 Flush 처리에 의해 세그먼트가 생성되면 커널 시스템 캐시에 세그먼트가 캐시되어 읽기가 가능해진다. 커널 시스템 캐시에 캐시가 생성되면 루씬의 openIfChanged()을 이용해 IndexSearcher에서도 읽을 수 있는 상태가 된다.

 

openIfChanged() 함수

루씬에서는 IndexSearcher 객체가 생성되고 나면 이후 변경된 사항들을 기본적으로 인지하지 못한다. 기존 IndexSearcher를 Close하고 다시 생성하면 변경된 사항을 인지하는 것이 가능하지만 문서의 추가나 변경이 빈번하게 일어날 경우 많은 리소스가 필요해지기 때문에 권장하지 않는다. 이때 사용하는 것이 openIfChanged() 함수다. 일정 주기마다 문서가 업데이트된다면 openIfChanged()함수를 이용해 좀더 효율적으로 리소스를 사용할 수 있다.

 

하지만 최악의 경우에는 Flush만으로는 100% 데이터의 유실을 보장할 수 없다고 했다. 즉, fsync() 함수를 이용하여 언젠가는 반드시 동기화를 해야한다. 이러한 작업을 Commit이라고 한다. 매번 Commit하는 것이 아니고 Flush 작업을 몇번 한 이후에 일정 주기로 Commit작업을 통해 물리적인 디스크로 기록 작업을 수행해야한다.

 

아무리 루씬이 세그먼트 단위 검색을 지원하지만 시간이 지날수록 세그먼트 수가 많아지면 커밋 포인트의 부하도 증가하고 여러개의 세그먼트를 검색해야하기 때문에 검색 성능도 저하된다.그래서 일정주기 동안 여러개의 세그먼트는 하나의 세그먼트로 병합이 된다. 이러한 병합 처리에 여러 장점이 존재한다.

 

 

병합의 장점

  • 검색 성능 향상 : 검색 요청이 들어오면 루씬 내부에 존재하는 모든 세그먼트를 검색해야하는데, 각 세그먼트는 순차적으로 검색되므로 세그먼트를 병합하여 세그먼트 수를 줄이면 순차 검색 횟수도 줄어든다.
  • 디스크 용량 최소화 : 삭제되는 문서의 경우 병합 작업 전에는 삭제 플래그 값을 가지고 삭제되지 않고 물리적인 디스크에 남아있는다. 이러한 삭제 플래그를 가진 문서는 병합 작업을 시작해야 비로소 삭제된다.

 

이러한 병합 작업은 Commit 작업을 반드시 동반해야한다.

 

루씬 Flush 작업

  • 세그먼트가 생성된 후 검색이 가능해지도록 수행하는 작업
  • write() 함수로 동기화가 수행됬기 때문에 커널 시스템 캐시에만 데이터가 생성된다.이를 통해 유저 모드에서 파일을 열어 사용하는 것이 가능해진다.
  • 물리적으로 디스크에 쓰여진 상태는 아니다.

루씬 Commit 작업

  • 커널 시스템 캐시의 내용을 물리적인 디스크로 쓰는 작업
  • 실제 물리적인 디스크에 데이터가 기록되기 때문에 많은 리소스 필요

루씬 Merge 작업

  • 다수의 세그먼트를 하나로 통합하는 작업
  • Merge 과정을 통해 삭제 플래그 값을 가진 데이터가 실제 물리적으로 삭제 처리된다.
  • 검색할 세그먼트의 수가 줄어들기 때문에 검색 성능이 향상된다.

 

posted by 여성게
:

 

바로 이전 포스팅에 이어 세그먼트 불변성에 대한 포스팅을 이어나가겠습니다.

 

세그먼트 불변성

세그먼트가 수정 불가능한 불변성을 가짐으로써 제공되는 장점들이 있다.

 

1)동시성 문제 회피

불변성이 보장된다면 Lock이 필요 없어진다. 다수의 스레드가 동작하는 복잡한 다중 스레드 환경에서 동시성 문제는 매우 중대한 문제이다. 루씬은 세그먼트의 불변성으로 이러한 동시성 문제를 간단히 피해갔다.

2)시스템 캐시 활용

데이터가 OS 커널에서 제공하는 시스템 캐시에 한번 생성되면 일정 시간 동안은 그대로 유지된다. 불변성을 보장하지 않을 경우 수정이 있을 때마다 시스템 캐시를 삭제하고 다시 생성해야하는 비용이 큰 작업을 수행하게 된다. 하지만 불변성이라면 이러한 시스템 캐시를 효율적으로 이용할 수 있다.

3)높은 캐시 적중률

불변성이기 때문에 시스템 캐시의 수명이 길어지므로 캐시의 적중률 또한 높아진다.

4)리소스 절감

역색인을 만드는 과정에서 많은 시스템 리소스(CPU,Memory)가 사용된다. 수정을 허용하게 되면 일부분이 변경되더라도 해당 역색인을 대상으로 작업해야하기 때문에 시스템 리소스가 소모된다.

 

하지만 이러한 불변성에도 단점은 존재한다. 우선 일부 데이터가 변경되더라도 전체 역색인 구조가 다시 만들어져야 하고, 또 다른 문제는 실시간 반영이 상대적으로 어려워지는 것이다. 하지만 이러한 단점을 극복하기 위해 루씬은 하나의 세그먼트를 사용하는 것이 아니라 다수의 세그먼트를 생성하기 때문에 변경 될때 마다 모든 세그먼트를 다시 만드는 것이 아니라 기존 세그먼트는 그대로 두고 추가로 세그먼트를 생성하기에 세그먼트 생성 중에 기존 세그먼트를 이용하여 검색 결과를 제공한다.

 

그렇다면 이런 불변성을 가진 세그먼트에서 수정/삭제연산은 어떻게 처리될까?

 

수정연산 같은 경우 세그먼트의 불변성을 유지하기 위해 해당 데이터를 삭제한 후 다시 추가하는 방식으로 동작한다. 기존 데이터는 삭제 처리되어 검색 대상에서 제외되고 변경된 데이터는 새로운 세그먼트로 추가되어 검색 대상에 포함된다. 삭제 연산 같은 경우는 삭제 여부를 표시하는 비트 배열을 찾아 삭제 여부만 표시하고 끝낸다. 즉, 바로 삭제되는 것이 아니라 단순히 삭제플래그만 남기고 검색 대상에서 제외만 시킨다. 이후 병합 과정이 시작된다면 비로소 삭제플래그값을 가진 데이터가 삭제되는 것이다. 기타 루씬의 Flush, Commit, Merge 작업관련 내용은 아래 링크를 참조하자.

 

 

Lucene - 인메모리버퍼(In-Memory-Buffer) 역할, 세그먼트 병합(Merge)

루씬은 색인 요청이 올때마다 새로운 세그먼트가 추가된다. 그리고 일정한 주기로 세그먼트들을 병합하는 과정을 갖는다. 만약 이러한 루씬에 인메모리버퍼가 하는 역할은 무엇일까? 우선 인메모리버퍼가 없는 루..

coding-start.tistory.com

 

고가용성을 위한 Translog

엘라스틱서치는 분산 시스템이 지원해야 하는 고가용성을 제공하기 위해 내부적으로 Translog라는 특수한 형태의 파일을 유지하고 관리하고 있다. 장애 복구를 위한 백업 데이터 및 데이터 유실 방지를 위한 저장소로써 Tranlog를 활용한다. 분산 코디네이터인 Zookeeper도 클러스터의 고가용성을 위하여 Data Directory에 Translog역할과 거의 동일한 log파일을 작성한다.

 

엘라스틱서치 샤드는 내부에 Translog라는 특수한 파일을 가지고 있다. 샤드에 데이터 변경사항이 생길 경우 Translog 파일에 먼저 해당 내역을 기록한 후 내부에 존재하는 루씬 인덱스로 데이터를 전달한다. 루씬으로 전달된 데이터는 인메모리 버퍼로 저장되고 주기적으로 처리되어 결과적으로 세그먼트가 된다. 엘라스틱서치에서는 기본적으로 1초에 한번씩 Refresh(루씬의 Flush) 작업이 수행되는데, 이를 통해 추가된 세그먼트의 내용을 읽을 수 있게 되고 검색에 사용된다. 하지만 해당 작업이 일어나더라도 Translog파일에 기록된 내용은 삭제되지 않고 계속 유지된다. 이처럼 Translog는 엘라스틱서치 샤드에 일어나는 모든 변경사항을 담고 있는 특수한 형태의 로그인 것이다. 이러한 특성을 이용해 엘라스틱서치는 Tranlog의 내역을 바탕으로 장애복구를 수행한다.

하지만 Translog 파일에 로그가 계속해서 누적될 수는 없다. 특정 시점이 되면 Tranlog 내부의 로그중 불필요한 과거의 로그는 삭제된다. 이 특정 시점은 엘라스틱서치의 Flush(루씬의 Commit)이 수행될때이다. 엘라스틱서치의 Flush는 내부적으로 fsync() 함수를 이용해 실제 물리적인 디스크에 변경 내역을 기록한다. 그리고 작업이 성공적으로 마무리되고 물리적으로 디스크 동기화에 성공하면 누적되어 있던 Tranlog 파일의 내용이 비로소 삭제된다. Flush가 일어난다는 것은 디스크에 물리적으로 기록된다는 것이고 이는 영구적으로 보관된다는 것을 의미하기 때문에 이 시점까지의 로그는 더는 필요하지 않게 된다.

 

그렇다면 엘라스틱서치에서 Translog가 장애복구를 위해서 필요한 것은 알겠지만 구체적으로 어떻게 장애복구에 이용될까? 예를 한번 들어보자. 엘라스틱서치는 1초 마다 Refresh(루씬의 Flush)를 한다. 하지만 이는 물리적인 디스크에 쓰여진 상태가 아니기에 불안정하기에 주기적으로 Flush(루씬의 Commit)를 해야한다. 하지만 Flush 작업은 매우 무겁고 긴 시간 동안 일어날 수 있다.

 

[상황 1]

엘라스틱서치의 Flush에 의해 루씬 Commit 작업이 시작됐고 완료되지 못한 상태에서 샤드에 장애발생.

[해결방법]

샤드가 강제로 종료될 경우 진행 중이던 루씬 Commit 작업이 롤백되기 때문에 샤드가 정상적으로 재실행되면 Translog의 로그 내역을 이용해 간단히 복구 가능하다. 왜냐하면 엘라스틱서치의 Flush가 완료되지 않아서 Translog는 아직 삭제되지 않은 상태이기 때문이다.

 

[상황 2]

변경사항이 순간적으로 많아져서 루씬 Commit이 긴 시간 동안 일어나게 되고 그동안 많은 데이터 변경 요청이 한꺼번에 샤드로 들어옴.

[해결방법]

루씬 Commit 작업이 수행되는 시간이 길어진다고 해서 Commit이 일어나는 동안 샤드로 전달된 변경사항이 Commit 작업이 끝날때까지 반영이 되지 않는다면 실시간 검색을 지원한다는 의미가 거의 없다. 그래서 엘라스틱서치는 Commit이 일어나는 동안 들어온 변경사항을 루씬의 인메모리 버퍼로 전달하지 않고 Translog에 임시로 저장해두고 다음 Commit에 반영될 때까지 유지한다.

 

이러한 Translog는 클러스터 장애복구에 아주 중요한 역할을 하는 로그 파일이다. 하지만 이러한 Translog도 파일이 커지면 복구하는데 장애유발을 할 수 있으므로 적절한 주기로 엘라스틱서치의 Flush(루씬 Commit)를 하여서 적절한 크기의 이하의 Translog 파일을 유지하는 것도 중요하다.

posted by 여성게
:

 

엘라스틱서치의 구성요소

엘라스틱서치는 기본적으로 클러스터라는 단위로 데이터를 제공한다. 클러스터는 하나 이상의 물리적인 노드로 이루어져 있으며 각 노드는 모두 데이터 색인 및 검색 기능을 제공하는 일종의 물리적인 서버와 같다. 내부에는 루씬 라이브러리를 사용하고 있으며 루씬은 엘라스틱서치의 근간을 이루는 핵심 모듈이다.

 

1)클러스터

클러스터는 데이터를 실제로 가지고 있는 노드의 모음이다. 엘라스틱서치에서는 관련된 모든 노드들을 논리적으로 묶어서 클러스터라고 부른다. 또한 노드들은 같은 클러스터 내부의 데이터만 서로 공유가 가능하다. 같은 클러스터를 구성하는 노드들을 같은 클러스터 이름으로 설정해야한다. 엘라스틱서치는 설정된 클러스터 이름을 이용해 같은 클러스터의 구성원으로 인식된다. 같은 클러스터 내부의 노드는 평소 데이터 색인이나 검색작업을 함께 수행하게 되고 장애가 발생했을 때 데이터 복구를 위한 다양한 작업도 서로 협력해서 함께 진행한다.

 

Cross Cluster Search

일반적으로 검색 시 하나의 클러스터 데이터만 검색하는 것이 원칙이긴 하지만 최초 설계 시 전혀 관련성 없어 보이던 데이터들을 시간이 지나서 데이터가 점점 많이 쌓일 수록 데이터 연관성이 생길 수도 있다. 엘라스틱서치에서는 이처럼 다양한 필요에 따라 다수의 클러스터를 한 번에 검색할 수 있는 Cross Cluster Search라는 기능을 제공한다.

 

 

Cross-cluster search | Elasticsearch Reference [7.1] | Elastic

The cross-cluster search feature allows any node to act as a federated client across multiple clusters. A cross-cluster search node won’t join the remote cluster, instead it connects to a remote cluster in a light fashion in order to execute federated sear

www.elastic.co

 

2)노드

물리적으로 실행된 런타임 상태의 엘라스틱서치를 노드라고 부른다. 노드는 위에서 설명한 클러스터를 이루는 구성원의 일부이며 실제 데이터를 물리적으로 가지고 있는 단일 서버이기도 하다. 실행 시 노드는 클러스터에 의해 UUID가 할당되고 클러스터 내에서는 할당된 UUID로 서로를 식별한다. 기본 값으로 부여되는 UUID를 원하지 않는다면 직접 이름을 설정할 수도 있다.(하지만 이름은 유일해야한다.) 노드는 내부에 다수의 인덱스를 가지고 있으며, 각 인덱스는 다수의 문서를 가지고 있다. 색인 작업을 통해 엘라스틱서치로 전송한 데이터는 인덱스라는 논리적인 자료구조 속에 문서라는 단위로 저장된다. 같은 클러스터 내부에서 존재하는 모든 노드는 서로 다른 노드와 수시로 정보를 주고 받는다. 기본적으로 모든 노드는 마스터 노드와 데이터 노드의 역할을 동시에 수행할 수 있도록 설정되어있지만 실제 대용량의 운영환경에서는 각각 용도에 맞는 노드를 적절히 분리하여 클러스터링하는 것이 좋다.

 

2)-1 노드의 형태

  • 마스터 노드(Master Node) : node.master 설정이 true로 설정된 노드다. 클러스터의 제어를 담당한다.
  • 데이터 노드(Data Node) : node.data 설정이 true로 설정된 노드다. 데이터를 보유하고 CRUD, 검색, 집계 등 데이터 관련 작업을 담당한다.
  • 인제스트 노드(Ingest Node) : node.ingestrk true로 설정된 노드다. 색인 전 전처리 작업을 담당한다.
  • 코디네이팅 노드(Coordinating Node) : 검색이나 집계 시 분산 처리만을 목적으로 설정된 노드다. 대량의 데이터를 처리할 경우에 효율적으로 사용할 수 있는 노드이다.

3)인덱스

엘라스틱서치 인덱스는 유사한 특성을 가지고 있는 문서를 모아둔 문서들의 모임이다. 클러스터 내부에 생성되는 모든 인덱스는 클러스터 내에서 유일한 인덱스명을 가져야한다. 또한 인덱스명은 모두 소문자로 설정해야 한다. 또한 과거 버전과는 다르게 현재 버전들은 하나의 인덱스에 하나의 타입만 생성해야 한다.

 

4)문서

문서는 검색 대상이 되는 실제 물리적인 데이터를 뜻한다. 문서는 인덱스를 생성할 수 있는 기본적인 정보 단위이고 엘라스틱서치에서는 JSON형식으로 문서를 표현한다.

 

5)샤드

인덱스에는 매우 많은 양의 문서가 저장될 수 있다. 일반적으로 하나의 하드웨어에서 제공되는 리소스 이상의 데이터를 저장할 수 없지만 물리적인 한계를 뛰어넘기 위해 샤드라는 개념을 도입했다. 이를 이용하면 데이터를 분산 저장하는 가능하다. 엘라스틱서치에서는 인덱스를 생성할 때 기본적으로 5개의 샤드로 데이터가 분산되도록 생성되고 설정에 의해 샤드의 개수를 원하는 만큼 변경할 수도 있다. 하나의 샤드는 인덱스의 부분 집합이다. 하지만 해당 부분집합으로만으로도 독립적인 검색 서비스가 가능하다. 실제로 인덱스에 질의를 요청하면 인덱스가 가지고 있는 모든 샤드로 검색요청을 보내고 각 샤드의 결과를 취합하여 하나의 결과로 제공한다.

 

6)레플리카

샤드의 복제본을 레플리카라고 한다. 엘라스틱서치에서는 인덱스를 생성할 때 기본적으로 1개의 레플리카를 생성한다. 장애 복구만이 아니고 검색에도 활용되기 때문에 이를 이용하면 읽기 분산에 유리해진다. 엘라스틱서치는 노드의 장애시 페일오버 메커니즘을 레플리카를 이용하여 제공하고 있다. 인덱스가 생성될 때 샤드 개수와 레플리카 개수를 자유롭게 설정할 수 있다. 하지만 인덱스가 생성된  이후에는 샤드 개수를 변경하는 것이 불가능하다. 만약 샤드의 개수를 늘리고 싶다면 샤드개수를 늘린 인덱스를 새로 만들고 기존 인덱스에서 새로 생성한 인덱스로 ReIndex하는 방법 밖에 없다. 데이터가 아주 크다면 Reindex 시간이 아주 길기 때문에 애초에 최적의 샤드의 개수를 정하는 것이 중요하다. 이에 반해 레플리카 개수는 인덱스를 생성한 후에도 자유롭게 변경하는 것이 가능하다.

 

6)-1 엘라스틱서치의 고가용성

엘라스틱서치에서는 샤드나 노드에 장애가 발생할 경우 즉각적인 복구가 가능하기 때문에 안정적인 클러스터 운영이 가능하다. 페일오버 메커니즘을 레플리카를 이용하기 때문에 원본 샤드가 존재하지 않는 노드에 레플리카 샤드를 생성한다. 또한 검색 시 샤드와 레플리카에서 병렬로 실행될 수 있기 때문에 검색 성능이 좋아지는 결과도 있다.

 

7)세그먼트

문서들은 빠른 검색에 유리하도록 설계된 특별한 자료구조로 저장된다. 루씬에 데이터가 색인되면 데이터는 토큰 단위로 분리되고 특수한 형태의 세그먼트라는 단위로 저장이 된다. 이러한 세그먼트는 읽기에 최적화된 역색인이라는 형태로 변환되어 물리적인 디스크에 저장된다. 검색엔진의 특성상 쓰기 연산보다 읽기 연산의 비중이 비교적 높기때문에 읽기에 최적화된 역색인이라는 구조로 저장하는 것이다.

 

 

엘라스틱서치와 RDB비교

엘라스틱서치 RDB
인덱스 데이터베이스
샤드 파티션
타입 테이블
문서
필드
매핑 스키마
Query DSL SQL

 

엘라스틱서치는 루씬 라이브러리를 샤드 내부에 가지고 있으며, 이 루씬 라이브러리가 핵심 모듈이라고 설명했다. 루씬은 검색 라이브러리이고 이 라이브러리에서 중요한 클래스가 바로 IndexWriter & IndexSearcher이다. 간단히 전자는 데이터를 색인하는 역할이고 후자는 검색하는 역할이다. 이 두개를 가지고 색인과 검색 역할을 제공하는 루씬 인스턴스를 루씬 인덱스라고 하는데, 사실 하나의 엘라스틱서치 샤드는 하나의 루씬 인덱스라고 설명할 수 있다. 즉, 샤드가 독립적으로 검색을 제공하는 이유이기도 하다. 이말은 각 샤드마다 데이터를 위한 물리적인 파일을 가지게 될 것이다. 이것을 간단히 표로 표현하면

 

 

엘라스틱서치 인덱스 구조

Elasticsearch Index
Elasticsearch shard Elasticsearch shard Elasticsearch shard Elasticsearch shard
Lucene Index Lucene Index Lucene Index Lucene Index
segment segment segment segment segment segment segment segment

 

루씬 인덱스는 독립적으로 자기가 가지고 있는 세그먼트 내에서만 검색이 가능하다. 하지만 엘라스틱서치는 이러한 루씬 인덱스를 가진 샤드들을 하나의 엘라스틱서치 인덱스로 묶여 있으므로, 다수의 루씬 인덱스에서 동시에 검색이 가능한 것처럼 기능을 제공하게 되는 것이다.

 

 

색인 작업 시 세그먼트의 동작 방식

하나의 루씬 인덱스는 내부적으로 다수의 세그먼트로 구성돼 있다. 읽기 성능이 중요한 검색엔진에서는 하나의 세그먼트로 검색 요청을 처리하는 것보다 다수의 세그먼트를 생성해서 나눠서 처리하는 것이 훨씬 효율적일 것이다. 루씬은 검색 요청을 받으면 다수의 작은 세그먼트 조각들이 각각 검색 결과 조각을 만들어 내고 이를 통합하여 하나의 결과로 합쳐서 응답하도록 설계돼 있다. 이러한 검색 방식을 세그먼트 단위 검색이라고 한다. 세그먼트는 역색인 구조를 지닌 파일 자체를 의미하는데 세그먼트 내부에는 실제로 색인된 데이터가 역색인 구조로 저장돼있다.

루씬은 세그먼트들을 관리하기 위한 용도로 커밋 포인트라는 자료구조를 제공한다. 커밋 포인트는 여러 세그먼트의 목록 정보를 가지고 있으며, 검색 요청 시 이를 적극적으로 활용한다. 최초 색인 작업 요청이 루씬에 들어오면 IndexWriter에 의해 색인 작업이 이루어지고 결과물로 하나의 세그먼트가 생성된다. 그 후 색인 작업이 추가로 요청될 때마다 새로운 세그먼트가 추가로 생성되고 커밋 포인트에 기록된다. 즉, 색인 작업이 일어날 때마다 세그먼트 개수는 점점 늘어난다. 하지만 너무 많은 세그먼트가 생성되면 읽기 성능이 저하될 수 있기 때문에 루씬은 백그라운드에서  주기적으로 세그먼트 파일을 병합하는 작업을 수행하고 이를 통해 모든 세그먼트들을 물리적으로 하나의 파일로 병합한다.

즉, 일정시간이 지나고 추가 색인 작업이 없는 상태라면 최종적으로 하나의 커다란 세그먼트만 남는다. 

색인 작업이 계속 될때마다 세그먼트는 계속 추가적으로 생성된다. 이말은 즉, 세그먼트는 불변성을 가지게 되고 병합과정 이외에는 절대 수정되지 않는다.

 

루씬의 색인 동작과정

  • 최초 색인 요청 - 1) IndexWriter가 세그먼트를 생성 -> 2)IndexSearch가 생성된 세그먼트를 읽어 검색결과 제공
  • 추가 색인 요청 - 1) IndexWriter가 세그먼트를 추가 생성 -> 2)세그먼트가 추가 생성되는 동안 기존 세그먼트만 읽어 검색 결과 제공 -> 3)세그먼트 생성이 완료되면 생성된 모든 세그먼트를 읽어 검색 결과 제공
  • 주기적으로 세그먼트 병합 작업이 일어날 경우 - 1)IndexWriter가 병합 대상이 되는 세그먼트들을 복제 -> 2)IndexWriter가 복제한 세그먼트들을 하나의 세그먼트로 병합 -> 3) 병합 과정 중에는 기존 원본 세그먼트로 검색 결과 제공 -> 4) 병합작업 완료시 원본 세그먼트와 병합 세그먼트를 교체하고 원본 세그먼트 삭제 

 

여기까지 내용을 끊고 다음 포스팅에서 이어서 설명합니다.

posted by 여성게
:

 

이번에 포스팅할 내용은 More Like This 입니다. More Like This 쿼리란 주어진 질의문과 가장 유사한 문서를 찾는 알고리즘입니다. 그리고 해당 알고리즘을 사용하지 위해서는 쿼리를 날릴 필드가 인덱싱되어야하며 분석 속도를 높이기 위하여 term_vector 속성을 지정해주는 것이 좋습니다. 필자를 참고로 간단한 Q&A를 위한 챗봇을 만들기 위하여 해당 알고리즘을 이용하였습니다. 물론 문맥을 이해하는 것이 아니라 유사도를 판단하는 것이지만 나름 성능이 나오는 알고리즘입니다. 모든 예제는 이전에 이용하였던 High Level Rest Client를 이용하였습니다.

 

Index

한글형태소 분석기가 포함된 인덱스를 생성하는 코드입니다. 참고로 질의의 대상이 되는 "question"필드의 term_vector 속성을 "yes"로 지정하였습니다. 생성 후에 몇개 예제로 문서를 색인해주세요.

 

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
@RequestMapping("/create")
    public Object createIndex() {
        
        boolean acknowledged = false;
        
        try(
                RestHighLevelClient client = createConnection()
        ){
            //index name
            String indexName = "sample-korean-index";
            //type name
            String typeName = "_doc";
            
            //settings
            XContentBuilder settingsBuilder = XContentFactory.jsonBuilder()
                    .startObject()
                        .field("number_of_shards",5)
                        .field("number_of_replicas",1)
                        
                        .startObject("analysis")
                            .startObject("tokenizer")
                                .startObject("sample-nori-tokenizer")
                                    .field("type","nori_tokenizer")
                                    .field("decompound_mode","mixed")
                                    .field("user_dictionary","user_dictionary.txt")
                                .endObject()
                            .endObject()
                            
                            .startObject("analyzer")
                                .startObject("sample-nori-analyzer")
                                    .field("type","custom")
                                    .field("tokenizer","sample-nori-tokenizer")
                                    .array("filter",new String[]{
                                        "sample-nori-posfilter",
                                        "nori_readingform",
                                        "sample-synonym-filter",
                                        "sample-stop-filter"
                                        }
                                    )
                                .endObject()
                            .endObject()
                            
                            .startObject("filter")
                                .startObject("sample-nori-posfilter")
                                    .field("type","nori_part_of_speech")
                                    .array("stoptaags",new String[] {
                                            "E","IC","J","MAG","MM","NA","NR","SC",
                                            "SE","SF","SH","SL","SN","SP","SSC","SSO",
                                            "SY","UNA","UNKNOWN","VA","VCN","VCP","VSV",
                                            "VV","VX","XPN","XR","XSA","XSN","XSV"
                                        }
                                    )
                                .endObject()
                                
                                .startObject("sample-synonym-filter")
                                    .field("type","synonym")
                                    .field("synonyms_path","synonymsFilter.txt")
                                .endObject()
                                
                                .startObject("sample-stop-filter")
                                    .field("type","stop")
                                    .field("stopwords_path","stopFilter.txt")
                                .endObject()
                            .endObject()
                        .endObject()
                    .endObject();
            
            //mapping info
            XContentBuilder indexBuilder = XContentFactory.jsonBuilder()
            .startObject()
                .startObject(typeName)
                    .startObject("properties")
                        .startObject("question")
                            .field("type","text")
                            .field("analyzer","sample-nori-analyzer")
                            .field("term_vector","yes")
                        .endObject()
                        .startObject("answer")
                            .field("type","keyword")
                        .endObject()
                    .endObject()
                .endObject()
            .endObject();
            
            //인덱스생성 요청 객체
            CreateIndexRequest request = new CreateIndexRequest(indexName);
            //세팅 정보
            request.settings(settingsBuilder);
            //매핑 정보
            request.mapping(typeName, indexBuilder);
            
            //별칭설정
            String aliasName = "chatbotInstance";
            request.alias(new Alias(aliasName));
            
            //인덱스생성
            CreateIndexResponse response = client.indices().create(request, RequestOptions.DEFAULT);
            
            acknowledged = response.isAcknowledged();
            
        }catch (Exception e) {
            e.printStackTrace();
            return "인덱스 생성에 실패하였습니다. - catch";
        }
        
        
        return acknowledged == true ? "인덱스가 생성되었습니다.":"인덱스생성에 실패하였습니다.";
    }
cs

 

More Like This Query

 

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
@RequestMapping("/mlt")
    public Object mlt() {
        String aliasName = "chatbotInstance";
        String typeName = "_doc";
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        searchSourceBuilder.query(QueryBuilders.moreLikeThisQuery
                (new String[] {"question"},new String[]{"사용자 발화 입니다. "},null)
                .minTermFreq(2)
                .minDocFreq(1)
                );
        searchSourceBuilder.from(0);
        searchSourceBuilder.size(5);
        searchSourceBuilder.sort(new FieldSortBuilder("answer").order(SortOrder.DESC));
        
        SearchRequest request = new SearchRequest(aliasName);
        request.types(typeName);
        request.source(searchSourceBuilder);
        
        
        SearchResponse response = null;
        SearchHits searchHits = null;
        List<Answer> resultMap = new ArrayList<>();
        
        try(RestHighLevelClient client = createConnection();){
            response = client.search(request, RequestOptions.DEFAULT);
            searchHits = response.getHits();
            for( SearchHit hit : searchHits) {
                Map<String, Object> sourceAsMap = hit.getSourceAsMap();
                Answer a = new Answer();
                a.setQuestion(sourceAsMap.get("question")+"");
                a.setAnswer(sourceAsMap.get("answer")+"");
                resultMap.add(a);
            }
        }catch (Exception e) {
            e.printStackTrace();
            /*
             * 예외처리
             */
        }
        return resultMap; 
    }
    private class Answer{
        private String question;
        private String answer;
        private String score;
        
        public String getQuestion() {
            return question;
        }
        public void setQuestion(String question) {
            this.question = question;
        }
        public String getAnswer() {
            return answer;
        }
        public void setAnswer(String answer) {
            this.answer = answer;
        }
        public String getScore() {
            return score;
        }
        public void setScore(String score) {
            this.score = score;
        }
    }
cs

 

파라미터 설명

 

Parameters

 

Document Input Parameters

  - like

    작성된 문서 또는 text를 바탕으로 문서를 검색 합니다.

  - unlike

    작성된 문서 또는 text에서 제외 시킬 term을 작성 합니다.

  - fields

    문서에서 analyzed text 를 가져올 필드를 지정 합니다.

    이 필드를 대상으로 질의 수행이 이루어 집니다.

  - like_text

    like 와 더불어 문서를 검색 할떄 추가적으로 사용할 text를 작성 합니다.

  - ids or docs

    @deprecated

 

Term Selection Parameters

  - max_query_terms

    작성된 문서 또는 text에서 추출하여 사용할 최대 query term size 입니다. (default 25)

  - min_term_freq

    작성된 문서 또는 text의 최소 TF 값으로 이 값보다 작을 경우 작성된 문서와 text는 무시 됩니다. (default 2)

  - min_doc_freq

    입력된 개별 term들에 대해서 각각 matching 된 문서의 최소 크기로 해당 크기 보다 작은 term의 결과는 무시 됩니다. (default 5)

  - max_doc_freq

    입력된 개별 term들에 대해서 각각 matching 된 문서의 최대 크기로 해당 크기 보다 큰 term의 결과는 무시 됩니다. (default unbounded 0)

  - min_word_length

    입력된 개별 term들의 최소 길이로 정의한 값보다 작은 term은 무시 됩니다. (default 0)

  - max_word_length

    입력된 개별 term들의 최대 길이로 정의한 값보다 큰 term은 무시 됩니다. (default unbounded 0)

  - stop_words

    불용어 목록을 등록 합니다.

  - analyzer

    입력한 문서와 text에 대한 analyzer 를 지정 합니다. 지정 하지 않을 경우 first field 의 analyzer 를 사용하게 됩니다.

 

Query Formation Parameters

  - minimum_should_match

    작성된 문서 또는 text에서 추출된 term matching 에 대한 minimum_should_match 정보를 구성 합니다. (default 30%)

  - boost_terms

    tems boost value 를 지정 합니다.

  - include

    검색 결과로 입력 문서를 포함 할지 말지를 결정 합니다. (default false)

  - boost

    전체 질의에 대한 boost value 를 지정 합니다. (default 1.0)

 

posted by 여성게
: