Carpe Diem

備忘録

Prometheus でAPIサーバの監視【HTTP】

概要

前回は既存のexporterを利用せず、自分でメトリクスを生成する方法を説明しました。

christina04.hatenablog.com

その方法を使って実際にAPIサーバ(HTTP)のメトリクスを生成し、可視化してみます。

環境

  • Ubuntu 18.04
  • Prometheus 2.11.1
  • Golang 1.12.7
  • Grafana 6.2.5

The Four Golden SignalsとREDメソッド

分散システムで何をモニタリングすべきかについては、The Four Golden Signalsが有名です。

それを踏まえてマイクロサービスではREDメソッドという指標があります。

項目 説明
Rate リクエスト数/sec
Errors エラー数
Duration リクエスト毎のduration

今回はこのREDメソッドに基づいて可視化できるようにします。

メトリクスの追加

  • 処理中のリクエスト数
  • 総リクエスト数
  • latency
  • response size

のメトリクスが取れれば基本的に可視化したいものは出せそうです。

コレクタ

  • 処理中のリクエスト数
  • 総リクエスト数
  • latency
  • response size

を取得できるようにするためのコレクタを用意します。

inFlight = prometheus.NewGauge(prometheus.GaugeOpts{
        Name: "http_requests_in_flight",
        Help: "A gauge of requests currently being served by the wrapped handler.",
})

counter = prometheus.NewCounterVec(
        prometheus.CounterOpts{
                Name: "http_requests_total",
                Help: "A counter for requests to the wrapped handler.",
        },
        []string{"handler", "code", "method"},
)

duration = prometheus.NewHistogramVec(
        prometheus.HistogramOpts{
                Name:    "request_duration_seconds",
                Help:    "A histogram of latencies for requests.",
                Buckets: []float64{.25, .5, 1, 2.5, 5, 10},
        },
        []string{"handler", "method"},
)

responseSize = prometheus.NewHistogramVec(
        prometheus.HistogramOpts{
                Name:    "response_size_bytes",
                Help:    "A histogram of response sizes for requests.",
                Buckets: []float64{200, 500, 900, 1500},
        },
        []string{},
)

ミドルウェア

promhttpにはメトリクスが簡単に取得できるようあらかじめミドルウェアが用意されています。

今回使うのは以下の4つです。

ミドルウェア 役割 備考
InstrumentHandlerInFlight 現在処理中のリクエスト数 -
InstrumentHandlerDuration latency
code, methodのラベルが用意されている
handler毎でも分類したいのでhandlerラベルをコレクタに追加
InstrumentHandlerCounter リクエスト数
code, methodのラベルが用意されている
handler毎でも分類したいのでhandlerラベルをコレクタに追加
InstrumentHandlerResponseSize レスポンスサイズ
code, methodのラベルが用意されている
-

ミドルウェアチェーンとしてセットできるので、共通関数を用意しておきます。

func genInstrumentChain(name string, handler http.HandlerFunc) http.Handler {
        return promhttp.InstrumentHandlerInFlight(inFlight,
                promhttp.InstrumentHandlerDuration(duration.MustCurryWith(prometheus.Labels{"handler": name}),
                        promhttp.InstrumentHandlerCounter(counter.MustCurryWith(prometheus.Labels{"handler": name}),
                                promhttp.InstrumentHandlerResponseSize(responseSize, handler),
                        ),
                ),
        )
}

handler

  • alive
  • hello

という2つのhandlerを用意します。
エラーレートも可視化したいのでランダムでエラーを返すようにしています。

func alive(w http.ResponseWriter, _ *http.Request) {
        dur := rand.Intn(1000)
        time.Sleep(time.Duration(dur) * time.Millisecond)  // 処理を表現するためのsleep
        w.WriteHeader(http.StatusOK)
        fmt.Fprintln(w, "OK")
}

func hello(w http.ResponseWriter, _ *http.Request) {
        dur := rand.Intn(1000)
        time.Sleep(time.Duration(dur) * time.Millisecond)  // 処理を表現するためのsleep
        n := rand.Intn(4)  // エラーレスポンスを返すためのランダム値
        switch n {
        case 0:
                w.WriteHeader(http.StatusOK)
                fmt.Fprintln(w, "Hello World")
        case 1:
                w.WriteHeader(http.StatusNotFound)
                fmt.Fprintln(w, "Not Found")
        case 2:
                w.WriteHeader(http.StatusBadRequest)
                fmt.Fprintln(w, "Bad Request")
        case 3:
                w.WriteHeader(http.StatusInternalServerError)
                fmt.Fprintln(w, "Internal Server Error")
        }
}

main

コレクタの登録やhandlerの登録をして/metricsにてexposeします。

func init() {
        prometheus.MustRegister(inFlight, counter, duration, responseSize)
}

