Grafana 완벽 가이드 · 편 1 / 4

Prometheus와
Grafana 기초

수치를 모으고, 질문하고, 그리기까지
— 메트릭 모니터링의 최소 구성 한 바퀴

Metrics PromQL Grafana Dashboard docker-compose
CPU 사용률 last 30m
100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)
인사 + 오늘 발표는 시리즈 4편 중 1편. 오른쪽 패널이 오늘 끝에 직접 만들게 될 화면이라는 점을 미리 알려주고 시작한다.
문제why monitoring

top을 치는 순간, 이미 늦었다

  • 서비스가 느려졌다는 얘기를 듣고 서버에 접속한다
  • 지금 CPU가 몇 퍼센트인지는 보인다
  • 30분 전에는 어땠는지 알 방법이 없다
  • 원인을 찾으려면 과거의 수치가 필요한데, 그게 아무 데도 남아 있지 않다
$ top
Processes: 612 total, 3 running
CPU usage: 78.4% user, 9.1% sys

# 그래서 30분 전에는?
…기록이 없다
모니터링이 하는 일 수치를 계속 쌓아두고, 나중에 되감아볼 수 있게 만드는 것.
현장 경험으로 공감대를 만드는 슬라이드. "장애 나고 나서 top 쳐본 적 있는 분?"으로 손을 들게 해도 좋다. 핵심은 실시간 값이 아니라 과거 기록이라는 점.
배경observability

Observability는 세 종류의 데이터로 나뉜다

요소설명대표 도구오늘
Metrics시간에 따른 수치 데이터 — CPU, 메모리, 요청 수Prometheus, Datadog다룬다
Logs이벤트 기록 — 에러 메시지, 요청 로그Loki, ELK Stack편 외
Traces요청의 흐름 추적 — 서비스 간 호출 경로Tempo, Jaeger편 3
오늘의 범위 셋 중 Metrics만. Prometheus로 수치를 모으고 Grafana로 그리는 데까지.
셋을 다 설명하려 하지 말 것. "로그와 트레이스는 다른 편에서"라고 선을 긋고 넘어간다. 카디널리티 얘기(15번)에서 로그가 다시 등장한다는 것만 기억.
시리즈4 parts

전체 시리즈 안에서 오늘의 위치

제목다루는 영역
편 1Prometheus와 Grafana 기초Metrics 기초
편 2Go 애플리케이션 커스텀 메트릭Metrics 심화
편 3Grafana Tempo 분산 트레이싱Traces
편 4Grafana Pyroscope Continuous ProfilingProfiles

오늘 나오는 화면 중 일부는 편 2에서 만드는 샘플 앱의 메트릭을 미리 당겨 쓴 것이다. 오늘 구축하는 환경만으로는 node-exporter와 Prometheus 자체 메트릭까지 볼 수 있다.

Counter/Histogram/Summary 스크린샷은 편 2의 Go 샘플 앱에서 나온 것이라는 점을 여기서 미리 밝혀두면, 뒤에서 "이건 왜 안 보이죠?" 질문이 안 나온다.
목차agenda

오늘 다루는 것

01 — Prometheus 핵심 개념

  • Pull 기반 아키텍처
  • 메트릭 타입 4가지
  • Label과 시계열, 카디널리티

02 — PromQL

  • 쿼리를 이루는 네 조각
  • instant vector와 range vector
  • 실전 쿼리 세 개 해부

03 — Grafana

  • Data Source 연동
  • Dashboard · Row · Panel · Query
  • Variables로 대시보드 재사용

04 — 실전 구축

  • docker-compose 파일 하나로 띄우기
  • node-exporter 메트릭 수집
  • 시스템 모니터링 대시보드 4패널
1·2장이 개념, 3·4장이 실습. 시간이 모자라면 2장 실전 쿼리(24~26)를 줄이고 4장 실습을 살린다.
01

Prometheus 핵심 개념

메트릭이 어떻게 들어오고, 어떤 모양으로 저장되는가. 여기를 이해하면 뒤의 PromQL과 대시보드는 그 위에 얹히는 이야기다.

이 장이 오늘의 뿌리. 특히 "시계열 하나"의 정의(14번)와 카디널리티(15번)는 나중에 사고를 막아주는 내용이라 시간을 아끼지 말 것.
01 · Prometheusarchitecture

메트릭을 직접 가져온다 — Pull 방식

대상이 /metrics HTTP 엔드포인트만 열어두면, Prometheus가 주기적으로 그 주소를 호출해(scrape) 값을 긁어온다. 대상 쪽은 어디로 보낼지 몰라도 된다.

Node Exporter:9100/metrics
Go Application:8080/metrics
MySQL Exporter:9104/metrics
Prometheus:9090 · TSDB · PromQL 엔진
Grafana:3000 · 대시보드
Alertmanager:9093 · 알림 규칙
Slack / Emailnotify

