[Prometheus & Grafana] Chapter 9. PromQL 기초

참고: 이 글은 Prometheus (v3.2.1)와 Grafana 공식 문서를 기반으로 요약·정리한 내용입니다. 정확한 내용은 공식 문서를 참조해 주세요.


Chapter 8. 서비스 디스커버리까지 오면서 Prometheus는 동적인 환경에서도 스스로 대상을 찾아 시계열을 쌓게 됐다. 하지만 그렇게 쌓인 데이터는 그 자체로는 의미가 없는 숫자의 더미일 뿐이다. "지금 CPU 사용률이 몇 퍼센트인가", "지난 5분간 에러율이 얼마나 올랐는가" 같은 질문을 던지고 답을 받아내려면 별도의 언어가 필요하다. 그 언어가 PromQL(Prometheus Query Language)이다.

PromQL은 Grafana 대시보드의 모든 패널, Alerting Rule의 모든 조건, Recording Rule의 모든 사전 계산 뒤에 깔려 있다. 이 언어를 모르면 Prometheus는 그저 데이터를 모으기만 하는 창고에 머문다. 이번 장은 PromQL을 본격적으로 다루기 전, 모든 쿼리의 토대가 되는 데이터 타입셀렉터 문법을 정리한다. 연산자와 집계, rate 같은 함수는 다음 장으로 미룬다.


9.1 PromQL의 네 가지 데이터 타입

PromQL을 이해하는 출발점은 모든 표현식이 네 가지 타입 중 하나로 평가된다는 사실이다. 어떤 쿼리를 쓰든 그 결과는 이 넷 중 하나이며, 함수나 연산자가 어떤 타입을 받고 어떤 타입을 뱉는지가 PromQL 문법의 절반을 차지한다.

타입 설명 예시
Instant vector (인스턴트 벡터) 동일한 시점의 샘플 하나씩을 가진 시계열의 집합 node_cpu_seconds_total
Range vector (레인지 벡터) 일정 시간 범위에 걸친 샘플들을 가진 시계열의 집합 node_cpu_seconds_total[5m]
Scalar (스칼라) 단일 부동소수점 숫자 3.14
String (문자열) 문자열 값 (현재는 거의 쓰이지 않음) "production"

이 중 실제 모니터링에서 압도적으로 많이 쓰는 것은 인스턴트 벡터레인지 벡터다. 둘의 차이는 한 문장으로 요약된다. 인스턴트 벡터는 "지금 이 순간의 값"이고, 레인지 벡터는 "과거 일정 구간의 값 묶음"이다. 이 구분이 PromQL 전체를 관통하므로 반드시 몸에 익혀야 한다.


9.2 인스턴트 벡터 셀렉터

가장 단순한 쿼리는 메트릭 이름 하나를 적는 것이다. 이것만으로 그 이름을 가진 모든 시계열의 가장 최근 값을 한 번에 가져온다.

node_cpu_seconds_total

이 쿼리는 node_cpu_seconds_total이라는 이름의 모든 시계열을 인스턴트 벡터로 반환한다. CPU 코어가 8개이고 모드가 7종이라면, 한 서버에서만 56개의 시계열이 나온다. 너무 많다. 여기서 필요한 것이 레이블 매처다.

레이블 매처

중괄호 {} 안에 레이블 조건을 넣어 시계열을 좁힌다. 매처는 네 종류이며, 그중 둘은 정규식을 쓴다.

매처 의미 예시
= 레이블 값이 정확히 일치 {mode="idle"}
!= 레이블 값이 일치하지 않음 {mode!="idle"}
=~ 정규식에 매치 {mode=~"user|system"}
!~ 정규식에 매치되지 않음 {instance!~"10\\.0\\..*"}

여러 매처를 쉼표로 나열하면 AND 조건으로 결합된다. 아래 쿼리는 "modeidle이 아니면서 instanceserver1:9100인" 시계열만 골라낸다.

node_cpu_seconds_total{mode!="idle", instance="server1:9100"}

정규식 매처의 두 가지 함정

=~!~는 강력하지만 두 가지를 반드시 기억해야 한다.

첫째, 정규식은 항상 완전 앵커링된다. =~"user"user포함하는 값이 아니라 정확히 user일치하는 값에만 매치된다. 부분 일치를 원하면 =~".*user.*"처럼 직접 .*를 붙여야 한다. 내부적으로 RE2 엔진을 쓰며, 패턴 앞뒤에 ^$가 암묵적으로 붙는다고 생각하면 된다.

