3. PromQL 마스터하기: 집계, 함수, 조인을 활용한 고급 쿼리

지난 시간에 프로메테우스의 데이터 모델과 PromQL의 기본 문법을 배웠습니다. 이제부터는 PromQL의 진짜 힘을 보여주는 핵심 함수와 연산자들을 통해, 흩어져 있는 메트릭 데이터에서 의미 있는 인사이트를 추출하는 방법을 배우게 됩니다. 이 장을 마스터하면 여러분은 그라파나 대시보드를 자유자재로 만들 수 있는 능력을 갖추게 될 것입니다.

다시 보는 벡터: Instant vs. Range

고급 함수를 배우기 전에, PromQL의 가장 기본이 되는 두 가지 벡터 타입을 다시 한번 상기해야 합니다. 모든 PromQL 함수는 이 두 타입 중 하나를 입력으로 받기 때문입니다.

Counter 다루기: `rate()`, `increase()`, `irate()`

Counter는 '총 요청 수'처럼 계속 누적되는 값이라 그 자체로는 의미를 파악하기 어렵습니다. "그래서 초당 요청 수(RPS)가 얼마인데?" 와 같은 질문에 답하기 위해 아래 함수들을 사용합니다.

함수 설명 언제 사용하나?
rate(v[d]) 시간 범위 d 동안의 초당 평균 증가율을 계산합니다. 가장 널리 사용되는 함수입니다. - "지난 5분간의 평균 RPS는 얼마인가?"
- 그래프로 표현했을 때 부드러운 추세를 보고 싶을 때.
increase(v[d]) 시간 범위 d 동안의 총 증가량을 계산합니다. - "지난 1시간 동안 발생한 총 에러 수는?"
- increase(errors_total[1h])
irate(v[d]) 시간 범위 d 내의 마지막 두 데이터 포인트만을 사용하여 순간적인 초당 증가율을 계산합니다. - 짧은 순간의 피크(peak)를 민감하게 감지하고 싶을 때.
- 그래프가 매우 뾰족하게(spiky) 나타나므로, 알림(Alerting) 설정에 유용할 수 있음.

`rate()` vs `irate()`: 건강검진 vs. 순간 심박수 측정

rate(metric[5m])는 지난 5분간의 데이터를 모두 고려하여 평균적인 변화율을 계산하므로, 마치 '5분간의 평균 심박수'처럼 안정적인 추세를 보여줍니다.

반면 irate(metric[5m])는 5분 범위 내에서 가장 최근의 두 데이터 포인트만 보고 변화율을 계산합니다. 이는 '지금 이 순간의 심박수'를 재는 것과 같아서, 갑작스러운 변화를 즉시 포착할 수 있지만 값이 계속 크게 튈 수 있습니다. 일반적인 대시보드에는 rate()를 사용하는 것이 좋습니다.

`rate()`와 `increase()`는 5분 동안의 데이터를 어떻게 계산할까?

`[5m]`이라는 시간 범위가 주어졌을 때, PromQL 함수가 실제로 어떻게 동작하는지 단계별로 살펴보겠습니다.

가정: http_requests_total 메트릭이 1분 간격으로 수집되고, 지난 5분 동안 아래와 같은 값을 가졌다고 가정해봅시다.

수집 시간 http_requests_total 값 (누적)
00:00 1000
01:00 1030
02:00 1050
03:00 1100
04:00 1140
05:00 1180

1. increase(http_requests_total[5m]) 계산 과정

increase는 주어진 시간 범위 내에서 순수하게 '총 증가량'만 계산합니다. 즉, (마지막 값 - 첫 값) 입니다.

1180 (05:00의 값) - 1000 (00:00의 값) = 180

결과: 180. "지난 5분 동안 총 180개의 요청이 더 들어왔다"는 의미입니다.

2. rate(http_requests_total[5m]) 계산 과정

rate는 총 증가량을 시간 범위(초 단위)로 나누어 '초당 평균 증가율'을 계산합니다.

(1180 - 1000) / (5 * 60초)  =>  180 / 300 = 0.6

결과: 0.6. "지난 5분 동안 초당 평균 0.6개의 요청(RPS)이 처리되었다"는 의미입니다.

3. irate(http_requests_total[5m]) 계산 과정