화살표는 데이터가 흐르는 방향이다. 호출은 반대로 일어난다 — Prometheus가 타겟의 /metrics를 부른다.

Exporter는 "번역기"라고 설명하면 잘 통한다. 대상 시스템의 상태를 Prometheus가 읽을 수 있는 텍스트 포맷으로 바꿔주는 역할. 오늘 실습에서는 node-exporter 하나만 띄운다.
01 · Prometheuspull vs push

Pull과 Push는 무엇을 맞바꾸는가

비교 항목Pull — PrometheusPush — Datadog, InfluxDB
데이터 흐름서버가 타겟에서 가져감타겟이 서버에 보냄
서비스 디스커버리필요 — 어디를 scrape할지 알아야 함불필요 — 타겟이 직접 전송
네트워크 요구서버 → 타겟 접근 필요타겟 → 서버 접근 필요
장점타겟 상태 확인 가능 — scrape 실패 = 장애방화벽 뒤 환경에서 유리
단점방화벽 뒤 타겟 수집 어려움타겟 장애 시 데이터 유실 감지 어려움
기억할 한 줄 Push 방식은 타겟이 조용해졌을 때 그게 장애인지 트래픽이 없는 건지 구분하기 어렵다. Pull은 호출 실패 자체가 신호다.
퀴즈 Q1과 직결되는 슬라이드. 반대로 Kubernetes CronJob처럼 짧게 살았다 죽는 워크로드는 Pull이 불리하고, 그때 Pushgateway를 쓴다는 예외도 한 줄 언급할 수 있다.
01 · Prometheusmetric types

메트릭 타입은 넷뿐이다

타입특징사용 예시주요 함수
Counter감소 없이 증가만 한다 (재시작하면 0)HTTP 요청 수, 에러 수rate(), increase()
Gauge오르내린다CPU 사용률, 메모리, 활성 연결 수직접 조회, avg_over_time()
Histogram값의 분포를 버킷에 나눠 담는다응답 시간, 요청 크기histogram_quantile()
Summary분위수를 미리 계산해 저장한다응답 시간 (클라이언트 계산)직접 조회
실무 감각 넷을 다 쓰지는 않는다. 실제로 손이 가는 건 CounterHistogram 둘이다.
여기서는 표만 훑고 넘어간다. 다음 다섯 장에서 하나씩 실제 그래프로 보여줄 거라고 예고할 것.
01 · 메트릭 타입counter

Counter — 답은 값이 아니라 기울기에 있다

http_requests_total

읽어낼 수 있는 건 “계속 올라간다” 정도다. 14시 11분에 바닥으로 떨어진 건 애플리케이션 재시작이다.

rate(http_requests_total[5m])

초당 5~10건, 실제 요청량이 드러난다. 재시작 지점에서도 선이 튀지 않는다 — 카운터 리셋을 자동 보정하기 때문.

규칙 Counter를 보면 rate()부터 떠올린다.
http_requests_total 누적값을 그대로 그린 Grafana 패널 rate()로 초당 증가율을 계산한 Grafana 패널
두 버튼을 번갈아 눌러 같은 데이터의 두 얼굴을 비교해 보여준다. 청중에게 먼저 "누적 그래프에서 지금 요청량을 읽을 수 있나요?"를 물어본 뒤 rate()로 넘어가면 효과가 크다. 퀴즈 Q2와 연결.
01 · 메트릭 타입gauge

Gauge — 값 자체가 곧 답이다

  • 지금 이 순간의 값이라 오르내린다
  • CPU 사용률, 메모리 사용량, 활성 연결 수
  • Counter와 달리 rate()를 씌울 일이 없다
# 현재 활성 요청 수
http_requests_in_flight

# 5분 평균
avg_over_time(node_cpu_seconds_total{mode="idle"}[5m])

여기서도 14시 11분 재시작 지점에 메모리가 잠깐 줄었다 다시 차오르는 게 보인다.

사용 중인 메모리를 보여주는 Gauge 패널
가장 직관적인 타입이라 빠르게 넘어가도 된다. 다만 23번에서 "Gauge에 rate()를 씌워도 에러가 안 난다"는 함정이 나오므로 여기서 "쓸 일이 없다"를 확실히 각인시켜 둔다.
01 · 메트릭 타입histogram

Histogram — 버킷은 누적이다

  • 저장하는 건 버킷별 개수뿐. 백분위수는 쿼리 시점에 계산한다
  • le="0.5" 버킷 = 0.5초 이하 전부. 0.25~0.5초 구간이 아니다
  • 분포를 눈으로 보려면 패널 Format을 Heatmap으로
# P99 응답 시간
histogram_quantile(0.99,
  rate(http_request_duration_seconds_bucket[5m]))
