Post

[모니터링 시스템 복구기 부록] 개념 정리 — 메트릭이 화면에 뜨기까지

이 글의 도메인, IP, 클러스터 이름 등 사내 식별 정보는 모두 일반화했다.

모니터링 시스템 복구기 #1을 쓰면서, 이전 시리즈 「모니터링 시스템 역추적기」 내내 써 온 개념들을 나 스스로 처음부터 설명할 수 있는지 돌아보게 됐다. 역추적할 때는 “이 설정이 왜 여기 있나”를 쫓느라 개념은 필요한 만큼만 짚고 넘어갔다. 이 글은 그 개념들을 처음 보는 사람도 따라올 수 있게 정리한 것이다.

순서대로 읽으면 “데이터가 어디서 생겨서 어떻게 화면까지 오는가”를 따라가게 된다. 각 개념 끝의 이번 사례에서는 항목은 복구기 #1 복구 작업에서 실제로 마주친 장면이다. 개념이 현장에서 어떻게 문제로 나타나는지 같이 보면 기억에 오래 남는다. 그리고 더 깊이 항목은 같은 개념을 이 시스템의 히스토리로 파고든 시리즈 편으로 이어진다.

이 글의 장이어지는 편
3·5·6. Prometheus, Operator, remote_write역추적기 #1 kube-prometheus-stack
4. Exporter역추적기 #3 JMX Exporter, 역추적기 #4 관리형 서비스 Exporter
7·8. Mimir, 멀티 테넌시역추적기 #2 Mimir
10. Grafana역추적기 #5 Grafana
11. Alertmanager역추적기 #6 Alertmanager → Airflow
12. Loki와 promtail역추적기 #7 Loki & Promtail
13. 네트워크복구기 #1

1. 큰 그림 — 택배 회사로 보기

모니터링 스택 전체를 택배 회사에 비유하면 이렇다.

택배 회사모니터링 스택하는 일
물건메트릭(숫자 데이터)“CPU 사용률 30%”, “요청 120건” 같은 측정값
지점각 Kubernetes 클러스터측정값이 생기는 곳
지점 직원Prometheus지점 안을 돌며 물건을 모으고, 잠깐 보관한다
통역사ExporterPrometheus가 못 알아듣는 시스템(MySQL 등)의 말을 번역한다
본사로 가는 트럭remote_write지점에서 모은 물건을 본사로 보낸다
본사 정문로드밸런서(ALB)·도메인트럭이 들어오는 입구
본사 중앙 창고Mimir모든 지점의 물건을 오래 보관한다
선반 이름표테넌트(X-Scope-OrgID)물건을 어느 선반에 둘지 구분한다
창고 들여다보는 창문Grafana창고 물건을 그래프로 보여준다
경보기Alertmanager이상하면 알린다
별도의 문서 창고Loki숫자가 아니라 글(로그)을 보관한다
문서 수거 직원promtail로그를 모아 Loki로 보낸다

이번 장애는 “지점 직원도, 본사 창고도 멀쩡한데 정문과 길이 막혀 8개월 동안 물건이 한 개도 도착하지 않은” 상황이었다.

1
2
3
4
[지점: 운영 클러스터]                       [본사: 관리 클러스터]
 Exporter ─┐                                ┌─ Mimir (창고) ─ Grafana (창문)
 앱 ───────┼→ Prometheus ─ remote_write ─→ 정문 ─┤
 노드 ─────┘   (직원)        (트럭)        (ALB)  └─ Alertmanager (경보기)

2. 메트릭이란 무엇인가

메트릭 = 시간에 따라 변하는 숫자

메트릭은 “어떤 대상을, 어느 시점에, 몇이라고 쟀는가”다.

1
2
node_cpu_seconds_total{cpu="0", mode="idle", instance="10.0.2.7:9100"}  123456.7  @ 11:35:00
└─────── 이름 ───────┘└──────────────── 라벨 ────────────────────────────┘  └─ 값 ─┘  └ 시각 ┘
  • 이름: 무엇을 쟀나 (node_cpu_seconds_total = CPU가 쓴 시간 누계)
  • 라벨: 어떤 대상을 쟀나. 키=값 쌍으로 붙는 꼬리표다
  • 값: 숫자 하나
  • 시각: 언제 쟀나

시계열(Time Series)

이름과 라벨 조합이 같은 값들이 시간 순으로 쌓인 것을 시계열이라 한다. 위 예시에서 cpu="1"로 라벨이 하나만 바뀌어도 별개의 시계열이다.

비유: 엑셀 시트 하나가 “CPU0의 idle 시간”이라는 시계열이다. 행마다 시각과 값이 하나씩 적힌다.

시계열 수는 라벨 조합 수만큼 늘어난다. 그래서 라벨에 사용자 ID처럼 값이 무한히 많은 것을 넣으면 시계열이 폭발한다(카디널리티 문제). Mimir가 테넌트별로 “시리즈 300만 개” 같은 한도를 두는 이유다.

메트릭 종류

종류성질예읽는 법
Counter계속 늘기만 한다 (재시작하면 0부터)요청 수 누계, 전송한 샘플 수그대로 보면 의미가 없다. rate()로 “초당 얼마나 늘었나”를 본다
Gauge오르내린다메모리 사용량, 현재 Pod 수값 자체를 본다
Histogram / Summary분포응답 시간 구간별 건수백분위(p95 등)를 계산한다

복구 작업 때 본 prometheus_remote_storage_samples_failed_total은 Counter다. 이름 끝의 _total이 Counter라는 관례적 표시다.