둘째, 빈 문자열에 매치되는 매처만으로는 쿼리할 수 없다. PromQL은 셀렉터에 최소한 하나는 빈 문자열에 매치되지 않는 매처가 있어야 한다고 요구한다. 즉 {job=~".*"} 하나만으로는 에러가 난다. .*가 빈 문자열에도 매치되어 사실상 "모든 시계열"을 의미하므로, 인덱스를 쓰지 못해 전체를 훑어야 하기 때문이다. 이 제약은 의도치 않은 전체 스캔을 막는 안전장치다.

메트릭 이름도 사실은 레이블이다

Chapter 3. 데이터 모델에서 다뤘듯, 메트릭 이름은 __name__이라는 특별한 레이블에 지나지 않는다. 따라서 다음 두 쿼리는 완전히 동일하다.

node_cpu_seconds_total{mode="idle"}
{__name__="node_cpu_seconds_total", mode="idle"}

이 사실을 활용하면 이름 자체에 정규식을 걸 수도 있다. 아래는 node_로 시작하는 모든 메트릭을 한 번에 잡는다.

{__name__=~"node_.*"}

9.3 레인지 벡터 셀렉터

인스턴트 벡터가 "지금 값"이라면, 레인지 벡터는 "과거 일정 구간의 값들"이다. 셀렉터 뒤에 대괄호로 시간 범위를 붙여 만든다.

node_cpu_seconds_total{mode="idle"}[5m]

이 쿼리는 각 시계열에 대해 최근 5분간 수집된 모든 샘플을 반환한다. scrape_interval이 15초라면 5분 동안 약 20개의 샘플이 쌓이므로, 시계열 하나당 20개씩의 값 묶음이 돌아온다.

시간 단위

대괄호 안에는 다음 단위를 쓸 수 있고, 조합도 가능하다.

단위 의미
ms 밀리초
s
m
h 시간
d
w
y

여러 단위를 이어 붙이면 큰 단위부터 작은 단위 순으로 적어야 한다. 1h30m은 유효하지만 30m1h는 에러다.

http_requests_total[1h30m]

레인지 벡터는 그 자체로는 그릴 수 없다

여기서 초보자가 가장 자주 걸리는 함정이 있다. 레인지 벡터는 그래프에 직접 그릴 수 없다. Grafana 패널이나 Prometheus 표현식 브라우저의 Graph 탭에 node_cpu_seconds_total[5m]을 넣으면 다음과 같은 에러를 만난다.

Error executing query: invalid expression type "range vector"
for range query, must be Scalar or instant Vector

이유는 명확하다. 그래프의 한 점은 하나의 값이어야 하는데, 레인지 벡터는 점 하나에 여러 값(5분치 샘플)을 담고 있기 때문이다. 그래서 레인지 벡터는 반드시 함수를 거쳐 인스턴트 벡터로 환원한 뒤 그려야 한다. 가장 흔한 것이 다음 장에서 다룰 rate()다.

rate(node_cpu_seconds_total{mode="idle"}[5m])

rate는 5분치 샘플 묶음을 받아 "초당 증가율"이라는 단일 값으로 압축한다. 결과가 인스턴트 벡터가 되므로 비로소 그래프에 올라간다. **"카운터에 [5m]를 붙였으면 거의 항상 rateincrease 같은 함수로 감싸야 한다"**는 것을 지금 단계에서 외워두면 다음 장이 한결 쉬워진다.


9.4 offset modifier: 과거로 시점 옮기기

기본적으로 모든 셀렉터는 쿼리가 평가되는 시점(현재)을 기준으로 값을 가져온다. offset을 붙이면 그 기준점을 과거로 옮길 수 있다. "지금"이 아니라 "1시간 전"이나 "어제"의 값을 보고 싶을 때 쓴다.

# 1시간 전 시점의 값
http_requests_total offset 1h

# 어제 같은 시각 기준 5분 레인지
rate(http_requests_total[5m] offset 1d)