읽는 법 진한 구간이 요청이 몰린 응답 시간대. 10~25ms250~500ms 두 군데가 따로 진하다 — 조회 API와 주문 생성 API다. 평균 하나만 봤다면 100ms 언저리 숫자 하나로 뭉개졌을 것.
응답 시간 분포를 보여주는 Heatmap 패널
"평균의 함정"을 보여주는 슬라이드. 쌍봉 분포가 평균으로 뭉개지는 사례는 어디서나 공감을 얻는다. 퀴즈 Q3과 연결.
01 · 메트릭 타입summary

Summary — 이미 계산이 끝나서 합칠 수 없다

비교 항목HistogramSummary
분위수 계산 위치서버 (쿼리 시점)클라이언트 (수집 시점)
여러 인스턴스 합산가능불가능
버킷/분위수 변경설정 변경 후 재시작코드 변경 필요
권장 사용대부분의 경우정확한 분위수가 꼭 필요할 때
판단 기준 서버 세 대의 p99를 구하려고 각 서버의 p99를 평균 내면 그건 p99가 아니다. 서버가 여러 대라면 Histogram을 쓴다.
go_gc_duration_seconds를 quantile 라벨별로 그린 Summary 패널
Go 런타임이 기본으로 내보내는 GC 일시정지 시간. 중앙값은 100µs 근처인데 최댓값은 500µs를 넘나든다.
Summary는 "쓸 일이 거의 없다"로 정리해도 무방하다. 다만 Go 런타임 메트릭에서 저절로 만나게 되므로 알아볼 수는 있어야 한다. 퀴즈 Q4와 연결.
01 · Prometheustime series

저장 단위는 시계열 하나

시계열 = 이름표 하나에 (시각, 값) 쌍이 계속 붙어 나가는 목록. 그래프의 점 하나하나가 이 목록의 한 줄이다.

시각        값
14:30:00   1,204
14:30:15   1,251     ← 15초마다 한 줄 (scrape 주기)
14:30:30   1,298
14:30:45   1,344

이름표가 하나뿐이면 “요청이 늘었다”까지만 읽힌다. 어느 API가, 그중 실패가 몇 건인지는 이 숫자 안에 없다. 그래서 꼬리표(Label)를 붙인다.

http_requests_total{method="GET" , path="/api/orders", status="200"}
http_requests_total{method="POST", path="/api/orders", status="201"}
http_requests_total{method="POST", path="/api/orders", status="500"}
핵심 이름은 같지만 Prometheus는 이걸 서로 다른 시계열 세 개로 저장한다. 쪼개는 것도 합치는 것도 Label이 있어서 가능하다.
Prometheus Table 탭에서 하나의 메트릭이 라벨 조합별로 갈라져 나오는 화면
메트릭 이름은 하나인데, 실제 시계열은 라벨 조합만큼 존재한다.
여기가 오늘 발표에서 가장 중요한 개념이다. "메트릭 이름 = 시계열"이라고 착각한 채로 PromQL을 쓰면 22번 집계와 25번 나눗셈에서 반드시 막힌다.
01 · Prometheuscardinality

Label의 그늘 — 시계열은 곱셈으로 늘어난다

method5×path20×status6
0 개 시계열 — 이 정도는 괜찮다
+ user_id100,000을 붙이면
0

시계열 하나하나가 메모리를 차지한다. Prometheus는 그대로 주저앉는다. 이걸 카디널리티 폭발이라고 부른다.

판단 기준은 하나 이 Label의 값이 몇 가지인지 미리 셀 수 있는가?

Label로 써도 된다

  • method
  • status
  • path

로그로 보내라

  • user_id
  • session_id
  • 이메일 · IP 주소
숫자가 올라가는 동안 잠깐 멈춰 청중이 자릿수를 읽게 둔다. 실무에서 Prometheus를 죽이는 1순위 원인이라는 점을 강조. 퀴즈 Q5와 연결.
01 · Prometheusnaming

그 이름들은 어디서 오는가

http_requests_total은 Prometheus에 미리 들어 있는 게 아니다. 전부 누군가 코드에 적어 넣은 이름이다.

var HttpRequestsTotal = prometheus.NewCounterVec(
    prometheus.CounterOpts{
        Name: "http_requests_total",
        Help: "Total number of HTTP requests",
    },
    []string{"method", "path", "status"},
)

세 번째 인자가 이 메트릭이 쓸 Label 이름이다. Prometheus 입장에서 이름은 그냥 문자열이라 my_stuff_counter라고 지었어도 똑같이 동작한다.