up — 가장 중요한 메트릭

Prometheus는 수집할 때마다 대상마다 up이라는 메트릭을 스스로 만든다.

  • up = 1: 수집 성공
  • up = 0: 수집 실패 (대상이 죽었거나 길이 막힘)

그래서 count(up)은 “수집 대상이 몇 개인가”이고, 복구 여부를 판정하는 데 썼다(관리 클러스터 89, 운영 클러스터 17).

이번 사례에서는: exporter가 up=1이어도 안심할 수 없다. up은 “Prometheus가 exporter에 접속했다”는 뜻일 뿐이다. exporter가 진짜 MySQL에 붙었는지는 mysql_up처럼 exporter가 따로 내는 지표를 봐야 한다.


3. Prometheus — 수집하고 저장하는 일꾼

끌어오기(Pull) 방식

Prometheus는 대상이 데이터를 보내주길 기다리지 않는다. 주기적으로 직접 찾아가서 가져온다. 이를 scrape(스크레이프)라고 한다.

1
2
Prometheus ──(15~30초마다 HTTP GET /metrics)──→ 대상
           ←── 텍스트로 된 메트릭 목록 ─────────

대상은 /metrics라는 주소에 현재 값들을 텍스트로 걸어두기만 하면 된다. 아무나 그 페이지를 열어 볼 수 있다. 복구 작업 때 curl localhost:18080/metrics로 distributor의 메트릭을 직접 본 것이 바로 이것이다.

비유: 직원이 각 책상(대상)을 돌며 칠판(/metrics)에 적힌 숫자를 옮겨 적는다.

Pull 방식의 장점:

  • 대상이 죽으면 바로 안다. 가져오기에 실패하니 up=0이 된다.
  • 대상은 Prometheus가 어디 있는지 몰라도 된다.

