2013年02月21日

RRD4J - RRD(Round Robin Database) + Graph API For Java

ネットワーク監視・管理者であれば馴染み深いRRD(Round Robin Database)。Webアプリケーションとして間接的に利用することはあっても、Javaプログラミングの視点でRRDと2軸グラフにアプローチしようと考える人は少ないかもしれません。

 rrd4j

実はJavaにはRRD4Jというrrdtoolに相当する機能を持ったライブラリ(API)が存在し、これさえ覚えておけば Pure Java で RRD + Graph アプリケーションが簡単に作れてしまうんです!

RRD4Jライブラリの入手と設定


まずはこちらのプロジェクト・ページから最新のライブラリをダウンロードします。

Downloads - rrd4j - A high performance data logging and graphing system for time series data. - Google Project Hosting

tarボールを解凍し、libフォルダに含まれる5つのjarファイル(converter、inspector、je、mongo、rrd4j)をクラス・パスに通せば準備完了。

rrd4j_01


RRD4Jの基本操作(RRDへのDST/RRA作成・フェッチ・グラフ描画)


ここでは、あるネットワーク機器のトラフィック値(ifInOctets、ifOutOctets)をRRDで管理・グラフ化する場合を例にRRD4Jの基本的な使い方を学んでみます。

まずは、一般的なrrdtoolを利用した場合がこちら。

$ rrdtool create traffic.rrd --start 1361329500 --step 300 \
DS:in:COUNTER:600:U:U DS:out:COUNTER:600:U:U \
RRA:AVERAGE:0.5:1:18

rrdファイル名をtraffic.rrd、ステップ(データの間隔)を300(秒)として、in/outの2つのデータソース(DS)を定義、アーカイブ(RRA)の属性は1ステップ毎に平均値(1ステップなので平均化する意味はありませんが)を算出、これを18 rows だけ、つまり300秒毎の平均値データを18個なので1.5時間分のデータをアーカイブ しています。

startで指定している値はRRDで取り扱う時間の開始時刻。UNIX時間(1970年1月1日からの経過秒数)なので直感的には分かりませんが、Javaとも関わりが深い時間の考え方ですから扱いが面倒ということは無いはずです。

DS部の詳しい書式は次の通り。

DS:DS名称:データ型(DST):最大更新間隔(heartbeat):min(未指定はU):max(未指定はU)

データ型には次の4種類があります。

DST(Data Source Type) 説明
COUNTER カウンター値。常に増加方向に変化する値に利用する。
DERIVE 常に変化する値で変化量がマイナスになることを許可する場合に利用。ハードディスクの空き容量など。
GAUGE 非カウンター値。温度など常にそれを自体を表す値を扱う場合に利用する。
ABSOLUTE COUNTERも実際には常に増加する訳では無く上限に達するとカウンター値が0に戻るが、頻度が少ないので無視することも出来る。しかし、高い頻度でカウンター値の初期化(0へのリセット)が起こる場合にABSOLUTEを利用する。但し、1ステップ内で複数回の初期化が起こる場合はABSOLUTEでも管理出来ない。


最後にデータのアーカイブ形式を定義するRRA部の書式は次の通り。

RRA:CF(アーカイブ・データの性質):xff(アーカイブ作成しきい値):steps(アーカイブ算出のステップの範囲):rows(アーカイブ数)

CFで利用出来るアーカイブの種類はAVERAGE、MIN、MAX、LAST(瞬時値)の4種類。但し、stepsで指定した範囲にxffの値(割合)を超える無効データが含まれるとアーカイブ値の算出は行いません。rowsはアーカイブを蓄積する量。もし、300秒 stepのデータを6 steps(300 x 6 = 1800 sec = 0.5 h)で平均化し、48 rows 蓄積するならば過去24時間(48 rows x 0.5 h / rows = 24 h)分のアーカイブを保持することになりますね。

え、頭が痛くなってきた?、そんな時は@kubotakuさんの「RRDToolを使う」を読むといいですよ。

ここまでをRRD4Jを使って書くと次のようになります。

RrdDef rrdDef = new RrdDef("traffic.rrd", 1361329500, 300);
rrdDef.addDatasource("in", DsType.COUNTER, 600, Double.NaN,Double.NaN);
rrdDef.addDatasource("out", DsType.COUNTER, 600, Double.NaN,Double.NaN);
rrdDef.addArchive(ConsolFun.AVERAGE, 0.5, 1, 18); // 300 x 18 = 1.5 h
RrdDb rrdDb = new RrdDb(rrdDef);
rrdDb.close();

こうして作成したrrdファイルにデータをアーカイブ(update)してみます。

