NoSQL Technical

Prometheus PromQL

지난번 포스팅(https://t3guild.com/archives/1865) 내용에서는 Java Client 입장에서 Prometheus 접근에 대해서 기술했다면, 이번에는 Prometheus 자체에 대한 PromQL을 사용해서 데이터를 어떻게 효율적으로 접근 할 수 있는지에 대한 내용이다. 많은 Function이 있지만, 주로 사용하는 몇가지를 가지고도 데이터를도 원하는데로 표현하는 정도의 수준으로 채워질 것 같다.

Basic

PromQL을 사용함에 있어서, 단순히 메트릭을 선언할 때는 Instant vector 데이터가 되어 특정 시점 1건을 표시하는 형태가 되며, 메트릭 뒤에 “[1d:5m]” 과 같이 표시하면, Range vector 데이터가 되어 1일 데이터를 5분 해상력으로 표현하게 된다. 후자는 그래프에 이용되어 변화상태 등을 감지함에 유용하지만, 특정 시점에 필요한 데이터를 취득하기에는 많은 양의 데이터를 가공해야 함이 따른다.

따라서, 목적에 따라 Instant vector로 한 시점이 필요할 수도 있고, Range vector로 추세에 접근하여 사용하는 형태다. 물론 Instant vector에 좌우 시간 축을 이동한다던지, Range vector 에서 특정한 시점을 축출해서 접근한다던지 해서 사용도 가능하고, Range vector의 데이터를 aggregation 함수를 통해 min, max, sum 등의 단일 형태로 표현도 가능하다.

또한 가공 과정에서, 2개 이상의 metric과 병합된 형태가 필요할 수 있는데 이는 RDBMS Outer Join과 유사해 보이는, 메트릭의 label을 다른 메트릭으로 제공하는 방식에서 유사하게 제어 할 수 있다.

시간을 축으로 모든 메트릭에는 데이터가 축적되고 있기 때문에, Time Series DB의 장점일 수도 있겠는데, 손쉽게 특정 기준으로 수집된 데이터의 보관주기를 선언할 수도 있다. 예를 들어 지정된 위치로 부터 scrapping 하고 있는 곳의 데이터는 30일만 보관 한다던지 그 외 다른 scrapping은 60일을 보관한다던지 등을 쉽게 설정할 수 있다.

# Prometheus 옵션에 아래와 같이 설정 하면 보관 주기를 정의 할 수 있다. 
# 최근에는 storage.tsdb.retention.size, storage.tsdb.retention.time 등 세분화된 옵션으로 제공하고 있다.
--storage.tsdb.retention=60

주요 Function

label_join

label_join(up{job="api-server",src1="a",src2="b",src3="c"}, "foo", ",", "src1", "src2", "src3")

#예) label_join(up, "server", "", "instance")
up{instance="localhost:9090",job="prometheus",server="localhost:9090"}	1
up{instance="node-exporter:9100",job="node",server="node-exporter:9100"}	1
up{instance="172.30.18.8:8080",job="custom_exporter",server="172.30.18.8:8080"}	0

label_join 함수는 기본적으로는 다수의 label을 특정한 구분자로 병합할 수 있도록 되어 있다. 구분자를 생략하면, 없는데로 병합된다. 그러나, 예제와 같이 기존에 있는 “instance” label을 “server” label로 새롭게 생성할 수 있다. 원래는 이렇게 replace가 필요한 경우 label_replace를 사용하여야 하나, label_replace함수를 직접사용하지 않고도 label_join 함수 만으로도 사용 가능하다. (label_replace 함수는 인자상에 regular 문법을 지원하고 있으며, 일치하는 내용에 대해서 replace 하도록 되어 있다.)

label_replace

label_replace(up{job="api-server",service="a:c"}, "foo", "$1", "service", "(.*):.*")

#예) label_replace(up, "server", "$1", "instance", "(.*)")
up{instance="localhost:9090",job="prometheus",server="localhost:9090"}	1
up{instance="node-exporter:9100",job="node",server="node-exporter:9100"}	1
up{instance="172.30.18.8:8080",job="custom_exporter",server="172.30.18.8:8080"}	0

label_replace 함수를 사용하여, 위와 같은 label_join과 같은 효과를 나타낼 수 있다. 필요한 내용으로 선택하면 되겠다.(정규식이 약하면, label_join으로 가자). 특별한 것은 label_replace를 사용할 때, 자주 있는 일은 아니지만, 일부 label 중에 특별히 “,” 구분자 또는 다른 구분자로 하나의 label의 값이 여러값을 구분하고 있고, 그중에 일부를 발췌해야 하는 난이도가 있다면 아래와 같이 사용할 수 있다.

label_replace(<metric>, <new label>, <출력 인자 arg>, <targe label>, "([^,]*).*")

label_join 과 label_replae의 용도가 그럼 어떻게 되는가? 서로 다른 곳에서 수집한 메트릭에 대해서 결합이나 합치기를 원할 수 있는데, 이때 필요한 작업이 label이 일치해야만 가능하다. 사실 더 좋은 방법은 config 수준에서 metric_relabel_configs로 수집단계부터 정리되는 것이 좋지만, 사용자가 판단해서 할일이고, 일단 함수로써는 위와 같이 사용이 가능하다.

min_over_time, max_over_time

go_threads[1d:5m]
go_threads{instance="localhost:9090",job="prometheus"}	
11 @1595891100
12 @1595892300
12 @1595892600
12 @1595892900
go_threads{instance="node-exporter:9100",job="
9 @1595891100
9 @1595891700
10 @1595892000
10 @1595892300


min_over_time(go_threads[1d:5m])
{instance="localhost:9090",job="prometheus"}	11
{instance="node-exporter:9100",job="node"}	9

max_over_time(go_threads[1d:5m])
{instance="localhost:9090",job="prometheus"}	12
{instance="node-exporter:9100",job="node"}	10

min_over_time 과 max_over_time 시간에 따른 Range vector 데이터에 대해서 min, max 를 제공한다. 유사하게 avg_over_time, count_over_time, sum_over_time, stddev_over_time, stdvar_over_time, quantile_over_time 이 존재하며, 의미적으로는 다 동일하다. 결과에 대한 내용을 보면 “go_threads” 메트릭에서는 2개의 label 유형에 따라, 11 ~ 12, 9~10까지 발생된 이력이 있다. 이는 Range vector 형식으로 1일간의 데이터를 5분 해상력으로 표현을 했는데 표기상 일부만 추출했다. 이와 같은 데이터를 min_over_time 과 max_over_time을 구현하면, 각각의 내용들은 의미적인 최소, 최대 값을 표현한다.

by, without

max without (instance, job) (max_over_time(go_threads[1d:5m]))
{}	12

by 와 without 은 기능적으로는 유사하나, 방식은 상충되는 형태이다. by는 특정한 label로 메트릭을 aggregation 하도록 하는 내용이고, without은 특정한 label을 제외하고 aggregation 하도록 하는 내용이다. 따라서, 위와 같이 without 으로 “instnace”, “job”을 제외 하면, 실질적으로 결과와 같이 label이 남지 않는 상태가 되고, 이 때 합쳐지는 데이터는 어떻게 할 것인가에 대해 “max”를 사용하여 데이터를 합쳐서 표시하게 되어 있다.

위에서 이미 우리가 사용한 max_over_time으로 인해 label 내용이 서로 달랐던 2건은 이제 label이 사라 졌으니 1건으로 합쳐져야 하는데, 이때 max를 사용하게 되어 표시하는 의미가 되어, 결과와 같이 12가 된다. 반대로 aggregation에 min 을 썼다면 10의 결과가 나왔을 것이다.

by, without은 RDBMS 측면으로 보면 데이터를 어떻게 group by 할 것인가라는 의미와 유사하게 접근할 수 있겠다. 다만, by는 명시적인 label로 지정하지만, 존재하지 않을 수도 있는 메트릭과 결합한다면 명확하게 제거해야 하는 label을 명시하는 without이 도움이 될 수도 있다. 개인적 견해이지만, Prometheus는 이미 RDBMS 보다 다른 포지션적으로 사용을 하는 의미가 내포되어 있으니 그 만큼 데이터 역시 정규화가 높지 않을 공산이 크다.

on, ignoring

prometheus_engine_queries_concurrent_max
prometheus_engine_queries_concurrent_max{instance="localhost:9090",job="prometheus"}	20

prometheus_engine_query_duration_seconds_sum
prometheus_engine_query_duration_seconds_sum{instance="localhost:9090",job="prometheus",slice="inner_eval"}	0.009081800000000001
prometheus_engine_query_duration_seconds_sum{instance="localhost:9090",job="prometheus",slice="prepare_time"}	0.006129400000000001
prometheus_engine_query_duration_seconds_sum{instance="localhost:9090",job="prometheus",slice="queue_time"}	0.006004999999999999
prometheus_engine_query_duration_seconds_sum{instance="localhost:9090",job="prometheus",slice="result_sort"}	0

prometheus_engine_query_duration_seconds_sum /  ignoring(slice) group_left prometheus_engine_queries_concurrent_max
{instance="localhost:9090",job="prometheus",slice="inner_eval"}	0.000436585
{instance="localhost:9090",job="prometheus",slice="prepare_time"}	0.00027872500000000007
{instance="localhost:9090",job="prometheus",slice="queue_time"}	0.00028719999999999993
{instance="localhost:9090",job="prometheus",slice="result_sort"}	0

위에서 마지막에 “prometheus_engine_query_duration_seconds_sum / ignoring(slice) group_left” 에서 “/ignoring”의 의미는 “slice”를 무시하고 매트릭간에 매칭을 하겠다는 의미가 된다. 반대로 on 을 쓰면 명확한 label에 대해서 확인하고 매칭을 의미한다.

결론적으로 “prometheus_engine_queries_concurrent_max” 매트릭에는 “slice” label이 존재하지 않기 때문에 이에 대해서는 무시하고 “/” 연산이 진행된다.

group_left, group_right

prometheus_engine_query_duration_seconds_sum /  ignoring(slice) group_left prometheus_engine_queries_concurrent_max
{instance="localhost:9090",job="prometheus",slice="inner_eval"}	0.000436585
{instance="localhost:9090",job="prometheus",slice="prepare_time"}	0.00027872500000000007
{instance="localhost:9090",job="prometheus",slice="queue_time"}	0.00028719999999999993
{instance="localhost:9090",job="prometheus",slice="result_sort"}	0

위에 내용을 다시 보면, group_left가 이미 사용되었는데, group_left와 group_right는 N:1, 1:N 매칭에서 사용되는데, 높은 카디널리티가 있는 쪽이 좌측이면 group_left, 우측이면 group_right를 사용한다. 위에 “prometheus_engine_query_duration_seconds_sum” 다수 건을 표시하고 “prometheus_engine_queries_concurrent_max” 1건을 매칭함에 있어서 group_left를 사용하는 예가 된다.

group_left는 이외에도 group_left(<label>)을 사용하여 우에서 좌로 지정된 label을 이동할 수 있다. 이는 정보성 메트릭이 별도로 생성한 경우 해당 메트릭과 label을 사용함에 있어서 유용하게 사용될 수 있다. 정보성 메트릭의 구성은 복수의 데이터를 하나의 메트릭으로써 label 자체만 의미가 있는 경우 주로 사용한다.

마무리

위에서 언급한 Function들은 일부에 해당하는 내용이며, https://prometheus.io/docs/prometheus/latest/querying/functions 홈페이지에서 더 많은 Function들에 대해 자세히 안내하고 있다.

일반적인 사례로 Prometheus 시계열 DB 활용은 지속적으로 발생되는 모니터링 성격의 유형에 주로 사용되는 듯하고, 추이 분석 등에 많이 이용되는 것으로 보인다. (사용하는 더 많은 구체적인 사례가 궁금한데… 자료조사에 한계가 많다.) 수집 Polling 방식의 장점으로 다양한 데이터 소스와 매치하여 여러 메트릭을 통합적인 뷰로 시점적인 변화 분석함은 막강하다고 본다.

물론, 이런 과정에서 각 메트릭 간에 발생된 내용을 서로 매칭하면서 Function들은 다양하게 사용될 수 밖에 없고, 자주 찾게 되는 항목들이 된다. 실전에서도 지속적으로 검색하고 찾았던 내용들을 정리하게 되어 의미 시간이 된 것 같다.

댓글 남기기

이 사이트는 스팸을 줄이는 아키스밋을 사용합니다. 댓글이 어떻게 처리되는지 알아보십시오.

%d 블로거가 이것을 좋아합니다: