대규모 로그 처리도 OK! Elasticsearch 클러스터 개선기

이준환 · 토스증권 Data Engineer
2023년 10월 12일

로그 수집 현황

토스증권이 운영하는 서비스와 인프라에서는 매일 수많은 로그들이 생성되고 있고, 이를 Elasticsearch 클러스터로 수집하여 로그를 검색하고 분석하고 있습니다. 이러한 로그들은 약 100여 개의 로그 파이프라인을 통해 하루 기준으로 22테라 바이트, 약 170억 건의 로그를 인덱싱하고 있는데요, 서비스가 커질수록 수집되는 로그는 더욱 늘어나기 때문에 수평적으로 확장하고 안정적으로 운영하기 위해 지속적으로 클러스터의 개선이 필요합니다.

실제로 SLASH23 발표를 준비하던 시기에는 피크 시간에 초당 60만의 인덱싱과 하루 56억 건 정도의 로그를 처리하였지만 수 개월이 지난 지금은 로그가 3배 이상 늘어나서 피크 시간에 초당 200만 이상의 인덱싱, 하루 기준으로 약 170억 건의 로그를 처리하고 있습니다.

토스증권 Elasticsearch 클러스터는 온프레미스로 운영하고 있어 클러스터가 커질수록 상면 공간과 관리 부담이 있기 때문에 가능한 효율적으로 구성을 하는 것이 필요한데요, 거의 대부분의 로그 검색과 분석은 최근 보름 이내의 로그를 대상으로 하기에 Hot-warm 아키텍처를 도입하면 보다 효율적으로 운영할 수 있다고 판단하였고 Hot 노드보다 더 큰 디스크를 가진 Warm 노드를 구성한 후 생성된 지 오래된 인덱스는 Warm 노드로 이동하고, 일정 기간이 지나면 삭제하도록 하는 ILM (Index Lifecycle Management)을 구성하였습니다.

이러한 구조를 채택하여 만약 최근 로그를 더 오래 보관하고 싶다면 Hot 노드 증설을 하고, 전체 로그를 더 오래 보관하고 싶다면 Warm 노드만 증설하는 등 목적에 따라 확장할 수 있도록 구성하였습니다.

Elasticsearch 클러스터 안정화

그런데 이처럼 수백 테라바이트 단위로 로그 수집 규모가 매우 커지면서 Elasticsearch 클러스터의 장애가 종종 발생하였는데요, 클러스터가 매우 느려져서 로그 인덱싱이 크게 지연되기도 하고 키바나에서 로그 검색을 할 때에도 검색 지연이 크게 발생했고 때로 데이터 노드가 내려가는 장애가 있었습니다.

클러스터가 크게 느려졌을 때 노드들의 상태는 공통적인 패턴을 보였는데요, 매우 많은 fielddata 메모리를 사용하고 있었고 매우 긴 가비지 컬렉션이 빈번하게 발생하여 가비지 컬렉션이 끝날 때까지 클러스터가 일시적으로 멈추는 패턴이었습니다.

매우 많은 fielddata 메모리 사용

잘못 사용한 fielddata 옵션

저희가 장애를 겪은 경우는 인덱스 매핑에서 일부 필드 설정에 fielddata 옵션을 잘못 사용한 경우였습니다.

Elasticsearch 및 Lucene에 Doc value 스토리지가 도입되기 전에는 필드에 대해 aggregation과 정렬을 하기 위해서 필드의 값들을 fielddata라는 메모리 영역으로 올려서 fielddata에서 aggregation을 수행하고 정렬을 하였었는데요, 매우 큰 인덱스의 필드들을 메모리로 올려서 사용하다 보니 Elasticsearch의 JVM 메모리가 항상 부족해지고 매우 긴 가비지 컬렉션이 발생하여 이로 인해 클러스터가 일시적으로 멈추는 현상이 빈번했습니다. 그래서 Elasticsearch는 파일 시스템을 사용하고 컬럼 기반인 Doc value storage를 도입하여 aggregation과 정렬을 수행할 때 힙 메모리를 덜 사용하고 운영체제 레벨 캐시를 적극적으로 사용하는 방향으로 발전되어왔습니다.

