CLOVER🍀

That was when it all began.

PrometheusのPushgatewayを試す

これは、なにをしたくて書いたもの?

PrometheusにPushgatewayというものがあるらしく、こちらを1度試してみたいなと思いまして。

Pushgateway?

PrometheusはPull型のアーキテクチャのため、監視対象はPrometheusからスクレイプできる必要があります。

ですが、これはバッチジョブのような短命であったり、終了してしまうものには向きません。そのような時に使うのが、Pushgatewayです。

Pushing metrics | Prometheus

When to use the Pushgateway | Prometheus

GitHub - prometheus/pushgateway: Push acceptor for ephemeral and batch jobs.

Pushgatewayを使うと、モニタリング対象からPushgatewayに対してメトリクスをPushすることで、Prometheusからのスクレイピング
Pushgatewayが代わりに受けることができます。バッチジョブなどはPushgatewayに対してメトリクスを送っておけば、
たとえ対象のバッチジョブが終了していてもPrometheusはPushgatewayからメトリクスを取得することができるというわけです。

とはいえ、Pushgatewayはあくまで限定的な利用にとどめた方が良いとドキュメントには書かれています。

When to use the Pushgateway | Prometheus

  • Pushgatewayが単一障害点になる(特に、複数のインスタンスをモニタリングしている場合)
  • モニタリング対象の「up」メトリクスが利用できなくなる(upでインスタンスが稼働していることを確認できる)
  • Pushgatewayは、Pushされたデータを忘れることがないため、手動で削除することが必要になる

通常のPrometheusのモニタリング対象は、対象自体が停止してしまうとそのメトリクスは削除されます。ですが、Pushgatewayの
場合はそうではありません。モニタリング対象が停止しても残りますし、ラベルなどが変更されても過去の値が残り続けます。
これを解消するには、自分でメトリクスを削除するなどを行う必要があります。

なので、通常のPushgatewayの唯一妥当な使用例は、バッチジョブで使われること、だそうです。

Instrumention / Batch jobs

Pushgatewayに対するメトリクスの操作は、HTTP APIで行います。

API

PUT、POST、DELETEの3つがあり、使い分けはそれぞれ以下になります。

  • PUT … 指定されたメトリクスで、指定されたグループキーのメトリクスを置き換える(既存のメトリクスは削除される)
  • POST … 指定されたメトリクスで、指定されたグループキーを持つ同じ名前のメトリクスを置き換える
  • DELETE … 指定されたグループキーを持つメトリクスをすべて削除する

グループキーについては、また後ほど。グループキーは指定しなくてもOKです。

とまあ、Pushgatewayについてはここまでにして、実際に使っていってみましょう。

環境、準備

今回利用するPrometheusは、2.10.0を使います。

また、Pushgatewayについては0.8.0を使用します。

$ wget https://github.com/prometheus/pushgateway/releases/download/v0.8.0/pushgateway-0.8.0.linux-amd64.tar.gz
$ tar xf pushgateway-0.8.0.linux-amd64.tar.gz
$ cd pushgateway-0.8.0.linux-amd64

ヘルプ。

$ ./pushgateway --help
usage: pushgateway [<flags>]

The Pushgateway

Flags:
  -h, --help                     Show context-sensitive help (also try --help-long and --help-man).
      --web.listen-address=":9091"  
                                 Address to listen on for the web interface, API, and telemetry.
      --web.telemetry-path="/metrics"  
                                 Path under which to expose metrics.
      --web.external-url=        The URL under which the Pushgateway is externally reachable.
      --web.route-prefix=""      Prefix for the internal routes of web endpoints. Defaults to the path of --web.external-url.
      --persistence.file=""      File to persist metrics. If empty, metrics are only kept in memory.
      --persistence.interval=5m  The minimum interval at which to write out the persistence file.
      --log.level="info"         Only log messages with the given severity or above. Valid levels: [debug, info, warn, error, fatal]
      --log.format="logger:stderr"  
                                 Set the log target and format. Example: "logger:syslog?appname=bob&local=7" or "logger:stdout?json=true"
      --version                  Show application version.

今回は、このまま起動しておきましょう。

$ ./pushgateway

