TIL/Monitoring(k8s, grafana)

[Prometheus] 적절한 scrape_interval을 찾아.. - Node exporter 코드 분석

쓱쓱565 2024. 11. 6. 12:43

목차

  1. Prometheus의 대략적인 구조 - pulling, exporter
  2. 결론
  3. 분석

1. Prometheus의 구조 - exporter, pulling

Prometheus 각종 exporter 들로부터 그들의 매트릭을 pull하여 저장한다. 그 주기는 scrape_config - rate_interval(이하 scrape interval) 을 참조한다.

 

docker에 node-exporter, cadvisor-exporter 등의 이름으로 container 를 돌리고 있다면, 아래처럼 접속해서 prometheus 가 수집하는 매트릭을 확인해볼 수 있다.

 

/metric 으로 request 보냈을 때의 반환값 샘플

 

http://node-exporter:9100/metric
http://cadvisor-exporter:9100/metric

그렇다면 이들 exporter들의 상태는 언제, 어떻게 변할까? '적절한 수집 주기'는 얼마일까?

2. 결론

  • exporter 들은 자체적으로 자신의 상태값을 갱신하지 않는다.
  • exporter들은 /metric http request 를 받을 때 metric을 수집하기 시작한다.
    • (적어도 prometheus 의 exporter들은 그러할 것이라고 추측할 수 있다)
  • Prometheus UI에서 metric 을 수집하는 데에 걸리는 시간, response에 걸리는 시간 등은 손쉽게 확인 가능하다. (target 페이지)
  • request -> scrap -> response에 100~500ms 정도 걸렸다.
  • (따라서 이론상) exporter 들은 1~2sec 의 scrape config도 충분히 감당 가능할 것으로 보인다.
  • 1~2 sec 의 scrape_interval 에 따른 Prometheus 및 Node의 부하는 확인하지 않았다. thanos 등을 운영하며 여러 개의 Prometheus pod를 운영하는 게 좋을 것이다.
  • 그렇지만 '정확한' 값을 수집하는 게 프로메테우스이 역할은 아니기에, 기존의 scrape_interval(15sec)를 유지해도 좋을 것이라 판단된다.

3. 분석

1) 구조

main : http 요청을 받아 처리하는 부분 빼고는 특기할 만 한 사항이 없다.
요청을 받을 때마다 newHandler를 생성해 http 요청을 적절히 처리한다.

http handler: 요청에 따라 적절한 값을 반환한다. metric을 요청할 시 요청한 metric을 반환한다.

collector : 실제로 매트릭을 수집한다.
https://github.com/prometheus/client_golang/blob/main/prometheus/collector.go

collector.

2) 개괄

Node exporter 의 코드 전문은 아래에서 확인할 수 있다.
https://github.com/prometheus/node_exporter

오픈소스 프로젝트이므로 예고 없이 코드의 내용이 변경될 수 있다.

코드는 Go로 작성되었다.

3) 실제 코드

(1) main 함수

a. main 함수

main 함수는 default 값들을 처리하는 부분과 http 요청을 처리하는 부분 2개로 나뉘어져 있다.

주기적으로 metric을 수집한 뒤 자신에게 저장할거라 생각했으나, 그런 코드는 존재하지 않는다.
http 요청을 받을 때에만 metric을 수집해 반환한다.

 

main 함수 - default 값들을 처리한다

b. defaults..

metric path의 기본값은 /metric 이다 .

        metricsPath = kingpin.Flag(
            "web.telemetry-path",
            "Path under which to expose metrics.",
        ).Default("/metrics").String()

(2) http handler

main 함수에서 http 요청을 처리하는 부분이다.

    http.Handle(*metricsPath, newHandler(!*disableExporterMetrics, *maxRequests, logger))
a. Handler

http 요청을 처리한다. /metric (기본값 기준) 에 요청을 받을 경우 metric을 수집해 반환한다.

http.handle

 

b. handler - type / struct

Handler 타입이다. handler에서는는 아래의 내용들을 다룬다.

1) node exporter 자체의 매트릭을 수집할지
2) 수집할 / 수집하지 않을 매트릭들의 목록
3) request 최대 제한

// handler wraps an unfiltered http.Handler but uses a filtered handler,

// created on the fly, if filtering is requested. Create instances with

// newHandler.

type handler struct {
    unfilteredHandler http.Handler
    // enabledCollectors list is used for logging and filtering
    enabledCollectors []string
    // exporterMetricsRegistry is a separate registry for the metrics about
    // the exporter itself.
    exporterMetricsRegistry *prometheus.Registry
    includeExporterMetrics  bool
    maxRequests             int
    logger                  *slog.Logger
}
c. handler 생성

(1-2) http handler의 main 함수에서 http 요청을 처리하는 부분이다.

    http.Handle(*metricsPath, newHandler(!*disableExporterMetrics, *maxRequests, logger))

http 요청을 받을 때마다 newHandler 메서드를 호출, handler 가 생성된다. handler는 innerHandler의 메타 정보를 다룬다. exporter 자체의 매트릭을 수집할지, metric 수집 실패 전까지 몇 번 metric 수집을 시도할지 등의 정보를 다룬다.

