collectdを試す

仕事だとモニタリング周りは既に整備されており、アプリケーション書きとしては社内用のライブラリを叩くだけでメトリクスが勝手に飛んでいってダッシュボードに表示されてしまう。それはそれで楽なんだけど、せっかく自宅サーバを立てたことだし、いい機会なのでモニタリング周りを勉強してみることにした。

いろいろ調べた結果、collectdでメトリクスを集めてCloudWatchに飛ばすのが良さそうという感じになった(このへんの話は後で書く(と思う))。

collectdはWikiがあって大体のことがまとまっているっぽい。 collectd Wiki

インストール

Ubuntuだとapt-get install collectdで入った。

設定

collectdは本体にいろいろプラグインを差し込むことで機能拡張ができるようになっている。たとえばcpuプラグインを読み込むとCPUの使用状況を集めてくるし、networkプラグインを読み込むと集めたデータを定期的にサーバへ送りつけるようになる。便利そうなプラグインはパッケージにバンドルされており、/usr/lib/collectd以下にインストールされている。

Ubuntuの初期設定だとcpuやmemoryといったシステムの基本的なメトリクスを集めるのに加え、RRDという形式でメトリクスを/var/lib/collectd/rrdに保存するようになっている。RRDの扱い方はいまいち分からないが、ドキュメントを流し読みして勘で以下のようなコマンドを叩いたらそれっぽい感じになった。

[2019-12-26 01:02:19-0600][/etc/collectd]
osamu@glados(:|✔)> rrdinfo /var/lib/collectd/rrd/glados/cpu-0/cpu-user.rrd
filename = "/var/lib/collectd/rrd/glados/cpu-0/cpu-user.rrd"
rrd_version = "0003"
step = 10
last_update = 1577343767
header_size = 3496
ds[value].index = 0
ds[value].type = "DERIVE"
ds[value].minimal_heartbeat = 20
ds[value].min = 0.0000000000e+00
ds[value].max = NaN
ds[value].last_ds = "1561932"
ds[value].value = 1.4700000000e+01
ds[value].unknown_sec = 0
rra[0].cf = "AVERAGE"
rra[0].rows = 1200
rra[0].cur_row = 397
rra[0].pdp_per_row = 1
rra[0].xff = 1.0000000000e-01
rra[0].cdp_prep[0].value = NaN
rra[0].cdp_prep[0].unknown_datapoints = 0
rra[1].cf = "MIN"
(snip)

[2019-12-26 01:04:33-0600][/etc/collectd]
osamu@glados(:|✔)> rrdtool graph --start now-3600 --end now /tmp/a.png DEF:a=/var/lib/collectd/rrd/glados/cpu-0/cpu-user.rrd:value:AVERAGE LINE:a#0000FF
481x141

[2019-12-26 01:04:37-0600][/etc/collectd]
osamu@glados(:|✔)> display /tmp/a.png

rrdtool graphの出力結果

なんかCPU使用率っぽいグラフが出ている。RRDのセマンティクスはなんか独特っぽい雰囲気があるが、さしあたりの目標はCloudWatchに送ることなのであんまり深追いはしないでおく。

ネットワーク経由の出力も見ておこう。こんな感じでnetwork pluginをロードする。接続先はデフォルトだとUDPのport 26826になる(参考)。

[2019-12-26 01:07:55-0600][/etc/collectd]
osamu@glados(:|✔)> cat /etc/collectd/collectd.conf.d/network.conf
LoadPlugin "network"

<Plugin "network">
  Server "localhost"
</Plugin>

[2019-12-26 01:08:59-0600][/etc/collectd]
osamu@glados(:|✔)> sudo systemctl restart collectd

[2019-12-26 01:09:04-0600][/etc/collectd]
osamu@glados(:|✔)> netcat -u -l localhost 25826 | hexdump -C
00000000  00 00 00 0b 67 6c 61 64  6f 73 00 00 08 00 0c 17  |....glados......|
00000010  81 17 29 14 85 16 7f 00  09 00 0c 00 00 00 02 80  |..).............|
00000020  00 00 00 00 02 00 09 73  77 61 70 00 00 04 00 09  |.......swap.....|
00000030  73 77 61 70 00 00 05 00  09 75 73 65 64 00 00 06  |swap.....used...|
00000040  00 0f 00 01 01 00 00 00  00 00 00 00 00 00 08 00  |................|
00000050  0c 17 81 17 29 14 8b 52  4d 00 04 00 0c 73 77 61  |....)..RM....swa|
00000060  70 5f 69 6f 00 00 05 00  07 69 6e 00 00 06 00 0f  |p_io.....in.....|