타깃(Target)과 job

  • 타깃: 스크레이프할 주소 하나 (예: http://10.0.2.7:9100/metrics)
  • job: 같은 종류의 타깃 묶음 (예: node-exporter job에 노드 6개)

복구 작업 때 돌린 점검 스크립트의 “job별 health 집계”가 이것이다. kubelet 6 down은 kubelet job의 타깃 6개가 실패했다는 뜻이었다. 원인은 교체돼 사라진 옛 노드였다.

로컬 저장소 (TSDB)와 보관 기간

Prometheus는 가져온 데이터를 자기 디스크에 저장한다. 이를 TSDB(시계열 데이터베이스)라고 한다.

  • retention(보관 기간): 얼마나 오래 둘지. 운영 클러스터는 7d(7일)다.
  • WAL(Write-Ahead Log): 받은 데이터를 먼저 적어두는 일지다. 갑자기 꺼져도 다시 켜지면 이 일지를 읽어 복구한다. 복구 작업 로그에 찍힌 Replaying WAL이 이 일지를 다시 읽는 장면이다.

Prometheus 혼자의 한계

  • 한 대의 디스크에 저장하니 오래, 많이 보관하기 어렵다.
  • 클러스터마다 따로 있으니 여러 클러스터를 한 화면에서 보기 어렵다.
  • 그래서 본사 창고(Mimir)로 보내는 구조를 쓴다.

이번 사례에서는: 운영 클러스터의 Prometheus는 영구 디스크(PVC)가 없다. 파드가 재시작하면 로컬 데이터가 사라진다. 운영 클러스터 B를 업그레이드할 때 로컬 데이터가 날아간 이유다. Mimir로 보내고 있으니 괜찮다는 설계다.


4. Exporter — 통역사

MySQL, Redis, Kafka, nginx 같은 프로그램은 스스로 /metrics 페이지를 만들지 않는다. 그래서 중간에 exporter를 둔다.

1
2
Prometheus ──scrape──→ mysqld-exporter ──SQL 질의──→ MySQL
                        (번역해서 /metrics로 내놓음)

exporter는 대상 시스템에 접속해 상태를 묻고, 그 답을 Prometheus 형식으로 바꿔 /metrics에 걸어둔다.

exporter대상기본 포트
node-exporter서버(노드) 자체: CPU, 메모리, 디스크, 네트워크9100
mysqld-exporterMySQL9104
redis-exporterRedis9121
kafka-exporterKafka9308
nginx exporternginx9113
kube-state-metricsKubernetes 오브젝트 상태(Pod 수, Deployment 상태 등)8080
JMX exporterJava 앱(JVM 메모리, 스레드 등)우리는 8090

주의: exporter가 살아 있다 ≠ 대상이 살아 있다

exporter와 대상 사이 연결이 끊겨도 exporter 프로세스는 멀쩡히 떠 있을 수 있다. 그래서 두 가지를 따로 봐야 한다.

  • up{job="mysqld-exporter"} = 1 → Prometheus가 exporter에 접속했다
  • mysql_up = 1 → exporter가 MySQL에 접속했다

이번 사례에서는: kafka·redis exporter는 대상에 붙지 못해 응답 자체가 늦어져 up=0(타임아웃)이었다. mysqld exporter는 up=1이지만 이전 연차 DB를 보고 있을 수 있다. 그래서 “엔드포인트만 바꾸기”가 아니라 “현재 대상 기준으로 새로 구성”이 과제다.

더 깊이 — 역추적기 #3 JMX Exporter — 클러스터에 없던 exporter를 이미지 빌드 경로에서 찾아낸 이야기, 역추적기 #4 — 에이전트를 설치할 수 없는 관리형 서비스를 원격으로 수집하는 패턴


5. Kubernetes에서 Prometheus를 운영하는 방법

Kubernetes에서는 Pod가 수시로 생기고 사라진다. 수집 대상 주소를 사람이 일일이 적을 수 없다. 그래서 다음 도구들이 있다.

CRD — Kubernetes에 새 종류의 오브젝트 추가하기

Kubernetes는 원래 Pod, Service, Deployment 같은 오브젝트만 안다. CRD(Custom Resource Definition)는 “이런 종류의 오브젝트도 있다”고 새로 등록하는 기능이다.

Prometheus 쪽에서 추가하는 주요 CRD:

CRD뜻복구 작업 때 본 예
Prometheus“이런 설정의 Prometheus를 하나 띄워라”클러스터마다 하나씩
ServiceMonitor“이 라벨이 붙은 Service를 수집 대상으로 삼아라”app-monitor
PodMonitorService 없이 Pod를 직접 수집—
PrometheusRule알림 규칙·계산 규칙기본 규칙들(운영 클러스터 B에서는 껐다)
AlertmanagerAlertmanager 설정클러스터마다 하나

Prometheus Operator — CRD를 보고 실제로 일하는 관리자

Operator는 CRD를 지켜보다가, 거기 적힌 대로 실제 Prometheus를 띄우고 설정 파일을 만든다.

1
2
3
4
사람: kubectl patch prometheus <이름> (remoteWrite 주소 변경)
  → Operator: "Prometheus 설정이 바뀌었네" → 설정 파일(Secret) 다시 생성
  → config-reloader(사이드카): 설정 파일 변경 감지 → Prometheus에게 "다시 읽어"
  → Prometheus: 재시작 없이 새 설정 적용 ("Completed loading of configuration file")

비유: CRD는 “주문서 양식”, 우리가 쓰는 Prometheus 오브젝트는 “주문서”, Operator는 “주문서대로 일을 처리하는 매니저”다.

Prometheus 주소를 바꿨을 때 재시작 없이 1~2분 만에 반영된 이유가 이것이다.

ServiceMonitor와 라벨 셀렉터 — “누구를 수집할까”

Prometheus 오브젝트에는 “어떤 ServiceMonitor를 따를지” 고르는 조건(셀렉터)이 있다.

1
2
3
serviceMonitorSelector:
  matchLabels:
    release: prometheus      # release=prometheus 라벨이 붙은 ServiceMonitor만 따른다

이번 사례에서는: 이게 운영 클러스터 B에서 앱 메트릭이 빠져 있던 이유였다.

 차트가 만든 ServiceMonitor앱 팀이 만든 ServiceMonitor셀렉터결과
운영 클러스터 Arelease=prometheusrelease=prometheusrelease=prometheus둘 다 수집
운영 클러스터 B (전)release=kube-prometheus-stackrelease=prometheus차트 기본값(kube-prometheus-stack)앱 누락
운영 클러스터 B (후)〃〃두 값 모두 허용둘 다 수집

Helm 차트는 셀렉터 기본값을 자기 릴리스 이름으로 잡는다. A는 릴리스 이름이 마침 prometheus여서 앱 팀 라벨과 우연히 맞았다. B는 이름이 달라서 어긋났다.

kube-prometheus-stack — 한 번에 설치하는 묶음

kube-prometheus-stack은 위 구성요소를 한 번에 설치하는 Helm 차트다. 들어 있는 것:

  • Prometheus Operator + CRD
  • Prometheus (CR 하나)
  • Alertmanager
  • Grafana (선택)
  • node-exporter (DaemonSet: 노드마다 하나씩)
  • kube-state-metrics
  • 기본 ServiceMonitor들(kubelet, apiserver, coredns 등)
  • 기본 알림 규칙(PrometheusRule)과 Grafana 대시보드(ConfigMap)

이번 사례에서는: 세 클러스터 모두 이 차트로 설치돼 있다. 운영 클러스터 B에서 helm diff를 했을 때 Grafana와 대시보드 ConfigMap, 기본 규칙이 삭제 목록에 뜬 것은 grafana.enabled: false, defaultRules.create: false로 이 묶음 중 일부를 끈 결과다.

externalLabels — 지점 도장

externalLabels는 이 Prometheus가 내보내는 모든 데이터에 공통으로 찍는 라벨이다.

1
2
3
externalLabels:
  cluster: b
  origin_prometheus: b

여러 클러스터의 데이터가 한 창고(같은 테넌트)에 섞여도, 이 도장으로 “어느 지점에서 왔는지” 구분한다. Grafana 대시보드 상단의 K8S 드롭다운에 b가 보인 것이 이 라벨 덕분이다. 대시보드 변수가 cluster·origin_prometheus 라벨 값을 읽어 목록을 만든다.

더 깊이 — 역추적기 #1 kube-prometheus-stack — values 파일 두 개, serviceMonitorSelectorNilUsesHelmValues, 클러스터별 values가 갈라진 과정


6. remote_write — 본사로 짐 보내기

무엇인가

Prometheus가 수집한 데이터를 다른 저장소로 실시간 복사해 보내는 기능이다. 로컬에도 저장하면서 동시에 본사(Mimir)로도 보낸다.

1
2
3
4
remoteWrite:
  - url: http://mimir.example.com/api/v1/push     # 어디로
    headers:
      X-Scope-OrgID: federation                   # 어느 선반에 (8장)

내부 동작 — 큐와 샤드

1
수집 → WAL에 기록 → WAL watcher가 읽음 → 큐(Queue) → 샤드 N개가 병렬 전송 → Mimir
  • 큐: 보낼 데이터가 줄 서는 곳
  • 샤드(shard): 동시에 보내는 일꾼 수. 보통 1개로 시작해서, 밀리면 늘린다.
  • resharding: 일꾼 수를 바꾸는 것. 복구 작업 때 본 Remote storage resharding from=1 to=50은 “밀린 게 많으니 일꾼을 1명에서 50명으로 늘린다”는 뜻이었다. 오류가 아니라 전송이 다시 성공하기 시작했다는 신호였다.

실패했을 때의 행동

실패 종류예Prometheus의 행동실패 카운터
재시도 가능타임아웃, 5xx, 429계속 재시도 (Failed to send batch, retrying)안 늘어난다
재시도 불가4xx (잘못된 데이터, 너무 오래된 데이터)그 묶음을 버린다늘어난다

이 표가 복구 작업 때 가장 함정이었던 부분이다. 8개월 내내 타임아웃이었는데 samples_failed_total은 0이었다. 타임아웃은 “재시도하면 될 수도 있는” 실패라 카운터에 안 잡힌다. 그래서 실패 카운터 대신 전송 지연을 봐야 한다.

time() - prometheus_remote_storage_queue_highest_sent_timestamp_seconds
# = 지금 시각 - 마지막으로 성공적으로 보낸 데이터의 시각
# 당시 값: 20,368,031초 ≈ 235.7일 → 3월 21일경부터 하나도 못 보냄

오래된 데이터는 결국 버려진다

막혀 있던 동안의 데이터를 나중에 다 보낼 수 있을까? 아니다.

  • WAL은 보통 몇 시간분만 유지된다.
  • Mimir는 너무 오래된 샘플을 거절한다(out of bounds).

그래서 복구 후 Mimir에는 “복구 시점 이후” 데이터만 쌓였다. 8개월치 공백은 메워지지 않는다.

더 깊이 — 역추적기 #1 — 클러스터별 Prometheus가 중앙으로 remote_write하는 구조가 만들어진 과정, 복구기 #1 — 그 경로가 8개월간 막혀 있던 이유


7. Mimir — 본사 중앙 창고

왜 필요한가

Prometheus 혼자서는 1) 오래 보관하기 어렵고 2) 여러 클러스터를 합쳐 보기 어렵다. Mimir는 여러 Prometheus가 remote_write로 보낸 데이터를 받아 오래, 많이 저장하고 한곳에서 조회하게 해준다. Grafana Labs가 만든 오픈소스다.