Pushgateway、PrometheusのIPアドレスは、それぞれ以下とします。

  • Pushgateway … 172.17.0.2
  • Prometheus … 172.17.0.3

サンプルアプリケーションを作る準備

Pushgatewayを使うということで、メトリクスをプッシュするアプリケーションを用意することにしましょう。

今回は、Node.jsで作成することにしました。

$ node -v
v10.15.3


$ npm -v
6.4.1

PrometheusのNode.js用のクライアントをインストール。

GitHub - siimon/prom-client: Prometheus client for node.js

$ npm i prom-client

今回のバージョンは、こちらです。

  "dependencies": {
    "prom-client": "^11.3.0"
  }

Prometheusの設定をする

では、Prometheusの設定を行います。

今回は、こんな感じで設定ファイルを用意しました。
prometheus.yml

global:
  scrape_interval:     5s
  evaluation_interval: 5s

scrape_configs:
  - job_name: 'prometheus'
    static_configs:
    - targets: ['localhost:9090']

  - job_name: 'pushgateway'
    honor_labels: true
    static_configs:
    - targets: ['172.17.0.2:9091']

Pushgatewayをスクレイピングする設定は、ここですね。

  - job_name: 'pushgateway'
    honor_labels: true
    static_configs:
    - targets: ['172.17.0.2:9091']

Pushgatewayを利用する場合は、「honor_labels: true」としておくべきです。

scrape_config

honor_labelsというのは、ラベルの競合を解決するための設定です。

instanceラベルのように、Pushgatewayにデータを送る側とPushgateway自身でのラベルがそれぞれ付与してしまうような
場合に、どちらを優先させるかです。「honor_labels」をtrueにすると、Pushgatewayに送る側のものが優先されるということになります。

POST(pushAdd)

最初に、POSTを使ったAPIから試していきましょう。クライアントライブラリのAPIでいくと、pushAddです。

まずは、こんなソースコードを作成。
push-add-client.js

const client = require('prom-client');
const registry = new client.Registry();

const counter = new client.Counter({
    name: 'my_counter',
    help: 'my counter metrics',
    registers: [registry]
});

const labeledCounter = new client.Counter({
    name: 'my_labeled_counter',
    help: 'my counter with label',
    labelNames: ['loop'],
    registers: [registry]
});


const loop = process.argv[2];

for (let i = 0; i < loop; i++) {
    counter.inc();
};

labeledCounter.labels(loop).inc();


const gateway = new client.Pushgateway('http://172.17.0.2:9091', {}, registry);
// const gateway = new client.Pushgateway('http://172.17.0.2:9091');   // デフォルトのRegistryを使うなら、これでもよい

gateway.pushAdd({ jobName: 'nodejs-client-job' }, (err, res, body) => {
    if (!err) {
        console.log('Finish!!');
    } else {
        console.log(err);
    }       
});

Counterを2つ用意し、Registryはデフォルトではなく専用に用意しました。

const registry = new client.Registry();

const counter = new client.Counter({
    name: 'my_counter',
    help: 'my counter metrics',
    registers: [registry]
});

const labeledCounter = new client.Counter({
    name: 'my_labeled_counter',
    help: 'my counter with label',
    labelNames: ['loop'],
    registers: [registry]
});

Counter自体は、コマンドライン引数で指定した値を使って、インクリメントしたりラベルを設定したりするようにしました。

const loop = process.argv[2];

for (let i = 0; i < loop; i++) {
    counter.inc();
};

labeledCounter.labels(loop).inc();

Registryについてはデフォルトのものを使っても良かったのですが、それだとNode.jsのランタイムの情報などのメトリクスが
大量に送信されてしまうので、今回は専用に作成することにしました。

このRegistryを使うように、Pushgatewayへの接続を設定します。

const gateway = new client.Pushgateway('http://172.17.0.2:9091', {}, registry);
// const gateway = new client.Pushgateway('http://172.17.0.2:9091');   // デフォルトのRegistryを使うなら、これでもよい

コメントにも書いていますが、デフォルトのRegistryを使う場合は指定しなくてもOKです。

https://github.com/siimon/prom-client/blob/v11.3.0/lib/pushgateway.js#L9-L16

あとは、ジョブ名をつけてpushAddします。