(snip)

確かになんかやっているっぽい。

StatsD

プラグインを書いたら対応するメトリクスを収集してくるという世界観はなかなかUNIXっぽくていいが、実際問題としてはアプリケーション固有のメトリクスも吐きたいし、かといってアプリケーションごとにプラグインを書くのはあまりにもめんどい。そもそもアプリケーションのカスタムメトリクスはpush的であり、CPUやメモリの使用率のように定期的にポーリングしてくるというアーキテクチャはあんまり適合しない……。幸いにして世の中には既にStatsDという標準があり、DataDogもStatsD互換のagentを提供しているほどに市民権を得ているので、これに乗っかるのが普通だと思う。

collectdはなんとStatsD Pluginを提供しており、StatsD形式の入力を受け付けてメトリクスとして吐いてくれるようになる。

[2019-12-26 01:26:52-0600][/etc/collectd]
osamu@glados(:|✔)> cat /etc/collectd/collectd.conf.d/statsd.conf
LoadPlugin "statsd"

<Plugin statsd>
  Host "localhost"
  Port "8125"
  DeleteSets     true
  TimerPercentile 90.0
</Plugin>

[2019-12-26 01:27:00-0600][/etc/collectd]
osamu@glados(:|✔)> sudo systemctl restart collectd

[2019-12-26 01:27:17-0600][/etc/collectd]
osamu@glados(:|✔)> echo "azunyan:1|c" | nc -u -w0 localhost 8125

[2019-12-26 01:28:03-0600][/etc/collectd]
osamu@glados(:|✔)> echo "azunyan:3|c" | nc -u -w0 localhost 8125

[2019-12-26 01:28:15-0600][/etc/collectd]
osamu@glados(:|✔)> echo "niconii:25252|c" | nc -u -w0 localhost 8125

[2019-12-26 01:29:06-0600][/etc/collectd]
osamu@glados(:|✔)> ls /var/lib/collectd/rrd/glados/statsd/
derive-azunyan.rrd  derive-niconii.rrd

[2019-12-26 01:52:57-0600][/etc/collectd]
osamu@glados(:|✔)> rrdtool graph -a CSV --start "20191226 01:27" --end "20191226 01:40" - DEF:a=/var/lib/collectd/rrd/glados/statsd/derive-azunyan.rrd:value:MAX LINE:a#0000FF
"time",""
1577345230,"NaN"
1577345240,"NaN"
1577345250,"NaN"
1577345260,"NaN"
1577345270,"NaN"
1577345280,"NaN"
1577345290,"NaN"
1577345300,"2.1000000000e-01"
1577345310,"0.0000000000e+00"
1577345320,"0.0000000000e+00"
1577345330,"0.0000000000e+00"

(snip)

うーん、なんか値は出ているものの、レートっぽい値が出力されてしまっている。合計値も見たいんだけど……。そもそもderiveってなんだ?

Data source - collectd Wiki

Wikiによると、Deriveは読んで字のごとくDerivativeで、要するに前回記録された値との差分を時間差で割ったものを保持しているらしい。なるほど?じゃあ生の値は保存してないってこと?確かに積分すればだいたい元には戻るけど、計算誤差が出るから嬉しくなさそう……。

さすがに合計値が出せないことはないだろうと思ってソースを読んでみると、どうもconf_counter_sumという変数がセットしてあると、Gaugeとして別に値を出力してくれるらしい(statsd.c:800)。そしてこれはCounterSumというコンフィグでコントロールされていることが分かる(statsd.c:639)。コメントの雰囲気的に、合計値が知りたいならDeriveを積分すりゃいいじゃんって思っていそうで不安だけど……。後のセクションで分かるように、statsd pluginではどうもDeriveの意味がオーバーライドされているっぽい。

[2019-12-26 02:25:19-0600][/etc/collectd]
osamu@glados(:|✔)> echo "azunyan:3|c" | nc -u -w0 localhost 8125
                                                                                