irate는 '순간' 증가율을 계산하기 위해, 주어진 시간 범위 내에서 가장 마지막 두 개의 데이터 포인트만을 사용합니다. 이 예시에서는 04:00과 05:00의 데이터입니다.

(1180 - 1140) / ( (05:00의 타임스탬프 - 04:00의 타임스탬프) 초 단위 ) => 40 / 60초 = 0.667

결과: 0.667. "가장 최근 샘플링 구간(04:00~05:00)의 순간적인 RPS는 약 0.67이다"라는 의미입니다. `rate`의 평균값 0.6보다 더 최근의 변화에 민감하게 반응하는 것을 볼 수 있습니다.

질문 1: `irate`가 마지막 두 개만 본다면, 왜 `[5m]` 같은 시간 범위가 필요한가요?

매우 중요한 질문입니다. [5m]은 "5분치 데이터를 모두 사용하라"는 의미가 아니라, "쿼리 시점으로부터 최대 5분 전까지 거슬러 올라가서 마지막 두 개의 데이터 포인트를 찾아라"는 '탐색 범위'를 의미합니다.

따라서, 안정적인 계산을 위해 시간 범위는 최소한 scrape 간격의 2배 이상으로 설정하는 것이 일반적인 규칙입니다. 이렇게 해야 네트워크 지연 등으로 scrape가 약간 늦어지더라도 최소 2개의 데이터 포인트를 확보할 수 있습니다.


질문 2: 그럼 마지막 두 데이터 포인트를 찾으면 거기서 찾기를 멈추면 되는거 아닌가요?

이것도 아주 날카로운 질문입니다. 이론적으로는 그게 더 효율적이겠죠. 하지만 PromQL의 동작 방식을 이해해야 합니다. PromQL은 '데이터 선택'과 '함수 실행'이라는 두 단계로 동작합니다.

  1. 1단계 (데이터 선택): metric[5m] 부분이 먼저 실행됩니다. 이 단계에서 프로메테우스는 지정된 시간 범위(5분) 내의 모든 데이터 포인트를 일단 메모리로 가져옵니다. 함수가 무엇인지는 아직 신경 쓰지 않습니다.
  2. 2단계 (함수 실행): irate() 함수는 1단계에서 전달받은 '5분치 데이터 묶음'을 가지고, 그중에서 마지막 두 개의 포인트만 골라서 계산을 수행합니다. 나머지 앞선 데이터들은 그냥 버려집니다.

비유하자면, "지난주 서류 뭉치 전부 다 가져와!" ([1w]) 라고 시킨 뒤, 막상 서류를 받아보고는 "음, 어제랑 오늘 것만 보면 되겠네" 하고 나머지는 보지 않는 것과 같습니다. 먼저 '가져오는' 행위가 끝나야 '보는' 행위를 할 수 있는 것이죠.

이러한 설계는 PromQL의 일관성과 예측 가능성을 높여줍니다. 어떤 함수를 쓰든 '데이터 선택' 단계는 동일하게 동작하기 때문입니다.

데이터 묶기: 집계 연산자 (Aggregation Operators)

수많은 시계열 데이터를 의미 있는 단위로 묶어서 합계, 평균 등을 계산할 때 사용합니다. sum, avg, count, min, max, topk(상위 k개), bottomk(하위 k개) 등이 있습니다.

이때 bywithout 키워드를 사용하여 어떤 레이블을 기준으로 그룹화할지 결정합니다.

# 1. 전체 클러스터의 총 RPS 계산
sum(rate(http_requests_total[5m]))

# 2. 각 API 엔드포인트(handler) 별로 RPS 계산
# by (handler) : handler 레이블은 남기고, 나머지 레이블은 버리고 합산
sum by (handler) (rate(http_requests_total[5m]))

# 3. 각 인스턴스(서버)를 제외하고, 서비스 전체의 평균 응답 시간 계산
# without (instance) : instance 레이블만 버리고, 나머지 레이블 기준으로 평균 계산
avg without (instance) (http_request_duration_seconds)

# 4. CPU 사용률이 가장 높은 5개의 파드(pod) 찾기
topk(5, pod_cpu_usage_seconds_total)

분포와 백분위수: `histogram_quantile()`

"API 평균 응답 시간은 100ms" 라는 말보다 "API 응답 시간의 99%는 500ms 미만" 이라는 말이 훨씬 유용합니다. 이렇게 백분위수(Percentile)를 계산하기 위해 사용하는 것이 바로 Histogram 타입의 메트릭과 histogram_quantile() 함수입니다.