func newHandler(includeExporterMetrics bool, maxRequests int, logger *slog.Logger) *handler {

    h := &handler{
        exporterMetricsRegistry: prometheus.NewRegistry(),
        includeExporterMetrics:  includeExporterMetrics,
        maxRequests:             maxRequests,
        logger:                  logger,

    }

    if h.includeExporterMetrics {
        h.exporterMetricsRegistry.MustRegister(
            promcollectors.NewProcessCollector(promcollectors.ProcessCollectorOpts{}),
            promcollectors.NewGoCollector(),
        )
    }

    if innerHandler, err := h.innerHandler(); err != nil {
        panic(fmt.Sprintf("Couldn't create metrics handler: %s", err))
    } else {
        h.unfilteredHandler = innerHandler
    }
    return h
}

innerHandler가 생성될 때, filter 조건에 맞게 NodeCollector 를 생성한다. 이 nodeCollector 들이 CPU사용량 등의 매트릭을 실제로 수집한다.

// innerHandler is used to create both the one unfiltered http.Handler to be

// wrapped by the outer handler and also the filtered handlers created on the

// fly. The former is accomplished by calling innerHandler without any arguments

// (in which case it will log all the collectors enabled via command-line

// flags).

func (h *handler) innerHandler(filters ...string) (http.Handler, error) {
    nc, err := collector.NewNodeCollector(h.logger, filters...)
    if err != nil {
        return nil, fmt.Errorf("couldn't create collector: %s", err)

    }


    // Only log the creation of an unfiltered handler, which should happen

    // only once upon startup.

    if len(filters) == 0 {
        h.logger.Info("Enabled collectors")
        for n := range nc.Collectors {
            h.enabledCollectors = append(h.enabledCollectors, n)
        }
        sort.Strings(h.enabledCollectors)
        for _, c := range h.enabledCollectors {
            h.logger.Info(c)

        }

    }


    r := prometheus.NewRegistry()

 r.MustRegister(versioncollector.NewCollector("node_exporter"))

    if err := r.Register(nc); err != nil {

        return nil, fmt.Errorf("couldn't register node collector: %s", err)

    }


    var handler http.Handler
    if h.includeExporterMetrics {
        handler = promhttp.HandlerFor(
            prometheus.Gatherers{h.exporterMetricsRegistry, r},
            promhttp.HandlerOpts{
                ErrorLog:            slog.NewLogLogger(h.logger.Handler(), slog.LevelError),
                ErrorHandling:       promhttp.ContinueOnError,
                MaxRequestsInFlight: h.maxRequests,
                Registry:            h.exporterMetricsRegistry,
            },
        )

        // Note that we have to use h.exporterMetricsRegistry here to

        // use the same promhttp metrics for all expositions.

        handler = promhttp.InstrumentMetricHandler(
            h.exporterMetricsRegistry, handler,
        )

    } else {
        handler = promhttp.HandlerFor(
            r,
            promhttp.HandlerOpts{
                ErrorLog:            slog.NewLogLogger(h.logger.Handler(), slog.LevelError),
                ErrorHandling:       promhttp.ContinueOnError,
                MaxRequestsInFlight: h.maxRequests,
            },

        )

    }



    return handler, nil

}

실제로 매트릭을 가져오는 부분이다. promhttp.HandlerFor 부분이 실제로 collectorgather() 메서드를 호출한다.

    var handler http.Handler

    if h.includeExporterMetrics {

        handler = promhttp.HandlerFor(

            prometheus.Gatherers{h.exporterMetricsRegistry, r},

            promhttp.HandlerOpts{

                ErrorLog:            slog.NewLogLogger(h.logger.Handler(), slog.LevelError),

                ErrorHandling:       promhttp.ContinueOnError,

                MaxRequestsInFlight: h.maxRequests,

                Registry:            h.exporterMetricsRegistry,

            },

        )

        // Note that we have to use h.exporterMetricsRegistry here to

        // use the same promhttp metrics for all expositions.

        handler = promhttp.InstrumentMetricHandler(

            h.exporterMetricsRegistry, handler,

        )

    } else {

        handler = promhttp.HandlerFor(

            r,

            promhttp.HandlerOpts{

                ErrorLog:            slog.NewLogLogger(h.logger.Handler(), slog.LevelError),

                ErrorHandling:       promhttp.ContinueOnError,

                MaxRequestsInFlight: h.maxRequests,

            },

        )

    }



    return handler, nil

handlerFor가 각각의 Gatherer 로부터 .gather() 메서드를 호출한다.
https://github.com/prometheus/client_golang/blob/main/prometheus/promhttp/http.go#L88C25-L165C33

gatherer 메서드가 실제로 매트릭을 수집하여 반환한다.
https://github.com/prometheus/client_golang/blob/v1.20.5/prometheus/registry.go#L140C1-L161C2

각각의 gatherer 로부터 gather() 를 호출한 뒤 metricFamily list를 생성해 append 한다.


// Gather implements TransactionalGatherer interface.
func (r *MultiTRegistry) Gather() (mfs []*dto.MetricFamily, done func(), err error) {
    errs := MultiError{}

    dFns := make([]func(), 0, len(r.tGatherers))
    // TODO(bwplotka): Implement concurrency for those?
    for _, g := range r.tGatherers {
        // TODO(bwplotka): Check for duplicates?
        m, d, err := g.Gather()
        errs.Append(err)

        mfs = append(mfs, m...)
        dFns = append(dFns, d)
    }

이 결과값들이 http.handler를 거쳐 prometheus로 반환된다.

기타

Prometheus dcos의 Gatherer 항목
https://pkg.go.dev/github.com/prometheus/client_golang/prometheus#Gatherer