본문 바로가기
DevOps, SRE

RED 그래프를 짜다 멈칫했다 - Otel·USE·Valkey Sentinel 모니터링 한복판에서 PromQL 기본기로 돌아간 기록

by Ramos 2026. 5. 2.

이 글은 새 기능 글이 아니다. 최근 alpha 환경에 백엔드 모니터링을 한 번에 깐 작업이 있었고, 그 한복판에서 내가 PromQL의 기본을 놓치고 있었다는 자각이 들어 따로 정리해둔 노트의 공개판이다. OTel 기반 RED 지표를 박고, USE 관점의 자원 모니터링을 덧붙이고, Valkey Sentinel(StatefulSet 구성)에 대해서는 Health · Topology · Memory · Throughput/Latency · Streams · Key Groups(분산락) 6축을 더 박는 작업이 메인이었다. 도구 스택(Grafana / Jaeger / OpenTelemetry / Prometheus)은 alpha 에 사전 셋업되어 있었고, 그 위에 BE 측 연동과 메트릭 설계는 내가 직접 짰다. 그러다 한 번씩 그래프가 비어 보이거나, p95 라인이 인스턴스 수만큼 갈라져 보이거나, 알림이 의도와 다르게 울리는 순간이 왔다. PromQL을 "문법"으로 외워두고 "사고 방식"으로 안 다듬어둔 대가였다. 그 자각을 정리한 글이다.

 

  • 도구 스택은 사전 셋업, BE 측 RED(OTel 기반) + USE + Valkey 6축은 내가 박았다. 새 빌드를 깔았다는 글이 아니라 그 과정에서 흔들렸던 PromQL 기본기를 정리한 글이다.
  • histogram_quantile 의 p95/p99는 sum by (le) (rate(_bucket[5m])) 위에 얹어야 정상이다. sum by (le) 빠뜨리면 인스턴스마다 분위수가 따로 계산돼 라인이 갈라진다.
  • 에러율 같은 비율 쿼리는 분자/분모 라벨 차원이 같아야 vector matching 된다. 한쪽만 sum 으로 묶고 한쪽은 라벨이 살아 있으면 결과가 통째로 빈다.
  • USE의 Saturation을 "메모리 사용량"으로 짤 때 container_memory_usage_bytes 가 아니라 container_memory_working_set_bytes 가 맞다. 전자는 페이지 캐시 포함이라 알림이 과민하게 운다.
  • Valkey Sentinel처럼 토폴로지가 라벨로 바뀌는 시스템(master ↔ replica) 은, 처리량/지연 메트릭을 그릴 때 role 라벨을 살릴지 죽일지를 의식해야 한다. 죽이면 절체 시점이 그래프에서 사라지고, 살리면 비율 계산이 깨진다.
  • Stream/락 메트릭은 카운터인지 게이지인지 한 번 더 확인한다. redis_stream_group_lag / redis_stream_length 류는 게이지(현재 쌓여 있는 양), app_redis_lock_acquire_total 류는 카운터(누적 시도). rate 잘못 붙이면 음수가 나오거나 의미 없는 값이 나온다.
  • 락 같은 "결과별 비율" 메트릭은 두 카운터를 두지 말고 단일 카운터 + result 라벨로 설계한다 - rate(...lock_acquire_total[5m]) by (result) 한 줄로 success/fail/timeout 이 나뉜다. 분자/분모 라벨 매칭 함정 자체가 사라진다.
  • 결국 PromQL을 잘 쓰는 건 타입 × 출처 × 프레임(USE/RED) 3축 매트릭스가 머리에 박혀 있느냐의 문제였다. 새 메트릭을 만나도 어떤 함수로 감싸고 어떤 차원으로 묶을지가 자동으로 떠오르게 만드는 게 목표.

시점 컨텍스트
본문에 등장하는 환경은 alpha 한 곳 이다. beta · prod 는 진입 준비 단계이고, 이번 모니터링 셋업도 alpha 에서 검증한 다음 다른 환경으로 끌고 갈 계획이다. 그래서 "알림이 너무 운다 / 그래프가 비어 보인다" 같은 표현은 운영 사고가 아니라 alpha 검증 단계에서 만난 작업 디테일로 읽어야 한다.


