DevOps
Observability를 위한 LGTM 첫걸음
Ellie여기어때
2025년 12월 22일
원문에서 보기 ↗안녕하세요.
여기어때컴퍼니 SRE팀에서 클라우드 업무를 담당하고 있는 엘리입니다.
여기어때 서비스 전반의 모니터링을 통합적으로 관리하기 위해 Observability 스택 LGTM을 25년에 도입했습니다. 처음 도입할 때는 Grafana를 제외하면 나머지 구성 요소를 직접 다뤄본 경험이 없었습니다. 그래서 각 구성 요소의 배포 모드, 아키텍처 구조, 컴포넌트별 역할을 이해하는 것이 큰 과제였습니다.
특히 Mimir, Tempo, Loki뿐만 아니라 EKS 환경에서 Helm Chart를 활용한 배포도 처음이었기 때문에 초반에는 시행착오가 많았습니다.
이 글은 저와 같이 LGTM을 처음 접하는 분들께, 각 구성 요소의 구조를 이해하는 데 도움이 되고자 작성했습니다.
1. LGTM이란?
LGTM은 Loki, Grafana, Tempo, Mimir의 앞글자를 딴 것으로 log, metric, trace를 하나로 통합 Grafana Labs의 Observability 스택입니다.
서버나 애플리케이션 규모가 커지면 “왜 느려졌지?”, “어디서 실패가 났지?” 같은 질문에 답하기가 점점 어려워지는데, 이런 상황에서 LGTM은 세 가지 신호를 함께 분석해 문제의 원인과 영향을 빠르게 파악할 수 있게 도와줍니다.
Loki
라벨 기반 인덱싱으로 log를 비용 효율적으로 저장하고 검색합니다.
대규모 환경에서도 특정 서비스나 네임스페이스의 오류 log를 쉽고 빠르게 좁혀가며 원인 단서를 찾기에 적합합니다.
Grafana
log, metric, trace를 하나의 인터페이스에서 탐색하고 시각화할 수 있습니다. 대시보드와 Explore, Alert, Oncall workflow 연계로 분석에서 대응까지 이어집니다.
Tempo
분산 trace 백엔드로, 요청의 실제 경로와 구간별 지연을 추적합니다. traceID 중심의 설계 덕분에 병목 지점을 정확하게 찾아내 원인 파악에 용이합니다.
Mimir
Prometheus와 완전히 호환되는 수평 확장형 metric 저장소입니다.
지표를 장기간 보관하고, PromQL을 사용해 빠르게 분석할 수 있어 대시보드와 알림 기능의 기반이 됩니다.
2. 배포 모드
Grafana Mimir, Tempo, Loki는 배포 모드에 따라 구성과 운영 방식이 달라집니다. 운영 환경의 규모와 목적에 따라 배포 방식을 선택하는 것은 단순히 리소스 크기를 결정하는 문제가 아닙니다. 시스템의 확장성과 안정성, 그리고 운영의 복잡성 사이에서 가장 알맞은 균형을 찾는 중요한 결정이기도 합니다.
Monolithic Mode
- 가장 단순한 배포 방식으로, 모든 필수 컴포넌트를 하나의 프로세스에서 실행합니다.
- 테스트나 PoC, 소규모 환경에 적합하며 빠르게 구성할 수 있습니다.
- 하지만 단일 장애 지점(SPOF)이 존재하고, 수평 확장은 구조적으로 한계가 있습니다.

출처 : https://grafana.com/docs/mimir/latest/references/architecture/deployment-modes/
Read-Write Mode
- Monolithic과 Microservice 중간 형태로, 운영 오버헤드를 줄이면서도 read, write, backend 워크로드를 각각 독립적으로 확장할 수 있는 구조입니다.
- Mimir는 Read-Write Mode라 부르며, 다음 세 그룹으로 구성됩니다.
- read: Query Frontend, Querier
- write: Distributor, Ingester
- backend: Compactor, Query Scheduler, Store Gateway
- Store Gateway는 Mimir 전용 컴포넌트이며, 현재 experimental 모드입니다.
- loki는 개념은 동일하지만 Simple Scalable Deploy Mode라 부르며, backend 그룹에 Index Gateway 를 추가로 사용합니다
해당 모드는 Grafana Mimir 3.0 릴리스 버전에서 공식으로 제거되었습니다.
“Removal of experimental features: Some deprecated and experimental flags, including the read-write deployment mode, have been removed.”