공식 명명 규칙
도메인 한 단어를 앞에node_ · process_ · prometheus_
단위를 복수형으로 뒤에_seconds · _bytes
누적 카운트는 _total로http_requests_total
이름의 출처누가 정하나
node_cpu_seconds_totalexporter 제작자
go_gc_duration_seconds라이브러리가 자동 등록
http_requests_total애플리케이션 개발자

이름을 외울 필요는 없다. curl -s :9090/api/v1/metadata로 물어보거나, Prometheus UI 입력창의 자동완성을 쓴다.

"아무도 선언하지 않았는데 go_ 메트릭이 왜 있냐"는 질문이 자주 나온다. Go 클라이언트 라이브러리를 붙이는 순간 런타임 메트릭이 딸려 온다고 답하면 된다.
01 · Prometheusexposition format

Prometheus가 실제로 긁어가는 것

# TYPE http_requests_total counter
http_requests_total{method="GET",path="/api/orders",status="200"} 272
http_requests_total{method="POST",path="/api/orders",status="201"} 64
http_requests_total{method="POST",path="/api/orders",status="500"} 8

# TYPE http_requests_in_flight gauge
http_requests_in_flight 0

# TYPE http_request_duration_seconds histogram
..._bucket{le="0.1"}   7
..._bucket{le="0.25"}  32
..._bucket{le="0.5"}   72
..._bucket{le="+Inf"}  72
..._sum   19.577
..._count 72

# TYPE go_gc_duration_seconds summary
go_gc_duration_seconds{quantile="0.5"} 4.82e-05
go_gc_duration_seconds_sum   8.74e-05
go_gc_duration_seconds_count 2
  • # TYPE 줄이 타입의 출처다. #으로 시작하지만 주석이 아니라 메타데이터다
  • Counter는 라벨 조합마다 한 줄. 여기서는 세 줄이니 시계열도 셋
  • Histogram은 이름 하나가 여러 줄로 펼쳐진다. 버킷 값이 7 → 32 → 72로 커지다 유지되는 게 누적이다. 72건 전부가 0.5초 안에 끝났다는 뜻
  • Summary에는 버킷이 없다. 계산이 끝난 값뿐이라 합칠 원재료가 없다
  • Gauge는 지금 값 하나로 끝
개념 설명을 다 한 뒤 원본을 보여주면 앞의 이야기가 한 번에 정리된다. 실습 환경이 떠 있다면 여기서 실제로 curl을 쳐 보여주는 게 가장 좋다.
02

PromQL

Grafana 패널 하나가 결국 PromQL 쿼리 한 줄이다. 대시보드를 만들려면 이건 피해갈 수 없다.

"암호처럼 보이지만 구조는 단순하다"로 문을 연다. 다음 장에서 네 조각으로 분해하는 게 이 장의 뼈대.
02 · PromQLanatomy

어떤 쿼리든 네 조각의 조합이다

sum by(path) (rate(http_requests_total{status="500"}[5m]))
메트릭 이름무엇을 볼지
라벨 필터그중 어떤 시계열만 볼지
시간 범위언제부터 언제까지
함수 · 집계그 값들을 어떻게 계산할지
읽는 순서는 안쪽부터 바깥 http_requests_total 중에서 → status="500"인 것만 골라 → 최근 5분 구간을 → 초당 증가율로 바꾸고 → path별로 합친다.
이 슬라이드를 이 장의 기준점으로 삼는다. 뒤에 나오는 슬라이드들은 각 조각을 하나씩 확대하는 것이라고 안내하면 흐름이 잡힌다.
02 · PromQLselector

조각 1·2 — 중괄호 안의 조건

연산자예시
=값이 같은 것{method="GET"}
!=값이 다른 것{device!="lo"}
=~정규식에 맞는 것{status=~"5.."}
!~정규식에 안 맞는 것{path!~"/health.*"}
자주 쓰는 관용구 {status=~"5.."}에서 .은 아무 문자 하나. 500·502·503이 전부 걸린다 — 즉 “5xx 전체”.
# 조건을 쉼표로 이으면 모두 만족하는 것만 남는다
http_requests_total{method="GET", status=~"5.."}

네트워크 패널에서 device!="lo"로 loopback을 빼는 것도, 디스크 패널에서 mountpoint="/"로 하나만 고르는 것도 전부 이 자리에서 일어난다.

퀴즈 Q7과 연결. 정규식은 부분 매칭이 아니라 완전 매칭(anchored)이라는 점을 덧붙이면 정확하다.
02 · PromQLinstant vs range

조각 3 — [5m]이 붙으면 달라지는 것

PromQL에서 제일 헷갈리는 지점. 함수마다 요구하는 입력의 모양이 다르기 때문이다.

instant vector — 지금 값 하나

http_requests_total
# 시계열마다 값 1개 (예: 1,344)

Gauge를 그냥 볼 때는 이걸로 충분하다.

range vector — 기간에 쌓인 값 전부