1. 출발점 - alpha 에 한 번에 셋팅한 6축

들어간 작업은 거칠게 6축이었다. 도구 자체는 이미 떠 있었기 때문에, 내가 한 일은 메트릭을 만들거나 끌어와서, 라벨을 정리하고, 대시보드와 알림에 얹는 것 이었다.

내가 박은 부분을 6축으로 나누면,

  1. RED (Rate / Errors / Duration) - OTel Java agent 가 트레이스를 OTLP로 송신하고, Collector 의 spanmetrics connector 가 거기서 traces_span_metrics_duration_milliseconds_* (count / sum / bucket) 시계열을 파생해 Prometheus 로 노출한다. 차원은 service_name · http_route · http_request_method · http_response_status_code · span_name, 서버 스팬만(span_kind="SPAN_KIND_SERVER") 한정. rate window 는 [3m]. burn-rate 기반 알림.
  2. USE (Utilization / Saturation / Errors) - Spring Boot Actuator + Micrometer 의 JVM·Tomcat·Hikari. Heap 영역별 사용률 (jvm_memory_used_bytes / jvm_memory_max_bytesarea="heap", id 별 — Eden/Old/Survivor), GC pause p99 (jvm_gc_pause_seconds_bucket), Tomcat busy ratio (tomcat_threads_busy_threads / tomcat_threads_config_max_threads), Hikari pending / acquire p95 / timeout rate. Saturation 자리를 의식적으로 채웠다.
  3. Valkey Healthredis_sentinel_master_ok_sentinels (quorum), redis_sentinel_master_status, redis_master_link_up, redis_connected_slaves.
  4. Valkey Topologyredis_instance_info{role,redis_mode,redis_version}role 라벨이 master ↔ replica 절체에 따라 움직인다.
  5. Valkey Memory / Throughput / Latencyredis_memory_used_bytes / redis_memory_max_bytes 사용률, redis_mem_fragmentation_ratio (메모리 100MB 이상에만 게이팅), redis_evicted_keys_total rate, redis_commands_total topk, command별 평균 latency (redis_commands_duration_seconds_total / redis_commands_total), redis_net_input_bytes_total / redis_net_output_bytes_total.
  6. Valkey Streams / Key Groups (분산락)redis_stream_length, redis_stream_group_lag, redis_stream_group_messages_pending (PEL), 마지막 produce 이후 경과 시간 (time() - redis_stream_last_generated_id_seconds). 분산락은 key_group=~"app:lock:.*" 필터의 redis_key_group_count / redis_key_group_memory_usage_bytes, 그리고 단일 카운터 app_redis_lock_acquire_totalresult 라벨로 결과 분기.

이걸 깔면서 PromQL을 손에 익혔다고 생각했는데, 막상 대시보드를 띄워보면 한 번씩 "어 이거 왜 비어 보이지?" 가 왔다. 그게 이 글의 발단이다.


2. 흔들렸던 순간들 - 기본을 놓친 자국들

2.1 p95 라인이 인스턴스 수만큼 갈라져 있었다

처음 RED Duration 패널을 그릴 때 이렇게 짰다 (메트릭은 spanmetrics connector 가 트레이스에서 파생한 traces_span_metrics_duration_milliseconds_bucket).

# ❌ 인스턴스마다 분위수가 따로 계산된다
histogram_quantile(0.95,
  rate(traces_span_metrics_duration_milliseconds_bucket{
    span_kind="SPAN_KIND_SERVER",
    service_name="$app", http_route=~"$route"
  }[3m])
)

Grafana 패널에서 라인이 인스턴스 수만큼 갈라져 있었다. 한 서비스의 p95 인데 라인이 여러 개. 그제서야 멈칫했다.

histogram_quantilele 라벨로 누적 분포를 본다. 그래서 분위수를 계산하기 전에 le 만 (또는 같이 보고 싶은 차원과 함께) 남기고 나머지를 합쳐야 한다.

# ✅ 정석 — sum by (le, span_name) 위에 histogram_quantile
histogram_quantile(0.95,
  sum by (le, span_name) (
    rate(traces_span_metrics_duration_milliseconds_bucket{
      span_kind="SPAN_KIND_SERVER",
      service_name="$app", http_route=~"$route"
    }[3m])
  )
)