gateway.pushAdd({ jobName: 'nodejs-client-job' }, (err, res, body) => {
    if (!err) {
        console.log('Finish!!');
    } else {
        console.log(err);
    }       
});

動かしてみましょう。

$ node push-add-client.js 10
Finish!!

PrometheusのWeb UIで確認してみます。

f:id:Kazuhira:20190602230405p:plain

ちゃんと認識していますね。

ちなみに、Pushgateway自身からも取得することができます。

$ curl -s 172.17.0.2:9091/metrics | grep nodejs
my_counter{instance="",job="nodejs-client-job"} 10
my_labeled_counter{instance="",job="nodejs-client-job",loop="10"} 1
push_time_seconds{instance="",job="nodejs-client-job"} 1.5594856998250048e+09

instanceラベルは空ですね。

この値は、更新することもできます。

$ node push-add-client.js 5
Finish!!


$ curl -s 172.17.0.2:9091/metrics | grep nodejs
my_counter{instance="",job="nodejs-client-job"} 5
my_labeled_counter{instance="",job="nodejs-client-job",loop="5"} 1
push_time_seconds{instance="",job="nodejs-client-job"} 1.5594857167390034e+09

以降は、curlで確認することにしましょう。

続いて、メトリクスを他に追加してみましょう。Summaryを足してみます。
push-add-client2.js

const client = require('prom-client');
const registry = new client.Registry();

const summary = new client.Summary({
    name: 'my_summary',
    help: 'my summary help',
    registers: [registry]
});

summary.observe(parseInt(process.argv[2], 10));


const gateway = new client.Pushgateway('http://172.17.0.2:9091', {}, registry);

gateway.pushAdd({ jobName: 'nodejs-client-job' }, (err, res, body) => {
    if (!err) {
        console.log('Finish!!');
    } else {
        console.log(err);
    }       
});

値は、やはりコマンドライン引数で指定することにします。

実行。

$ node push-add-client2.js 5
Finish!!

確認。

$ curl -s 172.17.0.2:9091/metrics | grep nodejs
my_counter{instance="",job="nodejs-client-job"} 5
my_labeled_counter{instance="",job="nodejs-client-job",loop="5"} 1
my_summary{instance="",job="nodejs-client-job",quantile="0.01"} 5
my_summary{instance="",job="nodejs-client-job",quantile="0.05"} 5
my_summary{instance="",job="nodejs-client-job",quantile="0.5"} 5
my_summary{instance="",job="nodejs-client-job",quantile="0.9"} 5
my_summary{instance="",job="nodejs-client-job",quantile="0.95"} 5
my_summary{instance="",job="nodejs-client-job",quantile="0.99"} 5
my_summary{instance="",job="nodejs-client-job",quantile="0.999"} 5
my_summary_sum{instance="",job="nodejs-client-job"} 5
my_summary_count{instance="",job="nodejs-client-job"} 1
push_time_seconds{instance="",job="nodejs-client-job"} 1.5594857714458518e+09

Summaryが追加されました。最初に追加した、Counterは残ったままです。

ところで、「push_time_seconds」というメトリクスですが、こちらはメトリクスを送った時に更新されるようです。

push_time_seconds{instance="",job="nodejs-client-job"} 1.5594857714458518e+09

メトリクスを更新したりせず、curlなどで取得するといつも同じ値が返ります。

PUT(push)

次は、PUTを試してみましょう。クライアントライブラリではpushになります。

Pushgatewayは、1度再起動してデータをクリアしておきました。

先ほど、pushAddで使ったソースコードとほぼ同じものを用意。 push-client.js

const client = require('prom-client');
const registry = new client.Registry();

const counter = new client.Counter({
    name: 'my_counter',
    help: 'my counter metrics',
    registers: [registry]
});

const labeledCounter = new client.Counter({
    name: 'my_labeled_counter',
    help: 'my counter with label',
    labelNames: ['loop'],
    registers: [registry]
});


const loop = process.argv[2];

for (let i = 0; i < loop; i++) {
    counter.inc();
};

labeledCounter.labels(loop).inc();


const gateway = new client.Pushgateway('http://172.17.0.2:9091', {}, registry);

gateway.push({ jobName: 'nodejs-client-job' }, (err, res, body) => {
    if (!err) {
        console.log('Finish!!');
    } else {
        console.log(err);
    }       
});