http_requests_total[5m]
# 시계열마다 값 20개 (15초 × 5분)

rate()는 “늘어난 양 ÷ 걸린 시간”이라 값이 최소 둘 필요하다.

rate(http_requests_total[5m])   ✓ 동작
rate(http_requests_total)       ✗ 에러
구간은 얼마로 잡나 최소한 값이 둘은 들어와야 하니 scrape 주기보다 넉넉히. 15초 주기면 1분 이상, 관례적으로 5분. 짧으면 예민하게 튀고, 길면 변화가 뭉개진다.
퀴즈 Q6과 연결. "왜 [5m]이 붙어 있냐"는 질문은 반드시 나온다. 여기서 확실히 정리하고 넘어갈 것.
02 · PromQLaggregation

조각 4 — 쪼갠 시계열을 다시 합치기

함수
sum()전부 더한다
avg()평균을 낸다
max() / min()최댓값 / 최솟값
count()시계열이 몇 개인지 센다
sum(rate(http_requests_total[5m]))
→ 전체 요청량 · 선 1개

sum by(path) (rate(http_requests_total[5m]))
→ path별 요청량 · path 수만큼 선

sum by(path, status) (...)
→ path × status 조합별
by와 without by = 이 라벨만 남긴다. without = 이 라벨만 버리고 나머지를 남긴다.
퀴즈 Q8과 연결. 그냥 sum(...)을 쓰면 라벨을 전부 버려 값 하나로 뭉갠다는 점이 25번 에러율 슬라이드의 복선이 된다.
02 · PromQLfunctions · 함정

함수 넷이면 대부분 커버된다 — 그리고 한 가지 함정

함수설명쓰는 메트릭 타입사용 예시
rate()초당 평균 변화율Counterrate(http_requests_total[5m])
increase()지정 기간 총 증가량Counterincrease(http_requests_total[1h])
histogram_quantile()백분위수 계산Histogramhistogram_quantile(0.99, rate(...))
avg_over_time()지정 기간 평균값Gaugeavg_over_time(metric[5m])
함정 — “쓰는 메트릭 타입”은 검사되지 않는다 Prometheus 서버는 어떤 메트릭이 Counter인지 Gauge인지 알지 못한다. 모든 타입을 타입 없는 부동소수점 시계열로 평탄화해 다룬다.
rate(node_memory_MemAvailable_bytes[5m])
→ 에러 없음. Gauge인데도 계산된다 (결과는 엉터리)

rate(http_requests_total)
→ 에러. range vector가 아니다

에러로 걸러주는 건 벡터 타입까지고, 메트릭 타입이 맞는지는 쓰는 사람이 책임진다. 이름으로 짐작할 수 있다 — _total이면 Counter, _bucket/_sum/_count 세트면 Histogram, quantile 라벨이 붙어 있으면 Summary.

실무에서 조용히 틀린 대시보드가 만들어지는 대표 경로다. Gauge에 rate()를 씌우면 값이 줄어든 구간을 카운터 리셋으로 오해해 보정해버린다는 점까지 설명하면 좋다.
02 · 실전 쿼리 1cpu %

CPU 사용률 — 안쪽부터 한 겹씩

100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)
1
node_cpu_seconds_total{mode="idle"}
CPU가 놀고 있던 누적 시간 중 idle 모드만 고른다
2
[5m] + rate(...)
5분 구간의 기울기 = 초당 idle 시간. 코어가 8개면 코어마다 시계열이 따로 나온다
3
avg by(instance)
코어별 값을 서버 단위로 평균. 결과는 0~1, 1이면 완전히 놀고 있다는 뜻
4
* 100
백분율로 바꾼다
5
100 - (...)
“논 비율”을 뒤집어 “일한 비율”로 만든다
Prometheus Graph 탭에서 CPU 사용률 쿼리를 실행한 결과
대시보드를 만들기 전에 Prometheus UI에서 쿼리가 원하는 값을 내는지 먼저 보는 편이 빠르다.
CPU 사용률을 직접 재는 메트릭이 없어서 idle을 뒤집는다는 점이 포인트. 처음 보면 이상해 보이지만 리눅스가 원래 그렇게 보고한다고 설명하면 납득한다.
02 · 실전 쿼리 2error rate · 함정

에러율 — sum()이 없으면 항상 1이 나온다

sum(rate(http_requests_total{status=~"5.."}[5m]))
  / sum(rate(http_requests_total[5m])) * 100
왜 sum()이 필요한가 PromQL의 나눗셈은 양쪽 시계열의 라벨이 전부 같은 것끼리 짝을 짓는다. sum() 없이 나누면 왼쪽 {status="500"}은 오른쪽 {status="500"}과 짝지어진다 — 자기 자신을 나눈 값이니 언제나 1.
# sum 없이 — 항상 1
{method="POST", path="/api/orders", status="500"}  1