따라서 Doc value 스토리지를 지원하는 지금은 특별한 경우가 아니면 fielddata를 사용할 일이 없습니다. 인덱스의 크기가 작다면 문제가 되지 않겠지만 인덱스의 크기가 커지면 이는 금방 장애로 이어질 수 있습니다. 저희의 장애 당시 설정에서는 단지 7개의 필드에서 fielddata 옵션이 설정되어 있었는데 유입되는 로그의 양이 매우 많다 보니 이로 인해 데이터 노드의 힙 메모리가 금방 가득 차게 되고 가비지 컬렉션이 빈번하게 발생하였습니다.

Doc value 스토리지를 지원하지 않는 text 타입을 aggregation 하려는 목적에서 fielddata를 사용할 수는 있지만 가급적 사용하지 말고 다른 방법으로 문제를 푸는 것이 좋습니다.

인덱스 매핑

또 하나의 Elasticsearch 클러스터의 안정성을 위협하는 것은 mapping explosion입니다.

유입되는 로그가 많아지고 규모가 커질수록 인덱스 매핑이 정말 중요해지는데요, Elasticsearch 클러스터가 느려지고 불안정한 경우 원인 분석을 하면 대부분 인덱스 매핑이 비효율적으로 정의되어 있는 경우가 많습니다.

만약 관리하는 Elasticsearch 클러스터가 느려지고 불안정하다면 맨 먼저 인덱스 매핑을 살펴보는 것이 좋은데요, 인덱스에 너무 많은 필드들이 매핑되어 있는지와 fielddata 옵션을 사용하는 필드가 있는지, default dynamic mapping을 사용하고 있는지 점검해 보면 좋습니다.

Elasticsearch는 명시적인 설정을 하지 않는다면 들어온 json의 형태 그대로 인덱싱을 하고 동적으로 인덱스 매핑을 업데이트하는데요,

이런 특성으로 인해 입력으로 들어오는 json에 key가 매우 많다면 mapping explosion이 일어나고 마스터 노드가 클러스터 상태를 업데이트하고 관리하는 데 리소스를 매우 많이 사용하여 클러스터가 불안정하게 됩니다. 따라서 인덱싱할 데이터가 너무 많은 키를 가지지 않도록 해야 하지만 어쩔 수 없이 임의의 구조를 가진 데이터를 인덱싱해야 한다면 flattened type를 사용하거나 dynamic field를 false로 하는 것이 필요합니다. dynamic field 옵션을 false로 설정하면 명시적으로 매핑한 필드만 인덱싱되고 그 이외의 필드는 인덱싱되지 않기 때문입니다.


{
 "dynamic_templates": [
   {
     "strings_as_keyword": {
       "mapping": {
         "ignore_above": 256,
         "type": "keyword"
       },
       "match_mapping_type": "string"
     }
   }
 ]
}
{
 "mappings": {
   "dynamic": false,
   "properties": {
     "user": {
       "properties": {
         "name": {
           "type": "keyword"
         }
       }
     }
   }
 }
}

명시적인 매핑, flattened type, dynamic field를 끄는 것들은 결국 인덱스에 의도하지 않게 너무 많은 필드가 생성되지 않도록 하는 것이 목적입니다. 안정적인 Elasticsearch 클러스터를 운영하기 위해서 가장 중요한 점은 인덱스 필드를 제어할 수 있어야 합니다.

적절한 샤드 개수

그 외의 인덱스 설정에서 고려해야 하는 것은 인덱스의 샤드 개수입니다. 샤드 수를 결정하는 데에 정답은 없지만 토스증권에서는 헤비 인덱스의 경우 프라이머리 샤드의 개수는 Hot 노드의 수와 동일하게 하고 있습니다.

데이터 노드의 CPU 사용량이 여유가 있다면 프라이머리 샤드 개수를 더 늘려서 초당 인덱싱 처리량을 개선할 수 있어요. 다만 프라이머리 샤드를 더 늘린다면 샤드가 특정 노드에 쏠려서 핫스팟 노드가 될 수 있기 때문에 샤드의 수를 Hot 노드의 배수로 설정하는 것이 좋습니다.