구성요소 — 창고 안의 부서들

Mimir는 역할별로 쪼개진 여러 프로그램(마이크로서비스)으로 돌아간다. 우리는 mimir-distributed 차트로 이 구성을 쓴다.

쓰기 경로 (데이터가 들어오는 길)

1
remote_write → nginx(게이트웨이) → distributor → ingester ×6 → (2시간마다) Object Storage
구성요소비유하는 일
nginx (gateway)창고 안내 데스크요청을 받아 쓰기는 distributor로, 읽기는 query-frontend로 보낸다
distributor입고 담당데이터를 검사(한도 초과?)하고 ingester들에게 나눠 준다
ingester임시 선반최근 데이터(약 2시간~12시간)를 메모리에 들고 있다가 블록으로 묶어 Object Storage에 올린다. 안전을 위해 같은 데이터를 3곳(zone a·b·c)에 복제한다
Object Storage대형 장기 보관소S3 호환 저장소(우리는 NCP Object Storage, 버킷 하나). 싸고 크다
compactor정리 담당작은 블록들을 큰 블록으로 합치고, 보관 기간이 지난 블록을 지운다

읽기 경로 (조회하는 길)

1
2
Grafana → nginx → query-frontend → query-scheduler → querier ─┬→ ingester (최근 데이터)
                                                               └→ store-gateway → Object Storage (오래된 데이터)
구성요소하는 일
query-frontend큰 조회를 쪼개고 결과를 캐시한다
query-scheduler조회 요청을 줄 세운다
querier실제로 계산한다. 최근 것은 ingester에, 오래된 것은 store-gateway에 물어본다
store-gatewayObject Storage에 있는 블록을 찾아 읽는다. 빨리 찾으려고 블록 목차(index-header)를 자기 디스크에 받아 둔다

기타

구성요소하는 일
ruler알림 규칙을 Mimir 쪽에서 계산
alertmanagerMimir 내장 알림 관리
overrides-exporter테넌트별 한도 설정을 메트릭으로 노출
rollout-operatorzone별 ingester·store-gateway를 순서대로 재시작하도록 관리

이번 사례에서는

  • store-gateway 3개가 0/1이었던 이유: 버킷 최상위에 정체불명 폴더(monitoring2025...)가 수천 개 있었다. Mimir는 버킷 최상위 폴더 하나하나를 테넌트로 취급한다. 그래서 store-gateway가 그 수천 개 가짜 테넌트의 목차를 받아 두다가 디스크(10Gi)가 꽉 찼다.
  • 그 결과 “최근(ingester) 조회는 되고, 오래된(store-gateway) 조회는 안 되는” 상태다. 약 12시간을 넘는 그래프는 비거나 에러가 날 수 있다.
  • 4월 시도에 있던 storage_prefix: blocks는 “블록을 버킷 최상위가 아니라 blocks/ 폴더 아래에 두라”는 설정이다. 다른 무언가가 버킷 최상위를 어지럽혀도 Mimir가 헷갈리지 않게 하려던 것으로 보인다.