# sum으로 라벨을 걷어낸 뒤 — 실제 에러율
{}  5.5

status를 라벨에서 없애는 게 핵심이라, sum() 대신 by로 남길 라벨을 지정해도 된다. 경로별 에러율이 필요하면:

sum by(path) (rate(http_requests_total{status=~"5.."}[5m]))
  / sum by(path) (rate(http_requests_total[5m])) * 100
일반화 두 쿼리를 나눌 때는 먼저 묻는다 — 양쪽 라벨 집합이 같은가?
"에러율이 계속 100%로 나와요"라는 문의의 90%가 이것이다. 그래프가 1.0에 평평하게 붙어 있으면 이 실수를 의심하라고 알려준다.
02 · 실전 쿼리 3memory · disk

메모리와 디스크 — 어떤 메트릭을 고르느냐가 절반

(1 - node_memory_MemAvailable_bytes
     / node_memory_MemTotal_bytes) * 100
  • rate()가 없다 — 둘 다 Gauge라 지금 값이 곧 답
  • 양쪽 라벨이 instance, job으로 똑같아 짝이 저절로 맞는다. 에러율 쿼리와 갈린 지점
  • MemFree가 아니라 MemAvailable — 리눅스는 남는 메모리를 파일 캐시로 쓰는데, 이 캐시는 필요하면 즉시 회수된다. MemFree로 계산하면 캐시가 찬 서버는 항상 메모리 부족처럼 보인다
(1 - node_filesystem_avail_bytes{mountpoint="/"}
     / node_filesystem_size_bytes{mountpoint="/"}) * 100
  • 계산 방식은 메모리와 같다
  • 파일시스템 메트릭은 마운트 지점마다 시계열이 따로 생기므로 mountpoint로 하나를 지정해야 한다. 안 하면 /, /boot, 도커 오버레이까지 다 나온다
  • free가 아니라 avail — 리눅스는 일부 블록을 root 전용으로 예약한다. 일반 프로세스가 실제로 쓸 수 있는 양은 avail 쪽
실습 주의 docker-compose로 띄운 node-exporter는 컨테이너 안에 갇혀 자기 파일시스템만 본다. 위 디스크 쿼리가 비어 있다면 curl -s :9100/metrics | grep "^node_filesystem_size_bytes"로 실제 마운트 지점부터 확인한다.
MemAvailable vs MemFree는 리눅스를 오래 만진 사람도 헷갈리는 부분이라 반응이 좋다. macOS/Windows Docker Desktop에서는 호스트 디스크가 안 보인다는 점도 미리 말해두면 실습 중 질문이 줄어든다.
03

Grafana

여러 데이터 소스를 붙여 시각화하는 오픈소스 대시보드 도구. Prometheus 말고도 Loki, Tempo, MySQL, Elasticsearch를 붙일 수 있다.

Grafana는 데이터를 저장하지 않는다는 점을 짚어준다. 저장은 Prometheus가, 그리기는 Grafana가.
03 · Grafanadata source

Data Source — 대시보드보다 먼저 등록한다

방법 1 — UI에서 직접

Connections → Data sources → Add Prometheus 선택 URL: http://prometheus:9090 Save & Test
포인트 localhost:9090이 아니라 prometheus:9090. Grafana도 컨테이너 안에서 돌기 때문에 컨테이너 이름으로 찾아가야 한다.
# 방법 2 — provisioning 파일로 자동 등록
apiVersion: 1
datasources:
  - name: Prometheus
    type: prometheus
    access: proxy
    url: http://prometheus:9090
    isDefault: true
Grafana의 Prometheus Data Source 설정 화면
Save & Test를 누르면 “Successfully queried the Prometheus API”가 뜬다.
퀴즈 Q9와 연결. 컨테이너 네트워킹을 모르는 청중이 있다면 "docker-compose가 만든 네트워크에서는 서비스 이름이 곧 호스트 이름"이라는 한 줄을 꼭 넣는다.
03 · Grafanadashboard · panel

Dashboard → Row → Panel → Query

Dashboard JSON으로 내보내기/가져오기 가능 ├─ Row Panel을 묶는 접이식 영역 │ ├─ Panel 하나의 시각화 단위 │ │ └─ Query PromQL 한 줄 │ └─ Panel └─ Row
기억할 한 줄 Panel 하나 = PromQL 쿼리 하나. 대시보드를 만드는 일은 결국 쿼리를 쓰는 일이다.
Panel 타입적합한 데이터
Time seriesCPU 사용률, 요청 수, 응답 시간
Stat현재 가동 시간, 총 요청 수
Gauge디스크 사용률, 메모리 사용률
Table인스턴스별 상태, Top N 목록
Bar chart서비스별 에러 수, 엔드포인트별 요청 수
Heatmap응답 시간 분포, Histogram 버킷
대시보드를 JSON으로 내보낼 수 있다는 점은 실무에서 매우 중요하다 — 코드로 관리하고 provisioning으로 배포한다는 흐름을 한 줄 언급.
03 · Grafanavariables

