ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Prometheus Label
    Metric 2021. 4. 16. 22:01
    반응형

    개요

    LabelPrometheus의 아주 강력한 기능 중 하나이다. Label은 키-값 쌍으로 이루어져 있으며, Prometheus가 시계열 데이터를 식별하는데 "메트릭 이름"과 더불어서 사용한다. 예를 들어보자. 모니터링 세계에서 HTTP 요청에 대한 상태 코드는 주로 다음과 같이 수집한다.

     

    • 2xx (응답 성공)
    • 3xx (응답 성공 - 리다이렉션)
    • 4xx (응답 실패 - 사용자 오류)
    • 5xx (응답 실패 - 서버 오류)

    어떻게 메트릭 이름을 지을 것인가? 아주 간단하게 다음과 같이 지을 수 있을 것이다.

    http_request_status_code_2xx
    http_request_status_code_3xx
    http_request_status_code_4xx
    http_request_status_code_5xx

     

    만약에, 2xx, 3xx가 아니라 각 상태 코드 별로 모아야 한다고 해보자. 현재 표준에 따르면 30개가 넘는 상태 코드가 존재한다. (실제로는 다 쓰이진 않더라도...) 이를 다 만들 것인가? Prometheus에서는 저런 패턴을 지양할 것을 강력하게 권고한다. 저런 패턴을 일컬어 "안티 패턴"이라고도 한다. Prometheus는 보통 상태 코드의 값에 대한 메트릭을 다음과 같이 수집한다.

    http_request{ status_code="200" }
    http_request{ status_code="201" }
    http_request{ status_code="301" }
    http_request{ status_code="404" }
    http_request{ status_code="400" }
    http_request{ status_code="500" }

     

    여기서 http_request가 메트릭 이름이고, status_codeLabel이다. 위의 6개의 시계열 데이터는 각각 다른 데이터라고 보면 된다. 그럼 여기서 드는 질문이 하나 있을 것이다. 왜 Label이 강력한 기능일까?

     

    Label을 이용해서, 메트릭에 대한 집계를 할 수 있기 때문이다. 상태코드 2xx에 대한 개수를 보고 싶으면 다음과 같이 쿼리를 만들 수 있다.

    sum(rate(http_request{status_code=~"2.."}[5m]))

     

    "2.."이라고 표현함으로써, Label의 키 status_code의 값이 2xx(200, 201 등의 2로 시작하는 3자리 숫자)인 모든 데이터를 집계할 수 있다.

    Label 만들어보기

    이번에도 간단한, 애플리케이션을 만들어보면서 Label을 조금 더 알아보자. 코드는 다음 URL에서 얻을 수 있다.

    프로젝트의 구조와 설정은 지난 장과 동일하다. 다만 웹 애플리케이션 코드인 main.go만 다르다. main.go의 전체 코드는 다음과 같다.

     

    part1/ch04/main.go

    package main
    
    import (
        "fmt"
        "html"
        "net/http"
    
        "github.com/prometheus/client_golang/prometheus"
        "github.com/prometheus/client_golang/prometheus/promhttp"
    )
    
    var (
        REQUEST = prometheus.NewCounterVec(
            prometheus.CounterOpts{
                Name: "http_requests_total",
                Help: "How many HTTP requests processed, partitioned by status code and HTTP method.",
            },
            []string{"path", "method"},
        )
    )
    
    func init() {
        prometheus.MustRegister(REQUEST)
    }
    
    func index(w http.ResponseWriter, r *http.Request) {
        REQUEST.WithLabelValues(r.URL.Path, r.Method).Inc()
        fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
    }
    
    func main() {
        http.HandleFunc("/", index)
        http.Handle("/metrics", promhttp.Handler())
        http.ListenAndServe(":2112", nil)
    }

    이 애플리케이션에 대해 간단하게 설명하자면 해당 URL과 HTTP Method 별로 누적 요청 개수를 수집한다. 여기서 알 수 있는 점은 Label은 한 메트릭 이름 당 여러 개를 지정할 수 있다는 것이다. 먼저 전역 변수인 REQUEST를 살펴보자.

     

    part1/ch04/main.go

    // ...
    var (
        REQUEST = prometheus.NewCounterVec(
            prometheus.CounterOpts{
                Name: "http_requests_total",
                Help: "How many HTTP requests processed, partitioned by status code and HTTP method.",
            },
            []string{"path", "method"},
        )
    )
    // ...

    수집하고 싶은 메트릭에서 Label을 설정하고 싶다면 prometheus.New[Metric]Vec을 함수를 써야 한다. 여기서는 URP과 HTTP Method에 따른 요청 누적 개수이기 때문에 Counter 타입을 수집해야 한다. 따라서 위의 코드처럼 NewCounterVec 함수를 사용했다. 여기서는 조금 더 깊이 실제 코드를 살펴 보자.

     

    github.com/prometheus/client_golang/blob/master/prometheus/counter.go

    func NewCounterVec(opts CounterOpts, labelNames []string) *CounterVec {
        desc := NewDesc(
            BuildFQName(opts.Namespace, opts.Subsystem, opts.Name),
            opts.Help,
            labelNames,
            opts.ConstLabels,
        )
        return &CounterVec{
            MetricVec: NewMetricVec(desc, func(lvs ...string) Metric {
                if len(lvs) != len(desc.variableLabels) {
                    panic(makeInconsistentCardinalityError(desc.fqName, desc.variableLabels, lvs))
                }
                result := &counter{desc: desc, labelPairs: MakeLabelPairs(desc, lvs), now: time.Now}
                result.init(result) // Init self-collection.
                return result
            }),
        }
    }

    NewCounterVec 함수는 CounterOptsLabel이름의 목록들을 전달 받아서 CounterVec이라는 구조체를 만들어서 그 주소를 반환하는 함수이다.

     

    github.com/prometheus/client_golang/blob/master/prometheus/counter.go

    // ...
    type CounterVec struct {
        *MetricVec
    }
    // ...

     

    CounterVec 구조체는 필드 이름 없이 *MetricVec 타입이 설정된 것을 확인할 수 있는데, 이는 MetricVec의 필드를 모조리 물려 받는 것을 뜻한다.

    참고! Golang의 상속

    엄격하게 말하면 Golang은 상속 개념이 없습니다. 위는 "컴포지션"이라는 것을 이용해서, 다른 구조체의 필드를 모조리 가져오는 것입니다. 상속을 흉내낸 것이라고 볼 수 있습니다.

     

    CounterVec은 다음 MetricVec의 필드들을 사용할 수 있다.

     

    github.com/prometheus/client_golang/blob/master/prometheus/vec.go

    // ...
    type MetricVec struct {
        *metricMap
    
        curry []curriedLabelValue
    
        // hashAdd and hashAddByte can be replaced for testing collision handling.
        hashAdd     func(h uint64, s string) uint64
        hashAddByte func(h uint64, b byte) uint64
    }
    // ...

     

    결국 NewCounterVecCounterOptsLabel 목록을 전달 받아서 MetricVec 구조체를 적절하게 초기화시키는 것이라고 보면 된다.

     

    part1/ch04/main.go

    // ...
    func main() {
        http.HandleFunc("/", index)
        http.Handle("/metrics", promhttp.Handler())
        http.ListenAndServe(":2112", nil)
    }

     

    main 함수를 보면 "/"에 index 함수가 바인딩 된 것을 볼 수 있다.

     

    part1/ch04/main.go

    // ...
    func index(w http.ResponseWriter, r *http.Request) {
        REQUEST.WithLabelValues(r.URL.Path, r.Method).Inc()
        fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
    }
    // ...

     

    index 함수를 보면, URL "/" 아래에서 요청이 일어날 때마다 그 Path와 Http Method를 Label 값으로 넘겨주어서, 1씩 누적 시키는 것을 확인할 수 있다. 이 때 중요한 것은 NewCounterVec 함수에서 전달할 때, Label 목록의 순서를 맞춰야 한다는 것이다. 따라서 Path, Http Method를 순서대로 값을 넣어준다.

     

    이제 제대로 수집하는지 확인해보자. docker-compose를 이용해 애플리케이션과 Prometheus를 실행한다.

    $ pwd
    # docker-compose.yml이 있는 곳.
    /Users/gurumee/Workspace/gurumee-prometheus-code/part1/ch04
    
    $ docker-compose up --build -d

     

    몇 초 후 다음 명령어를 입력하여 http 요청을 만든다.

    $ source request.sh

     

    requast.sh는 다음과 같은 요청을 한다.

    • Path: "/", HTTP Method: "GET" x4
    • Path: "/", HTTP Method: "POST" x2
    • Path: "/", HTTP Method: "PUT" x1
    • Path: "/", HTTP Method: "DELETE" x1
    • Path: "/test1", HTTP Method: "GET" x2
    • Path: "/test2", HTTP Method: "POST" x2
    • Path: "/test3", HTTP Method: "PUT" x2
    • Path: "/test4", HTTP Method: "DELETE" x2

    한 번 브라우저에서 "localhost:2112/metrics"를 접속해서 한 번 확인해보자. 다음과 같이 메트릭이 수집되었다면 성공이다.

    Label을 이용한 쿼리 및 집계

    이번엔 PromQL을 이용해서 수집된 메트릭을 집계해보자. 여기서는 간단한 PromQL을 다룰 것이며 추후, 이어지는 장에서 더 깊게 배우게 될 것이다. 지금은 그냥 간단히 훑는 느낌으로 살펴보자. 먼저 Prometheus UI(localhost:9090)를 접속하자. 먼저 path가 "/"면서, method가 "GET"인 http_requests_total를 쿼리해보자. 다음 쿼리를 입력한다.

    http_requests_total{ path="/", method="GET" }

     

    결과는 다음과 같다.

     

    Prometheus는 임의의 메트릭에 대해서 가지고 있는 Label 개수 이하만큼 지정을 해서 필터링 후 집계가 가능하다. 또한 = 연산자를 이용해서 Label의 값이 일치하는 메트릭에 대해서 쿼리가 가능하다. 이번엔 다음 쿼리를 입력하여 path는 "/", method는 "GET"이 아닌 http_requests_total를 쿼리해본다.

    http_requests_total{ path="/", method!="GET" }

     

    결과는 다음과 같다.

     

    != 연산자를 이용하면 값이 일치하지 않은 메트릭들에 대해서 쿼리가 가능하다. 또한 Prometheus의 강력한 기능은 Label의 값들을 이용해서 집계가 가능하다는 것이다. 위는 쿼리는 3개의 시계열 데이터를 쿼리(검색)한 것이다. 이를 합쳐보자. 다음 쿼리를 입력한다.

    sum(http_requests_total{ path="/", method!="GET" })

     

    결과는 다음과 같다.

    위 그림에서 확인할 수 있듯이 path가 "/"이면서 method가 "GET"이 아닌 http_requests_total의 합계는 총 4개이다.

     

    또한, Label이 강력한 이유는 앞에서 언급했듯 정규 표현식을 통해서도 쿼리 및 집계가 가능하다는 것이다. 이번에는 다음 쿼리를 입력해서 http_requests_total의 method가 "POST" 혹은 "PUT"인 총 개수를 집계해보자.

    sum(http_requests_total{ method=~"POST|PUT"})

     

    결과는 다음과 같다.

    =~ 연산자는 Label의 값에 대해서 정규표현식이 맞는 메트릭들에 대해서 쿼리할 수 있게 한다. 이번에는 path가 "/te"로 시작하는 http_requests_total를 쿼리해보자.

    http_requests_total{ path=~"/te.*"}

     

    결과는 다음과 같다.

    여기서 "/te.*"은 정규표현식으로써 "/te"로 시작하는 것들을 찾아낼 수 있다. 이번에는 path가 "/te"로 시작하지 않는 http_requests_total를 쿼리해보자.

    http_requests_total{ path!~"/te.*"}

     

    결과는 다음과 같다.

    !~ 연산자는 =~ 반대로 정규표현식이 맞지 않는 메트릭들을 쿼리할 수 있게 한다. 역시 이렇게 쿼리한 메트릭들도 sum등의 함수를 통해서 집계가 가능하다

    4.4 Label 사용 시 Tip

    Label 사용 시 매우 주의할 점이 있다. 한 메트릭에 대한 Label의 개수가 증가할수록 Cardinality가 증가한다. CardinalityPrometheus가 수집한 고유한 시계열 개수라고 보면 된다. 앞서 메트릭 이름과 Label에 따라서 시계열이 식별된다고 언급했었다.

    http_requests_total{ method="GET", path="/" } 2
    http_requests_total{ method="GET", path="/test" } 1

     

    즉 위의 시계열 데이터는 메트릭 이름은 같으나 다른 데이터라고 보면 된다. 이렇게 메트릭 이름에 대한 Label 개수가 증가할수록 Cardinality는 폭발적으로 증가하게 된다. 각각 서로 다른 method가 4개, path가 10개라면 http_requests_total 메트릭에 대한 Cardinality는 40(4 * 10)이 된다. 이렇게 높은 Cardinality는 데이터가 적을 때는 문제가 안되지만 많아지면 많아질수록 Prometheus에 치명적인 성능 문제를 야기할 수 있다.

     

    이에 대해서 책 "Prometheus Up & Running(번역판 : 프로메테우스 오픈소스 모니터링 시스템)"에서는 임의의 메트릭에 대한 Cardinality는 10 이하로 되도록 만들 것을 권장하고 있다.

Designed by Tistory.