라우트/메서드별로 더 쪼개고 싶으면 by (le, span_name, http_request_method, http_route) 로 차원만 더 살린다. 핵심은 분위수 함수에 들어가는 입력은 항상 sum by (le, ...) (rate(_bucket[3m])) 형태라는 것. 외워둘 만한 한 패턴이다.

작은 디테일: span_kind="SPAN_KIND_SERVER" 필터는 거의 모든 RED 쿼리에 들어간다. 안 넣으면 outgoing client 호출(SPAN_KIND_CLIENT)까지 같이 잡혀서, "요청 처리 시간" 으로 보고 싶었던 분위수가 외부 호출 시간까지 섞여버린다. 처음에 이 필터 없이 그렸다가 라인이 이상하게 떠 있었던 자국도 한 번 있었다.

2.2 에러율 그래프가 통째로 비었다

RED의 E를 짤 때 처음에 이렇게 적었다.

# ❌ 분자/분모 라벨 차원이 다름 → 매칭 실패
rate(traces_span_metrics_duration_milliseconds_count{
  span_kind="SPAN_KIND_SERVER", http_response_status_code=~"5.."
}[3m])
/
sum(rate(traces_span_metrics_duration_milliseconds_count{
  span_kind="SPAN_KIND_SERVER"
}[3m]))

분자에는 라벨이 다 살아 있는데 분모는 sum 으로 라벨 0개로 만들었다. PromQL의 binary operator는 양쪽 시계열의 라벨이 모두 같아야 매칭한다. 차원이 다르면 결과가 비어 나온다.

# ✅ 양쪽 모두 sum 으로 라벨 차원 일치 — 전체 오류율
sum(rate(traces_span_metrics_duration_milliseconds_count{
  span_kind="SPAN_KIND_SERVER", http_response_status_code=~"5.."
}[3m]))
/
sum(rate(traces_span_metrics_duration_milliseconds_count{
  span_kind="SPAN_KIND_SERVER"
}[3m]))

라우트별 에러율을 보고 싶으면 양쪽 똑같이 by (service_name, http_request_method, http_route) 로 묶는다. 분자와 분모의 차원은 항상 일치시킨다 — 이걸 한 번 손에 박고 나면 비율 그래프 만들 때 안 멈칫한다.

한 가지 더: 라우트별 패널에서는 분자에 http_response_status_code!~"2.*|3.*" 를 쓰는 패턴도 자주 쓴다 - 2xx/3xx 가 아닌 모든 것 (4xx + 5xx + 미설정) 을 에러로 본다. "5xx 만 진짜 에러" 인지 "4xx 도 비즈니스 에러로 카운트할지" 는 서비스 정책이고, 정책이 정해지면 정규식만 바꾸면 된다.

2.3 메모리 알림이 과하게 울었다

USE 의 Saturation 으로 "Pod 메모리 limit 대비 사용률 90% 이상" 알림을 짰을 때, 처음엔 이걸 썼다.

# ❌ 페이지 캐시 포함 — 과대평가
container_memory_usage_bytes{container!="POD", container!=""}
/
container_spec_memory_limit_bytes
> 0.9

알림이 자주 울었다. 그런데 실제 Pod 들은 OOM 근처도 아니었다.

container_memory_usage_bytespage cache까지 포함한 값이다. 반면 K8s 의 OOM Killer 는 working_set 기준으로 본다. 알림이 OOM 직전을 잡으려면 메트릭도 working set 으로 가야 일치한다.

# ✅ working set — OOM 판단 기준과 일치
container_memory_working_set_bytes{container!="POD", container!=""}
/
container_spec_memory_limit_bytes
> 0.9

이건 PromQL 문법이 틀린 게 아니라 메트릭을 잘못 골랐다. 그런데 결과적으로는 같은 부류의 실수다 — 그 메트릭이 무엇을 의미하는지 를 한 번도 안 짚고 그냥 이름이 그럴듯해서 골랐던 것.

한 가지 같이 박아둔 idiom - 분모에 > 0 필터.
JVM Heap 사용률 패널에서는 비율 분모 (jvm_memory_max_bytes{area="heap"}) 가 일시적으로 0/누락으로 잡히는 케이스가 있었다. PromQL의 division은 분모가 매칭 안 되면 결과 자체가 비고, 0이면 발산한다. 그래서 분모에 > 0 을 붙여 유효한 시계열만 매칭되게 한다.

sum by (app, id) (jvm_memory_used_bytes{area="heap"})
/
sum by (app, id) (jvm_memory_max_bytes{area="heap"} > 0)

이 한 줄로 시작 직후 / 스크레이프 누락 구간에 비율 패널이 발산하거나 비어 보이지 않는다. 비율 쿼리에서 같이 외워둘 만한 안전장치다.

2.4 Valkey master 절체 시점이 그래프에서 사라졌다

Valkey Sentinel은 master ↔ replica 가 시간에 따라 바뀐다. 토폴로지를 보는 패널은 따로 두고 (redis_instance_info{role,redis_mode,redis_version} 그대로), 처리량 그래프를 처음에 이렇게 짰다.

# 처리량 — pod 만 남기고 role 라벨은 죽임
sum by (pod) (rate(redis_commands_total[5m]))

이러면 master/replica를 구분 안 한다. 평소엔 깔끔해 보이는데, 절체가 일어난 순간 에 무엇이 master 였는지가 그래프에서 사라졌다. 디버깅 시점에 그게 가장 필요한 정보였다.

처리량 자체는 위처럼 pod 만 남기되, 옆 패널의 토폴로지(redis_instance_inforole) 와 시간 축을 맞춰 함께 본다. role 까지 같이 묶어 그릴 때는 topk 같은 함수가 (pod, role) 조합을 다른 시계열로 보기 때문에, 절체 직후 그래프가 한 번 끊어졌다가 새 라인으로 이어져 보인다 - 이건 버그가 아니라 의미 그대로 정상이다. 토폴로지 변동을 라벨로 표현하는 시스템의 특성으로 받아들이고, 그 어색함을 감안한 패널 배치를 한다.

2.5 Stream lag - 카운터인 줄 알고 rate 를 붙였다

Stream consumer lag 을 처음에 이렇게 짰다.

# ❌ lag 류는 보통 gauge — rate 붙이면 의미 없음
rate(redis_stream_group_lag[5m])

값이 들쭉날쭉했다. redis_stream_length / redis_stream_group_lag / redis_stream_group_messages_pending (PEL) 같은 게이지다. "지금 이 시점에 쌓여있는 양"이지 "초당 누적되는 양"이 아니다.

# ✅ gauge 그대로
max by (stream, group) (redis_stream_group_lag)
max by (stream, group) (redis_stream_group_messages_pending)

같이 박아둔 한 패턴 — "마지막 produce 이후 경과 시간".

# 스트림이 멈춘 지 얼마나 됐나? — 절대시각 차이
time() - max by (stream) (redis_stream_last_generated_id_seconds)

time() 은 Prometheus 가 평가 시점의 unix epoch 초를 돌려준다. 메트릭의 timestamp 가 아니라 메트릭 값 (마지막 entry 의 ms 환산 초) 과의 차이다. "프로듀서가 멈춘 지 5분 넘으면 알람" 같은 조건을 한 줄로 만들 수 있다.

PromQL 잘못 쓰는 가장 흔한 원인은 타입을 모르고 함수를 고른 것 이라고 정리해둔 부분 - 내가 직접 그 구덩이에 빠졌다. 메트릭 이름만 보면 카운터/게이지를 헷갈릴 때가 많아서, 이름에 _total 이 붙어 있으면 카운터(→ rate/increase), 아니면 게이지(→ 그대로 또는 *_over_time) 로 일단 가설을 세우고, exporter 문서로 검증하는 흐름이 안전하다.

2.6 분산락 - "두 카운터" 를 "단일 카운터 + result 라벨" 로 갈아탔다

처음엔 락 메트릭을 두 개로 쪼개서 카운트하고 있었다 — lock_attempts_totallock_acquire_failed_total. "획득 실패율" 그래프를 짤 때마다 §2.2 와 같은 함정이 다시 나왔다.

# 옛날 패턴 — 두 카운터 합쳐서 비율
sum by (key_group) (rate(lock_acquire_failed_total[5m]))
/
sum by (key_group) (rate(lock_attempts_total[5m]))

분자/분모 차원 맞추는 작업이 매번 같은 함정으로 돌아왔다. 대시보드를 다시 정리하면서 단일 카운터 + result 라벨 패턴으로 메트릭 자체를 갈아엎었다.

# ✅ 단일 카운터 — result 라벨로 success/fail/timeout 분기
sum by (result) (rate(app_redis_lock_acquire_total[5m]))

라벨 매칭 함정 자체가 사라진다. 그래프에서는 result 별 라인이 stack 으로 쌓이고, 비율은 시각적으로 비교한다. 진짜 비율 알림이 필요하면 한쪽을 {result="fail"} 로 좁혀 분자/분모 패턴을 쓰면 된다 — 그래도 양쪽이 같은 카운터에서 나오니 차원이 자연히 맞는다.

이 경험은 PromQL 기본기 너머의 교훈이다. 메트릭을 라벨로 분기시키면 PromQL 표현이 자동으로 단순해진다. exporter 가 노출할 때부터 이걸 의식하면 비율/조합이 한 줄짜리 쿼리가 된다. "두 개의 카운터 vs 하나의 카운터 + 라벨" 의 선택을 의식적으로 하기 시작했다.

키 그룹별 점유는 별도 게이지로 보고 있다 — redis_key_group_count / redis_key_group_memory_usage_byteskey_group=~"app:lock:.*" 필터.

# 락 prefix 별 키 개수
sum by (key_group) (
  redis_key_group_count{key_group=~"app:lock:.*"}
)

키 그룹 라벨은 prefix 단위로 정규화 되어 있다. 그래서 카디널리티 폭발 없이 "어떤 락이 가장 많이 잡혀 있는가" 가 한 패널에 나온다.

2.7 그리고 발견 - and on (...) 으로 의미 없는 구간 걸러내기

Valkey memory fragmentation ratio 패널을 짤 때, 메모리가 거의 안 차 있는 시작 직후 / replica idle 구간에서 fragmentation 값이 비정상적으로 크게 나왔다. 작은 메모리 풋프린트에서는 fragmentation 비율이 의미가 없다 — 알림이 새벽 3시에 사용량 0 인 replica 에서 울리면 곤란하다.

# ✅ 메모리 100MB 이상일 때만 fragmentation 표시
max by (pod) (
  redis_mem_fragmentation_ratio
  and on (pod, namespace)
  (redis_memory_used_bytes > 100 * 1024 * 1024)
)

A and on (l1, l2) B 는 "A 의 시계열 중, B 시계열과 (l1, l2) 라벨이 매칭되는 것만 남긴다" 는 뜻이다. 값은 A 의 값 그대로 통과시키고, B 는 게이트 역할만 한다.

이 한 줄로 기준 메트릭이 의미 있는 구간일 때만 보고 싶은 메트릭을 보여줄 수 있다. 알림 노이즈를 줄이는 가장 우아한 idiom 중 하나다 - if 가 없는 PromQL에서 "조건부 표시" 가 필요할 때 거의 항상 답이 된다. 자매 연산자로 unless on (...) (제외) / or on (...) (합집합) 도 같이 익혀두면 표현 폭이 넓어진다.


3. 다시 잡은 기본기 - 머릿속 매트릭스 3축

흔들린 자국을 모아보니 패턴이 있었다. PromQL을 잘 쓰는 건 세 개의 축이 머릿속에서 자동 매칭 되느냐의 문제였다.

3.1 타입 - 함수 선택의 근본

  • Counter (_total 접미사): 절대 그냥 안 쓴다. rate() 또는 increase() 로 변환. 그래프엔 rate, 알림엔 rate, 누적량엔 increase.
  • Gauge: 그대로 쓰거나 avg_over_time / max_over_time. rate 붙이면 안 됨.
  • Histogram (_bucket / _sum / _count): 분위수는 histogram_quantile(0.95, sum by (le) (rate(_bucket[5m]))), 평균은 rate(_sum)/rate(_count).
  • Summary: 클라이언트 사전 계산. 여러 인스턴스 분위수 합산 불가 — 다인스턴스 환경에서는 Histogram이 정답.

