티스토리 뷰

연휴에 Loki 구조에 대해 학습하며 간단하게 정리한 내용이 있어 공유해보고자 합니다.
아무런 이해 없이 PoC하며 여러 문서들을 살펴보고 처음 정리해 본 내용으로서 부족한 부분이 많습니다.

더 자세히 살펴보고 싶다면, 아래 공식문서들을 참조해보시면 좋을 것 같습니다.

 

Loki architecture | Grafana Loki documentation

Loki architecture Grafana Loki has a microservices-based architecture and is designed to run as a horizontally scalable, distributed system. The system has multiple components that can run separately and in parallel. The Grafana Loki design compiles the co

grafana.com

 

Loki components | Grafana Loki documentation

Loki components Loki is a modular system that contains many components that can either be run together (in “single binary” mode with target all), in logical groups (in “simple scalable deployment” mode with targets read, write, backend), or individ

grafana.com

 


Loki Architecture

로키의 내부 구조를 살펴보면 아래와 같습니다.

처음 로키의 구조에 대해 생각했을 땐 로키라는 애플리케이션이 있고
내부에 멀티 모듈처럼 여러 코드 컴포넌트들이 나뉘어져 소통하는 것으로 이해했습니다.

위 이해도 틀린 것은 아닙니다. 하나의 모놀리틱으로 배포가 가능하니 말이죠.
하지만, Loki 배포 방법에서 볼 수 있듯 MSA 처럼 모듈 별 Replicas를 다르게해서 배포가 가능합니다.


즉, Distributor는 3개의 Replicas를 두고, Query Frontend는 1개의 Replicas를 두는 것이 가능합니다.
(Loki의 배포방법에 대해서는 다음 글에서 정리해보겠습니다)


이러한 특징은 대규모 환경에서는 참 중요합니다.
리소스 부족에 따라 불필요하게 모든 컴포넌트의 Replicas를 늘릴 필요 없이,
적절히 필요한 컴포넌트의 Replicas만 늘려 비용 최적화를 이뤄낼 수 있습니다
(MSA 구조의 장점과 같습니다).

이 점을 염두해두고 Component 들의 특성들을 이해하시면 좋습니다.
상황에 따라 어떤 Component를 Scale-out하고, Scale-in 할지 결정하는데 도움이 될 것 입니다.

그럼 위 Architecture에서 각 Component 들의 역할에 대해 간단히 살펴보겠습니다.


 

Distributor (분배기)

Distributor는 Loki의 쓰기 경로에서 첫 번째로 만나는 핵심 컴포넌트입니다.
클라이언트(fluentd, logstash, server application 구현체 등)로부터 들어오는 모든 로그 데이터를 처리하는데, 이때 여러 가지 중요한 단계를 거치게 됩니다.

 

첫 번째로 유효성 검사(Validation)를 수행합니다.

들어오는 각 스트림이 올바른지, 설정된 테넌트(또는 전역) 제한 내에 있는지 확인합니다. Prometheus 형식의 라벨인지, 타임스탬프가 적절한지, 로그 라인의 길이는 제한 내인지 등을 검사하죠.

 

두 번째로 전처리 과정(Preprocessing)에서는 라벨을 정규화합니다.

예를 들어 {foo="bar", bazz="buzz"}와 {bazz="buzz", foo="bar"}를 동일하게 취급할 수 있도록 라벨을 정렬하는 거죠. 이렇게 하면 캐싱과 해싱을 결정적으로 수행할 수 있습니다.

 

세 번째로 Rate limiting을 수행하는데, 이는 테넌트별 최대 데이터 수집 속도를 제어합니다.

전체 클러스터 레벨에서 설정된 제한을 현재 활성화된 distributor 수로 나누어 관리하는데, 이렇게 하면 distributor를 스케일링해도 제한이 적절히 조정됩니다.
예를 들어 테넌트 A에 10MB 제한이 있고 distributor가 10개라면, 각 distributor는 1MB/s까지 허용합니다. 나중에 큰 테넌트가 추가되어 distributor를 10개 더 추가하면, 테넌트 A의 제한은 distributor당 500KB/s로 자동 조정되죠.

 

마지막으로 검증된 데이터는 설정된 복제 계수/replication_factor(보통 3)에 따라 여러 ingester로 전달됩니다.

