追記:ソースへの言及と少し詳しい解説を追加
RRDToolではデータベースの作成時にstepオプションで指定した間隔(デフォルトでは5分=300秒)で、データが記録されます。この値は実際のデータ更新(update)タイミングがどれだけずれようと、例えば5分間隔なら0:00 0:05 0:10 ...というタイムスタンプで記録されます。また、チュートリアルや解説記事では、stepと同じ間隔でデータを取得する、とだけ書かれていますが、それより長い、あるいは短い間隔でデータを取得すると何が起こるのか、ソースを読んで調べてみました。結論から書くと、かなりまじめに計算されていて、理論的には更新タイミングがどれだけ揺らいでもそれなりに正確な値が出るように工夫されていました。
まず、以下のように、step(上の赤い線)と同じ間隔だけど、少しずれたタイミングでデータが取得される(下の青い線)ケースを例に挙げます。更新処理はrrd_update.cに記述されています。
この場合、例えば0:05のあとの更新タイミングでは、calculate_elapsed_steps()関数内で次の図のように記録する時刻の前後の割合を求めます。
この割合をもとに、例えば更新時のデータの取得値がxである時に、 x * pre_int / interval (水色の部分)を0:05の値、x * post_int / interval(赤色の部分)を0:10時点の値として処理します。これを繰り返して5分ごとのデータをほぼ正確に切り出しています。
この処理をやっているのがprocess_pdp_st()関数で、
つまり、記録(赤線)と更新(青線)のタイミングがぴったり合っていれば、0:05時点のデータはpre_int = intereval, post_int =0より、0:00 - 0:05 の値を正確に指すことになり、図のようにずれた場合でも繰り越し処理により(前後の時間比率で単純に分割した値ではありますが)近いと思われる値が出るようになっています。
ここで少しわき道にそれて、記録と更新のタイミングは合わせたほうがよいのか、ずらした方が良いのか考えてみます。データが一定値で推移している場合、差がないのは明らかなので、バーストが発生したケースのみ扱います。
左の図がデータの登録タイミングを跨いでバーストが発生した場合、真ん中が登録タイミング前、右が登録タイミング後にそれぞれバーストが発生した場合です。また上段は登録と更新のタイミングが一致している時、下段はずれている場合です。話を単純化するために、バーストの計測値(山の面積)を10、pre_intとpost_intは同じ長さであるとします。
左の図の場合、上段では水色部分にかかっている山の半分のみが加算されるので"5"、下段では10 * pre_int / intervalという計算より"5"が登録され、結果としては同じ値となります。真ん中の図では上段では山全体が加算されるので"10"、下段では左図と同じ計算で"5"が登録され、さらに"5"が次の登録タイミングへと繰り越されます。右図では逆に上段は"0"となりますが、下段は左や真ん中の図と同じく今回"5"、次回も"5"となります。さらに細かく見ると、上段と下段の値が一致するのは、タイミングを跨いだバーストの面積比が、pre_int対post_intの比に正確に一致した場合のみであり、それ以外では必ずずれが生じることになります。
さらに図をみるとわかるとおり、登録と更新のタイミングがずれると、バーストの情報が前後のデータにも登録されてしまうため、バーストの発生や終了のタイミングが1step分前後に広がってしまうことになります。この誤差はタイミングのずれが大きいほど大きくなるため、RRDToolでは登録タイミングとデータの更新タイミングは極力合わせたほうがよいという結論になります。
次に、stepより長い間隔でデータを取得した場合の処理を見て行きます。このケースでは、以下のように1回のデータ取得で2step(以上)分の記録をするタイミングが発生します。
このケースでも、データ分割アルゴリズムはうまく働きます。
ただし、この時点で0:05までのデータは確定していないので、次回の計算に繰り越されます。
この時点で、0:05と0:10のデータが2つ同時に確定します。このとき、プログラム内部ではデータの最終登録時刻(0:00)と確定した最後のデータ(0:10)の差をstep(300秒=5分)で割って、現在の取得値が2step分であると判定されます(calculate_elapsed_steps()の戻り値)。
つまり、この場合の0:05と0:10時点の記録は、5分ごとの値ではなく、0:10時点の絶対値ないしは10分間ぶんの累積値を2で割ったものとなるようです。
最後に、データの更新タイミングが記録間隔より短いケースです。
記録タイミングを跨ぐデータ取得では、これまでの説明と同じデータ分割処理が行われます。
データの取得期間内に記録タイミングが現れないと、データの登録等の処理の代わりにsimple_update()関数が呼び出されて、データが単純に次回の処理へと繰り越されます。
simple_update()関数では、今回取得した値(水色部分)を丸ごとscratchに加算して、これまでの繰り越し分に今回のデータを追加します。
これを繰り返して累積されたデータが、次の記録タイミングが現れた時点で登録処理用に使われます。
つまり、COUNTER型の処理に問題はありませんが、ABSOLUTE型などでは途中のデータは無駄に破棄されるようです。
このようにRRDToolでは、データの取得間隔がstepより長くても短くてもそれなりに正確な値が記録されるようにアルゴリズムが工夫されていました。また、全体のアルゴリズムが、
という3つの処理の組み合わせでシンプルにまとまっているので、アルゴリズムの例題としても非常に興味深いです。
stepとデータの更新タイミングの関係については、特殊な事情がない限りは
という方針が、最も効率が良いという結論になります。が、そうでなくても正しく動くことを確認するのが、今回の調査の本来の目的で、その点に関してもしっかりと作りこんであることが確認できました。
というわけで特殊な事情が発生したケースの話へと続きます。
i386なFreeBSD上でmuninを使ってネットワークインターフェイスの監視をすると、詳細画面に以下のような注意書きが表示されます。
IMPORTANT: Since the data source for this plugin use 32bit counters, this plugin is really unreliable and unsuitable for most 100Mb (or faster) interfaces, where bursts are expected to exceed 50Mbps. This means that this plugin is usuitable for most production environments.
インターフェイスの通信量を計算するためのデータソースが32bitカウンタなので、50Mbps以上の通信が5分間続くと1周してしまって正しい値が計測できなくなりますよ。ということのようで、通信量の多いサーバで実際に観察したところ、確かにデータがまともに取れていませんでした。
stepを短くして更新間隔もそれに合わせてもよいですが、記録や表示は5分おきのデータでもとりあえず支障はありませんし、なによりmuninのパラメータを変えるのはめんどくさそうです。となると、要はカウンタが1周する前にデータを更新してしまえばよさそうだという結論に至ります。この方針で問題無いことは先ほどの調査で確認済みです。
というわけで、まずは/usr/local/bin/munin-cronでmunin-updateをコメントアウト
#!/bin/sh #[ -x /usr/local/share/munin/munin-update ] && /usr/local/share/munin/munin-update $@; [ -x /usr/local/share/munin/munin-limits ] && /usr/local/share/munin/munin-limits $@; [ -x /usr/local/share/munin/munin-graph ] && nice /usr/local/share/munin/munin-graph --cron $@ 2>&1 | fgrep -v "*** attempt to put segment in horiz list twice" [ -x /usr/local/share/munin/munin-html ] && nice /usr/local/share/munin/munin-html $@;
次に、/var/cron/tabs/muninを編集してmunin-updateを毎分起動
# cat /var/cron/tabs/munin # DO NOT EDIT THIS FILE - edit the master and reinstall. # (- installed on Sat Dec 26 03:38:01 2009) # (Cron version -- $FreeBSD: src/usr.sbin/cron/crontab/crontab.c,v 1.22.2.1.6.1 2008/10/02 02:57:24 kensmith Exp $) #BEGIN_MUNIN_MAIN MAILTO=root * * * * * /usr/local/share/munin/munin-update */5 * * * * /usr/local/bin/munin-cron #END_MUNIN_MAIN
これでも数字上は250Mbpsまでしか耐えられず、GbEにまともに対応するには15秒おきに更新する必要があります。
が、数十台のホストを監視しているとupdateスクリプトの処理自体に時間がかかるという別の問題が絡んでくる上、今のところは250Mbpsキャップでも問題なさそうなのでとりあえずこれでしばらく様子を見ることにしました。
ちょっと面白いspamの連作が届きました。一部伏字にしてます。