출처 : https://grafana.com/docs/mimir/latest/references/architecture/deployment-modes/
Scalable Monolithic Mode
- Tempo는 Scalable Monolithic Mode라는 Monolithic Mode와 유사 개념을 사용하지만, 동작 방식이 다릅니다.
- Tempo는 단일 바이너리 구조를 유지한 채, 내부적으로 모듈을 프로세스 내에서 병렬 실행합니다.
- 즉, 외형상은 Monolithic이지만 내부는 스케일 아웃이 가능하도록 설계되어 있어, Microservice와 Monolithic의 장점을 혼합한 형태라고 할 수 있습니다.
Microservice
- 모든 컴포넌트를 각각 독립 프로세스로 배포합니다.
- 가장 복잡한 방식이지만, 컴포넌트별로 세밀하게 확장할 수 있어 확장성과 안정성이 뛰어납니다.
- 장애 도메인이 잘게 나뉘기 때문에 운영 시 유연성이 큽니다.
- 컴포넌트별 설정이 복잡하다는 단점이 있지만, 대규모 운영 환경에서는 사실상 필수적인 모드입니다.

출처 : https://grafana.com/docs/mimir/latest/references/architecture/deployment-modes/
저희는 LGTM 스택을 Microservice 구조로 구축했지만, 이 글에서는 각 구성 요소를 읽기와 쓰기 흐름으로 나누어 설명드리는 편이 더 이해하기 쉬울 것 같습니다.
그래서 본격적으로 컴포넌트별 설명에 들어가기 전에, 아래의 기본 아키텍처 다이어그램을 통해 데이터가 어떻게 수집되고 조회되는지 큰 흐름을 먼저 살펴보겠습니다.
이 구조를 미리 이해하면, 뒤에 이어질 각 컴포넌트별 아키텍처 설명이 훨씬 쉽게 다가올 것입니다.

읽기 작업