Variables — 대시보드 하나로 서버 스무 대

항목
Nameinstance
TypeQuery
Data sourcePrometheus
Querylabel_values(node_cpu_seconds_total, instance)
Multi-value활성화
100 - (avg by(instance) (rate(node_cpu_seconds_total{
    mode="idle", instance=~"$instance"}[5m])) * 100)

드롭다운에서 인스턴스를 고르면 그 인스턴스의 메트릭만 남는다. 서버를 늘리면 label_values()가 찾아낸 만큼 항목도 따라 늘어난다.

대시보드 상단에 생긴 instance 변수 드롭다운
서버가 한 대일 때는 필요를 못 느끼지만, 대시보드를 서버 수만큼 복제하기 시작하면 그때가 늦은 시점이라는 점을 말해준다.
04

실전 — 로컬 환경 구축

docker-compose 파일 하나로 Prometheus · Grafana · node-exporter를 띄우고, 시스템 모니터링 대시보드 4패널을 만든다.

여기서부터는 가능하면 라이브 데모. 미리 이미지를 pull 받아두고, 데모가 실패할 때를 대비해 뒤 슬라이드의 스크린샷으로 대체할 수 있게 준비한다.
04 · 실전docker-compose.yml

서비스 셋이면 끝난다

services:
  prometheus:
    image: prom/prometheus:v2.51.0
    ports: ["9090:9090"]
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.retention.time=7d'

  grafana:
    image: grafana/grafana:11.4.0
    ports: ["3000:3000"]
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
    volumes:
      - ./provisioning:/etc/grafana/provisioning

  node-exporter:
    image: prom/node-exporter:v1.8.1
    ports: ["9100:9100"]
> docker compose up -d
서비스주소
Prometheuslocalhost:9090
Grafanalocalhost:3000 · admin/admin
node-exporterlocalhost:9100/metrics
보존 기간 --storage.tsdb.retention.time=7d — 로컬 실습이라 7일. 운영에서는 디스크와 맞바꾸는 값이다.
이미지 태그를 고정했다는 점을 짚어준다. latest로 두면 어제 되던 실습이 오늘 안 된다.
04 · 실전prometheus.yml

수집 대상은 scrape_configs에서 정한다

global:
  scrape_interval: 15s      # 수집 주기
  evaluation_interval: 15s  # 알림 규칙 평가 주기

scrape_configs:
  # Prometheus 자체 메트릭
  - job_name: 'prometheus'
    static_configs:
      - targets: ['localhost:9090']

  # node-exporter 시스템 메트릭
  - job_name: 'node-exporter'
    static_configs:
      - targets: ['node-exporter:9100']
  • scrape_interval: 15s — 이 값이 앞에서 본 시계열의 점 간격이 된다
  • job_name은 그대로 job 라벨이 되어 모든 시계열에 붙는다
  • 여기서는 static_configs지만, 실제 운영에서는 Kubernetes·Consul 같은 서비스 디스커버리를 붙인다
Pull의 대가 “어디를 긁을지”를 서버가 알아야 한다 — 그게 이 파일이다. Push 방식에는 없는 파일이다.
8번의 Pull vs Push 표에서 "서비스 디스커버리 필요"라고 했던 게 구체적으로 이 파일이라는 걸 연결해주면 이해가 붙는다.
04 · 실전status → targets

문제가 생기면 여기부터 본다

  • Prometheus 웹 UI → Status → Targets
  • Endpoint, 마지막 scrape 시각, 걸린 시간까지 여기서 확인된다
  • 여기가 DOWN이면 대시보드를 아무리 만져도 빈 그래프만 나온다
디버깅 순서 ① Targets가 UP인가 → ② Prometheus UI에서 쿼리가 값을 내는가 → ③ 그 다음이 Grafana 패널 설정이다. 순서를 건너뛰면 시간을 버린다.
Prometheus Targets 화면에서 두 타겟이 UP 상태
퀴즈 Q10과 연결. 실습 중 가장 많이 나오는 질문이 "그래프가 비어 있어요"인데, 답은 거의 항상 이 화면에 있다.
04 · 실전first dashboard

서버 한 대를 들여다보는 최소 구성

# 1. CPU 사용률 (%)  · Unit: Percent (0-100)
100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)