[2019-12-26 02:25:39-0600][/etc/collectd]
osamu@glados(:|✔)> echo "azunyan:4|c" | nc -u -w0 localhost 8125

[2019-12-26 02:26:16-0600][/etc/collectd]
osamu@glados(:|✔)> rrdtool graph -a CSV --start "20191226 02:25" --end "20191226 02:27" - DEF:a=/var/lib/collectd/rrd/glados/statsd/count-azunyan.rrd:value:AVERAGE LINE:a#0000FF
"time",""
1577348710,"0.0000000000e+00"
1577348720,"0.0000000000e+00"
1577348730,"0.0000000000e+00"
1577348740,"6.0000000000e-01"
1577348750,"3.2000000000e+00"
1577348760,"3.2000000000e+00"
1577348770,"0.0000000000e+00"
1577348780,"NaN"
1577348790,"NaN"
1577348800,"NaN"
1577348810,"NaN"
0x0

[2019-12-26 02:26:24-0600][/etc/collectd]
osamu@glados(:|✔)> ls /var/lib/collectd/rrd/glados/statsd/
count-azunyan.rrd  derive-azunyan.rrd  derive-niconii.rrd

[2019-12-26 02:27:07-0600][/etc/collectd]
osamu@glados(:|✔)> cat /etc/collectd/collectd.conf.d/statsd.conf
LoadPlugin "statsd"

<Plugin statsd>
  Host "localhost"
  Port "8125"
  DeleteSets     true
  TimerPercentile 90.0
  CounterSum true
</Plugin>

なんかひどいことになっている。3つの数を足すとちょうど7になるので、時間差で記録したcountが均されているっぽい。しかしstatsd.cを読んでもこういう平均っぽい処理は行われていない……。

collectdのソースコードを読んでみると、plugin_dispatch_valuesという関数を呼ぶことでこのプラグインが読んだ値をcollectdに通知しているっぽい。ドキュメントによるとこの関数は全てのwrite callbackを発火させる作用があるとのことなので(参考)、最終的にはRRD pluginがstatsd pluginの読んだ値を書いているはず。ということは、RRDがなんか変なことをしているだけで、CloudWatchと繋いだら普通に出てくる可能性が高い……のか?

プラグイン機構の実装

プラグイン機構の詳細はWikiに書いてあり、モジュール固有のinit, read, write, shutdownのコールバックを登録するという、よくある感じの構造になっている。

Plugin architecture - collectd Wiki

上でも書いたplugin_dispatch_valuesの実装はdaemon/plugin.cにある。この関数自体は本当に素直に、飛んできた値をwrite queueに詰めることしかしていない。このへんを見ても特に怪しいことはしていないので、やっぱりRRD pluginが変なことしてそう……と思いつつソースツリーを眺めていたら、csv pluginというのを見つけた。飛んできたメトリクスをCSVに書き出すやつのように見える。やってみましょう。

[2019-12-26 03:39:37-0600][/etc/collectd]
osamu@glados(:|✔)> cat collectd.conf.d/csv.conf
LoadPlugin "csv"

[2019-12-26 03:40:15-0600][/etc/collectd]
osamu@glados(:|✔)> echo "azunyan:3|c" | nc -u -w0 localhost 8125

[2019-12-26 03:40:24-0600][/etc/collectd]
osamu@glados(:|✔)> echo "azunyan:4|c" | nc -u -w0 localhost 8125

[2019-12-26 03:40:36-0600][/etc/collectd]
osamu@glados(:|✔)> cat /var/lib/collectd/glados/statsd/count-azunyan-2019-12-26
epoch,value
1577353227.505,3.000000
1577353237.505,4.000000
1577353247.505,0.000000

[2019-12-26 03:40:50-0600][/etc/collectd]
osamu@glados(:|✔)> cat /var/lib/collectd/glados/statsd/derive-azunyan-2019-12-26
epoch,value
1577353227.505,3
1577353237.505,7
1577353247.505,7
1577353257.505,7

ちゃんと記録されてるーーーーーー!!!ただDeriveのセマンティクスがWikiと違って累積和になってるっぽい……というかcountとderive逆じゃない?バグっぽい雰囲気がする。

おわり