쓰기 작업
3. Mimir 아키텍쳐
구조 및 설명
Distributor
Distributor는 Mimir의 쓰기 경로 시작점으로, 시계열 데이터를 수집하고 유효성을 검증한 뒤 여러 Ingester로 분산 전송합니다.
(1) 유효성 검사
- Prometheus exposition format 준수 여부 확인
- metric 이름·라벨 길이 검증
- 타임스탬프가 허용된 시간 범위 내에 있는지 확인
(2) 데이터 복제 및 분산
- replication factor 값에 따라 여러 Ingester로 복제 전송
- Consistent Hashing 링 구조 기반
(3) Rate Limit 관리
- Distributor는 테넌트별 요청/수집 속도 제한을 적용합니다.
- Request rate: 초당 처리 가능한 요청 수
- Ingestion rate: 초당 수집 가능한 샘플 수
- 초과 시 요청 drop 및 HTTP 429 반환
*아래에는 개념 설명과는 별도로, 초기에 알았으면 구현과 운영이 더 수월했을 옵션들을 간단히 메모로 묶어보았습니다.
(1) 해당 시리즈의 최신 타임스탬프보다 이전인 샘플(순서가 잘못된 샘플)은 err-mimir-sample-out-of-order 에러 log가 발생하며 -ingester.out-of-order-time-windowout_of_order_time_window를 통해 최신 샘플 이전의 샘플을 허용할 수 있음
Ingester
Distributor에서 전달받은 데이터를 메모리(WAL 버퍼)에 임시 저장하고, 주기적으로 장기 저장소에 업로드합니다.
(1) WAL(Write-Ahead Log)
- 데이터 손실 방지를 위한 log 파일
- Ingester 재시작 시 WAL 재생(replay)으로 상태 복원
(2) 데이터 업로드
- 주기적으로 TSDB 블록 단위로 장기 저장소 업로드
(3) Replication / HA 구조
- Distributor와 동일한 replication factor로 동작
- 동일 테넌트의 시계열은 일관성을 위해 동일 링 토큰에 매핑
(1) Ingester가 장기저장소에 보내는 주기가 있으므로, 데이터 누락없이 스케일인을 하기 위해 flush-blocks-on-shutdown 옵션을 활성화 해야 함.replication factor가 적용되는 컴포넌트의 replicas 수를 결정할 때는 replication factor의 수도 고려해야 함.
- loki, tempo는 Ingester 하위에 해당 옵션이 존재하지만 mimir는 Ingester가 아닌 blocks-storage하위인 -blocks-storage.tsdb.flush-blocks-on-shutdown에 있음.
(2) Ingester 리소스를 설정할 때는 replication factor 뿐만이 아니라 -ingester.max-global-series-per-usermax-global-series-per-user옵션, Querier가 Ingester를 조회하면서 발생하는 부하 등도 고려해야 함.
Compactor
장기 저장소에 업로드된 여러 블록을 병합하고, 중복 데이터 제거 및 retention 정책을 관리합니다.
(1) Block Compaction
- 여러 TSDB 블록을 하나의 큰 블록으로 병합
- 중복 샘플 제거 및 인덱스 최적화로 쿼리 성능 향상
(2) Retention / Deletion 관리
- 테넌트별 보존 주기(retention period)에 따라 오래된 블록 삭제
- deletion-mark.json 기반 블록 삭제 처리
(1) Compactor는 block_ranges단위로 compaction을 수행하는데 큰 시간의 블록으로 compaction을 할 때는 리소스를 많이 사용하는데, 이때 ebs의 처리량도 급증하기 때문에 필요 시 storageclass, vac를 통해 처리량 증설
(2) storage class의 경우 volumeClaimTemplates는 Immutable이기 때문에 vac를 추천하며, vac는 EKS 1.31부터 지원
Query Frontend
클라이언트의 쿼리 요청을 큐잉하고, 캐싱 및 쿼리 분할(splitting)을 수행합니다. Querier가 안정적으로 처리할 수 있도록 쿼리 부하를 조정합니다.
(1) Queuing
큐잉 메커니즘
- OOM 방지 및 재시도 : 쿼리어에서 OOM을 일으킬 수 있는 대형 쿼리가 실패했을 때 이를 재시도하며 쿼리를 병렬처리
- 대형 요청 분산 처리 : 여러 개의 대형 요청이 하나의 쿼리어에 몰리지 않도록, 모든 쿼리어에 분산하여 FIFO 큐로 처리
- 테넌트 간 공정성 보장 : 하나의 테넌트가 다른 테넌트를 서비스 거부(DoS) 하지 않도록, 테넌트 간 공정하게 쿼리를 스케줄링합니다.
(2) Splitting
- 장기 범위 쿼리를 여러 개의 작은 쿼리로 나눠 병렬 실행 후 결과를 집계
- 대형 쿼리의 실행 속도를 개선하고 OOM 발생을 방지
(3) Caching
- 쿼리 결과를 캐시하여 반복 요청 시 빠르게 반환
(4) Query Sharding
- 집계 함수(sum, min, max, count, avg)에 대해 쿼리를 샤딩하여 병렬 실행
- 샤딩 불가능한 연산(absent, histogram_quantile 등)은 단일 쿼리로 처리
(1) 쿼리 샤딩 기능인 parallelize_shardable_queries의 default가 false이므로 사용하기 위해 true로 변경해줘야 함.
(2) 캐싱 기능 옵션인 cache_results도 default가 false이므로 사용하기 위해 true로 변경하고, results_cache.backend를 지정해줘야 함.
Query Scheduler
Query Frontend로부터 전달된 쿼리 요청을 중앙 큐에서 관리하고, 여러 Querier로 효율적으로 분배합니다.
즉, 쿼리 부하를 균형 있게 분산하는 중앙 스케줄러입니다.
(1) 중앙 큐 관리
- Querier가 모든 Query Frontend에 직접 연결하지 않고 중앙 큐에서 쿼리를 일괄 관리
- Frontend 인스턴스가 여러 개로 확장되어도 큐의 일관성이 유지
(2) 쿼리 분배
- 여러 Querier가 동일한 큐에서 작업을 가져와 처리하며, Querier 간 부하를 자동으로 분산
- 단일 Querier로 대형 쿼리가 몰리는 현상을 방지
(1) grafana에 문의 시 Query Scheduler는 리소스 사용량이 매우 낮은 컴포넌트이므로 수평확장이 필요하지 않기 때문에 HPA 설정이 적용되어있지 않다는 답변을 받음
(출처: https://github.com/grafana/mimir/issues/7368#issuecomment-2772366031)
(2) scheduler가 동시에 처리할 수 있는 최대 요청 수 옵션인 -query-scheduler.max-outstanding-requests-per-tenantmax_outstanding_requests_per_tenant를 설정할 때는 Querier에 리소스도 고려해야 함
Querier
실제 쿼리 실행을 담당하는 워커입니다. Query Scheduler 또는 Frontend 큐에서 쿼리를 가져와 실행합니다.
(1) 데이터 수집
- Ingester(최근 데이터)와 Store Gateway(과거 데이터) 양쪽에서 조회
(2) 쿼리 실행
- PromQL 엔진 수행 후 결과 집계
(1) 대시보드에서 조회 시 Client.Timeout exceeded while awaiting headers 에러가 발생하는 경우 timeout을 늘려 해결하려고 하는 경우 Querier의 timeout을 늘려도 datasource timeout에 걸리면 Query Frontend에서 쿼리를 cancel 하기 때문에 -querier.timeout(defualt: 2m) 를 늘릴 때, datasource의 default가 30s이므로 같이 조정해야 함.
Store-Gateway
장기 저장소(Object Storage)에서 블록 데이터를 읽고, Querier의 쿼리에 필요한 청크와 메타데이터를 제공합니다.
(1) 주기적 동기화
- 주기적으로 bucket index를 다운로드
- 새로운 블록의 index-header 저장, 삭제된 블록 정리
(2) block index header
- 인덱스 헤더는 블록 인덱스의 부분집합으로 로컬 디스크에 캐시
- 쿼리 시 필요한 데이터만 메모리에 로드(lazy loading)
(3) 캐싱을 통한 성능 최적화
- Index cache: 시계열·라벨 조회 속도 향상
- Chunks cache: 청크 재사용으로 I/O 감소
- Metadata cache: meta.json, deletion-mark.json, bucket-index.json.gz 등 캐싱
(1) mimir Store Gateway의 캐싱 기능을 사용할 경우-blocks-storage.bucket-store.index-cache.backend, -blocks-storage.bucket-store.chunks-cache.backend, -blocks-storage.bucket-store.metadata-cache.backend옵션의 backenddefault가 index-cache의 기본값이 inmemory이고, chunks-cache, metadata-cache는 ““ 이므로 유의.
(2) replication factor 관련
replication factor가 적용되는 컴포넌트의 replicas 수를 결정할 때는 replication factor의 수도 고려해야 함.
replication factor가 너무 낮으면 데이터 유실 가능성이 있으며, 너무 높게 잡으면 메모리를 많이 사용.
(3) HPA/KEDA 및 Helm Chart 관련
HelmChart에서 hpa/keda를 제공하지 않는 컴포넌트가 있기 때문에 extraObjects를 통해 추가해 줘야 함.
extraObjects를 통해 오토스케일링을 추가하면 Ingester replicas와 hpa의 replicas가 충돌 날 수 있기 때문에 Ingester spec.replicas를 ignoreDifferences처리해 줘야 함.
(4) 링 컴포넌트 shutdown 관련
-링을 사용하는 컴포넌트의 경우 unregister_on_shutdown활성화 추천.
-파드가 종료되었을 때 링에서 파드가 빠지지 않는 경우 unhealthy가 되고, unhealthy가 유지되면 too many unhealthy instances in the ring 에러 발생.
4. Tempo 아키텍쳐
tempo의 구조는 mimir와 매우 유사하지만, Store Gateway가 없고, metric-generator가 존재합니다.
또한 mimir는 metric을 저장/조회하였지만, tempo는 trace를 저장/조회할 수 있습니다.
아래에서는 Mimir와 공통된 구성 요소는 생략하고, Tempo에서 달라진 부분 위주로 설명하겠습니다.
구조 및 설명
Metric Generator
- Distributor가 받은 스팬을 Ingester뿐만 아니라 Metrics Generator에도 전달합니다.
- 수집된 trace에서 metric을 파생하고 이를 metric 저장소에 기록합니다.
5. Loki 아키텍쳐
Loki의 전체 구조는 Grafana Mimir와 유사하지만, Store Gateway 대신 Index Gateway가 존재하며, log 데이터를 저장·조회하는 데 최적화되어 있습니다.
아래에서는 Mimir와 공통된 구성 요소는 생략하고, Loki에서 달라진 부분 위주로 설명하겠습니다.
구조 및 설명
Index Gateway
- 메타데이터 쿼리를 처리하고 제공합니다.
- 메타데이터 쿼리는 log 인덱스에서 청크 참조를 찾습니다.
- Querier는 Index Gateway를 통해 받은 참조를 기반으로 청크 데이터를 가져옵니다.
- Query Frontend도 Index Gateway를 활용해 쿼리 샤딩 여부를 판단합니다.
6. 마무리
이번 글에서는 LGTM 스택의 주요 구성 요소인 Mimir, Tempo, Loki의 구조와 특징을 정리했습니다. 각 시스템은 공통된 분산 아키텍처를 기반으로 하지만, 저장하는 데이터의 형태에 따라 설계 방향이 조금씩 다르다는 것을 알 수 있었습니다. 처음 LGTM을 도입했을 때는 모든 구성 요소가 낯설었지만, 구조를 하나씩 이해하고 직접 배포해 보며 Observability 스택의 동작 원리를 깊이 배울 수 있었습니다.
LGTM 스택이 실제 서비스 환경에 완전히 안착하기까지는 아직 갈 길이 남아 있지만, 이 과정을 통해 단순한 설정 이상의 운영 감각과 문제 해결 경험을 얻을 수 있었습니다.
앞으로도 LGTM 스택을 꾸준히 학습하고 개선하며, 보다 안정적인 Observability 환경을 만들어가고자 합니다.
이 글이 LGTM을 처음 접하는 분들께 구조를 이해하고 자신만의 Observability 스택을 설계하는데 작은 출발점이 되길 바랍니다.
읽어주셔서 감사합니다.