index refresh time은 60초로 설정해서 세그먼트 생성과 머지가 적게 발생하도록 하였고, flush_threshold_size는 기본값인 512MB의 두 배인 1GB로 설정하여 트랜스로그 플러시를 실행하는 빈도를 낮추도록 하였습니다.

마지막으로 슬로우 쿼리 로깅을 활성화하여 클러스터에 부하를 줄 수 있는 비용이 비싼 쿼리가 실행되는 것을 모니터링하고 있습니다.

{
 "index": {
   "translog": {
     "flush_threshold_size": "1024MB"
   },
   "refresh_interval": "60s",
   "codec": "best_compression",
   "search": {
     "slowlog": {  }
   },
   "indexing": {
     "slowlog": {  }
   }
}

Vector 로그 파이프라인 전환

그 이후에 진행한 것은 로그 파이프라인 전환이었습니다.

Elasticsearch로 로그를 인덱싱하는 로그 파이프라인은 현재까지 약 100여 개를 운영하고 있습니다. 로그 파이프라인은 Logstash를 사용하여 운영하였는데 Logstash는 범용적으로 사용할 수 있게 다양한 설정을 제공하지만 JVM 기반으로 만들어져있어 시스템 자원을 많이 사용하는 단점을 가지고 있습니다.

Logstash 기반 파이프라인이 점점 늘어나서 쿠버네티스 클러스터에서 260GB 이상의 메모리를 사용하는 상황이 발생하였고 앞으로 로그 파이프라인은 더욱 늘어날 예정이기 때문에 Logstash를 대체할 수 있는 경량 로그 파이프라인으로 전환이 필요하였습니다.

로그 파이프라인 전환을 위하여 여러 경량 로그 파이프라인 오픈 소스들을 검토하였고 그중 로그 가공과 정제를 코드로 유연하게 작성할 수 있는 vector를 선택하였습니다.

Vector는 Datadog에서 공개한 Rust 기반 경량 log shipper이고 고성능과 메모리 효율을 목표로 하여 높은 워크 로드 환경에서 리소스 효율적으로 로그 파이프라인을 운영할 수 있습니다. 또한 다양한 source들과 sink들을 제공하고 있어서 목적에 맞게 로그 파이프라인을 구성하고 확장할 수 있는 장점이 있습니다.

그리고 Logstash에서 아쉬웠던 부분이 로그 파이프라인 모니터링이었는데 Vector로 전환한 후 prometheus exporter로 로그 파이프라인 모니터링을 쉽게 구현할 수 있었고, 로그 파이프라인 모니터링을 통해 파이프라인에 문제가 생겼을 때 이를 빨리 파악하고 개선이 필요한 부분을 쉽게 알 수 있게 되었습니다.

Vector로 로그 파이프라인을 전환한 후 시스템 자원을 많이 절약할 수 있었는데요, Logstash는 범용적으로 사용하기 좋은 장점이 있지만 JVM 기반으로 되어 있어 시스템 리소스를 제법 많이 사용하는 문제가 있었고 운영의 편의성을 위해 로그 파이프라인들을 각각 별도의 프로세스로 띄우고 있었기 때문에 Logstash 인스턴스가 많아지면 메모리 사용량이 많을 수밖에 없었는데 이 부분을 해소할 수 있었습니다. 기존에 약 260GB 정도 사용하고 있던 메모리 사용량이 10GB 수준으로 크게 줄었습니다. 약 96% 이상 메모리를 절약하는 성과가 있었습니다.

Vector 전환 이후 시간이 흘러 로그 파이프라인이 더 늘어나고 유입되는 로그도 3배 이상 증가한 현재는 Vector 파이프라인들의 메모리 사용량은 약 23GB 정도로 측정되었습니다. 기존 Logstash 기반의 로그 파이프라인을 그대로 유지하고 있었다면 쿠버네티스 클러스터에서 수백 GB의 메모리를 사용하게 되었을 상황을 예방할 수 있었습니다.

여러 데이터센터 간 클러스터링

로그 파이프라인을 vector로 전환한 후 다음으로 진행한 것은 데이터센터 확장이었습니다. 데이터센터 이중화를 위하여 새로운 데이터 센터가 추가되어 새로운 데이터 센터에서 생성되는 로그를 수집하는 Elasticsearch 클러스터가 필요해졌는데요, 이렇게 커진 Elasticsearch 클러스터를 한 세트 더 구축하고 운영할 수도 있지만 더 나은 방법이 없을까 고민을 하였고 데이터센터 간에 하나의 Elasticsearch 클러스터를 구축할 수 있을지 검토하였습니다.

사실 서로 다른 데이터센터의 Elasticsearch 노드들을 하나의 클러스터로 묶는 방법은 elastic에서는 권장하지 않는 방법입니다. 이는 노드들이 빈번하게 통신하는데 데이터센터 간의 네트워크 레이턴시가 높으면 클러스터의 전체적인 성능 저하가 발생하기 때문입니다.

하지만 데이터센터 간의 거리가 짧고 네트워크 레이턴시가 작다면 리전 내의 가용성 존(AZ, Availability Zones)로 볼 수 있지 않을까 생각했습니다. AWS가 서울 리전에서 4개의 Zone으로 구성되어 있는 것처럼요. 그리고 수백 테라바이트의 로그 수집과 분석이 목적이기 때문에 조금의 지연보다 비용 절감으로 얻을 수 있는 장점이 더 크다고 생각했습니다.

대신 Elasticsearch가 샤드를 복제하거나 복구할 때 많은 네트워크 트래픽을 점유하기 때문에 클러스터 안정성을 위해서 Elasticsearch 클러스터를 위한 전용 회선을 별도로 구축하였고 하나의 IDC가 장애가 났을 경우를 대비하여 replica shard는 서로 다른 IDC에 저장하도록 구성하였습니다.

IDC 간에 하나의 Elasticsearch 클러스터를 구축하기 위해서는 투표 전용 마스터 노드가 필요합니다. 각각의 IDC에는 마스터 노드 1개를 배치하고, AWS에 투표 전용 마스터 노드를 배치하여 총 3대의 마스터 노드를 배치하는 구조입니다. 마스터 선출을 위한 투표만 수행하는 노드를 제3의 장소인 AWS에 배치하여 IDC1의 마스터 노드와 IDC2의 마스터 노드를 타이브레이커로 묶게 됩니다. 이를 통해 DCI 단절 시 발생할 수 있는 split brain 문제를 방지할 수 있습니다.

또한 하나의 데이터센터가 장애가 발생하였을 때 데이터 유실을 방지하기 위하여 Shard awareness를 설정하여 primary shard와 replica shard가 서로 다른 IDC에 배치되도록 하였습니다. 그리고 일시적인 DCI(Data Center Interconnect) 장애 시 샤드 복제가 과도하게 일어나는 것을 방지하기 위해 force awareness를 설정하여 IDC1과, IDC2의 데이터 노드들이 클러스터에 합류하였을 때만 샤드 배치가 일어나도록 설정하였습니다. 마지막으로 전송 계층에서 인덱싱 데이터에 대해 압축 설정을 하면 전송 시 발생하는 네트워크 대역폭을 많이 줄일 수 있습니다.

# IDC1의 데이터 노드
transport.compress: indexing_data
node.attr.zone: dc1
# IDC2의 데이터 노드
transport.compress: indexing_data
node.attr.zone: dc2
# 마스터 노드
transport.compress: indexing_data
cluster.routing.allocation.awareness.attributes: zone
cluster.routing.allocation.awareness.force.zone.values: dc1,dc2

다음은 IDC1과 IDC2의 Elasticsearch 노드들을 하나의 클러스터로 묶은 전체 아키텍처 그림입니다.

DCI 간 네트워크 단절이 발생한다면 IDC1 혹은 IDC2의 마스터 노드가 투표 전용 마스터 노드의 투표를 통해 마스터 노드로 선출되고 같은 구역에 있는 노드들만 클러스터에 남게 됩니다.

이후 DCI 장애가 해소되면 반대편 IDC에 있는 노드들이 다시 클러스터에 합류하게 됩니다. 이런 구조를 통해 하나의 데이터센터 장애에도 견딜 수 있는 Elasticsearch 클러스터를 운영할 수 있게 되었습니다.

댓글 0댓글 관련 문의: toss-tech@toss.im
연관 콘텐츠
㈜비바리퍼블리카 Copyright © Viva Republica, Inc. All Rights Reserved.