[엘라스틱서치] Elasticsearch in action 정리(7) - 클러스터 관리
엘라스틱서치 API를 조금더 확장하고, 성능을 개선하고 장애 복구 계획을 구현하기 위해 필요한 엘라스틱서치 클러스터 모니터링과 튜닝이라는 목적에 맞게 이 API들을 사용하는 방법에 대해서 알아본다.
개발자와 운영자 모두 엘라스틱서치 클러스터를 모니터링하고 관리할 수 있다. 시스템의 부하가 높은 적정 수준이든, 하드웨어나 시스템 장애에 대비하고 성능 병목 지점을 이해하고 확인하는 것은 매우 중요할 것이다.
REST API를 이용한 클러스터 관리기법에 대해 알아보고, 이를 통해 실시간 관리 기법과 다른 베스트 프랙티스들을 활용하여 잠재적인 성능 병목 지점을 파악하고 적절한 조치를 취할 수 있을 것이다. 효율적으로 성능을 모니터링하는 것은 시스템 최적화를 위해 매우 중요하고 시스템을 이해하는 것은 장애 시나리오에 대비하는데 있어 유용한 것은 자명하다.
기본 설정 향상
비록 엘라스틱서치의 기본 설정이 대부분의 사용자에게는 적합하지만, 엘라스틱서치는 더 좋은 성능을 위해서 기본 설정값을 튜닝할 수 있는 유연한 시스템이다.
프로덕션 환경에서 대부분의 엘라스틱서치는 전문 검색을 지원하기 위해 사용된다. 그리고 과거에는 특이 사례로 인식되었던 것들이 일반적으로 쓰여지고 있는데, 예를 들면 데이터의 유일한 저장소로서, 로깅 집계기로서, 혹은 다른 데이터베이스와 함께 하이브리드 형태의 저장소 아키텍처의 일부로서 엘라스틱서치를 사용하고 있는 것이다. 이와 같은 새로운 사용 목적은 엘라스틱서치 기본 설정을 튜닝하고 최적화하는데 있어 더욱 흥미롭게 해준다.
(1) 색인 템플릿
새로운 색인과 그에 대한 매핑을 엘라스틱서치에 생성하는 것은 일반적으로 초기 설계가 있다면 간단한 작업이다. 하지만 때로는 미래에 생성하게 될 색인들도 같은 설정과 매핑을 사용하게 될 경우도 있는데, 다음과 같은 시나리오를 생각해볼 수 있다.
- 로그 수집 - 이 경우 일자별 색인을 쓰는 것이 쿼리를 효율적으로 수행하기에 유리하다. 마치 rolling file appender와 같다고 생각할 수 있다. 이것의 일반적인 사례로 클라우드 환경을 생각해볼 수 있는데, 분산되어 있는 여러 시스템들이 각각의 로그를 중앙의 엘라스틱서치 클러스터로 전송하는 경우를 예로 들 수 있다. 클러스터가 자동으로 일별 로그를 관리하도록 템플릿을 설정하는 것은 데이터를 정리하고 필요한 정보를 얻기 위한 검색을 편리하게 하는데 유용하다.
- 규정 준수 - 이 경우 규정에 따라 데이터는 일정기간 동안 보관하여야 하거나 혹은 일정 기간 후에 제거되어야 한다. 예를 들면 금융 분야의 경우 Sarbanes-Oxley를 준수하여야 한다. 이런 규정은 체계화된 데이터 관리가 필요한데, 이 때 템플릿 체계가 매우 중요하다.
- 멀티 테넌시 - 새로운 사용자가 동적으로 추가되는 시스템은 대게 특정 사용자에게만 속하는 데이터를 분리해야할 필요가 있다.
템플릿은 균일한 데이터 저장 형태가 반복돼서 나타나는 것이 확인되었을 경우 유용하다. 엘라스틱서치가 템플릿을 자동적으로 적용해주는 것은 또한 매우 유용한 기능이다.
템플릿 생성하기
색인 템플릿은 새로 생성하고자 하는 모든 색인에 적용된다. 사전에 정의된 명명 규칙을 따르는 색인에는 같은 템플릿이 적용되어, 일관적인 색인 설정을 갖게 된다. 색인 생성 시에는 적용되기를 원하는 템플릿에 정의된 템플릿 패턴을 따라야 한다. 새로 생성할 색인에 색인 템플릿을 적용하는 방법은 1. REST API 와 2. 설정 파일 두가지가 존재한다.
첫번째 방법은 클러스터가 구동 중일 경우에만 사용할 수 있지만 두번째 방법은 구동 중이지 않을 경우에도 사용할 수 있고, 종종 데브옵스 엔지니어나 시스템 운영자가 프로덕션 환경을 사용하는 선배포 시나리오에서 사용한다.
로그 집계를 위해 사용할 간단한 색인 템플릿을 예로 들어보겠다. 이 로그 집계를 위한 도구는 일자별로 새 색인을 생성하게 된다. 로그스태시가 엘라스틱서치와 함께 쓰기 위한 로그 집계 도구 중에서는 가장 인기가 있다. 그리고 둘을 연동하는 것은 매우 편리하다. 따라서 로그스태시와 엘라스틱서치를 연동하는 색인 템플릿 생성 시나리오를 살펴보는 것이 의미가 있다.
기본적으로 로그스태시는 색인명에 일자별 타임스탬프를 추가하여 API 요청을 한다. (logstash-11-09-2014처럼 말이다) 엘라스틱서치 기본 설정을 사용할 경우 색인 자동 생성 기능이 활성화되어 있는데, 로그 스태시가 새로운 이벤트를 포함하여 요청을 엘라스틱서치로 보낼 경우, 새로운 색인은 logstash-11-09-2014라는 이름으로 생성되고 문서의 타입은 자동으로 매핑될 것이다. 다음과 같이 REST API를 사용하는 경우를 살펴보자.
curl -XPUT localhost:9200/_template/logging_index -d '{
"template": "logstash-*", /* 색인 이름이 패턴에 매치되는 모든 색인에 이 템플릿을 적용 */
"settings": {
"number_of_shards": 2,
"number_of_replicas": 1
},
"mappings": { ... },
"aliases": {
"november": {}
}
}'
PUT 명령을 사용하여 엘라스틱서치에게 logstash-* 패턴에 맞는 요청이 들어올 경우 이 템플릿을 사용하라고 할 수 있다. 이 경우 로그스태시가 새로운 이벤트를 엘라스틱서치로 보냈는데, 해당 이름의 색인이 존재하지 않으면, 새로운 색인은 템플릿에 기반하여 자동으로 생성된다.
이 템플릿은 또한 Alias를 자동 생성하는 것까지도 지원하고 있다. 따라서 특정 월의 모든 색인을 묶을 수도 있다. 매달 이 색인 이름을 수동으로 바꿔줘야 하긴 하겠지만, 어쨌든 이는 로그 이벤트의 색인들을 월별로 묶을 수 있는 편리한 방법을 제공해준다.
파일 시스템에 설정된 템플릿
템플릿은 파일 시스템에서 설정하고 싶을 때도 역시 방법이 있다. 때로는 이렇게 하는 것이 유지보수 면에서 더 편리할 수도 있다. 설정 파일은 다음과 같은 간단한 규칙을 따라야 한다.
- 템플릿 설정은 JSON 포맷이어야 한다. 편의성을 위해 <FILENAME>.json 처럼 .json 확장자를 붙여주도록 하자.
- 다템플릿 정의는 엘라스틱서치 설정이 있는 위치의 templates라는 디렉토리에 있어야 한다. 이 경로는 클러스터 설정 파일의 path.conf에서 지정할 수 있다.
- 템플릿 정의는 마스터로 선출될 가능성이 있는 노드의 디렉토리에 있어야 한다.
이전의 템플릿 정의를 사용한다면, template.json 파일은 다음과 같을 것이다.
{
"template": "logstash-*",
"settings": {
"number_of_shards": 2,
"number_of_replicas": 1
},
"mappings": { ... },
"aliases": { "november": {} }
}
REST API로 정의할 때와 유사하게, 이 템플릿은 이제 logstash-* 패턴에 매치되는 모든 색인에 적용될 것이다.
복수 템플릿 병합
엘라스틱서치를 사용할 때 서로 다른 설정을 가진 복수의 템플릿을 설정할 수도 있다. 월별 로그 이벤트를 관리하는 템플릿과 한 색인에 모든 로그 이벤트를 저장하는 템플릿을 설정할 수도 있다.
최상위 템플릿은 9월에 관련된 로그에만 적용한다. 이 템플릿은 이름이 "logstash-09-"로 시작하는 색인에 매치되기 때문이다. 두번째 템플릿은 더 광범위하게 적용되는데, 모든 로그스태시 색인에 반영되고 심지어 date 매핑에 대한 다른 설정을 갖고 있기도 하다.
이 설정에서 주의할 점 하나는 order 속성이다. 이 속성은 적은 숫자를 가진 것이 먼저 적용되고, 높은 숫자를 가진 것이 오버라이딩한다는 것을 의미한다. 이로인해, 두 템플릿 설정이 합쳐지기도 하는데, 이로 인해 11월의 로그 이벤트는 date 필드가 정렬되지 않게 된다.
색인 템플릿 조회하기
모든 템플릿을 조회하기 위한 편리한 API도 있다.
curl -XGET localhost:9200/_template/
이와 비슷하게, 하나 혹은 다수의 템플릿을 템플릿명으로 조회할 수도 있다.
curl -XGET localhost:9200/_template/logging_index
curl -XGET localhost:9200/_template/logging_index_1,logging_index_2
혹은 이름이 특정 패턴을 따르는 모든 템플릿을 조회할 수도 있다.
curl -XGET localhost:9200/_template/logging_*
색인 템플릿 삭제하기
색인 템플릿은 템플릿 이름으로 삭제할 수 있다. 다음과 같이 템플릿을 정의한다.
curl -XPUT 'localhost:9200/_template/logging_index' -d '{...}'
이 템플릿을 지우려면 다음과 같이 요청에 템플릿 이름을 사용하면 된다.
curl -XDELETE 'localhost:9200/_template/logging_index'
(2) 기본 매핑
매핑을 통해 구체적인 필드들과 자료형, 그리고 엘라스틱서치가 어떻게 필드를 해석하여 저장할지를 정의할 수 있다. 엘락스틱서치가 지원하는 동적 매핑으로 인해 색인 생성 시점에 반드시 매핑을 정의해야 하는 것은 아니다. 동적 매핑은 색인하게 될 초기 문서들에 의해 동적으로 생성되는 것이다. 매핑 개념을 통해 반복적인 매핑 생성작업을 단순하게 만들 수 있다.
색인 템플릿을 통해 유사한 자료형에 걸쳐 일관성을 확보하고 시간을 절약할 수 있다. 기본 매핑은 매핑 타입에서 템플릿의 역할과 비슷하며 유사한 장점들을 갖고 있다. 기본 매핑은 유사한 필드들을 가지고 있는 색인에서 가장 자주 사용된다. 기본 매핑을 한번 입력하면 각 색인 내에서 반복적으로 이를 입력하지 않아도 된다.
기본 매핑을 입력한다고 해서 이것이 소급 적용되지는 않는다. 기본 매핑은 새롭게 생성되는 타입에 대해서만 적용된다. 다음은 Person 타입을 제외한 모든 매핑에 대해 _source를 저장하는 방법에 대해서 디폴트 설정을 입력하는 것이다.
curl -XPUT 'localhost:9200/streamglue/_mapping/events' -d '{
"Person": {
"_source": {
"enabled": false
}
},
"_default_": {
"_source": {
"enabled": true
}
}
}'
이 경우, 모든 매핑은 기본적으로 문서의 _source를 저장하지만, Person 타입은 그렇지 않을 것이다. 이 동작은 개별 매핑 정의를 오버라이딩하여 변경할 수도 있다.
동적 매핑
기본적으로 엘라스틱서치는 동적 매핑을 사용한다. 동적 매핑이란 문서의 새 필드들에 대한 자료형을 판단하는 것을 의미한다. 처음 문서를 색인했을때 엘라스틱서치가 문서에 대한 매핑을 생성하고 각 필드에 대한 자료형을 정의하는 것을 경험해봤을 수도 있다. 엘라스틱서치가 새로운 필드를 무시하거나 혹은 알려지지 않은 필드가 들어왔을때 예외를 발생시키도록 하여 이런 동작 방식을 수정할 수도 있다. 일반적인 경우라면 새 필드가 추가되는 것을 제한하여 데이터 오염을 방지하고 정의된 스키마를 유지하고자 할 것이다.
동적 매핑 비활성화
elasticsearch.yml에서 index.mapper.dynamic을 false로 설정하여 매핑이 없는 타입에 대해 동적으로 새 매핑을 생성하는 기능을 비활성화할 수도 있다. 다음 목록은 동적 매핑을 추가하는 방법이다.
curl -XPUT 'localhost:9200/first_index' -d '{
"mappings": {
"person": {
"dynamic": "strict", // 색인 시점에 알려지지 않은 필드가 추가되면 예외를 발생시킨다.
"properties": {
"email": { "type": "string" },
"created_date": { "type": "date" }
}
}
}
}'
curl -XPUT 'localhost:9200/second_index' -d '{
"mappings": {
"person": {
"dynamic": "true", // 새로운 필드를 동적으로 생성하는 것을 허용한다.
"properties": {
"email": { "type": "string" },
"created_date": { "type": "date" }
}
}
}
}'
처음의 매핑은 person 매핑 내에서 새로운 필드를 생성하는 것을 제한하게 된다. 만약 매핑이 없는 필드를 포함한 문서를 삽입하려고 하면 엘라스틱서치는 예외를 반환하고 색인을 수행하지 않는다. 응답은 다음과 같다.
{
error: "StrictDynamicMappingException[mapping set to strict, dynamic introduction of [first_name] within [person] is not allowed]",
status: 400
}
동적 매핑과 템플릿을 함께 사용하기
동적 매핑과 템플릿을 함께 사용하는 것을 통해, 필드 이름이나 자료형에 따라 다른 매핑을 적용할 수 있게 된다. 색인 템플릿을 사용하면 일관된 매핑을 가져야 하는 색인들을 자동으로 생성되게 할 수도 있다. 이 아이디어에 동적 매핑을 적용해본다.
UUID를 포함하는 데이터를 다룰 때 간단한 문제이다. 이 고유한 영문자와 숫자로 이루어진 문자열은 하이픈 구분자를 포함하고 있다. 이것을 엘라스틱서치가 분석하기를 원하지는 않을 것이다. 왜냐하면 기본 분석기는 색인 토큰을 생성할때 이 UUID를 하이픈 기준으로 나눌 것이기 때문이다. 전체 UUID 문자열로 검색이 가능하기를 원할 것이기 때문에, 엘라스틱서치가 전체 문자열을 하나의 토큰으로 저장하게 할 필요가 있다. 이 예제에서는 엘라스틱서치가 특정 문자열 필드의 이름이 "_guid"로 끝날 경우 이 필드를 분석하지 않도록 할 필요가 있다.
curl -XPUT 'http://localhost:9200/myindex' -d '{
"mappings": {
"my_type": {
"dynamic_templates": [
{
"UUID": {
"match": "*_guid", // _guid로 끝나는 필드 이름에 매치됨
"match_mapping_type": "string", // 매치된 필드는 반드시 문자열 타입이어야 한다
"mapping": { // 매치되었을때, 적용할 매핑을 정의한다.
"type": "string", // 타입을 문자열로 지정
"index": "not_anlayzed" // 색인 시점에 이필드들을 분석하지 않는다.
}
}
}
]
}
}
}'
이 예제에서 동적 템플릿은 특정 이름과 형태에 매치되는 필드에 대한 매핑을 동적으로 부여하기 위해 사용되고 있다. 이를 통해 사용자의 데이터를 어떻게 저장하여 원하는 방식으로 검색할 수 있도록 만들지에 대해 더 적절하게 제어할 수 있다. 또한, path_match와 path_unmatch 키워드를 사용할 수도 있는데, 이는 온점(.) 표기법을 사용하여 동적 템플릿에 매치시킬 수도 있다. 예를 들어, person.*.email 같은 필드에 매치시키고자 하는 경우를 생각해볼 수 있다. 이 논리에 따르면, 다음과 같은 자료구조에 매치가 발생하는 것을 확인할 수 있다.
{
"person": {
"user": {
"email": { "bob@domain.com" }
}
}
}
동적 템플릿은 엘라스틱서치 관리의 몇몇 귀찮은 부분들을 자동화할 수 있는 편리한 방법이다.
할당 인식
클러스터의 위상(topology) 설계라는 개념을 통해 할당 인식(Allocation Awareness)를 사용하여 장애의 중심점을 줄이고 성능을 향상시키는 방법을 알아본다. 할당 인식이란 데이터의 복제본이 어디에 위치할지에 대한 인지 상태라는 개념이다. 이 개념을 이해하여 엘라스틱서치가 클러스터에 레플리카 데이터를 스마트하게 분배하도록 할 수 있다.
(1) 샤드 기반 할당
할당 인식은 샤드 할당을 사용자 정의 파라미터를 통해 설정할 수 있도록 해준다. 이는 일반적인 베스트 프랙티스라고 할 수 있는데, 이를 통해 데이터가 네트워크 위상에서 균일하게 분포하도록 하여 단일 고장점(single point of failure, SPOF)을 가질 확률을 낮출수 있기 때문이다. 이를 통해 또한 읽기 작업이 더 빨라지는 경험을 할 수도 있다. 예를 들어 같은 물리적인 랙에 위치한 노드들 간에는 데이터의 지역성이라는 이점이 있어 다른 네트워크와 통신하지 않아도 되기 때문이다.
할당 인식을 활성화하기 위해서는 그룹 키를 정의하고 이를 관련된 노드에 설정하면 된다. 할당 인식 속성은 하나 이상의 값을 가질 수 있다. rack/group/zone 속성이 존재한다.
cluster.routing.allocation.awareness.attibutes: rack
이 정의를 사용한다면 클러스터 내에서 샤드들을 rack이라는 파라미터 인식을 사용하여 나눌 수 있다. 원하는 네트워크 구성에 따라서 각 노드의 elasticsearch.yml에서 이 값을 원하는 값으로 수정할 수 있다. 엘라스틱서치는 노드에 메타데이터를 설정할 수 있도록 해주고 있다. 이 경우 메타데이터 키가 할당 인식 파라미터가 된다.
node.rack: 1
이것이 적용되지 이전과 이후를 비교해보는게 이해하기 쉽다. 기본 할당 설정 적용 클러스터는 주와 레플리카 샤드가 같은 랙에 위치하는 문제점을 갖을 수 있다. 할당 인식 설정을 통해 이러한 위험을 없앨 수 있다. 할당 인식을 사용하면 주 샤드는 이동하지 않았지만, 레플리카들이 다른 node rack 값을 가지고 있는 노드로 이동하게 된다. 샤드 할당은 장애의 중심점을 예방할 수 있는 편리한 기능이다. 일반적인 용도는 클러스터 위상을 지역이나 랙, 심지어 가상 머신에 따라 분리하는 것이다. 이어서 실제 AWS 존을 예로 들어 강제 할당에 대해 살펴본다.
(2) 강제 할당 인식
강제 할당 인식은 사전에 값들의 분류를 이해하고 어떤 그룹에 속하는 레플리카의 수를 제한하고자 할 때 유용하다. 이것이 일반적으로 사용되는 실제 사례는 AWS나 다른 다수의 zone을 지원하는 클라우드 환경에서 다수의 존에 걸치는 할당 전략을 들 수 있다. 유스케이스는 간단한데, 다른 존이 다운되거나 접근 불가능할 경우에도 한 존에 있는 레플리카의 수를 제한하는 것이다. 이렇게 함으로써, 다른 그룹에 레플리카가 과할당되는 위험을 줄일 수 있다.
이 유스케이스의 경우에는 존 레벨에서 할당을 강제하고 싶을 수 있다. 앞서 했던 것처럼 속성을 zone으로 지정하고, 이 그룹의 값들을 us-east와 us-west처럼 설정할 수 있다. elasticsearch.yml에 다음 내용을 추가하면 된다.
cluster.routing.allocation.awareness.attributes: zone
cluster.routing.allocation.force.zone.values: us-east, us-west
이 세팅에 따른 실제 시나리오는 다음과 같이 예상할 수 있다. 동부 리전에 node.zone: us-east인 몇몇 노드를 시작했다고 해보자. 색인 설정은 기본값인 5개의 샤드와 1개의 레플리카를 사용한다고 한다. 다른 존 값이 없기 때문에, 주 샤드만이 할당될 것이다.
여기서 일어나고 있는 현상은 레플리카가 기존의 값을 가진 노드 이외의 곳으로만 밸런싱되도록 제한되고 있는 것이다. 만약 서부 리전의 클러스터를 node.zone:us-west로 설정하여 시작한다면, us-east의 레플리카들이 여기로 할당될 것이다. node.zone: us-east인 노드에는 레플리카 샤드가 존재하지 않을 것이다. node.zone: us-west인 다른 노드에도 같은 작업을 수행하여 레플리카가 같은 지역에 존재하지 않도록 할 수 있다. 주의할 점은 us-west로의 연결이 끊어질 경우 us-east에는 레플리카가 생성되지 않고 반대의 경우도 마찬가지이다.
할당 인식은 사전 계획이 필요하다. 하지만 할당이 계획한 대로 작동하지 않는 경우에도 이 설정은 모두 런타임에 클러스터 세팅 API를 통해 수정할 수 있다. 변경된 설정을 엘라스틱서치가 재시작 후에도 적용하도록 영구적인 것(persistent)으로 할 수도 있고, 일시적인 것(transient)으로 할 수도 있다.
curl -XPUT localhost:9200/_cluster/settings -=d '{
"persistent": {
"cluster.routing.allocation.awareness.attributes": zone
"cluster.routing.allocation.force.zone.values": us-east, us-west
}
}'
클러스터 할당 전략에 따라 어떤 클러스터는 확장성이 있고 장애에 대해 회복력이 있는 반면, 어떤 클러스터는 그렇지 않을 수 있다. 이어 어떻게 클러스터의 일반적인 상태를 모니터링할 수 있는지 알아본다.
성능 병목 모니터링
엘라스틱서치는 API를 통해 메모리 사용, 노드 멤버쉽, 샤드 분배, I/O 성능 등에 관한 유용한 정보를 제공한다. 클러스터와 노드 API는 클러스터의 상태와 전반적인 성능 메트릭을 측정하는데 도움을 준다. 클러스터 상태 관련된 데이터를 이해하고 전반적인 상태를 평가할 수 있다면 할당되지 않은 샤드나 누락된 노드와 같은 성능 병목 지점을 파악할 수 있을 것이고, 쉽게 대처할 수 있을 것이다.
(1) 클러스터 상태 확인하기
클러스터 상태 API를 통해 클러스터, 색인, 샤드의 대략적인 상태를 편리하게 파악할 수 있다. 이는 일반적으로 현재 클러스터에서 발생하고 있는 문제를 진단해내는 첫번째 접근법이다. 다음은 클러스터 상태 API를 통해 클러스터 상태 전반을 어떻게 확인할 수 있는지 보여준다.
curl -XGET 'localhost:9200/_cluster/health?pretty';
{
"cluster_name": "es-beta",
"status": "green", // 클러스터 상태 지표
"timed_out": false,
"number_of_nodes": 3, // 클러스터 내의 노드의 갯수
"number_of_data_nodes": 3, // 클러스터 내에서 데이터를 가지고 있는 노드의 수
"active_primary_shards": 1160, // 클러스터 내의 모든 색인에 대한 주 샤드의 수
"active_shards": 2321, // 클러스터 내의 모든 색인에 대한 주와 레플리카를 포함한 모든 샤드의 수
"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
}
이 응답들을 그대로만 받아들여도 클러스터의 일반적인 상태에 대해 많은 것을 추론할 수 있다. 하지만 처음 보기에 명백한 정보들 이외에도 이를 통해 이해할 수 있는 부분이 많다. 코드의 마지막 세가지 지표인 relocating_shards, initializing_shards, unassigned_shards에 대해 더 살펴본다.
- relocating_shards - 0보다 큰 숫자의 의미는 엘라스틱서치가 장애복구나 더 좋은 균형 상태를 유지하기 위해 샤드를 클러스터의 다른 노드로 이동시키고 있다는 것이다. 이는 노드를 추가하거나, 장애가 발생한 노드를 재시작하거나 노드를 제거할 경우 일상적으로 발생하는 일시적인 현상이라고 볼 수 있다.
- initializing_shards - 색인을 새로 생성하거나 노드를 재시작한 경우 이 숫자는 0보다 클 것이다.
- unassigned_shards - 이 숫자가 0보다 크게 되는 일반적인 이유는 할당되지 않은 레플리카들 때문이다. 개발 환경에서는 빈번하게 발생하는데, 단일 노드 구성의 클러스터를 사용하고 색인 설정을 기본값으로 사용하는 경우 5개의 샤드와 하나의 레플리카를 갖게 되는데, 이때 다섯 개의 할당되지 않은 레플리카 샤드가 발생하기 때문이다.
클러스터 상태가 그린이 아닌 경우도 있다. 예를 들면, 노드가 정상저그올 실행되지 못했거나 혹은 클러스터로부터 떨어져 버리는 경우 등이 있다. 클러스터 상태 값은 클러스터 상태에 관한 일반적인 정보를 제공해 줄 뿐이지만, 각각이 상태 값들이 클러스터 성능 면에서 어떤 의미가 있는지 이해하면 좋다.
- 그린 - 모든 주와 레플리카 샤드가 정상적으로 동작하고 있다.
- 옐로 - 일반적으로 이것은 레플리카 샤드가 유실되었음을 의미한다. unassigned_shards 값이 0보다 클 가능성이 높고, 이는 클러스터가 불안정하다는 것을 뜻한다. 추가적인 샤드 유식은 치명적인 데이터 유실로 이어질 수 있다. 정상적으로 시작되지 않았거나 작동하지 않는 노드들을 살펴보아야 한다.
- 레드 - 이는 심각한 상태로, 클러스터에서 찾을 수 없는 주 샤드가 있다는 것을 의미한다. 이 상태에서 유실된 샤드로의 색인 요청은 금지되고, 검색 결과는 정확하지 않을 수 있다. 하나 혹은 다수의 노드가 클러스터에서 유실되었을 가능성이 높다.
클러스터 상태 API는 이슈를 더 깊이 있게 확인할 수 있도록 더 정밀한 작업 방법을 제공해준다. 이 경우, level이라는 파라미터를 추가하여 어떤 색인들이 미할당된 샤드들로 인해 문제가 되고 있는지를 더 깊이 확인해 볼 수 있다.
curl -XGET 'localhost:9200/_cluster/health?level=indices&pretty';
{
"cluster_name": "es-beta",
"status": "yellow",
"timed_out": false,
"number_of_nodes": 1, // 클러스터는 구동 중인 노드가 하나이다.
"number_of_data_nodes": 1,
"active_primary_shards": 10,
"active_shards": 10,
"relocating_shards": 0,
"initializing_shards": 0,
"unassigned_shards": 5,
"indices": {
"bitbucket": {
"status": "yellow",
"number_of_shards": 5, // 주샤드들
"number_of_replicas": 1, // 엘라스틱서치에게 주샤드 하나당 하나의 레플리카를 할당하도록 설정
"active_primary_shards": 5,
"active_shards": 5,
"relocating_shards": 0,
"initializing_shards": 0,
"unassigned_shards": 5 // 레플리카 정의를 지원해줄 사용 가능한 노드들이 모자라서 발생하게 되는 미할당된 샤드들
}...
}
}
단일 노드로 구성된 클러스터가 문제에 빠져 있는 상황을 확인할 수 있는데, 엘라스틱서치가 레플리카 샤드를 클러스터 내에 할당하려고 하지만, 노드가 하나밖에 없어 이를 수행할 수 없기 때문이다. 이로인해 레플리카 샤드는 어디에도 할당되지 않고, 클러스터 상태가 옐로로 남게 된다.
(2) CPU: 슬로우 로그, 핫 스레드, 스레드 풀
엘라스틱서치를 모니터링하다보면 가끔씩 CPU 사용률이 갑작스럽게 증가하거나 높은 CPU 사용률과 블락되었거나 대기 중인 스레드들로 인해 성능 병목이 발생하는 것을 확인할 수 있다. 잠재적인 성능 병목 현상과 이것들을 확인하고 대처할 수 있는 도구들을 살펴본다.
슬로우 로그
엘라스틱서치는 느린 작업들을 확인하기 위한 두 가지 형태의 로그(슬로우 로그 / 슬로우 색인 로그)를 제공한다. 이는 클러스터 설정 파일에서 쉽게 설정할 수 있다. 기본적으로 둘 다 비활성화되어 있다. 로그 출력의 범위는 샤드 레벨이다. 하나의 작업이 해당 로그 파일에서 몇 줄에 걸쳐 나타날 수 있다. 이와 같은 샤드 레벨 로깅의 장점은 로그를 통해 어떤 샤드와 노드가 문제인지를 더 잘 확인할 수 있다는 것이다. 여기서 또한 이 설정은 '{index_name}/_settings' 종단점을 통해 수정할 수 있다.
index.search.slowlog.threshold.query.warn: 10s
index.search.slowlog.threshold.query.info: 1s
index.search.slowlog.threshold.query.debug: 2s
index.search.slowlog.threshold.query.trace: 500ms
index.search.slowlog.threshold.fetch.warn: 1s
index.search.slowlog.threshold.fetch.info: 1s
index.search.slowlog.threshold.fetch.debug: 500ms
index.search.slowlog.threshold.fetch.trace: 200ms
여기서 볼 수 있듯이 검색의 검색과 조회(fetch) 두 측면 모두에 임계값을 설정할 수 있다. 로그 레벨(warn, info, debug, trace)를 통해 어떤 레벨의 로그를 남길지를 세밀하게 설정할 수 있는데, 이는 로그 파일을 단순히 grep을 통해 찾아보고자 할때 편리하다. 출력이 남겨진 실제 로그 파일과 다른 로깅에 관련된 기능은 logging.yml에서 설정할 수 있다.
index_search_slow_log_file:
type: dailyRollingFile
file: ${path.logs}/${cluster.name}_index_search_slowlog.log
datePattern: "'.'yyyy-MM-dd"
layout:
type: pattern
conversionPattern: "[%d{ISO8601}] [%-5p][%-25c] %m%n"
슬로우 쿼리 로그
성능이슈를 확인할 때 중요한 부분은 바로 took[##ms] 같은 쿼리 수행 시간이다. 또한, 쿼리와 연관된 샤드나 색인을 아는 것도 도움이 될 수 있다. 이 정보들은 [index][shard_number] 형태로 표기된다.
슬로우 색인 로그
색인 요청의 병목 지점을 파악하는 또 하나의 유용한 방법은 슬로우 색인 로그다. 이 임계값은 이전의 슬로우 로그의 경우와 유사하게 클러스터 설정 파일이나 색인 설정 갱신 API를 통해 정의할 수 있다.
index.indexing.slowlog.threshold.index.warn: 10s
index.indexing.slowlog.threshold.index.info: 5s
index.indexing.slowlog.threshold.index.debug: 2s
index.indexing.slowlog.threshold.index.trace: 500ms
이전처럼, 임계값을 초과하는 모든 색인 작업은 로그 파일에 기록되게 된다. 이 로그를 통해 색인 작업에 관한 내용인 [index][shard_number] 등을 확인할 수 있다.
느린 쿼리나 색인 요청이 어디에서 발생하는지를 확인하는 것은 엘라스틱서치 성능 문제를 해결하는데 많은 도움을 줄 것이다. 느려지는 성능을 조치없이 방치한다면 전체 클러스터에 순차적인 장애로 이어져 전체 시스템의 크래시로 이어질 수 있다.
핫스레드 API
클러스터의 CPU 사용률이 과도한 현상을 경험한 적이 있다면, HOT_THREADS API를 통해 특정 프로세스가 블락되어 문제를 일으키고 있는 것을 확인하는데 유용하다는 것을 알 수 있을 것이다. 핫스레드 API를 통해 클러스터 내 각각의 노드에서 블락된 스레드들의 목록을 조회할 수 있다. 다른 API와는 다르게 핫스레드 API는 JSON이 아닌 특정 포맷의 텍스트를 반환한다.
curl -XGET 'http://127.0.0.1:9200/_nodes/hot_threads';
응답 예시는 다음과 같다.
::: [ElasticIQ-Master][AtPvr5Y3ReWua7ZPtPfuQ][loki.local][inet[/127.0.0.1:9300]]{mater=true}
37.5% (187.6micros out of 500ms) cpu usage by thread 'elasticsearch[ElasticIQ-Master][search][T#191]'
10/10 snapshots sharing following 3 elements
...
핫스레드 API의 응답을 정확히 이해하기 위해서는 약간의 해석이 필요하다. 그러면 CPU 성능에 관한 정보는 어떤것이 있는지부터 확인한다. 응답의 최상단 행은 노드의 신원 정보를 포함하고 있다. 아마도 클러스터는 하나 이상으로 구성되어 있을 것이기 때문에, 스레드 정보가 어느 CPU에 속하는지에 관한 기초적인 정보라고 할 수 있다.
여기서 37.5%의 CPU 자원이 검색 스레드에 사용되고 있는 것을 확인할 수 있다. 이것이 중요한데, 이것에 대한 이해가 있어야 CPU를 과다하게 사용하는 검색 쿼리들을 튜닝할 수 있기 때문이다. 검색이라는 값이 항상 존재하지는 않을 수도 있다. 엘라스틱서치는 머지, 색인 혹은 해당 스레드에서 수행되고 있는 다른 작업을 보여줄 수도 있다.
CPU 사용률이라는 이름을 가지고 있기 때문에 이것이 뭔가 CPU와 연관되었음을 알 수 있다. 여기서 확인할 수 있는 다른 것들은 블락된 스레드들을 확인할 수 있는 블락 사용률이나 WAITING 상태에 있는 스레드들에 의한 대기 사용률 등이 있다.
Stack Trace 직전의 마지막 라인은 엘라스틱서치가 같은 Stack Trace를 가지고 있는 스레드가 최근 몇 밀리초 내에 뜬 10개 스냅샷 중 10개에 있다는 것을 의미한다.
당연하게도, 엘라스틱서치가 어떻게 핫스레드 API 정보를 수집하여 나타내는지를 이해하는 것은 가치가 있다. 매우 짧은 밀리초 기간마다, 엘라스틱서치는 스레드의 지속 기간, 상태(WAITING/BLOCKED), 각 스레드가 대기 혹은 블락된 기간 등에 대한 정보를 수집한다. 설정된 시간 간격마다 엘라스틱서치는 정보 수집 작업의 두번째 단계를 수행한다. 각 단계에서는 각각의 스택 트레이스에 대한 스냅샷을 떠놓는다. 이런 정보 수집 작업은 핫스레드 API 요청에 파라미터들을 추가하여 튜닝할 수 있다.
curl -XGET 'http://127.0.0.1:9200/_nodes/hot_threads?type=wait&interval=1000ms&threads=3';
- type - cput, wait, block 중 하나. 스냅샷의 대상이 되는 스레드 상태이다.
- interval - 첫번째와 두번째 단계 사이의 대기 시간. 기본값은 500ms이다.
- threads - 몇 개의 상위 핫스레드를 보여줄 것인지다.
스레드 풀
CPU와 메모리를 더 효율적으로 사용하기 위해 각 노드는 스레드 풀을 관리하고 있다. 엘라스틱서치는 실행되고 있는 노드에서 최대한의 성능을 달성하기 위해 스레드풀을 관리하려고 한다.
경우에 따라, 순차적인 실패 시나리오를 회피하기 위해 스레드풀이 관리되는 방법을 직접 설정하여 오버라이드할 필요가 있을 수 있다. 부하가 큰 상황에서 엘라스틱서치는 수천 개의 스레드를 생성하여 요청을 처리하고자 할 수 있는데, 이는 클러스터 전체를 마비시킬 수 있다. 스레드 풀을 어떻게 튜닝할지를 알기 위해서는 사용자 애플리케이션이 어떻게 엘라스틱서치 API를 사용하는지 이해해야할 필요가 있다.
예를 들어, 벌크 색인 API의 사용이 대부분인 애플리케이션의 경우 더 많은 스레드를 할당받을 필요가 있다. 그렇지 않다면 벌크 색인 요청에서 병목이 발생해서 다른 요청이 무시될 수 있다.
클러스터 설정을 통해 스레드 풀 관련 설정들을 튜닝할 수 있다. 스레드 풀은 요청 종류에 따라 나누어지고 요청 종류에 따라 각각 다른 기본값을 가지고 있다. 이중 일부를 살펴보면 다음과 같다.
- bulk - 기본값은 가용 프로세서의 수
- index - 기본값은 가용 프로세서의 수
- search - 기본값은 가용 프로세서의 수의 세 배
elasticsearch.yml을 보면 스레드풀 큐의 사이즈나 벌크 요청을 위한 스레드 풀의 수를 늘릴 수 있음을 알 수 있다. 또한 클러스터 설정 API를 통해 실행 중인 클러스터에도 이 값들을 갱신할 수 있다.
# 벌크 스레드 풀
threadpool.bulk.type: fixed
threadpool.bulk.size: 40
threadpool.bulk.queue_size: 200
스레드 풀에는 고정(fixed)과 캐시(cache)라는 두 가지 형태가 있다. 고정 스레드 풀은 고정된 수의 스레드를 가지고 요청을 처리하고 대기 중인 요청을 보관할 큐를 가지고 있다. 이때 queue_size 파라미터는 스레드의 수를 제어하는데 사용되고, 기본값은 CPU 코어의 5배이다. 캐시 스레드 풀은 무제한이라고 볼 수 있는데, 대기중인 요청이 있을 경우 새로운 스레드를 생성하기 때문이다.
클러스터 상태 API, 슬로우 쿼리/색인 로그, 스레드에 관한 정보를 통해, CPU 집약적인 작업과 병목을 더욱 쉽게 진단해낼 수 있을 것이다.
(3) 메모리: 힙 크기, 필드와 필터 캐시
엘라스틱서치 클러스터의 메모리를 효율적으로 관리하고 튜닝하는 방법에 대해 알아본다. 엘라스틱서치의 집계와 필터링 작업은 메모리에 의해 제약을 받는 작업이다. 따라서 엘라스틱서치의 기본 메모리 관리 설정과 기반 JVM의 성능을 향상시키는 방법을 이해하는 것은 클러스터를 확장하는데 있어 매우 중요하다.
힙 크기
엘라스틱서치는 JVM에서 동작하는 자바 애플리케이션이다. 따라서 가비지 컬렉션의 메모리 관리 작업에 의해 영향을 받는다. 가비지 컬렉터의 개념은 단순하다. 이는 메모리가 부족할때 실행되며, 레퍼런스가 끊긴 객체들을 삭제하여 다른 JVM 애플리케이션들이 사용할 수 있도록 메모리를 확보해준다. 이 가비지 컬렉션 작업은 오래 걸리고 시스템 멈춤 현상을 일으킨다. 지나치게 많은 데이터를 메모리에 올리는 것은 또한 OOM을 발생시키게 되는데, 이는 시스템 실패와 가비지 컬렉션이 처리할 수 없는 예상할 수 없는 결과를 초래하게 된다.
엘라스틱서치가 빠르게 동작하기 위해서는 몇몇 작업은 메모리 내에서 수행되어야 한다. 이렇게 할 때 필드 데이터에 대한 접근이 더 효율적이기 때문이다. 예를 들어, 엘라스틱서치는 사용자 쿼리에 맞는 문서들에 대한 필드 데이터만을 로딩하는것이 아니라 색인에 있는 모든 문서에 대한 값들을 로딩한다. 이는 데이터를 메모리 내에서 접근할 수 있도록 만들어 줌으로써 이후의 쿼리를 매우 빠르게 해준다.
JVM 힙이란 JVM 위에서 실행되는 애플리케이션에 할당된 메모리의 크기를 의미한다. 그렇기 때문에, 이것의 성능을 튜닝하는 법을 이해하는 것은 메모리 부족 예외나 가비지 컬렉션으로 인한 정지현상 등을 회피하기 위해서 매우 중요하다. JVM 힙 크기는 HEAP_SIZE라는 환경변수를 통해 설정할 수 있다. 힙 크기를 설정할때 기억하고 있어야할 규칙은 다음과 같다.
- 최대 시스템 램의 50% - JVM에 너무 많은 메모리를 할당하면 루씬이 빈번하게 사용하는 파일 시스템 캐시를 위한 메모리가 부족해질 수 있다.
- 최대 32GB 램 - JVM은 32GB 이상 메모리가 할당된 경우 압축된 OOP(object ordinary pointer)를 사용하지 않도록 그 동작이 변경된다. 다시 말해 32GB 이하로 힙을 사용할 경우 메모리 공간을 대략 절반 정로로 사용하게 된다는 뜻이다.
필터와 필드 캐시
캐시는 엘라스틱서치에서 중요한 역할을 담당한다. 이를 통해 필터와 facets, 색인 필드 정렬을 더 효율적으로 수행할 수 있다. 필터 캐시는 필터와 쿼리 작업의 결과를 메모리 내에 저장한다. 즉, 필터가 적용된 최초의 쿼리는 그 결과를 필터 캐시에 저장한다. 이후의 모든 같은 필터가 적용된 쿼리는 캐시 내의 데이터를 활용할 뿐 디스크의 데이터를 조회하지 않는다. 필터 캐시는 CPU와 I/O 자원을 더 효율적으로 사용하게 함으로써 필터가 적용된 쿼리를 더 빠르게 수행될 수 있도록 한다.
엘라스틱서치는 두 가지 종류의 필터 캐시를 사용할 수 있다.
- 색인 수준 필터 캐시
- 노드 수준 필터 캐시
노드 레벨 필터 캐시가 기본 설정으로 이것에 대해 다루게 될 것이다. 색인 레벨 필터 캐시는 사용을 권장하지 않는데, 왜냐하면 색인이 클러스터 내에서 어디에 존재할지 알 수 없으므로 메모리를 얼마나 사용하게 될지 예측할 수 없기 때문이다. 노드 레벨 필터 캐시는 LRU(least recently used) 형태의 캐시다. 즉 캐시가 가득 차면, 캐시 목록 중 가장 오래 전에 사용했던 것이 제거되어 새로운 목록을 추가하기 위한 공간을 확보하게 된다. 이 설정은 index.cache.filter.typed을 node로 설정하여 사용할 수 있다. 혹은 전혀 설정하지 않을 경우 기본값이기 때문에 사용하게 된다. 이제 indices.cache.filter.size 속성을 통해 크기를 설정할 수 있다. 이 값은 메모리에 대한 퍼센트(예를 들어 20%) 혹은 정적인 값(예를 들어 1024MB)를 입력할 수 있다. 참고로 퍼센트 설정시 전체 용량은 노드의 최대 힙 값이다.
필터 데이터 캐시
필드 데이터 캐시는 쿼리 수행 시간을 단축시키기 위해서 사용된다. 엘라스틱서치는 쿼리가 수행될 때 필드 값을 메모리로 불러오는데, 이를 다음 요청 시에 사용할 수 있도록 필드 데이터 캐시에 보관하고 있는 것이다. 이런 구조를 메모리에 형성하는 것은 비싼 작업이기 때문에, 엘라스틱서치가 모든 요청 시에 이 작업을 수행하기를 원하지는 않을 것이다. 따라서 캐시를 통한 성능적인 이점은 주목할 만하다. 기본적으로, 이 캐시의 크기에는 제한이 없고 필드 데이터 서킷 브레이커 값에 도달할 때까지 커질 수 있다. 필드 데이터 캐시의 값을 설정하여 엘라스틱서치가 상한점에 도달할 경우 데이터를 제거하도록 할 수 있다.
이 설정은 indices.fielddata.cache.size 속성을 통해 할 수 있으며 퍼센트값이나 정적인 값 등으로 설정할 수 있다. 이 값은 전체 힙 크기의 일정 비율 혹은 일정한 크기의 메모리 캐시를 위해 사용한다는 것을 나타낸다.
필드 데이터 캐시의 현재 상태를 조회하기 위해서는 다음과 같은 간편한 API들을 사용할 수 있다.
- 노드별
curl -XGET 'localhost:9200/_nodes/stats/indices/fielddata?field=*&pretty=1';
- 색인별
curl -XGET 'localhost:9200/_stats/fielddata?fields=*&pretty=1';
- 노드별 색인별
curl -XGET 'localhost:9200/_nodes/stats/indices/fielddata?level=indices&field=*&pretty=1'
fields=* 를 입력할 경우 모든 필드 이름과 값을 조회할 수 있다. 이 API들의 응답은 다음과 같다.
{
"indices": {
"bitbucket": {
"fielddata": {
"memory_size_in_bytes": 1024mb,
"evictions": 200,
"fields": { ... }
}
}, ...
}
}
이 작업은 캐시의 현재 상태를 분석해준다. 캐시 제거(eviction)의 횟수에 특히 주의하도록 한다. 캐시 제거는 비용이 큰 작업이고 또한 필드 데이터가 너무 작게 설정되었다는 신호이기도 하다.
서킷 브레이커
필드 데이터 캐시는 OOM 에러를 발생시킬 만큼 커지게 될 수 있다. 필드 데이터의 크기는 데이터가 로딩된 이후에 계산되기 때문이다. 이런 경우를 방지하기 위해 엘라스틱 서치는 서킷 브레이커를 제공하고 있다.
서킷 브레이커는 메모리 부족 에러의 가능성을 줄이기 위해 설정할 수 있는 인위적인 임계치다. 이는 쿼리 수행 시에 필요한 데이터 필드들을 조사하여 이 데이터를 캐시로 로딩하는 것이 전체 캐시 사이즈 임계치를 초과하게 만드는지를 판단하는 방식으로 동작한다. 엘라스틱서치에는 두 가지 서킷 브레이커가 존재하고, 또한 모든 서킷 브레이커가 사용할 수 있는 메모리의 총량을 설정할 수 있는 부모 서킷 브레이커도 있다.
- indices.breaker.total.limit - 기본값은 힙의 70%다. 필드 데이터와 요청 서킷 브레이커가 이 값을 초과하지 않도록 해준다.
- indices.breaker.fielddata.limit - 기본값은 힙의 60%다. 필드 데이터 캐시가 이 값을 초과하지 않도록 해준다.
- indices.breaker.request.limit - 기본값은 힙의 40%다. 집계 버킷 생성 등의 작업 등에 할당할 수 있는 힙의 크기를 조절한다.
서킷 브레이커 설정에 대한 기본적인 규칙은 그 값을 보수적으로 잡으라는 것이다. 서킷 브레이커가 제어하는 캐시는 메모리 공간을 메모리 버퍼, 필터 캐시, 다른 엘라스틱서치가 사용하는 메모리와 공유하기 때문이다.
스왑 사용 방지하기
운영체제는 메모리 페이지를 디스크로 내리기 위해 swap을 사용하곤 한다. 이는 운영체제에 메모리가 부족할 경우 발생한다. 운영체제가 스왑된 페이지를 다시 필요로 할 경우, 이는 다시 메모리로 로딩된다. 스왑은 성능 관점에서 비용이 매우 크므로 가능한 사용하지 않도록 해야한다. 엘라스틱서치는 많은 런타임에 필요한 데이터와 캐시를 메모리에 보관하고 있기 때문에, 비싼 디스크 읽기/쓰기 작업은 클러스터의 성능에 심각한 영향을 미칠 수 있다. 지금부터는 성능을 더 빠르게 하기 위해 스왑을 비활성화하는 방법을 알아볼 것이다. 엘라스틱서치의 스왑 사용을 가장 확실하게 비활성화하는 방법은 elasticsearch.yml 파일에서 bootstrap.mlockall을 true로 설정하는 것이다. 그리고 나서는 이 설정이 적용되었는지를 확인하여야 할 것이다.
로그에서 경고 메시지나 mlockall이 false로 설정되었다는 상태 확인 결과를 보게 된다면, 설정이 제대로 적용되지 않은 것이다. 엘라스틱서치를 구동하는 사용자의 접근 권한이 충분하지 않은 것이 설정이 제대로 적용되지 않은 것에 대한 가장 흔한 이유다. 이 경우 보통 루트 사용자로 접속해 셸에서 ulimit -l unlimited를 실행시켜 해결할 수 있다. 새로운 설정을 적용하기 위해서는 엘라스틱서치를 재시작하여야 한다.
(4) 운영체제 캐시
엘라스틱서치와 루씬은 루씬 세그먼트가 불변 파일이라는 특성으로 인해 운영체제의 파일 시스템 캐시를 적극적으로 사용한다. 루씬은 인메모리 자료구조의 경우 기반의 운영체제 파일 시스템 캐시를 적극적으로 사용하도록 설계되었다. 루씬 세그먼트는 불변 파일들의 형태로 저장된다. 불변의 파일은 캐시 친화적이라고 할 수 있고, 기반이 되는 운영체제는 "핫한" 세그먼트를 빠른 접근이 가능하게 메모리에 상주시키도록 설계되어 있다.
이 결과 작은 색인은 운영체제에 의해 메모리에 통째로 캐싱되어 디스크를 사용하지 않고 빠르게 작업을 수행하게 될 확률이 높아진다. 루씬은 운영체제의 파일 시스템 캐시를 적극적으로 사용하기 때문에, 앞서 추천했던 것처럼 물리 메모리의 절반을 JVM 힙으로 설정했다면, 루씬이 나머지 절반의 대부분을 캐싱에 사용할 것이라고 기대해도 좋다.
이런 단순한 이유로 인해 자주 사용되는 색인들을 성능이 좋은 장비에 위치시키는 것은 좋은 프랙티스라고 여겨진다. 여기서의 아이디어는 루씬이 핫한 데이터 세그먼트를 메모리에 위치하여 빠르게 접근할 수 있도록 해줄 것이고, 이것은 힙에 할당되지 않은 메모리가 충분할수록 쉽게 수행할 수 있을 것이라는 점이다. 하지만 이를 위해서는 라우팅을 이용하여 특정 색인을 성능이 좋은 장비에 할당되도록 해야 할 것이다.
먼저, 특정한 속성인 tag를 모든 노드에 지정해야 한다. 모든 노드는 tag 속성에 대한 유일한 값을 갖고 있다. 예를 들어 node.tag: mynode1 혹은 node.tag: mynode2처럼 말이다. 개별 노드의 설정을 사용하여 특정 태그값을 가진 노드에만 위치할 색인을 생성할 수 있다. 이 예제의 목표는 새로 생성된, 혹은 빈번하게 사용되는 색인을 루씬이 사용할 더 많은 힙 이외의 메모리를 가진 노드에 위치하도록 하기 위한 것이다. 이를 위해서 아래의 명령을 통해 새로운 색인인 myindex는 mynode1과 mynode2라는 태그를 가진 노드에서만 생성될 것이다.
curl -XPUT localhost:9200/myindex/_settings -d '{
"index.routing.allocation.include.tag": "mynode1,mynode2"
}'
이 특정 노드들이 더 많은 힙 이외의 메모리가 할당되어 있다고 가정하면, 루씬이 세그먼트들을 메모리에 캐싱할 것이고, 이를 통해 색인에 대한 응답시간이 세그먼트를 디스크에서 탐색했을 경우보다 매우 빨라질 것이다.
(5) 저장 제한
아파치 루씬은 데이터를 디스크에 불변 세그먼트 파일 형태로 저장한다. 불변 파일은 정의상 루씬에 의해 한번만 쓰이고 여러번 읽히는 것을 의미한다. 머지 작업은 이 세그먼트들에 대해 이루어지는데, 새로운 세그먼트가 생성할 때 동시에 다수의 세그먼트를 읽어야 하기 때문이다. 비록 이런 머지 작업이 보통은 시스템에 큰 부하를 초래하지는 않지만, I/O 성능이 낮은 시스템은 머지나 색인, 검색 작업이 동시에 일어날 때 성능이 크게 저하될 수 있다. 다행히도, 엘라스틱서치는 제한 긴으을 제공하여 I/O가 얼마나 사용할지를 조절할 수 있도록 해주고 있다.
이 제한은 노드 수준 혹은 색인 수준에서 설정할 수 있다. 노드 수준에서의 제한 설정은 전체 노드에 영향을 미치지만, 색인 수준에서의 설정은 지정된 색인에서만 효과를 갖는다.
노드 수준의 제한은 indices.store.throttle.type 속성을 통해 설정할 수 있고 값으로는 none, merge, all을 설정할 수 있다. merge라는 값은 엘라스틱서치에게 노드 전체, 즉 노드에 있는 모든 샤드에 대해 머지 작업으로 인한 I/O를 제한하도록 한다. all 값은 노드의 모든 샤드에 대해 모든 작업의 제한 한계점을 적용하게 된다. 색인 수준의 제한은 거의 같은 방식으로 설정할 수 있는데, index.stroe.throttle.type 속성을 사용하면 된다. 추가로 이 경우는 값으로 node를 사용할 수 있는데, 이는 노드 전체에 제한 한계를 적용하라는 의미를 갖는다.
노드 수준과 색인 수준 제한 모두의 경우, 엘라스틱서치는 I/O가 사용할 수 있는 최대 초당 바이트를 설정할 수 있는 속성을 제공하고 있다. 노드 수준 설정의 경우에는 indices.store.throttle.max_bytes_per_sec를 사용하면 되고 색인 수준 설정의 경우는 index.store.throttle.max_bytets_per_sec를 사용하면 된다. 값들은 초당 메가바이트 형태로 표현된다.
indices.store.throttle.max_bytes_per_sec: "50mb"
index.store.throttle.max_bytes_per_sec: "10mb"
자신의 클러스터에 가장 적절한 값을 찾아내는것은 실험을 통해 확인해야 한다. 시스템의 I/O 대기 빈도가 높거나 성능이 저하되고 있다면, 이 값을 낮추는 것이 문제의 심각성을 줄여줄 것이다.
데이터 백업하기
엘라스틱서치는 풍부한 기능을 가진 증분식 데이터 백업 방법을 제공한다. 스냅샷과 복원 API는 개별 색인 데이터, 모든 색인, 클러스터 설정까지도 원격 저장소나 다른 연동할 수 있는 백엔드 시스템에 백업할 수 있도록 해준다. 이후에는 이를 통해 기존 클러스터나 새로운 클러스터에 이 백업을 복원할 수 있다.
스냅샷의 일반적인 용도는 물론 장애 복구를 위한 백업이겠지만, 프로덕션 환경의 데이터를 개발이나 테스트 환경으로 복제하는 경우나 큰 규모의 변경작업을 수행하기 이전의 보험 용도로서도 유용할 수 있다.
(1) 스냅샷 API
데이터를 백업하기 위해 처음으로 스냅샷 API를 사용한다면, 엘라스틱서치는 클러스터의 상태와 데이터를 복사하게 된다. 이후의 모든 스냅샷은 이전 것으로부터의 변경 사항만을 포함하게 된다. 스냅샷은 논블로킹 작업이기 때문에, 구동중인 시스템에서 수행하여도 성능에 미치는 가시적인 영향은 없다. 게다가, 추가적인 스냅샷들은 기존 것으로부터의 증분인 형태이기 때문에, 시간이 지날수록 스냅샷의 크기는 작아지고 빠르게 수행된다.
스냅샷은 저장소에 저장된다는 점을 주의할 필요가 있다. 저장소는 파일 시스템이나 URL로 정의할 수 있다.
- 파일 시스템 저장소의 경우 공유 파일 시스템이어야 하고, 그 공유 파일 시스템은 클러스터의 모든 노드에 마운트되어 있어야 한다.
- URL 저장소는 읽기 전용이고 따라서 스냅샷을 보관할 대안적인 방법이라고 할 수 있다.
어떻게 스냅샷을 생성하고 복원하며 벤더가 제공하는 클라우드 저장소와 연동하는 플러그인을 어떻게 활용하는지에 대한 것들을 살펴보자.
(2) 공유 파일 시스템에 데이터 백업하기
클러스터 백업은 아래 세가지 절차를 통해 이루어지고, 각각에 대해서 자세히 살펴본다.
- 저장소 정의 - 엘라스틱서치에게 저장소가 어떤 구조의 것인지를 알려주는 것
- 저장소의 존재 여부 확인 - 저장소 정의를 통해 생성한 저장소를 다시한번 검증하는 것
- 백업실행하기 - 첫번째 스냅샷을 간단한 REST API 명령을 통해 실행해볼 수 있다.
스냅샷을 활성화하는 첫번째 단계에서는 공유 파일 시스템 저장소를 정의해야 한다. 다음 예제의 curl 명령은 네트워크에 마운트된 저장 장치에 새로운 저장소를 정의하기 위한 것이다.
curl -XPUT 'localhost:9200/_snapshot/my_repository' -d '{
"type": "fs",
"settings": {
"location": "smb://share/backups",
"compress": true,
"max_snapshot_bytes_per_sec": "20mb",
"max_restore_bytes_per_sec": "20mb"
}
}'
클러스터에서 저장소가 한번 정의되고 나면, 간단한 GET 명령을 통해 이것이 실제로 존재하는지를 확인할 수 있다. 기본 명령이 저장소 이름을 입력하지 않는다면, 엘라스틱서치는 클러스터에 등록된 모든 저장소를 반환한다. 클러스터에 대한 저장소를 생성하고 난 후에는, 최초의 스냅샷(혹은 백업)을 생성할 수 있다.
curl -XPUT 'localhost:9200/_snapshot/my_repository/first_snapshot'
이 명령은 스냅샷 작업을 실행시키고 즉각 반환된다. 만약 스냅샷이 종료된 후까지 기다린 후 요청이 반환되기를 원한다면, 다음과 같이 wait_for_completion 플래그를 추가하면 된다.
curl -XPUT 'localhost:9200/_snapshot/my_repository/first_snapshot?wait_for_completion=true'
저장소 위치의 스냅샷명령이 저장한 목록을 확인하면 엘라스틱서치가 백업한 것들의 패턴을 확인할 수 있다. 스냅샷은 클러스터의 모든 색인, 샤드, 세그먼트, 연관된 메타데이터 정보를 포함하고 있고, 파일 경로의 구조로 볼때 /<index_name>/<shard_number>/<segment_id>와 같은 형태를 가지고 있다. 스탭샷 파일은 크기, 루씬 세그먼트, 그리고 디렉토리 구조에 포함된 각각의 스냅샷들이 가리키고 있는 파일 등을 포함하고 있다.
두번째 스냅샷
스냅샷은 증분 방식, 즉 두 스냅샷 간의 차이만을 저장하는 방식을 사용하기 때문에 두번째 스냅샷 명령은 약간의 파일만을 더 생성할 뿐 전체 스냅샷을 처음부터 재생성하지는 않는다.
curl -XPUT 'localhost:9200/_snapshot/my_repository/second_snapshot'
일반 색인에 대한 스냅샷
스냅샷은 개별 색인 기반으로도 뜰 수 있는데, 다음과 같이 색인명을 PUT 요청에 포함하면 된다.
curl -XPUT 'localhost:9200/_snapshot/my_repository/third_snapshot' -d '{
"indices": "logs-2014,logs-2013"
}'
특정(혹은 모든) 스냅샷의 기본적인 상태 정보를 조회하는 것은 같은 종단점에 GET 요청을 통해 실행할 수 있다.
curl -XGET 'localhost:9200/_snapshot/my_repository/first_snapshot?pretty'
스냅샷은 증분 방식으로 생성되기 때문에, 더이상 필요 없는 스냅샷을 삭제할때는 주의를 기울여야 한다. 오래된 스냅샷을 지울때는 언제나 스냅샷 API를 사용할 것을 권고하는데, 왜냐하면 이 API는 현재 사용되지 않는 세그먼트만 삭제하기 때문이다.
(3) 백업으로부터 복원하기
스냅샷은 실행 중인 어떤 클러스터로도 쉽게 복원시킬 수 있다. 심지어 스냅샷을 생성하 클러스터가 아니어도 가능하다. 스냅샷 API에 _restore 명령을 추가하여 실행하여 전체 클러스터 상태를 복원할 수 있다.
curl -XPOST 'localhost:9200/_snapshot/my_repository/first_snapshot/_restore';
이 명령은 스냅샷들에 저장되어 있는 클러스터의 데이터나 상태를 복원해준다. 이 작업을 통해 클러스터를 특정 시점의 것으로 쉽게 복원할 수 있다.
이전에 스냅샷 작업에서 살펴본 것과 유사하게, 복원 작업은 wait_for_completion 플래그를 포함할 수 있다. 이는 HTTP 요청을 작업이 완전히 종료되기 전까지 블락되도록 하기 위해 사용할 수 있다. 기본적으로 복원 HTTP 요청은 즉각 응답을 반환하고 작업은 백그라운드에서 수행된다.
복원시 추가적인 옵션을 통해 하나의 색인을 이름을 바꿔복원할 수도 있다. 이는 색인을 복제하기를 원하거나 백업으로부터 복원된 색인의 내용을 검증하고자 할때 유용하다.
curl -XPOST 'localhost:9200/_snapshot/my_repository/first_snapshot/_restore'
-d '{
"indices": "logs_2014",
"rename_pattern": "logs_(.+)",
"rename_replacement": "a_copy_of_logs_$1"
}'
이 명령을 수행하면 스냅샷으로부터 다른 모든 색인을 무시하고 logs_2014라는 이름의 색인 하나만을 복원하게 된다. 색인 명이 rename_pattern에서 정의한 것에 매치되기 때문에, 스냅샷 데이터는 새로운 이름의 색인 a_copy_of_logs_2014로 복원될 것이다.
기존에 존재하는 색인을 복원하고자 할 경우, 구동 중인 색인은 반드시 닫아 놓아야 한다. 완료될 경우 복원 작업은 닫힌 색인들을 다시 열어준다.
출처: https://12bme.tistory.com/484?category=737765 [길은 가면, 뒤에 있다.]
'Big Data > 빅데이터' 카테고리의 다른 글
[Kafka] 아파치 카프카 알아보기(2) - 운영 가이드 (0) | 2020.08.03 |
---|---|
[Kafka] 아파치 카프카 알아보기(1) - 프로듀서/컨슈머 (0) | 2020.08.03 |
[Kafka] 카프카 파티션의 이해 (0) | 2020.08.03 |
[ELK] 키바나 사용법 정리 (0) | 2020.08.03 |
[엘라스틱서치] Elasticsearch in action 정리(6) - 성능 극대화 (0) | 2020.08.03 |
[엘라스틱서치] Elasticsearch in action 정리(5) - 집계 데이터 (0) | 2020.08.03 |
[엘라스틱서치] Elasticsearch in action 정리(4) - 유사도 검색 (0) | 2020.08.03 |
[엘라스틱서치] Elasticsearch in action 정리(3) - 데이터 분석 (0) | 2020.08.03 |