違いは、使っているAPIがpushAddからpushになっただけです。

gateway.push({ jobName: 'nodejs-client-job' }, (err, res, body) => {
    if (!err) {
        console.log('Finish!!');
    } else {
        console.log(err);
    }       
});

実行結果も、そう変わりません。

$ node push-client.js 10
Finish!!


$ curl -s 172.17.0.2:9091/metrics | grep nodejs
my_counter{instance="",job="nodejs-client-job"} 10
my_labeled_counter{instance="",job="nodejs-client-job",loop="10"} 1
push_time_seconds{instance="",job="nodejs-client-job"} 1.5594849796879923e+09

違いが出るのは、次のメトリクスを送信した時です。

今度は、Summaryを送ってみます。
push-client2.js

const client = require('prom-client');
const registry = new client.Registry();

const summary = new client.Summary({
    name: 'my_summary',
    help: 'my summary help',
    registers: [registry]
});

summary.observe(parseInt(process.argv[2], 10));


const gateway = new client.Pushgateway('http://172.17.0.2:9091', {}, registry);

gateway.push({ jobName: 'nodejs-client-job' }, (err, res, body) => {
    if (!err) {
        console.log('Finish!!');
    } else {
        console.log(err);
    }       
});

実行。

$ node push-client2.js 5
Finish!!

確認してみます。

$ curl -s 172.17.0.2:9091/metrics | grep nodejs
my_summary{instance="",job="nodejs-client-job",quantile="0.01"} 5
my_summary{instance="",job="nodejs-client-job",quantile="0.05"} 5
my_summary{instance="",job="nodejs-client-job",quantile="0.5"} 5
my_summary{instance="",job="nodejs-client-job",quantile="0.9"} 5
my_summary{instance="",job="nodejs-client-job",quantile="0.95"} 5
my_summary{instance="",job="nodejs-client-job",quantile="0.99"} 5
my_summary{instance="",job="nodejs-client-job",quantile="0.999"} 5
my_summary_sum{instance="",job="nodejs-client-job"} 5
my_summary_count{instance="",job="nodejs-client-job"} 1
push_time_seconds{instance="",job="nodejs-client-job"} 1.559485053659234e+09

わかりにくいですが、POST(pushAdd)の時には残っていた、最初に送信したCounterのメトリクスがなくなりました。

これが、PUTとPOSTの違いで、PUTの場合はメトリクスを置き換えるということですね。グループキーについては、今回は
指定していません。

DELETE

最後に、DELETEを試してみます。今回は、先ほどのPUTの結果を残したまま使います。

ダミー的に、コマンドライン引数を元にして値を作る、Summaryを使っています。
delete-client.js

const client = require('prom-client');
const registry = new client.Registry();

const summary = new client.Summary({
    name: 'my_summary',
    help: 'my summary help',
    registers: [registry]
});

summary.observe(parseInt(process.argv[2], 10));


const gateway = new client.Pushgateway('http://172.17.0.2:9091', {}, registry);

gateway.delete({ jobName: 'nodejs-client-job' }, (err, res, body) => {
    if (!err) {
        console.log('Finish!!');
    } else {
        console.log(err);
    }       
});

メトリクスがまだある状態で

$ curl -s 172.17.0.2:9091/metrics | grep nodejs
my_summary{instance="",job="nodejs-client-job",quantile="0.01"} 5
my_summary{instance="",job="nodejs-client-job",quantile="0.05"} 5
my_summary{instance="",job="nodejs-client-job",quantile="0.5"} 5
my_summary{instance="",job="nodejs-client-job",quantile="0.9"} 5
my_summary{instance="",job="nodejs-client-job",quantile="0.95"} 5
my_summary{instance="",job="nodejs-client-job",quantile="0.99"} 5
my_summary{instance="",job="nodejs-client-job",quantile="0.999"} 5
my_summary_sum{instance="",job="nodejs-client-job"} 5
my_summary_count{instance="",job="nodejs-client-job"} 1
push_time_seconds{instance="",job="nodejs-client-job"} 1.559485053659234e+09

実行。

$ node delete-client.js 5
Finish!!

すると、メトリクスがキレイに全部なくなります。

$ curl -s 172.17.0.2:9091/metrics | grep nodejs

