RRDtoolは一昔前までCPUの負荷の時間変異をグラフ化したりするのに よく使われていたデータ管理、グラフ作成ツールです。
今どれくらい使われてるか知りませんが、ちょっと使ってるところがあって 色々更新しようと思った際に色々調べたことについてのメモ。
インストール
Linuxならyum
やapt
でrrdtool
が見つかると思います。
# yum install rrdtool
# apt install rrdtool
MacでもHomebrewでrrdtool
が見つかります。
$ brew install rrdtool
もしくは 直接ダウンロードしてインストール: RRDtool - RRDtool Download
今回はrrdtool 1.4.8を使ってます。
RRDファイルの作成
まずはデータを保存するファイルを作ります。
詳しくは色々なところにあるので参考にしてもらって1 今回は簡単なものを作ります。
$ rrdtool create data.rrd --start 1573603200 --step 300 DS:myvar:GAUGE:600:U:U RRA:AVERAGE:0.5:1:5
--start
で開始時刻を設定。1573603200はUTCで2019/11/13 00:00:00--step
を300(秒)で5分起きにデータを入力する様に設定DS:myvar:GAUGE:600:0:U
でmyvar
という値を入力値(Data Source)として設定。600
はHEARTBEAT設定でこの時間以内に来ない場合にはNaN(値不明)にする。その後の0:Uは値のMinとMax。決まっていなければUでUndefined(この場合いくらでも大きな値を入れられる。)RRA:AVERAGE:0.5:1:5
で実際に記録するRound Robin ArchivesDS:...
で設定した値はあくまで入力に使うフォーマットでこれ自身が記録されるわけではない- この入力値の一定期間の平均やMAXなどを保存していく
- まず、4番目の
1
が記録に使うステップ数を指定。--step
で設定した時間を1 stepとして入力されて来た数字をステップ数だけ集めて平均やその中のMAXを取る。ここでは1を指定しているので5分毎に記録されたそのものを使う。なのでAVERAGEでもMAXでも同じ値になる。 AVERAGE
の部分で平均を取るように指定。ただし上に書いたようにstepを1にしているので他のもの(MAX
/MIN
/LAST
)でも変わらない。0.5
は集めたstepの中のうち、この割合を超えるものが値不明なら結果も値不明にする設定。(0.5なので半分以上)。ただし、これも使うのが1stepなので意味ない。- 最後の
5
は保持する量。5つ分なので5分毎の記録x5で25分分の記録になる。もしstepを12とかにしておいたら5min x 12 steps x 5で300分(5時間分の記録、ということになる。
このDS
とRRA
の関係がちょっと最初わかりにくいかと思いますが
実際、複数のDSやRRAを使ったRRDファイルを作ってみて理解した方が早いです。
RRDファイルの中身
rrdtool dump
することで記録されている中身を見ることが出来ます。
$ rrdtool dump data.rrd
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE rrd SYSTEM "http://oss.oetiker.ch/rrdtool/rrdtool.dtd">
<!-- Round Robin Database Dump -->
<rrd>
<version>0003</version>
<step>300</step> <!-- Seconds -->
<lastupdate>1573603200</lastupdate> <!-- 2019-11-13 00:00:00 UTC -->
<ds>
<name> myvar </name>
<type> GAUGE </type>
<minimal_heartbeat>600</minimal_heartbeat>
<min>NaN</min>
<max>NaN</max>
<!-- PDP Status -->
<last_ds>U</last_ds>
<value>0.0000000000e+00</value>
<unknown_sec> 0 </unknown_sec>
</ds>
<!-- Round Robin Archives -->
<rra>
<cf>AVERAGE</cf>
<pdp_per_row>1</pdp_per_row> <!-- 300 seconds -->
<params>
<xff>5.0000000000e-01</xff>
</params>
<cdp_prep>
<ds>
<primary_value>0.0000000000e+00</primary_value>
<secondary_value>0.0000000000e+00</secondary_value>
<value>NaN</value>
<unknown_datapoints>0</unknown_datapoints>
</ds>
</cdp_prep>
<database>
<!-- 2019-11-12 23:40:00 UTC / 1573602000 --> <row><v>NaN</v></row>
<!-- 2019-11-12 23:45:00 UTC / 1573602300 --> <row><v>NaN</v></row>
<!-- 2019-11-12 23:50:00 UTC / 1573602600 --> <row><v>NaN</v></row>
<!-- 2019-11-12 23:55:00 UTC / 1573602900 --> <row><v>NaN</v></row>
<!-- 2019-11-13 00:00:00 UTC / 1573603200 --> <row><v>NaN</v></row>
</database>
</rra>
</rrd>
1573603200が最後に来る空のデータが出来ています。 5つ分保持するのですが、最初に初期化する時点から5つ分を保持するような形で作られ、今後追加されるたびに古いデータが削除されるような形になります。
値の追加
値の追加はrrdtool update
:
$ rrdtool update data.rrd 1573603500:10
これでdump
してみると
<database>
<!-- 2019-11-12 23:45:00 UTC / 1573602300 --> <row><v>NaN</v></row>
<!-- 2019-11-12 23:50:00 UTC / 1573602600 --> <row><v>NaN</v></row>
<!-- 2019-11-12 23:55:00 UTC / 1573602900 --> <row><v>NaN</v></row>
<!-- 2019-11-13 00:00:00 UTC / 1573603200 --> <row><v>NaN</v></row>
<!-- 2019-11-13 00:05:00 UTC / 1573603500 --> <row><v>1.0000000000e+01</v></row>
</database>
の様に新たに値が加わって最初の1573602000の値が消えてることがわかります。
update
では常に新しいデータのみを追加できます。
また、大きく時間が空くとHEARTBEATの時間より短い場合には前の値と同じもの、それを超えるとNaN(値不明)になります。
$ rrdtool create data.rrd --start 1573603260 --step 300 DS:myvar:GAUGE:600:U:U RRA:AVERAGE:0.5:1:20
$ rrdtool update data.rrd 1573603500:10
$ rrdtool update data.rrd 1573603800:20 # + 300
$ rrdtool update data.rrd 1573604400:30 # + 600
$ rrdtool update data.rrd 1573605300:40 # + 900
$ rrdtool update data.rrd 1573605600:50 # + 300
とすると以下のようなデータになります。
<database>
<!-- 2019-11-12 23:05:00 UTC / 1573599900 --> <row><v>NaN</v></row>
<!-- 2019-11-12 23:10:00 UTC / 1573600200 --> <row><v>NaN</v></row>
<!-- 2019-11-12 23:15:00 UTC / 1573600500 --> <row><v>NaN</v></row>
<!-- 2019-11-12 23:20:00 UTC / 1573600800 --> <row><v>NaN</v></row>
<!-- 2019-11-12 23:25:00 UTC / 1573601100 --> <row><v>NaN</v></row>
<!-- 2019-11-12 23:30:00 UTC / 1573601400 --> <row><v>NaN</v></row>
<!-- 2019-11-12 23:35:00 UTC / 1573601700 --> <row><v>NaN</v></row>
<!-- 2019-11-12 23:40:00 UTC / 1573602000 --> <row><v>NaN</v></row>
<!-- 2019-11-12 23:45:00 UTC / 1573602300 --> <row><v>NaN</v></row>
<!-- 2019-11-12 23:50:00 UTC / 1573602600 --> <row><v>NaN</v></row>
<!-- 2019-11-12 23:55:00 UTC / 1573602900 --> <row><v>NaN</v></row>
<!-- 2019-11-13 00:00:00 UTC / 1573603200 --> <row><v>NaN</v></row>
<!-- 2019-11-13 00:05:00 UTC / 1573603500 --> <row><v>1.0000000000e+01</v></row>
<!-- 2019-11-13 00:10:00 UTC / 1573603800 --> <row><v>2.0000000000e+01</v></row>
<!-- 2019-11-13 00:15:00 UTC / 1573604100 --> <row><v>3.0000000000e+01</v></row>
<!-- 2019-11-13 00:20:00 UTC / 1573604400 --> <row><v>3.0000000000e+01</v></row>
<!-- 2019-11-13 00:25:00 UTC / 1573604700 --> <row><v>NaN</v></row>
<!-- 2019-11-13 00:30:00 UTC / 1573605000 --> <row><v>NaN</v></row>
<!-- 2019-11-13 00:35:00 UTC / 1573605300 --> <row><v>NaN</v></row>
<!-- 2019-11-13 00:40:00 UTC / 1573605600 --> <row><v>5.0000000000e+01</v></row>
</database>
1573604100
の時刻では前の1573603800
からこの時刻までにデータは来ませんでしたが、
次のデータが1573604400
に来て、前にデータが来た1573603800
から600秒以内だったので
まとめて1573604400
に来た値を1573604100
にも使っています。
そのあと、1573605300
にデータが来るまで900秒かかってしまい、この間はNaN
にされています。
1573605300
にはデータは来てますがこの時点でHEARTBEATは切れてるとみなされてこのデータは捨てられ次のデータから使われることになります。
注意点
以下、やってて気になったことや注意すべき点など。 特に1 stepごとにまとめて記録する場合などに関して。
データのスタート時間は常に基準時間
基準時間という表現が正しいかは分かりませんが、
--start
が中途半端な時間(0:03)とかだったとしても5分置きの記録であれば0:05, 0:10, …という記録時間になります。
ここで
$ rrdtool create data.rrd --start 1573603260 --step 300 DS:myvar:GAUGE:600:U:U RRA:AVERAGE:0.5:1:5
の様に(--start
の値を1573603200から1573603260に一分後に変更)しても
dumpしてみると最後が2019-11-13 00:00:00 UTC / 1573603200
になっているはずです。
--start
に設定した時間は記録できない
通常update
は最後に記録された時間よりも後の時間を追加していくような形でしか使えません。
上で見たように--start
で指定した時間はすでにデータの中に入っているので
これがちょうどの時間の場合、同じ時刻のデータをupdate
で上書きすることは出来ません。
どの時刻で作っても新しくデータが追加されればその時点から過去に対して保持量を計算する形なので
create
するときは十分古い時間を--start
に指定しておけば良いと思います。
(指定しないと現在時刻になります。)
ここでちょっと問題になるのは日毎の値を見たい場合。
UNIX時間での基準時間になるので UTCの1970年1月1日 00:00:00からスタートです。
上までの例ではシステム時刻設定がUTCになっていることが前提でしたが、 JSTにすると9時間ずれます。 5分おきとかなら何ら問題ないのですが、1日毎に記録する場合、 基準時刻はUTCの00:00:00になるのでJSTでは9:00:00の値になります。
なので下に書くような毎日それぞれ記録した値をそのままグラフに載せたい、という場合には
rrdtool update
で時刻をUTCの00:00:00になるような時間で入力する必要があります。
Lost HEARTBEATしたあとの最初のデータは捨てられる
上で見たようにHEARTBEATで指定した時間以上入力が入ってこない場合、一度Lost HEARTBEATな状態になって 次にデータが来るまでの間RRAとして記録されるものはNaNになります。
さらに新しくデータが来ても、その時点では直前までの状態が不定なため、最初のデータは捨てられます。
細かくデータを見てる場合にはちょっと困る挙動かな、と思うのですが(もしかするとオプションで変更出来たりするかもしれませんが)、 ちょっと注意が必要。
基準時間をまたぐような入力は時間平均される
これまでの例では入力の時間を5分おきのちょうどの時間を指定して行ってきましたが、実際にはそうならないこともあります。
そのような場合には時間を挟んだ前後(もしくはちょうどの時間がの入力があった場合にはそれまでの値)の時間平均みたいなものが 入力として使われます。
$ rrdtool create data.rrd --start 1573603200 --step 300 DS:myvar:GAUGE:600:U:U RRA:AVERAGE:0.5:1:5
$ rrdtool update data.rrd 1573603400:10
$ rrdtool update data.rrd 1573603700:20
$ rrdtool dump data.rrd
...
<!-- 2019-11-13 00:05:00 UTC / 1573603500 --> <row><v>1.3333333333e+01</v></row>
...
1573603500
に対して100秒前と200秒後に10と20の入力がそれぞれあります。
これらを使って計算が行われるわけですが、計算方法としては
1573603200
から1573603400
の200秒間10
、その後の1573603500
までの100秒間が20
だったと思って平均を取って
(10 x 200 + 20 x 100)/300 = 13.333333333
みたいな感じで。 実際には入力時点での値がその時刻での平均のようなものに当たる場合が多いと思うので これだとちょっと全体的に前のめりな計算になりがちですが、 Round Robin Databaseの特性上 全ての入力を保持しておくわけではないのでこのような方法が採用されています。
各入力が残っているわけではないわけですが、
update
するたびに値はきちんと平均されているわけで
何かしら情報が残っているはずですが、それがPrimary Data Point (PDP)と呼ばれるものになります。
上のdump
したときに
<!-- PDP Status -->
の部分にあるPrimary Data Point (PDP)の情報のvalue
などの値。
この値はrrdtool info
でも見れます。
$ rrdtool create data.rrd --start 1573603200 --step 300 DS:myvar:GAUGE:600:U:U RRA:AVERAGE:0.5:1:5
$ rrdtool info data.rrd
filename = "data.rrd"
rrd_version = "0003"
step = 300
last_update = 1573603200
header_size = 584
ds[myvar].index = 0
ds[myvar].type = "GAUGE"
ds[myvar].minimal_heartbeat = 600
ds[myvar].min = NaN
ds[myvar].max = NaN
ds[myvar].last_ds = "U"
ds[myvar].value = NaN
ds[myvar].unknown_sec = 0
rra[0].cf = "AVERAGE"
rra[0].rows = 5
rra[0].cur_row = 3
rra[0].pdp_per_row = 1
rra[0].xff = 5.0000000000e-01
rra[0].cdp_prep[0].value = NaN
rra[0].cdp_prep[0].unknown_datapoints = 0
このds[myvar].value = NaN
の部分。
値を追加してみると
$ rrdtool update data.rrd 1573603400:10
$ rrdtool info data.rrd
filename = "data.rrd"
rrd_version = "0003"
step = 300
last_update = 1573603400
header_size = 584
ds[myvar].index = 0
ds[myvar].type = "GAUGE"
ds[myvar].minimal_heartbeat = 600
ds[myvar].min = NaN
ds[myvar].max = NaN
ds[myvar].last_ds = "10"
ds[myvar].value = 2.0000000000e+03
ds[myvar].unknown_sec = 0
rra[0].cf = "AVERAGE"
rra[0].rows = 5
rra[0].cur_row = 1
rra[0].pdp_per_row = 1
rra[0].xff = 5.0000000000e-01
rra[0].cdp_prep[0].value = NaN
rra[0].cdp_prep[0].unknown_datapoints = 0
の様にvalue
が2000になっています。
入力した値は10ですが、1573603400
はその前の基準時間の1573603200
から200秒経った時刻なので10 x 200
がvalue
として入っている形です。
このあともう一回次の基準時間の1573603500
前に値を入れてみると
$ rrdtool update data.rrd 1573603450:20
...
ds[myvar].last_ds = "20"
ds[myvar].value = 3.0000000000e+03
...
となります。これは10 x 200 + 20 x 50
の値になってることが分かると思います。
こんな感じでデータを保持しておいて、基準時価のをまたいだらそこまでの値を計算、
value
としては新たに基準時間からの時間で計算しなおし、を繰り返していきます。
RRAの値の計算
各基準時の値、もrrdファイルに記録されてるわけではありません。
今回の場合はたまたま1ステップ毎にRRAに記録する形なので
上のような感じでステップごと全てが記録されていますが、
通常は上の様にvalue
を使って計算される入力値も次のupdate
で捨てられます。
複数のステップをまとめる場合には
rra[0].cdp_prep[0].value = NaN
の部分の値が使われます。
AVERAGEであれば上の基準時毎の計算と同じ様に計算することが可能です。 MAXやMIN、LASTは単に毎回入力が来るたびに必要なステップ数の中で比較して残してあげればよいだけです。
整数値を出すような出力をきっちり5分毎に観測して記録したいとき
最終的なグラフを5分置きの間隔にしたくてRRAとしても5分おきに記録したいとします。
この場合、単純には入力値も5分置きに取って入力していくことになると思います。
ただ、例えば整数値しか出さないデータだとしても何も考えずにやるとグラフが少数になったりします。
cronジョブで記録したりするとして、ちょうどの時刻に始めたとしても入力を作るまでに1分かかるようなものだったり、 記録自体が00:01, 00:06, …みたいにずれて行われている場合。
これらの場合は時刻をまたいだ2つの平均を取るのでデータがそれぞれ整数でも少数になる可能性があります。
これだとちょっと気持ち悪いので
timestamp=$(date +%s)
timestamp=$(printf "\$xx =%s;print \"\$xx \\n\"" "$timestamp - ($timestamp % 300)"|perl)
みたいな感じで300秒ごとちょうどになるようにします。
日毎にやりたい場合には300のところを60x60x24=86400に置き換えます。
まあそもそもRRDtool自体がそういった入力時間をそのままグラフ表示というためのツールではなくて データを集めて規定通りに整形して表示するためのツール、という感じだとは思うのですが、 直感的にはそのまま表示、ということが最初に思い浮かぶんじゃないかな、と思うので 最初にグラフを作ってみて表示してみてなんかおかしいな、と思ってしまうことも多いのではないかと。
まとめ
今から進んで導入すること、特にRRDtool単体で使うことはあまりないかもしれませんが、 ちょっとさわる必要があったので気になったところをまとめてみました。
Round Robin Databaseの仕組みを今更ちゃんと知りましたが、 Round Robin Archiveを作る際、過去の入力(Data Source, DS)がなくても前回残したPrimary Data Pointの値だけあれば平均などが出せる、というのは単純ですが賢い方法だな、と。 上にも述べたように(RRDtoolの問題も含め)細かい問題はいくつかありますが。
ただちょっと直感的に理解できない部分も多くて使ってみないと 分からないこともあるものでした。