이때 Consistent hashing을 사용해 데이터를 분산시키는데, 각 스트림(특정 라벨셋을 가진 로그들의 집합)에 대해 라벨을 해시하고 그 값을 사용해 ring에서 replication_factor 수만큼의 ingester를 찾아 데이터를 전송합니다.

 

이러한 작업들은 모두 stateless하게 수행되어 쉽게 스케일링할 수 있고, ingester에 가해지는 부하를 분산시킬 수 있습니다. 또한 이런 방식으로 DoS 공격으로부터도 보호할 수 있죠.

모든 distributor 앞에는 로드 밸런서가 있어야 하는데, Kubernetes에서는 서비스 로드 밸런서가 이 역할을 담당합니다.

 


Ingester (수집기)

Ingester는 distributor로부터 받은 로그 데이터를 메모리에서 관리하고 장기 저장소로 보내는 역할을 합니다.
스트림을 받으면 이를 메모리상의 여러 "청크(chunks)"로 구성하는데, 이 청크들은 정해진 간격으로 저장소로 플러시됩니다.

청크는 세 가지 경우에 압축되고 읽기 전용으로 표시됩니다.

  • 현재 청크가 용량(설정 가능)에 도달했을 때
  • 너무 오랫동안 업데이트되지 않았을 때
  • 플러시가 발생할 때

청크가 읽기 전용이 되면 새로운 쓰기 가능한 청크가 그 자리를 대체합니다.

 

ingester는 자신의 상태를 lifecycler를 통해 관리하는데, 다섯 가지 상태가 있습니다.

  • PENDING: 다른 ingester로부터 핸드오프를 기다리는 상태 (레거시 모드)
  • JOINING: ring에 토큰을 삽입하고 초기화하는 상태
  • ACTIVE: 완전히 초기화된 상태로 읽기와 쓰기 요청을 모두 처리
  • LEAVING: 종료 중인 상태로 읽기 요청만 처리
  • UNHEALTHY: 하트비트 실패로 distributor가 설정한 상태

 

데이터 손실 방지를 위해 WAL(Write Ahead Log)을 지원하며, 이는 복제 기능과 함께 작동하면 더욱 안전합니다.
갑자기 프로세스가 중단되더라도 WAL을 통해 데이터를 복구할 수 있죠.

 

기본적으로 타임스탬프 순서를 검증하여 로그가 시간순으로 들어오는지 확인합니다. out-of-order 쓰기를 허용하도록 설정할 수도 있습니다.
동일한 나노초 타임스탬프를 가진 로그의 경우, 내용이 완전히 동일하다면 중복으로 간주하여 무시하고, 내용이 다르다면 허용합니다.

 


Query Frontend (쿼리 프론트엔드)

Query Frontend는 Loki의 읽기 경로를 최적화하는 선택적 컴포넌트입니다.

이 컴포넌트가 배치되면 모든 쿼리 요청은 먼저 Query Frontend로 전달되어야 하며, 실제 쿼리 실행은 여전히 Querier가 담당합니다.

 

Query Frontend는 내부적으로 쿼리를 조정하고 큐에 보관합니다. 이때 Querier들은 마치 작업자처럼 동작하면서 이 큐에서 작업을 가져가 실행하고, 결과를 다시 Query Frontend로 반환하여 집계가 이뤄집니다.

이를 위해 Querier들은 설정에서 Query Frontend의 주소를 알고 있어야 합니다.

 

또한 Query Frontend는 큰 쿼리를 여러 개의 작은 쿼리로 분할하여 병렬로 실행합니다.

예를 들어, 여러 날에 걸친 큰 쿼리가 들어오면 이를 일단위로 나누어 여러 Querier에 분산시키고, 그 결과를 다시 모아서 제공하는 식입니다.

이렇게 하면 단일 Querier의 메모리 부족 문제를 방지하고 쿼리 실행 속도도 높일 수 있습니다.
추가적으로 여러 캐싱 기능들도 제공하고 있습니다.

 


Query Scheduler (쿼리 스케줄러)

Query Scheduler는 Query Frontend보다 더 발전된 형태의 Queuing을 제공하는 선택적 컴포넌트입니다.

 