これで、基本的な使い方はわかった感じですね。

グループキーを使ってみる

ここまで、グループキーには触れてきませんでした。

最後に、グループキーも試してみようと思います。

POST(pushAdd)で試してみましょう。最初にPOST(pushAdd)で用意したソースコードと、似たようなものを用意します。 push-add-grouping-client.js

const client = require('prom-client');
const registry = new client.Registry();

const counter = new client.Counter({
    name: 'my_counter',
    help: 'my counter metrics',
    registers: [registry]
});

const labeledCounter = new client.Counter({
    name: 'my_labeled_counter',
    help: 'my counter with label',
    labelNames: ['loop'],
    registers: [registry]
});


const groupKey = process.argv[2];
const loop = process.argv[3];

for (let i = 0; i < loop; i++) {
    counter.inc();
};

labeledCounter.labels(loop).inc();


const gateway = new client.Pushgateway('http://172.17.0.2:9091', {}, registry);

gateway.pushAdd({ jobName: 'nodejs-client-job', groupings: { key: groupKey } }, (err, res, body) => {
    if (!err) {
        console.log('Finish!!');
    } else {
        console.log(err);
    }       
});

違うのは、グループキーもコマンドライン引数で受け取るようにしたことと

const groupKey = process.argv[2];
const loop = process.argv[3];

その値を、pushAddのgroupingsとして渡します。ここでは、keyという名前で登録することにしましょう。