더 깊이 — 역추적기 #2 Mimir — 테넌시가 세 번 바뀐 이유와 한도 설정의 정합성 버그


8. 멀티 테넌시 — 창고 선반의 이름표

Mimir는 데이터를 테넌트(tenant, 입주자)별로 완전히 분리해 저장한다. 어느 테넌트인지는 HTTP 헤더 하나로 정한다.

1
X-Scope-OrgID: federation
  • 보낼 때 이 헤더를 붙이면 그 선반에 저장된다.
  • 조회할 때도 같은 헤더를 붙여야 그 선반이 보인다. 다른 이름표로 조회하면 데이터가 있어도 빈 결과가 나온다.

이번 사례의 테넌트

테넌트누가 넣나누가 보나
mgmt관리 클러스터 PrometheusGrafana의 Mimir 데이터소스로는 안 보인다 (로컬 Prometheus 데이터소스로 본다)
federation운영 클러스터 A·B PrometheusGrafana Mimir 데이터소스(기본)
그 밖의 테넌트지금은 아무도 안 넣음예전 시도의 흔적

federation 하나에 여러 클러스터가 같이 들어가니, 그 안에서 클러스터를 구분하는 게 5장의 externalLabels(cluster=a/b)다. 테넌트는 “선반”, externalLabels는 “선반 안 상자에 찍힌 지점 도장”이다.

더 깊이 — 역추적기 #2 Mimir — 단일 테넌트에서 클러스터별 테넌트, 다시 공용 테넌트로 바뀐 변천사


9. PromQL — 창고에 질문하는 언어

PromQL은 Prometheus와 Mimir에 데이터를 묻는 질의 언어다.

질의뜻복구 작업 때 쓴 곳
up모든 타깃의 수집 성공 여부(0/1)Grafana Explore 확인
count(up)타깃이 몇 개인가복구 확인 (89, 17)
count by (cluster) (up)클러스터별 타깃 수클러스터 A·B 구분 확인
sum by (url) (prometheus_remote_storage_samples_failed_total)전송 대상별 실패 누계진단 (0이라 함정)
time() - prometheus_remote_storage_queue_highest_sent_timestamp_seconds전송 지연(초)진단 (235.7일)
rate(http_requests_total[5m])최근 5분간 초당 요청 수(Counter 읽는 일반적인 방법)

읽는 요령:

  • {...} 안은 라벨 조건이다. up{job="node-exporter"}는 node-exporter job만.
  • by (라벨)은 그 라벨로 묶어 집계한다.
  • [5m]은 “최근 5분 구간”이다.

API로 직접 묻기: Grafana 없이도 HTTP로 물을 수 있다. 복구 작업 때 이렇게 했다.

1
2
3
4
5
curl -s -G localhost:18081/prometheus/api/v1/query \
  -H 'X-Scope-OrgID: federation' \        # 선반 이름표
  --data-urlencode 'query=count(up)'           # 질문
# {"status":"success","data":{"result":[{"value":[시각,"17"]}]}}
#  결과가 [] 이면 → 그 선반에 해당 데이터가 없다

10. Grafana — 창고를 들여다보는 창문

Grafana는 데이터를 저장하지 않는다. 데이터소스(어디에 물어볼지)를 등록해 두고, 대시보드를 열 때마다 그 데이터소스에 PromQL로 물어서 그린다.

개념뜻
데이터소스“이 주소에, 이 헤더(테넌트)로 물어봐라”는 연결 설정
대시보드패널(그래프·표) 묶음
패널PromQL 하나 이상 + 그리는 방식
변수대시보드 상단 드롭다운. 라벨 값을 읽어 목록을 만들고, 고른 값을 쿼리에 끼워 넣는다 (K8S=b)
Explore대시보드 없이 PromQL을 바로 쳐보는 화면
provisioning데이터소스·대시보드를 파일(ConfigMap)로 미리 넣어 두는 방식. Grafana는 켜질 때 읽는다

이번 사례에서는

  • Grafana의 Mimir 데이터소스는 ConfigMap에 들어 있다. 이 설정은 켜질 때만 읽으니 주소를 바꾼 뒤 재시작했다.
  • 대시보드에 “No data”가 뜨는 경우는 세 가지다. 1) 데이터가 정말 없다, 2) 라벨 이름이 쿼리와 다르다, 3) 다른 테넌트에 있다. 파일 시스템·네트워크 패널 No data는 아직 이 중 무엇인지 모른다.

더 깊이 — 역추적기 #5 Grafana — 대시보드 프로비저닝 경로, __inputs 치환, 데이터소스 uid 교체


11. Alertmanager — 경보기

Prometheus(또는 Mimir ruler)가 알림 규칙으로 “이상하다”고 판단하면 Alertmanager에 알린다. Alertmanager는 그 알림을 묶고, 중복을 걸러 receiver(수신처)로 보낸다.

1
알림 규칙(PrometheusRule) → Prometheus 평가 → Alertmanager → receiver(슬랙, 메일, 웹훅...)

이번 사례에서는: receiver가 "null" 하나뿐이었다. 이름 그대로 “아무 데도 보내지 않음”이다. 경보기는 울리는데 선이 끊겨 있었다. 수집이 8개월간 멈춘 걸 아무도 몰랐던 근본 원인이다.

더 깊이 — 역추적기 #6 Alertmanager → Airflow — 알림 목적지가 사람이 아니라 Airflow DAG였던 설계와 미검증 판정


12. 로그 축 — Loki와 promtail

메트릭은 숫자, 로그는 글이다. 로그는 별도의 길로 간다.

 메트릭로그