Query Frontend가 쿼리를 분할하면, 이 조각들을 Query Scheduler의 내부 메모리 큐로 전달합니다.

Querier들은 이 스케줄러에 연결되어 큐에서 작업을 가져가고 실행한 뒤, 결과를 다시 Query Frontend로 전달합니다.

 

특히 각 테넌트별로 별도의 큐를 유지하여 테넌트 간 공정성을 보장하는 것이 특징입니다.

예를 들어, 테넌트 A와 B가 있을 때 각각의 큐를 별도로 관리하므로, 테넌트 A가 많은 쿼리를 보내더라도 테넌트 B의 쿼리 처리가 지연되지 않습니다.

무상태(stateless) 컴포넌트이지만, 내부 메모리 큐를 사용하기 때문에 고가용성을 위해 보통 둘 이상의 복제본을 운영합니다. 대부분의 경우 두 개의 복제본이면 충분합니다.

 


Querier (질의기)

Querier는 LogQL 쿼리를 실제로 실행하는 핵심 컴포넌트입니다.

운영 모드에 따라 클라이언트의 HTTP 요청을 직접 처리하거나("Single binary" 모드), Query Frontend

또는 Query Scheduler로부터 서브쿼리를 받아 처리할 수 있습니다("Microservice" 모드).

 

Querier는 두 곳에서 데이터를 가져옵니다.

먼저 모든 Ingester에 쿼리하여 아직 저장소로 플러시되지 않은 최신 데이터를 확인하고, 그다음 장기 저장소를 조회하여 과거 데이터를 가져옵니다.

복제로 인해 동일한 데이터가 여러 번 조회될 수 있는데, Querier는 내부적으로 같은 나노초 타임스탬프, 라벨셋, 로그 메시지를 가진 데이터의 중복을 제거합니다.

 


Index Gateway (인덱스 게이트웨이)

Index Gateway는 메타데이터 쿼리를 효율적으로 처리하기 위한 특별한 컴포넌트입니다.

여기서 메타데이터 쿼리란 인덱스에서 데이터를 조회하는 쿼리를 의미하는데, 이는 Single Store TSDB나 Single Store BoltDB와 같은 "shipper stores"를 사용할 때만 활용됩니다.

 

이 컴포넌트의 역할을 이해하기 위해서는 실제 동작 흐름을 살펴보면 좋습니다.

예를 들어, Query Frontend가 큰 쿼리를 어떻게 나눌지 결정하려면 로그 데이터의 볼륨을 알아야 하는데, 이때 Index Gateway에 로그 볼륨 쿼리를 보냅니다.

또한 Querier가 특정 쿼리에 필요한 청크들을 찾으려면 먼저 어떤 청크들이 관련이 있는지 알아야 하는데, 이를 위해 Index Gateway에 청크 참조를 요청합니다.

 

Index Gateway는 두 가지 모드로 운영할 수 있습니다.

Simple 모드에서는 각 Index Gateway 인스턴스가 모든 테넌트의 모든 인덱스를 서비스합니다. 이는 구성이 간단하지만 확장성에 제한이 있죠.

Ring 모드에서는 consistent hash ring을 사용해 테넌트별 인덱스를 여러 인스턴스에 분산시킵니다. 이렇게 하면 부하를 분산시킬 수 있고, 특정 인스턴스에 문제가 생겨도 다른 인스턴스가 그 역할을 이어받을 수 있습니다.

 


Compactor (압축기)

Compactor는 Loki에서 단일 인스턴스로 운영되는 특별한 컴포넌트입니다.

주된 역할은 두 가지인데,
첫째는 인덱스 파일들을 효율적으로 압축하는 것이고,
둘째는 로그 보존 정책을 관리하는 것입니다.

 

압축 과정을 통해 Ingester들이 생성한 여러 개의 인덱스 파일들을 테넌트와 일자별로 하나의 파일로 통합합니다.

예를 들어, 하루 동안 여러 Ingester가 각각 생성한 인덱스 파일들이 있다면 Compactor는 이들을 하나의 효율적인 일간 인덱스 파일로 만드는 거죠.

이렇게 하면 나중에 인덱스를 조회할 때 여러 파일을 열어볼 필요 없이 하나의 파일만 확인하면 되므로 성능이 크게 향상됩니다.

 