# 2. 메모리 사용률 (%)
(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100

# 3. 디스크 I/O  · Unit: Bytes/sec (IEC)
rate(node_disk_read_bytes_total[5m])
rate(node_disk_written_bytes_total[5m])

# 4. 네트워크 트래픽  · Unit: Bytes/sec (IEC)
rate(node_network_receive_bytes_total{device!="lo"}[5m])
rate(node_network_transmit_bytes_total{device!="lo"}[5m])
범례가 지저분하다면 디스크·네트워크는 장치 수만큼 시계열이 잡힌다. device=~"nvme.*|sd.*"로 실제 장치만 남기거나 범례를 끈다.
CPU, 메모리, 디스크, 네트워크 4개 패널로 구성된 시스템 모니터링 대시보드
Unit 설정을 빼먹으면 y축이 그냥 숫자로 나와 읽기 어렵다는 점을 강조. 패널마다 Unit을 지정하는 습관이 대시보드 품질을 크게 바꾼다.
마무리recap

오늘 가져갈 것

  • Prometheus는 Exporter가 열어둔 /metrics를 주기적으로 긁어 가는 Pull 방식이다
  • 메트릭 타입은 넷이지만 실제로 손이 가는 건 Counter와 Histogram이다
  • PromQL은 rate() · increase() · histogram_quantile() · avg_over_time() 네 개로 대부분 커버된다
  • Label은 메트릭을 여러 차원으로 쪼개주지만, 고유 식별자를 넣는 순간 카디널리티가 터진다
  • Grafana 대시보드는 Data Source → Dashboard → Row → Panel → Query 구조다
  • Prometheus + Grafana + node-exporter는 docker-compose 파일 하나로 띄울 수 있다
여섯 줄을 소리 내어 읽는 것만으로 정리가 된다. 여기서 질문을 받기 시작해도 좋다.
확인클릭하면 답이 열립니다

스스로 답해보기

애플리케이션이 재시작돼 카운터가 0부터 다시 시작한 것이다. rate()는 이 단절을 자동 보정하므로 같은 구간에서도 값이 튀지 않는다. → 슬라이드 10
0.5초 이하 요청 전부다. 0.25~0.5초 구간만 세는 게 아니다. 버킷이 누적이라 오른쪽으로 갈수록 값이 계속 커진다. → 슬라이드 12
Histogram. Summary는 각 서버가 미리 계산한 분위수만 넘기므로 합칠 수 없다 — 세 서버의 p99를 평균 내도 그건 전체 p99가 아니다. → 슬라이드 13
시계열 수가 Label 값 가짓수의 곱으로 늘기 때문이다. 600개짜리 메트릭에 사용자 10만 명을 곱하면 6천만 개가 된다. 셀 수 없는 값은 로그로 보낸다. → 슬라이드 15
범위 표기가 없으면 시계열마다 지금 값 하나(instant vector)만 온다. “늘어난 양 ÷ 걸린 시간”을 계산하려면 값이 최소 둘 필요하므로 [5m]을 붙여 range vector로 만들어야 한다. → 슬라이드 21
Prometheus 웹 UI의 Status → Targets. 타겟이 DOWN이면 애초에 데이터가 없는 것이라 쿼리나 패널 설정을 만져도 소용없다. → 슬라이드 34
청중에게 먼저 답하게 한 뒤 클릭해서 연다. 시간이 없으면 Q4와 Q6 두 개만 골라도 충분하다.
다음part 2

다음 편 — Go 애플리케이션 커스텀 메트릭

node-exporter가 주는 시스템 메트릭에서 한 걸음 더 들어가, Go 애플리케이션에 직접 메트릭을 심는다. HTTP 요청 수와 응답 시간은 물론 주문 건수 같은 비즈니스 지표까지 계측해서 Grafana에 올려본다.

오늘 본 화면 중 Counter · Histogram · Summary 스크린샷이 바로 그 샘플 앱에서 나온 것이다.

참고 자료

  • 전체 코드 — github.com/kenshin579/tutorials-go /monitoring/grafana-metrics
  • Prometheus 공식 문서 — prometheus.io/docs
  • PromQL 쿼리 가이드 — prometheus.io/docs/prometheus/latest/querying/basics
  • Grafana 공식 문서 — grafana.com/docs/grafana/latest
  • node-exporter — github.com/prometheus/node_exporter
  • Dashboard Best Practices — grafana.com/docs/grafana/latest/dashboards/build-dashboards/best-practices
감사합니다 질문 환영합니다 — advenoh.pe.kr
GitHub 저장소 주소를 화면에 띄워둔 채로 Q&A를 받는다.
발표자 노트
← 글 보기 Prometheus & Grafana 기초
01 / 38

단축키

→ · Space · PgDn
다음 슬라이드
← · PgUp
이전 슬라이드
Home · End
처음 · 마지막
O
전체 슬라이드 개요
N
발표자 노트
T
밝은 테마 / 어두운 테마
F
전체 화면
Esc
닫기