Histogram 메트릭은 _bucket이라는 접미사를 가지며, le(less than or equal)이라는 특별한 레이블을 가집니다.

# http_request_duration_seconds_bucket{le="0.1"} 20  => 0.1초 이하로 걸린 요청이 20개
# http_request_duration_seconds_bucket{le="0.5"} 50  => 0.5초 이하로 걸린 요청이 50개

이 누적 데이터를 사용하여 p99(99번째 백분위수)를 계산하는 쿼리는 다음과 같습니다.

# 각 핸들러별로 99%의 요청이 몇 초 안에 처리되었는지 계산
histogram_quantile(
  0.99,
  sum by (le, handler) (
    rate(http_request_duration_seconds_bucket[5m])
  )
)

비유: 학생들 키 재기

전교생의 키 분포를 알고 싶을 때, 한 명 한 명의 키를 모두 기록하는 것은 비효율적입니다. 대신, "160cm 이하 몇 명", "170cm 이하 몇 명", "180cm 이하 몇 명" 과 같이 구간별로 인원수(_bucket)를 세는 것과 같습니다. histogram_quantile(0.95, ...)는 이 분포표를 보고 "상위 5%를 제외한 학생들의 키는 몇 cm인가요?" 라고 묻는 것과 같습니다.

메트릭 합치기: 벡터 매칭 (Vector Matching)

마치 SQL의 JOIN처럼, 서로 다른 메트릭을 레이블 기준으로 합쳐서 계산하는 PromQL의 가장 강력한 기능입니다. 하지만 처음에는 가장 헷갈리는 부분이기도 합니다. 예시를 통해 차근차근 이해해봅시다.

벡터 매칭 완전 정복: One-to-One vs. Many-to-One

벡터 매칭에는 크게 두 가지 관계가 있습니다.

1. One-to-One 매칭: 레이블이 완전히 같은 벡터끼리의 연산

가장 간단한 경우입니다. 예를 들어 '전체 요청 수'와 '실패한 요청 수'를 가지고 '에러율'을 계산해봅시다.

# 메트릭 예시
http_requests_total{instance="A", handler="/api/users"}  100
http_requests_failed_total{instance="A", handler="/api/users"} 5

이 두 메트릭은 instancehandler라는 레이블을 똑같이 가지고 있습니다. 따라서 프로메테우스는 자동으로 같은 레이블을 가진 시계열끼리 짝을 지어 계산합니다.

# 에러율 계산 (지난 5분간)
rate(http_requests_failed_total[5m]) / rate(http_requests_total[5m])

만약 레이블이 다르다면? on(label)이나 ignoring(label)을 사용합니다. 예를 들어, http_requests_total에만 version 레이블이 있다면, version을 무시하고 계산해야 합니다.

# version 레이블을 무시하고 instance, handler로 매칭
rate(http_requests_failed_total[5m]) / ignoring(version) rate(http_requests_total[5m])

2. Many-to-One 매칭: 정보가 적은 벡터를 많은 쪽에 붙이기

이것이 group_leftgroup_right가 필요한 진짜 이유입니다. 'Pod별 요청 수' 메트릭에 '어떤 Node에서 실행 중인지'라는 정보를 붙여보고 싶다고 가정해봅시다.

# 요청 수 메트릭 (레이블: pod, namespace) - Many 쪽
http_requests_total{pod="A", namespace="prod"} 200
http_requests_total{pod="B", namespace="prod"} 300

# Pod 정보 메트릭 (레이블: pod, namespace, node) - One 쪽 (정보를 제공)
# 이 메트릭의 값은 보통 1이며, 레이블 자체가 정보입니다.
kube_pod_info{pod="A", namespace="prod", node="node-1"} 1
kube_pod_info{pod="B", namespace="prod", node="node-2"} 1

우리의 목표는 http_requests_total 결과에 node 레이블을 추가하는 것입니다. 즉, 정보가 많은 쪽(Many)인 http_requests_total을 기준으로, 정보가 적은 쪽(One)인 kube_pod_infonode 레이블을 가져와야 합니다.

# http_requests_total 결과에 node="node-1", node="node-2" 레이블을 붙여보자!
rate(http_requests_total[5m]) * on(pod, namespace) group_left(node) kube_pod_info