가장 흔한 용도는 과거 대비 비교다. 현재 트래픽을 일주일 전 같은 시각과 비교해 급증·급감을 잡아내는 식이다. (실제 뺄셈 연산은 연산자를 다루는 다음 장의 몫이지만, 시점을 옮기는 도구가 offset이라는 점만 여기서 짚어둔다.)

문법에서 주의할 점은 위치다. offset은 셀렉터 바로 뒤에 와야 하며, 레인지 벡터의 경우 대괄호 다음에 붙는다. [5m] offset 1d는 맞고, offset 1d [5m]은 틀리다.


9.5 @ modifier: 고정된 시점에 평가하기

offset이 "현재로부터 얼마나 전"이라는 상대적 이동이라면, @ modifier는 "정확히 이 순간"이라는 절대적 시점을 지정한다. 값으로는 유닉스 타임스탬프(초)를 받는다.

# 2026-06-01 00:00:00 UTC(=1748736000) 시점의 값
http_requests_total @ 1748736000

쿼리가 그래프 위 어느 점에서 평가되든, @가 붙은 셀렉터는 항상 지정한 그 순간의 값만 본다. 이 덕분에 "특정 기준 시점 대비 현재 비율" 같은 표현이 가능해진다.

특수 값으로 start()end()도 쓸 수 있다. 각각 쿼리 범위의 시작과 끝 시점을 가리킨다.

# 그래프 표시 구간의 시작 시점 값에 고정
http_requests_total @ start()

@offset은 함께 쓸 수도 있는데, 이 정도는 기초를 벗어나므로 "절대 시점은 @, 상대 이동은 offset"이라는 구분만 기억하면 충분하다.


9.6 첫 쿼리 실전: 표현식 브라우저

이론을 정리했으니 Prometheus의 표현식 브라우저(http://localhost:9090)에서 직접 확인해보자. 세 가지 타입을 차례로 던져보면 차이가 손에 잡힌다.

스칼라를 넣으면 단일 값이 그대로 나온다.

2 + 3
5

인스턴트 벡터를 넣으면 시계열마다 현재 값이 표로 나온다. up은 각 대상의 스크래핑 성공 여부(1=정상, 0=실패)를 담은 가장 기본적인 메트릭이다.

up
up{instance="localhost:9090", job="prometheus"}   1
up{instance="server1:9100", job="node"}           1
up{instance="server2:9100", job="node"}           0

이 결과에서 server2up0이라면 그 대상의 스크래핑이 실패하고 있다는 뜻이다. 셀렉터로 좁혀 확인할 수도 있다.

up{job="node"} == 0

마지막으로 레인지 벡터를 Graph 탭에 그대로 넣어보면, 앞서 본 대로 타입 에러가 난다. 이것을 직접 한 번 마주쳐 보는 것이 "레인지 벡터는 함수로 감싼다"는 규칙을 각인시키는 가장 빠른 길이다.


정리

항목 핵심
데이터 타입 인스턴트 벡터 / 레인지 벡터 / 스칼라 / 문자열 — 모든 표현식은 이 넷 중 하나로 평가
인스턴트 벡터 "지금 값". 메트릭 이름 + {} 레이블 매처
레이블 매처 = != =~ !~, 쉼표는 AND, 정규식은 완전 앵커링
빈 매처 제약 빈 문자열에 매치되지 않는 매처가 최소 하나 필요 ({job=~".*"} 단독 불가)
메트릭 이름 __name__ 레이블의 사탕발림, 이름에도 정규식 가능
레인지 벡터 "과거 구간 값 묶음". [5m], 단위 ms~y 조합 가능
레인지 벡터 함정 직접 그릴 수 없음 → rate 등 함수로 인스턴트 벡터로 환원
offset 기준 시점을 상대적으로 과거로 이동, 셀렉터 뒤([5m] offset 1d)
@ modifier 절대 시점에 고정, 유닉스 타임스탬프·start()·end()

이제 시계열을 골라내고 시간 축을 다루는 법은 익혔다. 하지만 지금까지의 쿼리는 데이터를 선택만 할 뿐, 더하거나 비교하거나 합산하지는 못한다. "전체 노드의 CPU 평균", "서비스별 에러율 합계" 같은 진짜 질문에 답하려면 연산자와 집계가 필요하다. 다음 Chapter 10. PromQL 연산자와 집계에서는 산술·비교·논리 연산자와 sum·avg·by·without, 그리고 벡터 매칭까지 다룬다.

댓글