이 작업을 위해 Compactor는 정기적으로 저장소에서 파일들을 다운로드하고, 병합하고, 새로운 통합 인덱스를 업로드한 다음 원본 파일들을 정리합니다.

또한 설정된 보존 기간에 따라 오래된 로그 데이터를 자동으로 삭제하는 역할도 수행합니다.

 


Ruler (규칙 관리자)

Ruler는 Loki에서 규칙과 알림을 관리하는 중요한 컴포넌트입니다.

마치 Prometheus의 규칙처럼, Loki에서도 로그 데이터를 기반으로 특정 조건을 모니터링하고 필요할 때 알림을 발생시킬 수 있습니다.

 

규칙 설정은 객체 저장소나 로컬 파일시스템에 저장되며, 관리 방법은 두 가지가 있습니다.

Ruler API를 통해 프로그래매틱하게 관리하거나, 직접 저장소에 설정 파일을 업로드하는 방식입니다. 이러한 유연성 덕분에 다양한 환경에서 효과적으로 운영할 수 있습니다.

 

`remote rule evaluation` 기능을 이용하여 Ruler가 규칙 평가를 Query Frontend에 위임할 수도 있습니다.

이렇게 하면 Query Frontend가 제공하는 쿼리 분할, 샤딩, 캐싱 등의 장점을 활용할 수 있어 더 효율적인 규칙 평가가 가능합니다.

 


Bloom 관련 컴포넌트들 (Experimental feature)

마지막으로 실험적으로 제공되는 Bloom 관련 컴포넌트들이 있습니다.

이들은 쿼리 성능을 최적화하기 위한 실험적인 기능들을 제공합니다.

 

Bloom Planner는 싱글톤으로 운영되며 bloom 생성 작업을 계획합니다.

특정 날짜와 테넌트에 대해 어떤 bloom이 이미 만들어졌는지, 어떤 시리즈가 새로 추가되어야 하는지를 고려하여 작업을 계획하고, 이를 큐에 넣어 Bloom Builder들이 처리할 수 있게 합니다.

 

Bloom Builder는 이 계획된 작업들을 실제로 수행합니다.

로그 엔트리의 메타데이터를 분석하여 bloom 블록을 생성하는데, 이 블록들은 하루 단위로 여러 시리즈와 청크들을 포함합니다. 또한 어떤 블록들이 각 시리즈와 TSDB 인덱스 파일에 사용 가능한지 추적하는 메타데이터 파일도 만듭니다.

 

Bloom Gateway는 청크 필터링 요청을 처리합니다.

Index Gateway가 쿼리와 관련된 청크들을 찾을 때, Bloom Gateway를 통해 불필요한 청크들을 미리 걸러낼 수 있습니다. 이는 마치 도서관에서 책을 찾을 때 카테고리를 먼저 확인하여 찾아볼 필요 없는 섹션을 제외하는 것과 비슷합니다.

 


이렇게 Loki Architecture와 내부 Components들에 대해 살펴보았습니다.

처음에 이야기드렸듯 내부 구조를 아는 것은 어떻게 동작하는가를 살펴볼 뿐 아니라 배포할 때 어떻게 배포할 지에 영향을 중요한 부분 입니다.

어떠한 상황에서 어떤 Component를 Scale-out 할지, Scale-in 할지 고민이 필요하기 때문입니다.
이러한 배포 방법에 대해서는 다음 글에서 정리해보도록 하겠습니다.

 

부가적인 내용으로 Tempo도 Loki Components와 유사한 Components를 이용하여 구성되어져 있습니다.

 

Tempo architecture | Grafana Tempo documentation

Scaling your distributed tracing with Grafana Tempo In this demo, we’ll show how Grafana Tempo allows you to scale tracing as far as possible with less operational cost and complexity than ever before.

grafana.com

이름이 같은 Component들이 실제 동작하는 액션은 다를 수 있어도 비슷한 목적을 가진 것을 보실 수 있습니다.
(자세한 동작에 대해서는 위 공식문서를 참조해주세요)

 

긴 글 읽어주셔서 감사합니다.

320x100
반응형
댓글
반응형
250x250
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/05   »
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
글 보관함