이 쿼리의 결과는 다음과 같은 형태가 됩니다. node 레이블이 성공적으로 추가되었죠.

# 쿼리 결과 (가상)
{pod="A", namespace="prod", node="node-1"} 0.5  # 초당 0.5 요청
{pod="B", namespace="prod", node="node-2"} 0.8  # 초당 0.8 요청

이제 sum by (node) (...) 같은 집계 연산을 통해 '노드별 총 요청 수' 같은 고급 정보를 만들어낼 수 있습니다.

Grafana에서 본다면? (시각적 예시)

위 쿼리가 실제로 그라파나의 테이블 패널에서 어떻게 보이는지 확인하면 group_left의 마법을 직관적으로 이해할 수 있습니다.

1. 쿼리 A: rate(http_requests_total[5m]) 결과

pod namespace Value (RPS)
A prod 0.5
B prod 0.8

2. 쿼리 B: kube_pod_info 결과

pod namespace node Value
A prod node-1 1
B prod node-2 1

3. 최종 쿼리: 쿼리 A * on(pod, namespace) group_left(node) 쿼리 B 결과

on(pod, namespace)로 두 테이블을 조인하고, group_left(node)를 통해 쿼리 B의 node 레이블을 쿼리 A에 붙여줍니다. 그 결과, 아래와 같이 node 레이블이 추가된 새로운 테이블이 만들어집니다.

pod namespace node Value (RPS)
A prod node-1 0.5
B prod node-2 0.8

면접관의 시선 💡

Q: "PromQL에서 `rate()`와 `increase()`의 차이점은 무엇이고, 각각 어떤 상황에서 사용해야 하나요?"

A: "`rate()`와 `increase()`는 둘 다 Counter 타입의 메트릭을 처리하는 함수이지만, 반환하는 값의 의미가 다릅니다. `rate(metric[5m])`는 지난 5분간의 데이터를 기반으로 '초당 평균 증가율'을 계산합니다. 따라서 RPS(초당 요청 수)처럼 시간에 따른 변화율을 그래프로 보거나 알림을 설정할 때 유용합니다. 반면, `increase(metric[1h])`는 지난 1시간 동안의 '총 증가량'을 계산합니다. 이는 "지난 1시간 동안 총 몇 개의 에러가 발생했는가?" 와 같이 특정 기간 동안의 누적치를 확인할 때 사용합니다. 즉, 추세를 볼 때는 `rate()`, 총량을 볼 때는 `increase()`를 사용합니다."


Q: "우리 서비스의 API 응답 시간 p99 값을 PromQL로 어떻게 계산할 수 있을까요? 필요한 메트릭 타입과 쿼리를 설명해주세요."

A: "API 응답 시간의 p99 값을 계산하기 위해서는 먼저 메트릭을 `Histogram` 타입으로 수집해야 합니다. Histogram은 `http_request_duration_seconds_bucket`과 같이 `_bucket` 접미사와 `le` 레이블을 가진 메트릭을 생성합니다. 이 메트릭을 사용하여 `histogram_quantile()` 함수로 p99 값을 계산할 수 있습니다.

쿼리는 다음과 같습니다: `histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket[5m])))`. 이 쿼리는 먼저 `rate()` 함수로 지난 5분간 각 버킷의 초당 증가율을 계산하고, `sum by (le)`로 다른 레이블들을 제거하여 순수한 분포를 만듭니다. 마지막으로 `histogram_quantile()` 함수가 이 분포 데이터를 보고 99%의 요청이 몇 초 안에 처리되었는지를 계산해줍니다."


Q: "CPU 사용량이 가장 높은 상위 5개의 컨테이너를 찾는 PromQL 쿼리를 작성해주세요."

A: "`topk()`라는 집계 연산자를 사용하면 간단하게 찾을 수 있습니다. 먼저 `rate(container_cpu_usage_seconds_total[1m])` 쿼리를 통해 각 컨테이너의 분당 평균 CPU 사용 시간을 계산합니다. 그런 다음, 이 결과를 `topk()` 함수에 넣어주면 됩니다. 전체 쿼리는 `topk(5, rate(container_cpu_usage_seconds_total[1m]))` 입니다. 이 쿼리는 모든 컨테이너 중 CPU 사용률이 가장 높은 상위 5개의 시계열 데이터를 반환해줍니다."