func main() {
        aliveChain := genInstrumentChain("alive", alive)
        helloChain := genInstrumentChain("hello", hello)

        http.Handle("/metrics", promhttp.Handler())

        http.Handle("/", aliveChain)
        http.Handle("/hello", helloChain)
        log.Fatal(http.ListenAndServe(":8080", nil))
}

時系列データを溜める

abで適当にリクエストを投げて時系列データを溜めていきます。

alive handler

$ ab -n 10000 -c 10 localhost:8080/

hello handler

$ ab -n 10000 -c 10 localhost:8080/hello

POSTリクエス

$ ab -n 10000 -c 10 -p hoge.json localhost:8080/hello

PUTリクエス

$ ab -n 10000 -c 10 -u hoge.json localhost:8080/hello

各ラベルでメトリクスが生成されるので以下のようになります。

f:id:quoll00:20190718193020p:plain

Grafanaで可視化

先程実装したメトリクスをGrafanaで可視化します。

Rate

直近1分間の秒間リクエスト数を可視化します。

handler毎

handlerとmethodで分類できるようにします。

PromQL

sum by(handler, method)(rate(http_requests_total[1m]))

と書きます。

rate(http_requests_total[1m]

だけだとcodeラベルによってstatus code毎にメトリクスがあるため凡例が増えてしまいます。
なのでhandlerとmethodでグルーピングします。

codeをまとめたいだけなので

sum without(code)(rate(http_requests_total[1m]))

でも同じ結果を出せます。

Grafana設定

QueryとLegendを設定します。

f:id:quoll00:20190718184211p:plain

VisualizationはAxesのUnitをrpsにし、

f:id:quoll00:20190718184310p:plain

Legendの表示設定をいじります。

f:id:quoll00:20190718184334p:plain

結果以下のように表示されます。

f:id:quoll00:20190718184354p:plain

HTTP status毎

HTTP status codeで分類します。

PromQL

sum by(code)(rate(http_requests_total[1m]))

今度はcodeラベルでグルーピングしてます。

Grafana設定

先程と同じように設定します。

f:id:quoll00:20190718184627p:plain

Errors

handler毎に4xxエラー・5xxエラーの%を可視化します。

4xxエラー

PromQL

sum by(handler, method)(rate(http_requests_total{code=~"4.."}[1m]))
/
sum by(handler, method)(rate(http_requests_total[1m]))

と書きます。http_requests_total{code=~"4.."}正規表現一致をさせてます。

単純に

rate(http_requests_total{code=~"4.."}[1m])
/
rate(http_requests_total[1m])

だとベクトルがマッチするもの(=分子も分母もcode="4xx")同士で割り算してしまうので常に1になってしまいます。
なのでcodeを無視したグルーピングが必要です。

by()ではなくwithout()を使って

sum without(code)(rate(http_requests_total{code=~"4.."}[1m]))
/
sum without(code)(rate(http_requests_total[1m]))

でも同じ結果になります。

Grafana設定

QueryとLegendを設定し、

f:id:quoll00:20190718190003p:plain

AxesのUnitを%にします。

f:id:quoll00:20190718190027p:plain

これで以下のように表示されます。

f:id:quoll00:20190718185943p:plain

5xxエラー

PromQL

sum by(handler, method)(rate(http_requests_total{code=~"5.."}[1m]))
/
sum by(handler, method)(rate(http_requests_total[1m]))

とします。正規表現のパターンだけの変更です。

Grafana設定

先程と同様に設定します。

f:id:quoll00:20190718190340p:plain

Duration

handler毎に平均duration, 50%ile, 90%ileを可視化します。

Average

PromQL

rate(request_duration_seconds_sum[1m])
/
rate(request_duration_seconds_count[1m])

マリコレクタでやったように平均値を出します。

Grafana設定

QueryとLegendを設定し、

f:id:quoll00:20190718190529p:plain

AxesのUnitをミリ秒に。

f:id:quoll00:20190718190554p:plain

すると以下のように表示されます。

f:id:quoll00:20190718190621p:plain

Percentile

duration 50%ileと90%ileを可視化します。
ほぼ同じなので50%ileのみ説明します。

PromQL

ヒストグラムコレクタでやったようにhistogram_quantile()を使います。

histogram_quantile(0.5, rate(request_duration_seconds_bucket[1m]))

Grafana設定

QueryとLegendを設定し、

f:id:quoll00:20190718191207p:plain

AxesのUnitをミリ秒に。

f:id:quoll00:20190718191228p:plain

すると以下のように表示されます。

f:id:quoll00:20190718191246p:plain

最終的なダッシュボード

それぞれをまとめると以下のように表示されます。

f:id:quoll00:20190718175433p:plain

サンプルコード

今回使用したコードはこちらです。

github.com

まとめ

独自メトリクスを用いてREDメソッドに基づく要素を可視化しました。
試した感想としてはPrometheusにメトリクスさえ保存してしまえばGrafanaでかなり柔軟に可視化できると思いました。

次回はgRPCのメトリクスを可視化してみます。

ソース