3.2 출처 - 라벨이 보이게 만든다

K8s 에서 같은 "메모리 사용량" 이라도 라벨이 다르다.

출처 메트릭 prefix 자주 나오는 라벨
cAdvisor (kubelet) container_* pod, container, namespace, image
kube-state-metrics kube_* pod, phase, condition, reason
node-exporter node_* instance, device, mountpoint, cpu
OTel Collector spanmetrics traces_span_metrics_* service_name, span_kind, span_name, http_route, http_request_method, http_response_status_code
Spring Actuator + Micrometer jvm_*, tomcat_*, hikaricp_* app, pool, area (heap/nonheap), id (Eden/Old/Survivor), cause
valkey-exporter / 앱 노출 redis_*, app_redis_* pod, role, db, cmd, key_group, stream, group, result

prefix 만 봐도 어느 라벨로 묶을지가 자동으로 떠오르게 만들어두는 게 목표였다. traces_span_metrics_* 가 보이면 span_kind 필터부터 떠올리고, jvm_memory_* 가 보이면 area / id 로 쪼개고, redis_* 가 보이면 pod / role 매핑을 떠올린다. prefix → 라벨 패턴이 손에 박혀 있으면 처음 보는 메트릭이라도 라벨을 추측해서 쿼리를 한 번에 바로 짤 수 있다.

3.3 프레임 - 만든 메트릭이 어디로 가야 하는지

같은 PromQL 쿼리라도 "RED 대시보드의 D 패널" 이냐 "USE 알림의 Saturation 조건" 이냐에 따라 차원과 임계치가 달라진다. 메트릭을 만든 시점에 이게 어느 프레임에 속하는지를 같이 결정해두면 대시보드/알림 정렬이 흔들리지 않는다.

  • RED: 서비스 단위 — Rate / Errors / Duration
  • USE: 자원 단위 — Utilization / Saturation / Errors
  • Golden Signals: Latency / Traffic / Errors / Saturation (RED + USE 합성)

이번 작업에서는 RED는 OTel spanmetrics, USE는 cAdvisor/KSM/node-exporter + JVM, Valkey 6축은 valkey-exporter 메트릭을 USE-RED 매핑으로 다시 정렬했다.


4. 이 매트릭스로 다시 봤더니 - 작업으로 돌아와서

기본기를 다시 잡고 나니 같은 작업이 PromQL을 한 단계 위에서 보이기 시작했다.

4.1 burn-rate 알림이 왜 임계치 알림보다 나은가

처음엔 5xx 비율 5% 이상 10분 지속 같은 단순 임계치 알림을 짰는데, RED를 SLO 기반으로 다시 정리하면서 burn-rate 알림으로 갈아탔다.

# 1h 윈도우 burn rate (예: 99.9% SLO)
(
  sum(rate(traces_span_metrics_duration_milliseconds_count{
    span_kind="SPAN_KIND_SERVER", http_response_status_code=~"5.."
  }[1h]))
  /
  sum(rate(traces_span_metrics_duration_milliseconds_count{
    span_kind="SPAN_KIND_SERVER"
  }[1h]))
) > (14.4 * 0.001)

이 식의 "14.4" 가 어디서 오는지를 유도해서 이해할 수 있느냐는 결국 §2.2 의 라벨 매칭과 §3.1 의 타입 이해 위에 서 있다. 기본이 흔들리면 burn-rate 같은 응용도 그냥 외워서 박는 것 이상이 되지 못한다.

4.2 USE의 Saturation 이 가장 비기 쉬운 자리

RED 는 OTel agent + spanmetrics 만 깔면 자동으로 채워지는데, USE의 Saturation — JVM Heap / GC / Hikari Pool / Tomcat thread / OS load / 디스크 IO 대기는 누군가 의식적으로 박지 않으면 비어 있다. 이번에 USE를 "Utilization 만 있고 Saturation 비었음" 상태에서 채우면서 가장 시간이 든 부분이다. 한 번 채워두면 왜 느려졌는가 의 절반이 비어 있는 대시보드를 더 이상 보지 않아도 된다.