내용cpu=30%ERROR: connection refused at ...
수집Prometheus (가져오기)promtail (밀어 넣기)
저장MimirLoki
질의 언어PromQLLogQL
  • promtail: 각 노드에서 컨테이너 로그 파일을 읽어 Loki로 보낸다(push). 노드마다 하나씩 뜬다(DaemonSet).
  • Loki: 로그를 받아 저장한다. 구조는 Mimir와 거의 같다(distributor, ingester, querier, gateway, Object Storage). 같은 회사(Grafana Labs)가 같은 설계로 만들었기 때문이다.

이번 사례에서는

  • promtail이 loki.example.com로 보내다 타임아웃이 나서, 재시도를 다 쓰고 버린다(final error sending batch).
  • Mimir와 마찬가지로 “외부 도메인을 돌아 들어오는” 구조라, 같은 종류의 문제일 가능성이 크다. 아직 확인하지 않았다.
  • promtail은 지원 종료 수순이다. 후속은 Grafana Alloy다. 그래서 운영 클러스터로 넓힐 때는 Alloy를 함께 검토한다.

더 깊이 — 역추적기 #7 Loki & Promtail — Kafka 완충로 4차 시도와 롤백, 최종 Loki 직결 구조


13. 네트워크 — 데이터가 지나가는 길

이번 장애는 전부 이 영역에서 났다. 데이터가 지나가는 길을 바깥에서 안쪽 순서로 정리한다.

도메인과 DNS

  • 도메인: 사람이 읽는 주소(mimir.example.com)
  • DNS: 도메인을 실제 IP로 바꿔 주는 전화번호부
  • A 레코드: 도메인 → IP를 직접 적는다
  • CNAME 레코드: 도메인 → 다른 도메인으로 연결한다. LB처럼 뒤쪽 IP가 바뀔 수 있는 대상에 쓴다. IP가 바뀌어도 따라간다.
  • TTL: “이 답을 몇 초 동안 기억해도 된다.” DNS를 바꿔도 TTL이 지나기 전까지는 옛 답이 나온다. 복구 작업 때 DNS를 바꾸고 몇 분을 기다린 이유다.

nslookup, dig는 DNS에 직접 물어보는 명령이다. dig는 남은 TTL까지 보여준다.

VPC와 서브넷

  • VPC: 클라우드 안에 나만의 사설 네트워크를 만든 것. 같은 VPC 안은 사설 IP(10.x.x.x)로 서로 통한다.
  • 서브넷: VPC를 용도별로 나눈 구역. 우리는 노드용 서브넷과 LB 전용 서브넷(공인용, 사설용)이 따로 있다.
 대역 
관리 클러스터 노드10.0.1.x같은 VPC, 다른 서브넷
운영 클러스터 노드10.0.2.x〃
사설 ALB10.0.3.10사설 LB 전용 서브넷

NAT Gateway — 바깥으로 나가는 출구

사설 IP만 가진 서버가 인터넷에 나갈 때 거치는 출구다. 나갈 때 출발지 주소를 NAT의 공인 IP 하나로 바꿔 준다.

이번 사례에서는: 관리 클러스터와 운영 클러스터가 바깥에서 둘 다 같은 공인 IP로 보였다(checkip). 같은 NAT를 쓴다는 뜻이다. NAT를 거치는 트래픽은 과금 대상이다. 동료가 지적한 비용 문제가 이것이다.

방화벽 두 종류 — ACG와 NACL

 ACGNACL
단위서버(인스턴스) 하나하나서브넷 전체
비유개인 출입증층 출입문
응답 트래픽자동 허용 (상태 기억)따로 허용해야 함 (상태 기억 안 함)
규칙 순서순서 없음번호가 작은 규칙부터 적용

이번 사례에서는: 공인 LB 서브넷의 NACL이 허용 목록 방식이었다. 우리 출구 IP는 8200(Vault)만 허용돼 있었고 443은 없었다. 사무실 IP는 따로 허용돼 있어서 PC에서는 들어갔다. “PC에서는 되는데 클러스터에서는 안 된다”는 이상한 증상의 원인이었다.

로드밸런서 — 정문

  • 로드밸런서(LB): 들어온 요청을 뒤쪽 서버 여러 대에 나눠 준다.
  • ALB(Application LB): HTTP를 이해하는 LB. 도메인(Host)이나 경로별로 다른 곳에 보낼 수 있다.
  • 공인 LB: 인터넷에서 접근 가능한 공인 IP를 가진다.
  • 사설 LB: VPC 안에서만 접근 가능한 사설 IP를 가진다. 내부용 서비스는 이쪽이 맞다.
  • HTTPS 리스너에는 인증서가 필요하다. 인증서가 만료되면 리스너를 만들 수 없다.

Kubernetes에서 LB를 만드는 방법 — Service와 Ingress

 Service (type: LoadBalancer)Ingress
계층TCP (4계층)HTTP (7계층)
하나의 LB에서비스 하나여러 서비스를 도메인·경로로 나눠 연결 가능
NCP에서NLB/NPLB 생성ALB 생성 (ALB Ingress Controller)

Ingress Controller는 Ingress 오브젝트를 보고 실제 클라우드 LB를 만드는 프로그램이다. NCP NKS에서는 관리형이라 클러스터에 파드가 안 보인다. 그래서 복구 작업 때 컨트롤러 로그를 볼 수 없었다.

Ingress의 상태 칸 ADDRESS에 LB 주소가 채워지면 LB가 만들어진 것이다. 비어 있으면 생성에 실패한 것이다. 복구 작업 때 첫 번째 단서였다.