//rrdtool update traffic.rrd 1361329500:3459049106:1097195656
RrdDb rrdDb = new RrdDb("traffic.rrd");
Sample sample = rrdDb.createSample(); sample.setAndUpdate("1361329500:3459049106:1097195656"); sample.setAndUpdate("1361329800:3459356927:1097423507"); sample.setAndUpdate("1361330100:3459622828:1097630514"); sample.setAndUpdate("1361330400:3460289084:1103217472"); sample.setAndUpdate("1361330700:3460874190:1108228090"); sample.setAndUpdate("1361331000:3461252132:1108967719"); sample.setAndUpdate("1361331300:3462233484:1110300533"); sample.setAndUpdate("1361331600:3462731685:1111429517"); sample.setAndUpdate("1361331900:3464374999:1114310912"); sample.setAndUpdate("1361332200:3465560226:1124028120"); sample.setAndUpdate("1361332500:3468909634:1158427587"); sample.setAndUpdate("1361332800:3470094141:1163481162"); sample.setAndUpdate("1361333100:3470847119:1167026238"); sample.setAndUpdate("1361333400:3471693145:1168645180"); sample.setAndUpdate("1361333700:3474475441:1173590166"); sample.setAndUpdate("1361334000:3475724272:1175177666"); sample.setAndUpdate("1361334300:3478060785:1176745780"); sample.setAndUpdate("1361334600:3481032288:1180501441");
rrdDb.close();

今回はSNMPでルーターから取得したifInOctets、ifOutOctetsの2つをSampleオブジェクトのsetAndUpdate()でアーカイブ(epochTime:value1:value2)しています。

実際に値がアーカイブされているかフェッチしてみましょうか。

// rrdtool fetch traffic.rrd AVERAGE --start 1361329500 --end 1361334600
RrdDb rrdDb = new RrdDb("traffic.rrd"); FetchRequest fetchRequest = rrdDb.createFetchRequest(ConsolFun.AVERAGE, 1361329500, 1361334600); FetchData fetchData = fetchRequest.fetchData(); System.out.println(fetchData.dump());
rrdDb.close();

実行すると次のような出力になります。

1361329500:  NaN  NaN  
1361329800:  +1.0260700000E03  +7.5950333333E02
1361330100:  +8.8633666667E02  +6.9002333333E02
1361330400:  +2.2208533333E03  +1.8623193333E04
1361330700:  +1.9503533333E03  +1.6702060000E04
1361331000:  +1.2598066667E03  +2.4654300000E03
1361331300:  +3.2711733333E03  +4.4427133333E03
1361331600:  +1.6606700000E03  +3.7632800000E03
1361331900:  +5.4777133333E03  +9.6046500000E03
1361332200:  +3.9507566667E03  +3.2390693333E04
1361332500:  +1.1164693333E04  +1.1466489000E05
1361332800:  +3.9483566667E03  +1.6845250000E04
1361333100:  +2.5099266667E03  +1.1816920000E04
1361333400:  +2.8200866667E03  +5.3964733333E03
1361333700:  +9.2743200000E03  +1.6483286667E04
1361334000:  +4.1627700000E03  +5.2916666667E03
1361334300:  +7.7883766667E03  +5.2270466667E03
1361334600:  +9.9050100000E03  +1.2518870000E04

DSTがCOUNTERですから、差分を算出することが出来ない(前回データが無い)、初回だけはNaNになります。

最後に二軸グラフを作成してみましょう。

// rrdtool graph traffic.png ...
RrdGraphDef graphDef = new RrdGraphDef(); graphDef.setTitle("Traffic - Router1"); graphDef.setVerticalLabel("B/s"); graphDef.setTimeSpan(1361329500L, 1361334600L);
graphDef.datasource("ds-in", "traffic.rrd", "in", ConsolFun.AVERAGE); graphDef.datasource("ds-out", "traffic.rrd", "out", ConsolFun.AVERAGE); graphDef.line("ds-in", Color.BLUE, "input traffic"); graphDef.line("ds-out", Color.GREEN, "output traffic"); graphDef.setFilename("traffic.png"); RrdGraph graph = new RrdGraph(graphDef); BufferedImage bi = new BufferedImage(1000, 1000, BufferedImage.TYPE_INT_RGB); graph.render(bi.getGraphics());

まず、RrdGraphDefオブジェクトにrrdファイル、データソース(DS)、アーカイブ・タイプをそれぞれds-in、ds-outという名前で登録し、ds-in、ds-outのグラフ種別をlineグラフ、イメージ・ファイル名をtraffic.pngに設定しています。

最後にRrdGraphオブジェクトでレンダリングすればおしまい。

traffic

え、Bytes/secじゃなくてbits/secにしたい? 勿論、本家と同じくCDEF記法もサポートしているので大丈夫。

CDEF記法を使ってbits/secにするならばこんなふうに書きます。

graphDef.datasource("ds-in", "traffic.rrd", "in", ConsolFun.AVERAGE);
graphDef.datasource("ds-out", "traffic.rrd", "out", ConsolFun.AVERAGE);
//graphDef.line("ds-in", Color.BLUE, "input traffic");
//graphDef.line("ds-out", Color.GREEN, "output traffic");
graphDef.datasource("ds-inbit", "ds-in,8,*");
graphDef.datasource("ds-outbit", "ds-out,8,*");
graphDef.line("ds-outbit", Color.BLUE, "output traffic");
graphDef.line("ds-inbit", Color.GREEN, "input traffic");