4.3 Valkey 6축 - 게이지/카운터/토폴로지가 한 시스템에 다 있다

Valkey Sentinel 모니터링이 PromQL 기본기 점검에 가장 좋은 자료였다. 한 시스템 안에 다음이 다 있기 때문이다:

  • 카운터: redis_commands_total, redis_evicted_keys_total, redis_net_input_bytes_total / output_bytes_total, app_redis_lock_acquire_total → rate/increase
  • 게이지: redis_memory_used_bytes, redis_connected_clients, redis_stream_length, redis_stream_group_lag, redis_master_link_up → 그대로 (또는 max by (pod) 로 dedup)
  • 평균 latency idiom: redis_commands_duration_seconds_total / redis_commands_total (rate 양쪽 적용 후 비율) - Histogram 이 없을 때 카운터 두 개로 평균을 만든다
  • 토폴로지 라벨: redis_instance_info{role,...} - master ↔ replica 절체에 따라 라벨이 움직임
  • 분산 키 그룹: key_group 라벨로 락 prefix 분리, result 라벨로 결과 분기 → 비율/카운트가 한 줄짜리 쿼리
  • 벡터 매칭 게이팅: and on (pod, namespace) 으로 "의미 있는 구간" 만 표시 (fragmentation × 메모리 100MB 가드)
  • 시간 idiom: time() - redis_stream_last_generated_id_seconds - "마지막 produce 이후 경과 시간" 한 줄

타입 × 출처 × 프레임 매트릭스를 점검할 수 있는 가장 풍부한 케이스였고, 그래서 이번 작업이 PromQL 기본기를 다시 잡는 자리가 됐다.


5. 마무리 - "문법" 이 아니라 "사고 방식" 으로

PromQL을 잘 쓰는 건 함수 100개를 외우는 게 아니라, 새 메트릭을 봤을 때 (타입 × 출처 × 프레임) 매트릭스가 자동 매칭 되는 상태를 만드는 것이다.

이번 작업으로 얻은 건 RED·USE·Valkey 6축 대시보드만이 아니라, 그 매트릭스가 머리에 한 단계 더 박혔다는 사실이었다. 다음에 새 컴포넌트(예: Kafka 컨슈머 lag, 객체 스토리지 IO, 외부 API 호출 통계)를 모니터링에 얹을 때, 이 매트릭스가 자동으로 돌아주면 PromQL은 더 이상 "그래프 비어 보이는데 왜지?" 의 자리에 머물지 않을 것이다.

그 점검 자국을 남겨둔다는 의미로 이 글을 정리해뒀다. 다음에 같은 자국을 또 내지 않기 위한 개인용 체크리스트이기도 하다.

다음에 같은 자국을 안 내기 위한 한 페이지 체크리스트

  • 새 메트릭 보면 타입 4종(Counter/Gauge/Histogram/Summary) 부터 식별
  • _total 접미사 보이면 무조건 rate() / increase()
  • _bucket 보이면 sum by (le, ...) (rate(_bucket[3m])) 위에 histogram_quantile
  • RED 쿼리에는 span_kind="SPAN_KIND_SERVER" 필터 (server span만)
  • 비율 쿼리는 분자/분모 라벨 차원 일치 점검, 분모에 > 0 필터 로 div-by-zero 방어
  • 메모리는 working_set_bytes (OOM 판단), 사용률은 limit 분모 0 케이스 점검
  • 토폴로지가 라벨로 바뀌는 시스템은 role/state 라벨을 살릴지 죽일지 명시적 결정
  • "결과별 비율" 메트릭은 두 카운터 → 단일 카운터 + result 라벨 로 설계 단계에서 갈아엎기
  • 의미 없는 구간 걸러낼 땐 and on (...) 게이팅 활용 (fragmentation × memory>X)
  • "마지막 발생 후 경과 시간" 은 time() - <epoch_seconds_metric> idiom
  • 알림은 rate 안정적인 함수로, irate 는 디버깅 그래프 한정
  • 라벨 카디널리티 폭발 방지: user_id / request_id / 정규화 안 된 path 노출 금지
  • 무거운 쿼리는 Recording Rule 로 사전 계산, 알림은 burn-rate 패턴 우선

References