Ingress 어노테이션(alb.ingress.kubernetes.io/...)으로 LB 성격을 정한다.

어노테이션뜻
network-type: private사설 LB로 생성 (생성 시에만 적용)
listen-ports열 포트(HTTP 80, HTTPS 443)
ssl-certificate-noHTTPS에 쓸 인증서 번호
ssl-redirectHTTP로 오면 HTTPS로 넘기기 (복구 작업 때 본 301)

finalizer — 지우기 전 체크리스트

finalizer는 Kubernetes 오브젝트에 붙는 “이 일을 끝내기 전에는 지우지 마라”는 꼬리표다. Ingress에는 “연결된 LB 정리 담당”이라는 finalizer가 붙는다. 그래서 Ingress를 지우면 LB도 같이 정리된다.

이번 사례에서는: Ingress를 JSON으로 저장해 다시 만들 때 이 꼬리표까지 복사됐다. 새 Ingress가 “이미 삭제된 옛 LB를 정리해야 한다”는 꼬리표를 달고 태어나서 멈췄다. 꼬리표를 떼자 새 LB가 만들어졌다.

클러스터 내부 주소 — 건물 안 복도

Kubernetes Service는 클러스터 안에서만 통하는 이름을 가진다.

1
2
<서비스 이름>.<네임스페이스>.svc
mimir-nginx.monitoring.svc

같은 클러스터 안이라면 이 주소로 바로 간다. LB도, NAT도, 인증서도, DNS 레코드도 필요 없다. 복구 작업 때 관리 클러스터의 Prometheus와 Grafana를 이 주소로 바꿨다.

전환 전후 경로 비교

AS-IS와 TO-BE 아키텍처

위 그림을 글로 줄이면 이렇다.

1
2
3
[전] 같은 클러스터 Prometheus → NAT → 인터넷 → (NACL) → 공인 ALB(HTTPS, 인증서) → mimir-nginx
[후] 같은 클러스터 Prometheus → mimir-nginx.monitoring.svc → mimir-nginx
[후] 다른 클러스터 Prometheus → (DNS) → 사설 ALB(HTTP, VPC 내부) → mimir-nginx

더 깊이 — 복구기 #1 — 인증서·DNS·NACL 세 겹의 장애를 구간 격리로 찾은 과정과 사설 ALB 전환


14. Helm — 설치와 설정 관리

  • Helm: Kubernetes용 패키지 관리자. 여러 YAML을 하나의 차트로 묶어 설치한다.
  • 차트: 템플릿 묶음 (예: kube-prometheus-stack 65.5.0)
  • values: 차트에 넣는 설정값. 같은 차트라도 values에 따라 결과가 달라진다.
  • 릴리스: 차트를 values와 함께 설치한 결과물 하나 (예: 운영 클러스터 B의 kube-prometheus-stack)
  • 리비전: 릴리스를 바꿀 때마다 1씩 오르는 번호. helm rollback 이름 번호로 되돌린다.
명령뜻
helm list설치된 릴리스 목록
helm history리비전 이력 (언제 누가 바꿨나의 흔적)
helm get values지금 리비전에 넣은 values (--revision N으로 과거도)
helm diff upgrade (플러그인)바꾸기 전에 무엇이 바뀌는지 미리 보기
helm upgrade새 values로 적용
helm rollback이전 리비전으로 되돌리기

드리프트 — 기록과 실제가 어긋나는 것

kubectl로 직접 바꾸면 Helm이 기억하는 values와 실제가 달라진다. 이를 드리프트(drift)라고 한다. 다음에 누가 helm upgrade를 하면 Helm은 자기 기억대로 되돌린다. 그러면 손으로 고친 것이 사라진다.

이번 사례에서는: 관리 클러스터와 운영 클러스터 A에 드리프트가 4건 있다. 운영 클러스터 B는 처음부터 values로 적용해서 드리프트가 없다. values에 반영한 뒤 helm diff를 돌렸을 때 아무 변경도 안 나오면 기록과 실제가 일치한다는 뜻이다.


15. 복구 중 본 로그·명령 읽는 법

로그 / 결과뜻좋은 신호?
Failed to send batch, retrying ... context deadline exceeded보내다가 시간 초과. 재시도 중나쁨 (길이 막힘)
Remote storage resharding from=1 to=50밀린 걸 보내려고 일꾼을 늘림좋음 (전송 재개)
Starting WAL watcher / Done replaying WAL새 전송 줄을 시작하고 일지를 다시 읽음정상 (설정 반영)
WAL watcher stopped, Failed to flush all samples on shutdown count=11999옛 전송 줄을 닫으며 들고 있던 데이터를 버림전환 때 정상
Completed loading of configuration file설정을 다시 읽음정상
final error sending batch (promtail)재시도를 다 쓰고 로그 묶음을 버림나쁨
no space left on device (store-gateway)디스크가 꽉 참나쁨
Readiness probe failed: 503아직 준비 안 됨 → 트래픽 안 받음나쁨
Ingress ADDRESS 비어 있음LB가 안 만들어짐나쁨
lb instance ... from finalizer not found옛 LB 꼬리표를 달고 있음나쁨 (finalizer 제거로 해결)
curl 결과 301HTTP로 왔으니 HTTPS로 가라 (= LB가 응답함)좋음 (길은 열림)
wget 결과 OK (mimir-nginx /)게이트웨이가 살아 있음좋음
{"result":[]}그 테넌트·조건에 데이터 없음상황에 따라 다름