gateway.pushAdd({ jobName: 'nodejs-client-job', groupings: { key: groupKey } }, (err, res, body) => {

実行。keyに指定する値は、「key1」とします。

$ node push-add-grouping-client.js key1 20
Finish!!

確認。

$ curl -s 172.17.0.2:9091/metrics | grep nodejs
my_counter{instance="",job="nodejs-client-job",key="key1"} 20
my_labeled_counter{instance="",job="nodejs-client-job",key="key1",loop="20"} 1
push_time_seconds{instance="",job="nodejs-client-job",key="key1"} 1.5594859475088847e+09

groupingsの値は、ラベルとして使われるようですね。

もうひとつ、登録してみましょう。

$ node push-add-grouping-client.js key2 15
Finish!!


$ curl -s 172.17.0.2:9091/metrics | grep nodejs
my_counter{instance="",job="nodejs-client-job",key="key1"} 20
my_counter{instance="",job="nodejs-client-job",key="key2"} 15
my_labeled_counter{instance="",job="nodejs-client-job",key="key1",loop="20"} 1
my_labeled_counter{instance="",job="nodejs-client-job",key="key2",loop="15"} 1
push_time_seconds{instance="",job="nodejs-client-job",key="key1"} 1.5594859475088847e+09
push_time_seconds{instance="",job="nodejs-client-job",key="key2"} 1.5594860007325053e+09

別々ものとして、扱われていますね。

ここで、1番最初に作ったpushAddするプログラムも実行してみます。

$ node push-add-client.js 10
Finish!!


$ curl -s 172.17.0.2:9091/metrics | grep nodejs
my_counter{instance="",job="nodejs-client-job"} 10
my_counter{instance="",job="nodejs-client-job",key="key1"} 20
my_counter{instance="",job="nodejs-client-job",key="key2"} 15
my_labeled_counter{instance="",job="nodejs-client-job",loop="10"} 1
my_labeled_counter{instance="",job="nodejs-client-job",key="key1",loop="20"} 1
my_labeled_counter{instance="",job="nodejs-client-job",key="key2",loop="15"} 1
push_time_seconds{instance="",job="nodejs-client-job"} 1.559486040798703e+09
push_time_seconds{instance="",job="nodejs-client-job",key="key1"} 1.5594859475088847e+09
push_time_seconds{instance="",job="nodejs-client-job",key="key2"} 1.5594860007325053e+09

こちらも増えました。つまり、グループキーはラベルとして管理されるものだ、ということですね。指定しないと、ラベルが
付与されません、と。

では、deleteしてみます。
※こうなってしまうと、引数で指定する値の意味はありませんが…

$ node delete-client.js 20
Finish!!

消えたのは、グループキーを指定しない情報だけですね。

$ curl -s 172.17.0.2:9091/metrics | grep nodejs
my_counter{instance="",job="nodejs-client-job",key="key1"} 20
my_counter{instance="",job="nodejs-client-job",key="key2"} 15
my_labeled_counter{instance="",job="nodejs-client-job",key="key1",loop="20"} 1
my_labeled_counter{instance="",job="nodejs-client-job",key="key2",loop="15"} 1
push_time_seconds{instance="",job="nodejs-client-job",key="key1"} 1.5594859475088847e+09
push_time_seconds{instance="",job="nodejs-client-job",key="key2"} 1.5594860007325053e+09

これを削除する場合は、delete側でもやはりグループキーを指定する必要があります。
delete-grouping.js

const client = require('prom-client');
const registry = new client.Registry();

const summary = new client.Summary({
    name: 'my_summary',
    help: 'my summary help',
    registers: [registry]
});

const groupKey = process.argv[2];

summary.observe(parseInt(process.argv[3], 10));


const gateway = new client.Pushgateway('http://172.17.0.2:9091', {}, registry);

gateway.delete({ jobName: 'nodejs-client-job', groupings: { key: groupKey } }, (err, res, body) => {
    if (!err) {
        console.log('Finish!!');
    } else {
        console.log(err);
    }       
});

第1引数で、グループキーを受け取るようにしました。

実行。
※こちらも、やはり第2引数の値の意味はありませんが…

$ node delete-grouping.js key1 5
Finish!!

確認。

$ curl -s 172.17.0.2:9091/metrics | grep nodejs
my_counter{instance="",job="nodejs-client-job",key="key2"} 15
my_labeled_counter{instance="",job="nodejs-client-job",key="key2",loop="15"} 1
push_time_seconds{instance="",job="nodejs-client-job",key="key2"} 1.5594860007325053e+09

指定されたグループキーに関するデータだけが、なくなりましたね。

というわけで、グループキーはPushgatewayに送るデータに対するラベルで、この単位でメトリクスを管理できるということが
わかりましたね。

グループキーの指定方法を見ると、単なる「キー=値」、そしてラベルにすぎないようなので、任意に複数付けたりといったことも
できるのでしょうが…あんまりたくさん付けるものでもないような気も。

まとめ

Prometheusの、Pushgatewayを試してみました。

なんとなく使い方を把握したのと、POSTとPUTの違い、グループキーについてなどを把握できたかなぁと思います。

Pushgatewayを使う場合、送信して登録したメトリクスの扱い、ライフサイクルを考えないといけなさそうなので、ふつうに
Prometheusを使う時とは違った考慮事項が要求されるんでしょうねぇ…。

オマケ

ところでですね、今回使ったPrometheusの設定のうち、ここまでの話だと「honor_labels」がよくわからないような気がします。

1度、設定を解除して試してみましょう。
prometheus.yml

global:
  scrape_interval:     5s
  evaluation_interval: 5s

scrape_configs:
  - job_name: 'prometheus'
    static_configs:
    - targets: ['localhost:9090']

  - job_name: 'pushgateway'
#    honor_labels: true
    static_configs:
    - targets: ['172.17.0.2:9091']

「honor_labels」をコメントアウトしました。

この状態で、Prometheusからメトリクスを見ると、こんな感じになります。

f:id:Kazuhira:20190602234536p:plain

「instance」に、Pushgateway自身の値が入っています。

一方で、このエントリの最初に見たグラフでは、以下のようになりました。

f:id:Kazuhira:20190602230405p:plain

こちらは、「instance」の値が入っていません。

Pushgateway自身から取得した時は、instanceラベルが空でしたよね。こちらが、空のまま維持された、ということですね。

$ curl -s 172.17.0.2:9091/metrics | grep nodejs
my_counter{instance="",job="nodejs-client-job"} 10
my_labeled_counter{instance="",job="nodejs-client-job",loop="10"} 1
push_time_seconds{instance="",job="nodejs-client-job"} 1.5594856998250048e+09

Prometheusから見ると、メトリクスの取得先はPushgatewayなのですが、メトリクス自体を作成したのはPushgatewayではなく、 Pushgatewayにメトリクスを送信したアプリケーションです。

なので、ラベルが重複する場合は、オリジナルのものを残すように設定する、ということでしょうね。