1クール続けるブログ

とりあえず1クール続けるソフトウェアエンジニアの備忘録

ZenHubのPrometheus Exporter作ってみた

記事一覧はこちら

背景・モチベーション

仕事でexporterを書くときにスッと書けるようにという練習がてら。
オンボーディングの中で自分でマイクロサービスをデプロイしてみるというタスクもあり、そこでも使える気がしたので一石二鳥を狙っている。

プロダクトバックログとかの量とかタスクの見積もりとかをaggregateしたりで可視化出来るのは悪くないような気も。GitHubAPIも利用するとLabelでいろんな属性を付けられるので良さげなんだけれども、とりあえず今回はZenHubのAPIのみを使っている。

ちなみにGrafanaのorgの下には、ほとんど手が付けられていないzenhub-exporterがあったりする。

本編

Exporterの書き方はここに書いてある。公式でツールセットだったりも提供してくれていて非常に手厚い。

prometheus.io

exporterのデフォルトのポート番号を選ぶ

Prometheusのexporterは1つのホスト上で複数動くことが充分に考えられるため、デフォルトのポート番号がwikiで整理されている(詳細)。使われていないポート番号を選ぶことを始めた。今回はポート番号9861を利用することにした。実際に広く使われるexporterを作成する場合には、このwikiを編集する必要がある。

github.com

Exporterを実装する上で気をつけたいこと

前述のWriting Exporterというページを参考に一部抜粋してみる。
📝 のマークが付いているのは自分の感想でドキュメントに書いてあった内容ではありません。

Configuration

YAMLがPrometheusの標準的な設定記述なので、もし設定ファイルを必要とする場合にはYAMLを利用すること。

Metrics

Naming

メトリクスの名前は特定のシステムに詳しくない人でも類推できるものする必要がある。そのため、メトリクス名は、通常 haproxy_up のように、exporter名をprefixに付ける必要がある。

メトリクスはsecondsやbyteなどのbase unitsを使用して、Grafanaなどのgraphing toolsで読みやすいようにしておく。同様に、パーセンテージではなくratioを利用する。
📝 これは自分の中でも印象が強く、node_exporterがいつだったかのバージョンアップの時に今までなかった単位がメトリクスのsuffixに足されたのを覚えている。