자주 쓴 명령:

명령왜 썼나
kubectl port-forward svc/X 18081:80클러스터 안 서비스를 내 터미널 localhost로 잠깐 연결
kubectl exec ... -- wget ...파드 안에서 요청을 보내 봄. “클러스터 입장에서 길이 열렸나”를 보려고
wget -qO- http://checkip.amazonaws.com바깥에서 내가 어떤 IP로 보이는지
nslookup / dig도메인이 지금 어디를 가리키나, TTL은 얼마 남았나
kubectl get ingress -w값이 바뀔 때마다 새 줄로 보여 줌 (바뀌지 않으면 그대로)
kubectl describe ... \| tail맨 아래 Events에서 실패 사유 확인 (약 1시간 지나면 사라짐)

16. 용어 사전

용어한 줄 설명
메트릭시간에 따라 변하는 측정 숫자
시계열이름+라벨이 같은 값들을 시간순으로 쌓은 것
라벨메트릭에 붙는 키=값 꼬리표
카디널리티라벨 조합 수 = 시계열 수. 많으면 부담
scrapePrometheus가 대상의 /metrics를 가져오는 것
타깃scrape 대상 주소 하나
job같은 종류 타깃 묶음
exporter다른 시스템 상태를 Prometheus 형식으로 번역
TSDBPrometheus 로컬 시계열 저장소
WAL받은 데이터를 먼저 적는 일지
retention보관 기간
remote_write다른 저장소로 실시간 복사 전송
shard / resharding병렬 전송 일꾼 / 그 수를 조정
externalLabels이 Prometheus가 보내는 모든 데이터에 찍는 도장
CRDKubernetes에 새 오브젝트 종류를 등록
OperatorCRD를 보고 실제로 일하는 관리 프로그램
ServiceMonitor“이 Service를 수집하라”는 CRD
셀렉터라벨 조건으로 대상을 고르는 규칙
kube-prometheus-stackPrometheus 일습을 한 번에 까는 Helm 차트
Mimir여러 Prometheus 데이터를 오래·많이 저장하는 중앙 저장소
distributor / ingesterMimir 입고 담당 / 임시 선반
querier / query-frontendMimir 조회 계산 / 조회 접수
store-gatewayObject Storage의 오래된 블록을 읽는 담당
compactor블록 합치기·오래된 블록 삭제
Object StorageS3 같은 대용량 장기 저장소
테넌트Mimir 안의 분리된 저장 공간
X-Scope-OrgID테넌트를 지정하는 HTTP 헤더
PromQLPrometheus 질의 언어
up수집 성공(1)/실패(0)
데이터소스Grafana가 물어볼 곳
대시보드 변수상단 드롭다운, 라벨 값으로 목록 생성
Alertmanager / receiver알림 관리자 / 알림 받을 곳
Loki / promtail / LogQL로그 저장소 / 로그 수집기 / 로그 질의 언어
Grafana Alloypromtail 후속 수집 에이전트
VPC / 서브넷사설 네트워크 / 그 안의 구역
NAT Gateway사설망에서 인터넷으로 나가는 출구
ACG / NACL서버 단위 방화벽 / 서브넷 단위 방화벽
ALBHTTP 이해하는 로드밸런서
공인 LB / 사설 LB인터넷용 / VPC 내부용
Ingress / Ingress ControllerHTTP 진입 규칙 / 그 규칙대로 LB를 만드는 프로그램
어노테이션오브젝트에 붙이는 부가 설정
finalizer지우기 전 정리할 일이 있다는 꼬리표
클러스터 내부 주소<svc>.<ns>.svc, 클러스터 안에서만 통함
A / CNAME 레코드도메인→IP / 도메인→도메인
TTLDNS 답을 기억해도 되는 시간
Helm 차트 / values / 릴리스 / 리비전패키지 / 설정값 / 설치 결과 / 변경 번호
드리프트Helm 기록과 실제 상태가 어긋남
helm diff적용 전 변경 미리 보기

이 시리즈 — 모니터링 시스템 복구기 1편 8개월간 멈춰 있던 스택을 되살리다 — 세 겹의 장애와 내부 경로 전환 부록 — 개념 정리: 메트릭이 화면에 뜨기까지 (본 편) 2편부터 — 페이즈 3: Loki 확인, exporter 재구성, 대시보드 검증 (예정)

이전 시리즈 — 모니터링 시스템 역추적기 (이 시스템이 어떻게 만들어졌는지 되짚은 기록) 0편 프롤로그 — 문서 없는 모니터링 시스템을 물려받았다 1편 kube-prometheus-stack — values 파일 두 개의 수수께끼 2편 Mimir — 멀티테넌시는 왜 세 번 바뀌었나 3편 JMX Exporter — 빌드타임에 이미지로 굽는 설정의 함정 4편 관리형 서비스 시대의 Exporter — 에이전트를 설치할 수 없는 대상을 감시하는 법 5편 Grafana — PMM 대시보드 이식기와 __inputs의 배신 6편 Alertmanager → Airflow — 스펙에 없는 필드와 싸운 기록 7편 Loki & Promtail — 로그 파이프라인: 폐기된 Kafka 완충로와 헬스체크 방랑기 8편 에필로그 — 역추적이 남긴 것


이 시리즈의 모든 내용은 특정 조직·시스템을 식별할 수 없도록 도메인, 명칭, 일부 수치를 일반화/변경했습니다.

This post is licensed under CC BY 4.0 by the author.

© noah. Some rights reserved.