オリジナルのデータソース(ds-in, ds-out)をCDEF記法で8倍値にラッピングしたds-inbit, ds-outbitを追加し、これをグラフ化しています。

traffic


ちなみに、lineグラフ以外にもGraphDefクラスにはarea()、stack()、hrule()、vrule()メソッドも用意されていますよ。

本家チュートリアルとの比較はこちら。

Tutorial - rrd4j - A high performance data logging and graphing system for time series data. - Google Project Hosting


RRD4J Tips(日本語表示、複数rrdファイルを使ったグラフの作成)


ここからは細かなTipsを幾つか紹介します。

まずは、グラフ・イメージで日本語を使う方法。 

RrdGraphDef graphDef = new RrdGraphDef();
graphDef.setTitle("ルーター1のトラフィック");
//graphDef.setSmallFont(new Font("Takaoゴシック", Font.PLAIN, 12)); // Physical font
//graphDef.setLargeFont(new Font("Takaoゴシック", Font.BOLD, 16)); // Physical font
graphDef.setSmallFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12)); // Logical font
graphDef.setLargeFont(new Font(Font.SERIF, Font.BOLD, 16)); // Logical font
graphDef.setVerticalLabel("ビット/秒");
graphDef.setTimeSpan(1361329500L, 1361334600L);
graphDef.datasource("ds-in", "traffic.rrd", "in", ConsolFun.AVERAGE);
graphDef.datasource("ds-out", "traffic.rrd", "out", ConsolFun.AVERAGE);
graphDef.datasource("ds-inbit", "ds-in,8,*");
graphDef.datasource("ds-outbit", "ds-out,8,*");
graphDef.line("ds-outbit", Color.BLUE, "アウトバウンド");
graphDef.line("ds-inbit", Color.GREEN, "インバウンド");

FontはRrdGraphDefオブジェクトにsetSmallFont()/setLargeFont()で設定出来ます。
(これを指定しないと組み込みのDejaVuSansMonoが使われます)

traffic

無事日本語化出来ましたね。


気温、湿度、降雨量など共通の事象を個々の管理オブジェクトに組み合わせて1つのグラフにしたいと思うこともあるかもしれません。

2つのrrdファイルを組み合わせて1つのグラフを作成する場合には次のようなコードになります。

 public void renderGraph(Date start, Date end) throws IOException {
  
  RrdGraphDef graphDef = new RrdGraphDef();
  graphDef.setTitle("2013年2月 東京の天候(気温と湿度)");
  graphDef.setSmallFont(new Font("Takaoゴシック", Font.PLAIN, 12));
  graphDef.setLargeFont(new Font("Takaoゴシック", Font.BOLD, 16));
  graphDef.setVerticalLabel("気温・湿度");
  graphDef.setTimeSpan(start.getTime()/1000, end.getTime()/1000);
  graphDef.datasource("ds-temp", "rrd/temperature.rrd", "tokyo", ConsolFun.AVERAGE);
  graphDef.datasource("ds-hum", "rrd/humidity.rrd", "tokyo", ConsolFun.AVERAGE);

  graphDef.area("ds-hum", Color.GREEN, "湿度");
  graphDef.area("ds-temp", Color.RED, "気温");
  graphDef.hrule(80, Color.BLUE, "80");
  
  graphDef.comment("\\r");
  
  graphDef.comment("期間:" + start.toString() + " - " + end.toString());
  
  graphDef.comment("\\r");
  graphDef.gprint("ds-temp", AVERAGE, "平均気温 = %.2f%S\\c");
  graphDef.gprint("ds-hum", AVERAGE, "平均湿度 = %.2f%S\\c");
        
  graphDef.setFilename("graph/tokyo.png");

  RrdGraph graph = new RrdGraph(graphDef);
  BufferedImage bi = new BufferedImage(1000,1000,BufferedImage.TYPE_INT_RGB);
  graph.render(bi.getGraphics());
 }

RrdGraphDefオブジェクトのdatasource()をrrdリソースだけ呼び出せば実現出来ます。
※addDatasource()、setDatasource()に別れていた方が分りやすい気もしますね。

tokyo


このグラフには気象庁の公開している全国気象データ(東京/10分ステップ)を利用したんですが、RRD4Jを使って気象グラフ及びアーカイブ・データを提供するWeb API/サービスを作ったら面白いかもしれませんね。

RRD4Jは非常によく作りこまれた、有償でも不思議では無いrrdtoolのJavaスタックです。

是非一度、遊んでみて下さい。


ビジュアライジング・データ ―Processingによる情報視覚化手法
Ben Fry
オライリージャパン
売り上げランキング: 56,374


Posted by netbuffalo at 20:30│Comments(0)TrackBack(0)プログラミング | ネットワーク


この記事へのトラックバックURL

http://trackback.blogsys.jp/livedoor/netbuffalo/4380042

コメントする

名前
URL
 
  絵文字