Metrics名にはexportされる際のラベルを含めるべきではない。ただし、同じデータを複数のメトリクスで異なるラベルを付けてエクスポートしている場合は例外だ。単一のメトリクスにすべてのラベルを付けてエクスポートするとカーディナリティが高くなりすぎる場合にのみこの例外対応が必要になる。
📝 ここでのカーディナリティはRDBでなく時系列DBの用語を指すので注意(fukabori.fm #53

Prometheus のメトリクスとラベル名はsnake_caseになる。
_sum _count _bucket _total といったsuffixはSummaryやHistograms、Countersに使われるため、それら以外であればこれらのsuffixは利用しないこと。_total はcounter用に予約されているのでCOUNTERタイプのメトリクスであれば利用するべきsuffixである。

process_scrape_ というprefixは予約されているので利用しないこと。ただし、これらに独自のprefixを追加した場合には利用可能となる。たとえば、jmx_scrape_duration_secondsは問題ない。

成功リクエストと失敗リクエストの数がある時に、これを公開する一番良い方法は一つのメトリクスではトータルのリクエスト数、もう一つのメトリクスでは失敗したリクエストを扱うこと。 1つのメトリクスに失敗または成功のラベルを付けないように注意する!
同様に、キャッシュのヒットまたはミスについても、1つのメトリクスを総計に、もう1つのメトリクスをヒット数にするのがよい。
📝 ついついメトリクスを分けるのではなくLabelで対応しがちなので特に気をつけたい項目

Labels

ラベルには関してはこちらのドキュメントにおいても言及されています。

prometheus.io

一般的なガイドラインとしてメトリクスのカーディナリティを10以下にするようにして超えるものは一握りとする。
📝 ここではLabelの取りうる値のことについて言っていると自分は解釈しました。保存するデータ量はLabelの取りうる値の掛け算によって決まるので非常にパフォーマンスに影響が出やすい部分のはずです。cardinality-is-keyというブログを参考にしました。

なにか事象が起こるまで存在しない時系列データは扱いが難しくなるため、事前に存在する可能性があるとわかっている時系列データに関しては0などのデフォルト値を書き出しておくこと。

ターゲットラベルと衝突するラベル名は避けることが推奨されている。例えば、region, zone, cluster, availability_zone, az, datacenter, dc, owner, customer, stage, service, environment, envなど。
📝 ターゲットラベルと言っているのはおそらくExporterごとに付与されるラベルのことと思います。例えば、ec2のService Discoveryを利用されている場合にはmetaラベルでAZが取得できるので、それをavailability_zoneにrelabelするのはよくあるかなあと思います。

Read/writeとsend/receiveはラベルとしてではなく、別々のメトリクスとして使用するのが最適です。これは通常、一度に1つだけを気にするからであり、その方が使いやすいからです。

ラベルの付与は慎重に。ラベルが増えると、ユーザがPromQLを作成するときに考慮しなければならないことが増えます。メトリックに関する追加情報は、infoメトリックを介して追加することができます。例えば、kube_pod_infoとかがそれに当たります。

Types

MetricsのTypeは通常CounterかGaugeだけれども、もしデクリメントのあるCounterの場合には誤解を招かないようにGAUGEではなく、UNTYPEDを利用する。

Help Strings

ヘルプをメトリクスそれぞれを付けておく、メトリクスのSourceが分かると良い。

Collector

ExporterのCollectorを実装する場合には、scrapeごとにメトリクスを更新するという方法を取るべきではない。
毎回新しいメトリクスを作成してください。GoではCollect()メソッドでMustNewConstMetric()を使ってメトリクスの作成を行います。
📝 作成されるメトリクスの構造体は変更できないようにフィールドがunexportedになっています。
新しいメトリクスを毎回作成する理由は2つあります。

  • 2つのscrapeが同時に発生する可能性があり、direct instrumentationの場合にはグローバル変数が競合する可能性がある
  • ラベルの値が消失したときにもexportされてしまう

Elasticsearch などの多くのシステムでは、CPU、メモリ、ファイルシステム情報などのマシン・メトリクスが公開されています。Prometheusのエコシステムではnode_exporterがこれらを提供しているので、このようなメトリクスは削除すべきです。

Deployment

それぞれのexporterは1つのinstanceのアプリケーションを監視すべきで、できれば同じマシン上で隣り合っているのが望ましい。

ただし2つの例外があります。

  1. 監視しているアプリケーションの横で実行することが全く無意味な場合
    • 📝 今回作ったzenhub_exporterblackbox_exporterはこのケースに当てはまる
  2. システムのランダムなインスタンスからいくつかの統計情報を引き出し、どのインスタンスと話しているかを気にしない場合

Scheduling

メトリクスは、Prometheus がscrapeするときにのみアプリケーションから引き出されるべきであり、exporterは独自のタイマーに基づいてscrateを実行すべきでない。

公開するメトリクスにタイムスタンプを設定すべきではなく、Prometheusに任せたほうが良い。

メトリクスの取得に1分以上かかるなど、特にコストがかかる場合はキャッシュしても構いわないが、HELP Stringsにその旨を記載する。
Prometheusのデフォルトのscrape timeoutは10秒です。もしexporterがこれを超えることが予想される場合は、ドキュメントの中で明示的にその旨を伝える必要がある。

Failed scrapes

Scrapeに失敗したときの処理はパターン主に2つ。2つ目はexporterのダウンとアプリケーションのダウンの見分けが付く点で優れる。

  1. 5xxエラーを返す
  2. myexporter_up(例:haproxy_up)という変数を用意して、スクレイプが成功したかどうかに応じて0または1の値を持つ

Landing page

http://yourexporter/ にユーザがアクセスした時に見せるシンプルなHTMLを用意して、そこにはexporterの名称と/metrics ページへのリンクを貼るべし。

f:id:jrywm121:20210920213503p:plain

上記踏まえて ZenHub Exporter 作った

上記を踏まえて、今回作ったアプリケーションのリポジトリはこちらです。

github.com

ビルドやリリースのツールもPrometheusが提供していて、haproxy_exporternode_exporterなどのPrometheusのorgで管理されているリポジトリはだいたいpromuというツールを使ってリリースなどが行われているそうです。実際には、promuを利用したCircleCIのOrbが作られており、それを各リポジトリから利用されているようです。ドラフトのリリースを作ってくれます。

今回はそのpromuというツールでビルドやリリースを行うようにしてみました。便利ではありますし、Officialのexporterとリリース形式が同じになるため良いですが、必ずしも利用する必要性が無さそうなところも見えましたので普通にgoreleaserを使っても良いのではと思います。ただしその際には、ldflagsgithub.com/prometheus/common/version.Versionなどにバージョン設定をしないと、--version実行時に歯抜けで表示されるので注意です。

まとめ(雑記?)

OpenMetricsを有効化したけど、どこからUnitを設定するのかよく分からなかった。スペックを読む限りだとTypeやHelpと同じところに表示されそうな気がしてるんだけど、Prometheusのclientライブラリからは読み取れなかった…。

github.com