概要 #
「CPU使用率が高いノードを知りたい」「p95レイテンシを見たい」と思っても、目的からPromQLの書き方にすぐ辿り着けないことがある。第1回では、Prometheus・Grafanaの役割を扱った。PromQLもrate()・sum()・avg()・by()という最小限だけに触れている。実際の調査でよく使うincrease()・topk()・histogram_quantile()や、instant vectorとrange vectorの違いはまだ扱っていない。
この記事では、これらの関数を実際のクエリ結果とともに整理する。対象読者は、第1回でPrometheusにPromQLクエリを送信する操作は経験した人。[5m]のような時間指定の意味や、rate()とincrease()の使い分けがまだ曖昧な人を想定している。Kubernetes・kubectlの基本操作とPromQLでの単純なmetric取得は前提とする。
この記事ではAlerting rules・Alertmanager(第3回)は扱わない。Kubernetes Service Discovery・kube-state-metrics(第4回)も扱わない。新しいコンポーネントの追加も行わず、第1回のminikube環境をそのまま使う。
まず見る:目的から引けるチートシート #
式の形だけ先に引きたい場合はここを見る。それぞれの根拠・実測は後続の節にある。
| やりたいこと | PromQL |
|---|---|
| いまの値を1点だけ見る | metric_name{label="value"} |
| 直近5分間のサンプルをまとめて取得する | metric_name{label="value"}[5m] |
| counterの1秒あたりの増加率を出す(グラフ・記録ルール向け) | rate(metric_name[5m]) |
| counterの期間中の増加量を出す(人が読む数値向け) | increase(metric_name[5m]) |
| ラベルごとに集計しつつ、そのラベルだけ残す | sum by(label) (expr) |
| 特定のラベルだけ落として集計する | sum without(label) (expr) |
| 複数本のメトリクスのうち最大値だけ見る(ラベルは残らない) | max(expr) |
| 最大値と、それがどのラベルの組み合わせかも見る | topk(1, expr) |
| メモリ使用率を出す | (1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100 |
| ディスク使用率を出す(マウントポイント指定) | (1 - node_filesystem_avail_bytes{mountpoint="/path"} / node_filesystem_size_bytes{mountpoint="/path"}) * 100 |
| ネットワークの受信量(bytes/sec)を出す | rate(node_network_receive_bytes_total{device!="lo"}[5m]) |
| コア単位で上位N件だけ抽出する(single-node) | topk(N, sum by(cpu) (expr)) |
| ノード単位で上位N件だけ抽出する(複数ノード) | topk(N, <instanceでまとめた式>)(集約の仕方はmetricの意味次第。CPU使用率ならavg by(instance)) |
| ヒストグラムからp95などのパーセンタイルを出す | histogram_quantile(0.95, sum by(le) (rate(metric_bucket[5m]))) |
今回使う環境 #
第1回で作成したmonitoring Namespaceの構成(Node Exporter・Prometheus・Grafana)をそのまま使う。バージョンも第1回から変えていない。minikube v1.39.0(--driver=docker)、Kubernetes v1.37.0、Prometheus prom/prometheus:v3.14.0、Node Exporter prom/node-exporter:v1.12.1である。すべてローカルのminikube上で完結し、クラウドの課金は発生しない。
kubectl get pods,svc -n monitoring
NAME READY STATUS RESTARTS AGE
pod/grafana-7d9d4457b8-lrjhj 1/1 Running 0 21h
pod/node-exporter-nh7g5 1/1 Running 0 16h
pod/prometheus-66b579486-5f6bn 1/1 Running 0 20h
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/grafana ClusterIP 10.96.9.150 <none> 3000/TCP 21h
service/node-exporter ClusterIP 10.105.9.76 <none> 9100/TCP 21h
service/prometheus ClusterIP 10.110.18.197 <none> 9090/TCP 21h
1点だけ追加している。histogram_quantile()の例に使えるヒストグラム型のメトリクスが、今回の環境のNode Exporterの/metricsには無かった。次の節で示すとおり実測して確認した。そのうえで、Prometheus自身の/metricsを新しいscrape対象に加えた。新しいDeploymentやServiceは追加せず、既存のprometheus-config ConfigMapに1つjobを足すだけの変更である。
設定ファイルの変更を反映する方法は複数ある。公式ドキュメントによると、SIGHUPシグナルの送信か、--web.enable-lifecycleを有効にしたうえでの/-/reloadエンドポイントへのPOSTでも再読み込みできる。今回のDeployment定義には--web.enable-lifecycleフラグが無いため、後述のkubectl rollout restartでPodを置き換える方法を使う。
# sample/prometheus-configmap.yaml(第1回のConfigMapにjob_nameを1つ追加したもの)
apiVersion: v1
kind: ConfigMap
metadata:
name: prometheus-config
namespace: monitoring
data:
prometheus.yml: |
global:
scrape_interval: 15s
scrape_configs:
- job_name: node-exporter
static_configs:
- targets:
- node-exporter:9100
- job_name: prometheus
static_configs:
- targets:
- localhost:9090
kubectl apply -f sample/prometheus-configmap.yaml
kubectl rollout restart deployment/prometheus -n monitoring
kubectl rollout status deployment/prometheus -n monitoring --timeout=60s
rollout restartは既存のPodを削除し、新しいPodを起動する操作である。第1回のDeployment定義(sample/prometheus/deployment.yaml)はTSDBの保存先をemptyDirにしているため、Podが置き換わるとそれまで蓄積したメトリクスは消える。以降のクエリは、再起動後にPrometheusが新しく集めたメトリクスに対して実行する。
このConfigMapは次回(第3回)以降も同じhistogram_quantile()の例に使うため、元の設定には戻さずそのまま残す。元に戻す場合の削除方法は、job_nameがprometheusのエントリだけをConfigMapから取り除き、kubectl rollout restart deployment/prometheus -n monitoringをもう一度実行すればよい。残存物はmonitoring Namespace内のPrometheus 1台のscrape対象が増えるだけで、他のNamespaceやminikubeクラスタ自体には及ばない。
configmap/prometheus-config configured
deployment.apps/prometheus restarted
Waiting for deployment "prometheus" rollout to finish: 1 old replicas are pending termination...
deployment "prometheus" successfully rolled out
flowchart LR
subgraph minikube["minikube node"]
NE["Node Exporter
:9100/metrics"]
Prom["Prometheus
:9090"]
end
Prom -- "scrape request" --> NE
Prom -- "scrape request(自分自身)" --> Prom
追加したのは、Prometheusが自分自身をscrapeする経路だけである。Node Exporterとの経路は第1回と変わらない。Grafanaは今回のクエリでは使わない。
localhostという名前が2つの意味で出てくるので区別する。ConfigMapのlocalhost:9090は、Prometheusのプロセス自身が自分の/metricsにアクセスするための、Pod内から見たアドレスである。これに対し、以降のクエリで使うhttp://localhost:9090は、kubectl port-forwardを経由して手元の端末からPrometheusのHTTP APIに到達するためのアドレスであり、同じ文字列でも指す経路が異なる。
kubectl port-forward -n monitoring svc/prometheus 9090:9090
# 別ターミナルで実行する。以降のcurlはすべてこのport-forward越し
curl -s http://localhost:9090/api/v1/targets | python3 -m json.tool
scrape対象の動作確認 #
Targetsのjobがnode-exporterとprometheusの2つになり、どちらも"health": "up"であれば、以降のクエリを実行できる状態である。kubectl rollout statusの成功はPodが起動したことしか示さず、Prometheusが実際にscrapeできているかとは別の確認になる。
label matcherとinstant vector・range vector #
PromQLのクエリは、まずmetric名とlabelでどの時系列を選ぶかを決める。label matcherには4種類ある。
| 演算子 | 意味 | 例 |
|---|---|---|
= |
完全一致 | {mode="idle"} |
!= |
不一致 | {mode!="idle"} |
=~ |
正規表現一致 | {device=~"eth.*|veth.*"} |
!~ |
正規表現不一致 | {device!~"lo|docker.*"} |
公式ドキュメントは、正規表現マッチが文字列全体に対する完全一致として扱われる(=~"foo"は=~"^foo$"と同じ)ことと、空文字列にマッチするlabel matcher({mode=""}など)は、そのラベル自体が設定されていない時系列にも一致することを説明している。!=・!~でラベルの有無まで確認したい場合は、この挙動に注意する。
metric名とlabelだけを指定すると、instant vectorになる。「各時系列から、同じ時刻のサンプルを1つずつ集めたもの」という定義どおり、結果は時系列ごとに1点の値になる。
curl -s --data-urlencode 'query=node_cpu_seconds_total{mode="idle",cpu="0"}' \
http://localhost:9090/api/v1/query | python3 -m json.tool
{
"status": "success",
"data": {
"resultType": "vector",
"result": [
{
"metric": {
"__name__": "node_cpu_seconds_total",
"cpu": "0",
"instance": "node-exporter:9100",
"job": "node-exporter",
"mode": "idle"
},
"value": [
1790387987.956,
"560921.27"
]
}
]
}
}
resultTypeがvectorで、valueが[タイムスタンプ, 値]の1組だけになっている。
同じセレクタの末尾に[1m]のように時間を付けると、range vectorになる。公式ドキュメントは「時系列ごとに、一定期間にわたる複数のデータポイントを含む集合」と定義している。範囲は左が開区間・右が閉区間である(境界の1点は含み、範囲の始点ちょうどのサンプルは含まない)。
curl -s --data-urlencode 'query=node_cpu_seconds_total{mode="idle",cpu="0"}[1m]' \
http://localhost:9090/api/v1/query | python3 -m json.tool
{
"status": "success",
"data": {
"resultType": "matrix",
"result": [
{
"metric": {
"__name__": "node_cpu_seconds_total",
"cpu": "0",
"instance": "node-exporter:9100",
"job": "node-exporter",
"mode": "idle"
},
"values": [
[1790387932.046, "560878.55"],
[1790387947.046, "560892.95"],
[1790387962.046, "560906.8"],
[1790387977.046, "560921.27"]
]
}
]
}
}
resultTypeがmatrixに変わり、valuesに4組のペアが並ぶ。scrape間隔は15秒(今回使う環境のConfigMapで指定)である。そのため1分の範囲に4点入る。
range vectorは、時系列ごとに一定期間の複数サンプルを保持するPromQLのデータ型である。 上のクエリのように、/api/v1/query(instant query、単一時刻での評価)にrange vectorの式をそのまま渡すこともでき、その場合の結果が今見たmatrixである。rate()やincrease()のように、一定期間のサンプルから1つの値を計算する関数への入力としてよく使う。
一方、/api/v1/query_range(range query、start・end・stepを指定して同じ式を複数の評価時刻で繰り返し評価する仕組み)には、range vectorの式をそのまま渡せない。
curl -s -g --data-urlencode "start=$(date -d '-1 minute' +%s)" --data-urlencode "end=$(date +%s)" --data-urlencode 'step=15' \
--data-urlencode 'query=node_cpu_seconds_total{mode="idle",cpu="0"}[1m]' \
http://localhost:9090/api/v1/query_range | python3 -m json.tool
{
"status": "error",
"errorType": "bad_data",
"error": "invalid parameter \"query\": invalid expression type \"range vector\" for range query, must be Scalar or instant Vector"
}
エラーメッセージのとおり、range queryのトップレベル式にはscalarかinstant vectorしか渡せない。range vector(PromQLのデータ型)とrange query(複数時刻で評価するAPIの仕組み)は別の概念である。この記事で使う/api/v1/queryはinstant queryである。
rate()とincrease() #
rate()とincrease()は、どちらもcounter型のmetric(node_cpu_seconds_totalのように単調増加する値)の変化を扱う関数である。公式ドキュメントによると、rate(v range-vector)は「range vector内の時系列について、1秒あたりの平均増加率を計算する」関数である。
curl -s --data-urlencode 'query=rate(node_cpu_seconds_total{mode="idle",cpu="0"}[5m])' \
http://localhost:9090/api/v1/query | python3 -m json.tool
{
"status": "success",
"data": {
"resultType": "vector",
"result": [
{
"metric": {"cpu": "0", "instance": "node-exporter:9100", "job": "node-exporter", "mode": "idle"},
"value": [1790387992.428, "0.7295244281479876"]
}
]
}
}
increase()は「range vector内の時系列について、増加量を計算する」関数である。公式ドキュメントは「rate(v)に秒数を掛けたもの(syntactic sugar for rate(v) multiplied by the number of seconds)」と位置づけている。同じ[5m](300秒)に対して両方を実行すると、この関係を数値で確認できる。
curl -s --data-urlencode 'query=increase(node_cpu_seconds_total{mode="idle",cpu="0"}[5m])' \
http://localhost:9090/api/v1/query | python3 -m json.tool
{
"status": "success",
"data": {
"resultType": "vector",
"result": [
{
"metric": {"cpu": "0", "instance": "node-exporter:9100", "job": "node-exporter", "mode": "idle"},
"value": [1790387992.454, "218.88176266661847"]
}
]
}
}
rate()の結果0.7295に300をかけると218.85になり、increase()の実測値218.88に近い。ただし、この2つは別々の問い合わせで得た値であり、評価時刻がわずかに異なる(1790387992.428と1790387992.454)ため、この差だけでは近似の精度を判断できない。
評価時刻をそろえるには、increase(v[5m]) - 300 * rate(v[5m])を1つのクエリにまとめて実行する。
curl -s --data-urlencode 'query=increase(node_cpu_seconds_total{mode="idle",cpu="0"}[5m]) - 300*rate(node_cpu_seconds_total{mode="idle",cpu="0"}[5m])' \
http://localhost:9090/api/v1/query | python3 -m json.tool
{
"status": "success",
"data": {
"resultType": "vector",
"result": [
{
"metric": {"cpu": "0", "instance": "node-exporter:9100", "job": "node-exporter", "mode": "idle"},
"value": [1790389051.308, "0"]
}
]
}
}
同一の評価時刻・同一のrange vectorに対して計算すると、差は0になる。increase()がrate()に秒数を掛けたものであることを、この1回の問い合わせで確認できる。両関数とも、counterのリセット(Podの再起動などで値が0に戻る現象)を自動で調整する点は共通している。
使い分けは公式ドキュメントの推奨に従う。ダッシュボードのグラフや記録ルール(recording rule)にはrate()を使う。increase()は、人が読んですぐ量がわかるようにしたい場合に使う。
sum() / avg() / max()とby() / without() #
第1回ではsum()とavg()を軽く扱った。ここでは、ラベルを残す・落とす操作であるby()とwithout()を中心に確認する。
公式ドキュメントは「withoutは指定したラベルを結果から取り除き、それ以外のラベルはすべて残す。byはその逆で、by句に列挙していないラベルを落とす」と説明している。
node_cpu_seconds_totalはcpu(コア番号)とmode(idle/user/systemなど)の2つのラベルを持つ。CPUコアごとの使用率を見たい場合はcpuだけを残す。
curl -s --data-urlencode 'time=1790389061.500' \
--data-urlencode 'query=sum by(cpu) (rate(node_cpu_seconds_total{mode!="idle"}[5m]))' \
http://localhost:9090/api/v1/query | python3 -m json.tool
cpu=0 0.06221052631579352
cpu=1 0.01771929824561627
cpu=2 0.03207017543859724
cpu=3 0.01575438596491201
cpu=4 0.025964912280703644
cpu=5 0.013087719298243084
cpu=6 0.027614035087721256
cpu=7 0.013789473684208117
cpu=8 0.02733333333333193
cpu=9 0.010315789473683168
cpu=10 0.026736842105262112
cpu=11 0.012666666666669456
cpu=12 0.02487719298245575
cpu=13 0.011438596491228786
cpu=14 0.02617543859649023
cpu=15 0.015403508771931994
timeパラメータで評価時刻を固定した。次のtopk()にも同じ時刻を指定し、この16件の中から実際に何位が返るかを照合する。
逆に、コアをまたいでまとめたい場合はwithout(cpu)でcpuラベルだけを落とす。
curl -s --data-urlencode 'query=avg without(cpu) (rate(node_cpu_seconds_total{mode="idle"}[5m]))' \
http://localhost:9090/api/v1/query | python3 -m json.tool
{
"status": "success",
"data": {
"resultType": "vector",
"result": [
{
"metric": {"instance": "node-exporter:9100", "job": "node-exporter", "mode": "idle"},
"value": [1790387999.261, "0.7754136570833097"]
}
]
}
}
by(cpu)は16本のメトリクスのまま、without(cpu)はcpu以外のラベル(instance・job・mode)が同じ組み合わせごとに集約される。出力が1本になったのは、今回の環境がinstanceもjobも1種類しかないsingle-node・single-jobだからである。ノードやjobが複数あれば、without(cpu)の結果もその数だけ複数本になる。どちらを使うかは、残したいラベルの数で決める。 残したいラベルが少なければby()、落としたいラベルが少なければwithout()のほうが書きやすい。
CPU以外の実用例:メモリ・ディスク・ネットワーク #
ここまでのsum()・avg()はCPUの例だけを使ってきた。max()も含め、他のNode Exporterのメトリクスでも同じ考え方が使える。
メモリ使用率は、node_memory_MemAvailable_bytes(すぐ使える空き容量)とnode_memory_MemTotal_bytes(総容量)の比から求める。
curl -s --data-urlencode 'query=(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100' \
http://localhost:9090/api/v1/query | python3 -m json.tool
{
"status": "success",
"data": {
"resultType": "vector",
"result": [
{
"metric": {"instance": "node-exporter:9100", "job": "node-exporter"},
"value": [1790390407.764, "8.769188542033667"]
}
]
}
}
ディスク使用率は、mountpointラベルでマウントポイントを指定し、node_filesystem_avail_bytesとnode_filesystem_size_bytesの比から求める。
curl -s --data-urlencode 'query=(1 - (node_filesystem_avail_bytes{mountpoint="/var"} / node_filesystem_size_bytes{mountpoint="/var"})) * 100' \
http://localhost:9090/api/v1/query | python3 -m json.tool
{
"status": "success",
"data": {
"resultType": "vector",
"result": [
{
"metric": {"device": "/dev/sdd", "fstype": "ext4", "instance": "node-exporter:9100", "job": "node-exporter", "mountpoint": "/var"},
"value": [1790390407.793, "21.85737640471743"]
}
]
}
}
ネットワークはnode_network_receive_bytes_total(deviceラベル持ち、counter型)を使う。loopback(device="lo")を除いてrate()をかけると、デバイスごとの受信量(bytes/sec)が複数本返る。ここにmax()を組み合わせると、最も受信量が多いデバイス1本の値だけを取り出せる。
curl -s --data-urlencode 'time=1790390414.283' \
--data-urlencode 'query=rate(node_network_receive_bytes_total{device!="lo"}[5m])' \
http://localhost:9090/api/v1/query | python3 -m json.tool
vetha492d0ae 103.44561403508771
eth0 90.60701754385964
vethb79c2784 50.86666666666666
docker0 0
veth8ecd509d 0
curl -s --data-urlencode 'time=1790390414.283' \
--data-urlencode 'query=max(rate(node_network_receive_bytes_total{device!="lo"}[5m]))' \
http://localhost:9090/api/v1/query | python3 -m json.tool
{
"status": "success",
"data": {
"resultType": "vector",
"result": [
{"metric": {}, "value": [1790390414.283, "103.44561403508771"]}
]
}
}
max()の結果103.44561403508771は、上のデバイス別一覧で最大のvetha492d0aeの値と一致する。ただしmax()の結果のmetricは空({})で、どのデバイスの値かというラベルは残っていない。
値だけでなく、どのデバイスが最大かも知りたい場合は、次節で扱う集約演算子topk()をtopk(1, ...)として使う。
curl -s --data-urlencode 'time=1790390414.283' \
--data-urlencode 'query=topk(1, rate(node_network_receive_bytes_total{device!="lo"}[5m]))' \
http://localhost:9090/api/v1/query | python3 -m json.tool
{
"status": "success",
"data": {
"resultType": "vector",
"result": [
{
"metric": {"device": "vetha492d0ae", "instance": "node-exporter:9100", "job": "node-exporter"},
"value": [1790390414.283, "103.44561403508771"]
}
]
}
}
値はmax()と同じ103.44561403508771だが、device="vetha492d0ae"というラベルが残っている。最大値だけ知りたいならmax()、最大値を持つのがどのメトリクスかも知りたいならtopk(1, ...)を使う。 sum()・avg()・max()はどれも、by()やwithout()と組み合わせて使う点は共通している。
topk()で上位N件を抽出する #
topk()は関数ではなく集約演算子(aggregation operator)に分類される。公式ドキュメントは「topk(k, v)とbottomk(k, v)は、他の集約演算子と違い、元のラベルを保ったまま入力サンプルのうちk個の部分集合を結果として返す。byとwithoutは入力ベクトルのグルーピングにのみ使われる」と説明している。
前節のsum by(cpu)(...)は16コア分のメトリクスを返した。この中から使用率が高い上位2コアだけをtopk()で絞り込む。
curl -s --data-urlencode 'time=1790389061.500' \
--data-urlencode 'query=topk(2, sum by(cpu) (rate(node_cpu_seconds_total{mode!="idle"}[5m])))' \
http://localhost:9090/api/v1/query | python3 -m json.tool
{
"status": "success",
"data": {
"resultType": "vector",
"result": [
{"metric": {"cpu": "0"}, "value": [1790389061.5, "0.06221052631579352"]},
{"metric": {"cpu": "2"}, "value": [1790389061.5, "0.03207017543859724"]}
]
}
}
前節に掲載した全16コアの値と照らし合わせる。cpu="0"(0.06221052631579352)とcpu="2"(0.03207017543859724)は、timeを固定して取得した16件の中で確かに最大の2件であり、topk(2, ...)の結果と完全に一致する。
このクエリはsum by(cpu)でinstanceラベルを落としているため、今回のsingle-node環境で使用率が高いCPUコアを知りたい、という目的に対する式である。複数ノードの環境にそのまま持ち込むと、cpu="0"のようなラベルはノードをまたいで合算されてしまい、ノードの区別がつかなくなる。
ノード単位で見る場合 #
概要で挙げた「CPU使用率が高いノードを知りたい」を素直に書くと、cpuではなくinstance(ノード)でまとめる式になる。
curl -s --data-urlencode 'query=topk(3, 100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100))' \
http://localhost:9090/api/v1/query | python3 -m json.tool
{
"status": "success",
"data": {
"resultType": "vector",
"result": [
{"metric": {"instance": "node-exporter:9100"}, "value": [1790390422.904, "2.582236842107818"]}
]
}
}
今回の環境はinstanceが1つしかないsingle-nodeなので、topk(3, ...)を指定しても1件しか返らず、実際のランキングにはならない。複数ノードの環境であれば、この式がノードごとのCPU使用率上位を返す。topk()に渡す式は、何を1本のメトリクスとして数えたいか(コアか、ノードか)によって、by()の対象を変える必要がある。
histogram_quantile()でパーセンタイルを出す #
histogram_quantile()はヒストグラム型のmetricからパーセンタイル(p95など)を計算する関数である。公式ドキュメントには2種類の説明がある。_bucketサフィックスとleラベルを使うclassic histogram向けと、_bucketもleも使わず単一のmetricをそのまま渡すnative histogram向けである。
次節で確認するprometheus_http_request_duration_seconds_bucketは、_bucketサフィックス付きで公開されているclassic histogramである。この記事でもclassic histogramだけを扱う。
まず、今回使う環境で触れたとおり、Node Exporterにヒストグラム型のmetricが存在するか実測で確認する。
kubectl exec deploy/prometheus -n monitoring -- wget -qO- http://node-exporter:9100/metrics \
| grep "^# TYPE" | awk '{print $NF}' | sort | uniq -c
79 counter
178 gauge
1 summary
49 untyped
grepで数えているのは# TYPE <metric name> <type>という宣言行である。これはexposition formatでmetric familyと呼ばれる単位に対応する(時系列そのものの本数ではない)。合計307種類(79+178+1+49)のmetric familyのうち、histogram型は0件だった。summaryが1件あるが、histogram_quantile()に使えるバケットを持たないため対象にならない。
そこで、今回使う環境で追加したPrometheus自身の/metricsにあるprometheus_http_request_duration_secondsを使う。HTTPリクエストのレイテンシを表すhistogram型のmetricである。
公式ドキュメントは、classic histogramについて「各floatサンプルはleラベルを持ち、その値はバケットの上限(inclusive upper bound)を表す」と説明している。まずleラベルごとのバケットの中身を見る。
curl -s --data-urlencode 'query=sum by(le) (prometheus_http_request_duration_seconds_bucket{handler="/api/v1/query"})' \
http://localhost:9090/api/v1/query | python3 -m json.tool
le=0.1 76
le=0.2 76
le=0.4 76
le=1.0 76
...
le=+Inf 76
累積カウントが76件で全区間一定ということは、/api/v1/queryへのリクエストは全76件がle="0.1"(100ミリ秒)以下に収まっている。この状態でhistogram_quantile(0.95, ...)を計算する。
curl -s --data-urlencode 'query=histogram_quantile(0.95, sum by(le) (rate(prometheus_http_request_duration_seconds_bucket{handler="/api/v1/query"}[5m])))' \
http://localhost:9090/api/v1/query | python3 -m json.tool
{
"status": "success",
"data": {
"resultType": "vector",
"result": [
{"metric": {}, "value": [1790388033.903, "0.095"]}
]
}
}
p95は0.095秒(95ミリ秒)になった。公式ドキュメントは、classic histogramのquantile計算について「バケット内で観測値が一様に分布していると仮定する(assumes a uniform distribution of observations within the bucket)」と説明している。
今回はサンプルが全て最初のバケット(0, 0.1]に収まっている。そのため95パーセンタイルの位置は、その範囲を線形補間した0.1 × 0.95 = 0.095になる。この値は、バケットの分布から補間で求めた近似値である。 実際のリクエスト1件ごとの応答時間そのものではない。
式の一覧は冒頭の「まず見る:目的から引けるチートシート」にまとめてある。
まとめ #
instant vectorは1時点の値の集合、range vectorは時系列ごとの値の並びである。rate()はrange vectorを受け取ってinstant vectorを返す。histogram_quantile()は、rate()とsum by(le)で処理したあとのinstant vector(leラベルごとのバケット値)を受け取る。
increase()はrate()に秒数を掛けたものであり、両者の実測値を比べるとこの関係を確認できた。sum()・avg()・max()はCPU以外にもメモリ・ディスク・ネットワークのメトリクスに同じように使える。
topk()は他の集約演算子と違い、元のラベルを保ったまま上位k件を返す。ただしby()の対象(コアかノードか)によって何のランキングになるかが変わる。histogram_quantile()はleラベルのバケットから、バケット内一様分布を仮定した線形補間でパーセンタイルを求める。
今回追加したPrometheus自身のscrape設定は、次回以降もhistogram_quantile()の実例として使うため、そのまま残す。次回(第3回)はAlerting rulesとAlertmanagerを扱う。