RRD ファイルのデータを InfluxDB に移行する

RRDtool を使っていて、InfluxDB に乗り換えるときに、既存のデータを引き継ぎたいことがあると思います。そんな時に RRD ファイルからデータを抽出して、LineProtocol 形式に変換し、InfluxDB にインポートする方法です。

# RRD ファイルの構成をみる
`rrdtool info (ファイル名)` コマンドで、RRD ファイルの構成を確認することが出来ます。MRTG 等と連携して使っていると、RRD ファイルにどんなデータが格納されるかは特に意識していないと思いますが、その場合でも下記のような内容で RRD ファイルが生成されています。

```
$ rrdtool info localhost_2.rrd 
filename = "localhost_2.rrd"
rrd_version = "0003"
step = 300
last_update = 1515986183
header_size = 2912
ds[ds0].index = 0
ds[ds0].type = "COUNTER"
ds[ds0].minimal_heartbeat = 600
ds[ds0].min = 0.0000000000e+00
ds[ds0].max = 1.2500000000e+09
ds[ds0].last_ds = "46732397779"
ds[ds0].value = 0.0000000000e+00
ds[ds0].unknown_sec = 83
ds[ds1].index = 1
ds[ds1].type = "COUNTER"
ds[ds1].minimal_heartbeat = 600
ds[ds1].min = 0.0000000000e+00
ds[ds1].max = 1.2500000000e+09
ds[ds1].last_ds = "8826395"
ds[ds1].value = 0.0000000000e+00
ds[ds1].unknown_sec = 83
rra[0].cf = "AVERAGE"
rra[0].rows = 800
rra[0].cur_row = 368
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
rra[0].cdp_prep[1].value = NaN
rra[0].cdp_prep[1].unknown_datapoints = 0
rra[1].cf = "AVERAGE"
rra[1].rows = 800
rra[1].cur_row = 299
rra[1].pdp_per_row = 6
rra[1].xff = 5.0000000000e-01
rra[1].cdp_prep[0].value = NaN
rra[1].cdp_prep[0].unknown_datapoints = 3
rra[1].cdp_prep[1].value = NaN
rra[1].cdp_prep[1].unknown_datapoints = 3
rra[2].cf = "AVERAGE"
rra[2].rows = 800
rra[2].cur_row = 114
rra[2].pdp_per_row = 24
rra[2].xff = 5.0000000000e-01
rra[2].cdp_prep[0].value = NaN
rra[2].cdp_prep[0].unknown_datapoints = 15
rra[2].cdp_prep[1].value = NaN
rra[2].cdp_prep[1].unknown_datapoints = 15
rra[3].cf = "AVERAGE"
rra[3].rows = 800
rra[3].cur_row = 364
rra[3].pdp_per_row = 288
rra[3].xff = 5.0000000000e-01
rra[3].cdp_prep[0].value = NaN
rra[3].cdp_prep[0].unknown_datapoints = 39
rra[3].cdp_prep[1].value = NaN
rra[3].cdp_prep[1].unknown_datapoints = 39
rra[4].cf = "MAX"
rra[4].rows = 800
rra[4].cur_row = 685
rra[4].pdp_per_row = 1
rra[4].xff = 5.0000000000e-01
rra[4].cdp_prep[0].value = NaN
rra[4].cdp_prep[0].unknown_datapoints = 0
rra[4].cdp_prep[1].value = NaN
rra[4].cdp_prep[1].unknown_datapoints = 0
rra[5].cf = "MAX"
rra[5].rows = 800
rra[5].cur_row = 789
rra[5].pdp_per_row = 6
rra[5].xff = 5.0000000000e-01
rra[5].cdp_prep[0].value = NaN
rra[5].cdp_prep[0].unknown_datapoints = 3
rra[5].cdp_prep[1].value = NaN
rra[5].cdp_prep[1].unknown_datapoints = 3
rra[6].cf = "MAX"
rra[6].rows = 800
rra[6].cur_row = 629
rra[6].pdp_per_row = 24
rra[6].xff = 5.0000000000e-01
rra[6].cdp_prep[0].value = NaN
rra[6].cdp_prep[0].unknown_datapoints = 15
rra[6].cdp_prep[1].value = NaN
rra[6].cdp_prep[1].unknown_datapoints = 15
rra[7].cf = "MAX"
rra[7].rows = 800
rra[7].cur_row = 734
rra[7].pdp_per_row = 288
rra[7].xff = 5.0000000000e-01
rra[7].cdp_prep[0].value = NaN
rra[7].cdp_prep[0].unknown_datapoints = 39
rra[7].cdp_prep[1].value = NaN
rra[7].cdp_prep[1].unknown_datapoints = 39
```

以下、各パート毎に内容を見ていきます。

## 冒頭部分
```
filename = "localhost_2.rrd"
rrd_version = "0003"
step = 300
last_update = 1515986183
header_size = 2912
```
RRD ファイル全体の情報です。今回の目的では以下の2つの値が必要になります。

- `step`
データの時間間隔(秒数)です。上記の例では300秒 = 5分となっています。

- `last_update`
データの最終更新時刻(UNIX時間)です。

## DS 部分
```
ds[ds0].index = 0
ds[ds0].type = "COUNTER"
ds[ds0].minimal_heartbeat = 600
ds[ds0].min = 0.0000000000e+00
ds[ds0].max = 1.2500000000e+09
ds[ds0].last_ds = "46732397779"
ds[ds0].value = 0.0000000000e+00
ds[ds0].unknown_sec = 83
```
データソース(DS)の定義情報です。例では`ds0`、`ds1`と2セットの DSが定義されています。名前や数以外にはここでは重要な情報は無いのですが、type だけは後で考慮する必要が出てきます。

- `type`
RRD ファイルではなく、データソース(情報取得元)のタイプを示しています。上記の `COUNTER` はインタフェースのトラフィック等のような累積値であることを示し、他には観測された時点での瞬間値である `GAUGE` などがあります。

# RRA部分
```
rra[0].cf = "AVERAGE"
rra[0].rows = 800
rra[0].cur_row = 368
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
rra[0].cdp_prep[1].value = NaN
rra[0].cdp_prep[1].unknown_datapoints = 0
```

RRA とは Round Robin Archive のことで、実際にデータが保存される領域の情報です。例では0から7まで、8つのRRAが定義されています。ここでは以下3つの値が必要になります。

- `cf`
Consolidation Function の略で、集約関数のことです。RRDtool では、長期間のデータに対して、一定時間ごとに値を集約し、ダウンサンプリングしてデータを保存しています。ここでは `AVERAGE`(平均値)が選択されており、このほかに `MAX`(最大値)、`MIN`(最小値)、`LAST`(最新値)が選択できます。

- `rows`
データを保存する最大件数です。

- `pdp_per_row`
集約関数を適用するデータの件数です。上記の例では1となっているため、1件ごとの値がそのまま保存されます。

ここで、冒頭部分での `step` の値が300であったため、5分毎のデータが800件保存されます。5 * 800 = 4000分 = 66.666... 時間、つまり3日弱ほどデータが保存されることになります。
同様に `RRA[1]` では、 `pdp_per_row`が6、`step` の値が300であるため、5分毎のデータ6件毎 = 30分毎に集約されることになります。また、そのデータが800件保存されるため、 30 * 800 = 24000 分 = 16.6666... 日、つまり2週間強のデータが保存されることになります。

# 保存先のInfluxDBのデータベース構成を考える

RRDToolでは、情報を取得する対象毎にファイルが生成されます。MRTG と連携した場合は対象機器のインタフェース毎に1つずつファイルが生成され、その中にIn/Out のトラフィック情報が記録されます。一方 `InfluxDB` では、measurement と(RDBでのテーブルのようなもの)に各機器、各インタフェースのトラフィック情報がまとめて記録され、機器毎、インタフェース毎の情報は、tag (RDPでのインデックスのようなもの)によって識別されます(他のやり方もありますが、ここではこのようにします)。こうした情報は、RRD ファイルの中には含まれていないので、別途補完してやる必要があります。

**RRDtool**

- sw1 の Ethernet1 インタフェースのトラフィック

| timestamp | inoctets | outoctets |
|:-:|:-:|:-:|
| 0:00:00 | ... | ... |
| 0:05:00 | ... | ... |

- sw1 の Ethernet2 インタフェースのトラフィック

| timestamp | inoctets | outoctets |
|:-:|:-:|:-:|
| 0:00:00 | ... | ... |
| 0:05:00 | ... | ... |

- sw2 の Ethernet1 インタフェースのトラフィック

| timestamp | inoctets | outoctets |
|:-:|:-:|:-:|
| 0:00:00 | ... | ... |
| 0:05:00 | ... | ... |

- sw2 の Ethernet2 インタフェースのトラフィック

...

**InfluxDB**

- measurement: iftraffic(各インタフェースのトラフィック情報)
 
| timestamp | hostname(tag) | ifname(tag) | inoctets | outoctets |
|:-:|:-:|:-:|:-:|:-:|
| 0:00:00 | sw1 | Ethernet0 | ... | ... |
| 0:00:00 | sw1 | Ethernet1 | ... | ... |
| 0:00:00 | sw2 | Ethernet0 | ... | ... |
| 0:00:00 | sw2 | Ethernet1 | ... | ... |
| 0:05:00 | sw1 | Ethernet0 | ... | ... |
| 0:05:00 | sw1 | Ethernet1 | ... | ... |
| 0:05:00 | sw2 | Ethernet0 | ... | ... |
| 0:05:00 | sw2 | Ethernet1 | ... | ... |
| ... | ... | ... | ... | ... |

また、RRD ファイルでは、複数の RRA によってデータの集約方法、データ間隔、保持期間などが定義されていました。これは、(DS数) x (rows) のテーブルがファイル内に複数内包されていると見ることが出来ます。これらは InfluxDB ではRP(Retention Policy) によって定義されます。

**RRDtool**

| RRA | CF | rows | pdp_per_row |
|:-:|:-:|:-:|:-:|
| 0 | AVERAGE | 800 | 1 |
| 1 | AVERAGE | 800 | 6 |
| 2 | AVERAGE | 800 | 24 |
| 3 | AVERAGE | 800 | 288 |
| 4 | MAX | 800 | 1 |
| 5 | MAX | 800 | 6 |
| 6 | MAX | 800 | 24 |
| 7 | MAX | 800 | 288 |

**InfluxDB**

| RP | Duration |
|:-:|:-:|
| 5m_avg | 4000min |
| 30m_avg | 400hour |
| 2h_avg | 400day |
| 1d_avg | 800day |
| 5m_max | 4000min |
| 30m_max | 400hour |
| 2h_max | 400day |
| 1d_max | 800day |

RRDtool と異なり、InfluxDB の RP は、`Duration` (保持期間)そのものを直接記述することになります。ここで例として、RPに `5m_avg` のような名前をつけていますが、これだけでは5分毎に平均値が計算されるわけではなく、別途集約の設定が必要になります。RP名の方は単なる名前なので、1分間隔でデータを保存することも出来ますし、平均ではなく最大値を入れることも出来ますが、どのようなデータであれ、タイムスタンプが保持期間より古くなったものは自動的に削除されます。

# RRD ファイルのデータを取り出す

以上を考慮したうえで、RRD ファイルからデータを取り出します。データの取出しには `rrdtool fetch` コマンドを使います。以下のようにオプション指定して実行します。

```
rrdtool fetch (filename) (cf) -r (rra_step) -s (start) -e (end)
```

`rrdtool fetch` コマンドでは、DSはいくつあっても全て取り出されます。RRAについては1つずつ取り出すことになるため、対象の RRA を1つ選んで、それぞれのオプションのパラメータを以下のように算出します。

- cf = (RRA の cf の値)
- rra_step = (冒頭パートの step の値) * (RRA の pdp_per_row の値)
- end = (冒頭パートの last_updated の値)
- start = end - (rra_step * (RRA の rows の値))

これにより、以下のようにデータが出力されます。

```
...
1514945400: 6.0928663622e+05 1.2707754876e+05
1514945700: 5.0461152387e+05 1.5845791731e+05
1514946000: 3.3789609046e+06 1.1214391444e+05
1514946300: 1.2580118305e+07 1.5101373564e+05
1514946600: 5.4780592913e+06 1.6438402521e+05
1514946900: 2.5134735666e+07 2.8850280432e+05
1514947200: 1.7252457387e+07 2.7190702148e+05
1514947500: 1.2248027847e+06 2.3167245662e+05
1514947800: -nan -nan
```

まず最終行の nan は必ず出てしまうようなので、単純に grep でフィルタしておきます。タイムスタンプはそのままでOKですが、末尾のコロンを取り除きます。また、数字が指数表記になっているので、これを通常の表記に直します。これらは先の `rrdtool fetch` コマンドに続けて、以下のようにパイプでつなげてやることで実現できます。DSの数が異なる場合は、$2、$3の部分を必要な数だけ記載します。

```
... | grep -v nan | awk '{OFMT="%f"} match($1, /^(.*):$/, time) {print time[1], $2+0, $3+0, ....}' 
```

これにより、以下のようにデータが変換されます。

```
1514944200 580111.612930 109212.289840
1514944500 541420.616990 107903.566430
1514944800 3453743.209700 125706.994240
1514945100 12872065.154000 184412.353490
1514945400 609286.636220 127077.548760
1514945700 504611.523870 158457.917310
1514946000 3378960.904600 112143.914440
1514946300 12580118.305000 151013.735640
1514946600 5478059.291300 164384.025210
1514946900 25134735.666000 288502.804320
1514947200 17252457.387000 271907.021480
1514947500 1224802.784700 231672.456620
```

これを最後に InfluxDB の LineProtocol 形式に変換します。

```
... | awk '{print "iftraffic,host=sw1,ifname=Ethernet1", "inoctets="$2",outoctets="$3 ..., $1}'
```

ここでは前述のように、メタデータの補完を行っています。スペースで区切られた1フィールド目の`iftraffic,host=sw1,ifname=Ethernet1`には、InfluxDB 側の measurement 名と、tag のキー＆バリューペアをカンマ区切りで記述します。上記の例では、sw1 の Ethernet1 インタフェースの RRD ファイルからデータ抽出した想定です。これ以外にも、ラックや建物のようなロケーション情報等を、必要に応じて追加することも出来ます。2フィールド目にはトラフィック情報そのものを、やはりキー&バリューペアとしてカンマ区切りで記載しますが、ds0、ds1 という名前ではわかりづらいと思われる場合は、ここで名前の付け替えを行うことが出来ます。末尾の3フィールド目はタイムスタンプです。

```
iftraffic,host=sw1,ifname=Ethernet1 inoctets=589407.346100,outoctets=116289.819100 1514943300
iftraffic,host=sw1,ifname=Ethernet1 inoctets=520140.532400,outoctets=106573.073400 1514943600
iftraffic,host=sw1,ifname=Ethernet1 inoctets=487860.270680,outoctets=103971.537800 1514943900
iftraffic,host=sw1,ifname=Ethernet1 inoctets=580111.612930,outoctets=109212.289840 1514944200
iftraffic,host=sw1,ifname=Ethernet1 inoctets=541420.616990,outoctets=107903.566430 1514944500
iftraffic,host=sw1,ifname=Ethernet1 inoctets=3453743.209700,outoctets=125706.994240 1514944800
iftraffic,host=sw1,ifname=Ethernet1 inoctets=12872065.154000,outoctets=184412.353490 1514945100
iftraffic,host=sw1,ifname=Ethernet1 inoctets=609286.636220,outoctets=127077.548760 1514945400
iftraffic,host=sw1,ifname=Ethernet1 inoctets=504611.523870,outoctets=158457.917310 1514945700
iftraffic,host=sw1,ifname=Ethernet1 inoctets=3378960.904600,outoctets=112143.914440 1514946000
iftraffic,host=sw1,ifname=Ethernet1 inoctets=12580118.305000,outoctets=151013.735640 1514946300
iftraffic,host=sw1,ifname=Ethernet1 inoctets=5478059.291300,outoctets=164384.025210 1514946600
iftraffic,host=sw1,ifname=Ethernet1 inoctets=25134735.666000,outoctets=288502.804320 1514946900
iftraffic,host=sw1,ifname=Ethernet1 inoctets=17252457.387000,outoctets=271907.021480 1514947200
iftraffic,host=sw1,ifname=Ethernet1 inoctets=1224802.784700,outoctets=231672.456620 1514947500
```

ここまでをまとめると、以下のワンライナーにより、RRD ファイルから上記のような LineProtocol の出力が得られます。

```
rrdtool fetch (filename) (cf) -r (step * rows) -s (start) -e (end) | grep -v nan | awk '{OFMT="%f"} match($1, /^(.*):$/, time) {print time[1], $2+0, $3+0, ...}' | awk '{print "iftraffic,host=sw1,ifname=Ethernet1", "inoctets="$2",outoctets="$3, ... $1}' > (output).lp
```

最後に、これを InfluxDB にインポートする前に、インポート先のDB名と、RP名をヘッダとして追記します。

```
# DDL
CREATE DATABASE network
CREATE RETENTION POLICY "5m_avg" ON network DURATION 4000m REPLICATION 1

# DML
# CONTEXT-DATABASE: network
# CONTEXT-RETENTION-POLICY: 5m_avg

iftraffic,host=sw1,ifname=Ethernet1 inoctets=589407.346100,outoctets=116289.819100 1514943300
...
```

DDLについては、一度だけ記載して実行すれば二度目以降は不要ですが、何度実行してもエラーにはならず、かつ二度目以降は何も動作しないので、とりあえず書いておけばOKです。DMLは実行の度に必要な情報であり、複数ホスト、複数インタフェースの情報をインポートする際には、毎回記述しておく必要があります。

# LineProtocol ファイルのデータをインポートする

以上、データファイルの準備が出来たら、InfluxDB 付属のコマンドラインツール(`influx` コマンド)でインポートします。

```
$ influx -host (InflusDBのホスト名) -import -path=(インポートするファイル名) -precision=s 
2018/01/15 17:37:57 Processed 2 commands
2018/01/15 17:37:57 Processed 800 inserts
2018/01/15 17:37:57 Failed 0 inserts
```

`-host` オプションは、対象がローカルホストのInfluxDBの場合は省略できます。また `influx` コマンドは他にもポート番号や認証情報のオプションがあり、必要に応じて指定します。末尾の `precision` オプションは、タイムスタンプの精度が秒単位のため、`s` を指定しておきます。
なお、データをエクスポートしてからインポートするまでの経過時間によっては、インポートの時にすでに保持期間を超過しているデータがある場合もあります。その場合、超過したデータのみエラーで弾かれることになりますが、とりあえず全データをインポートしたいということであれば、一旦 DURATION を 0m などとしてインポートします。

こうしてインポートされたデータは、おなじくコマンドラインツールから確認できます。

```
$ influx
Connected to http://localhost:8086 version 1.4.1
InfluxDB shell version: 1.4.1
> use network
Using database network
> select * from "5m_avg".iftraffic  limit 10
name: iftraffic
time                host ifname    inoctets     outoctets
----                ---- ------    --------     ---------
1441553700000000000 sw1  Ethernet1 70470148.943 236714516.6
1441554000000000000 sw1  Ethernet1 68724813.204 229937451.58
1441554300000000000 sw1  Ethernet1 66281403.53  223447415.69
1441554600000000000 sw1  Ethernet1 63625542.517 217583389.06
1441554900000000000 sw1  Ethernet1 61224971.768 212842930.94
1441555200000000000 sw1  Ethernet1 58553636.477 203758027.99
1441555500000000000 sw1  Ethernet1 56104843.687 198343587.69
1441555800000000000 sw1  Ethernet1 53625205.112 193078552.21
1441556100000000000 sw1  Ethernet1 51362796.694 188773701.17
1441556400000000000 sw1  Ethernet1 49747326.443 183634924.66
```

あとは、データ出力、インポートを各 RRA に対して繰り返し行うことで、RRD ファイル内のデータを全て InfluxDB にインポートすることが出来ます。

# 運用を移行する

既存のデータを InfluxDB に移行し、そこに続けて最新のデータを入れる際に注意しなければならないのは、RRDtool では生データをそのまま保存するのではなく、DS Type に応じて処理を行って保存している、という点です。

例えばネットワークインタフェースのトラフィックカウンタなどの累積値の場合、DS Type を COUNTER とすることで、前回の値との差分を取り、2つの値の間の秒数で割るといった処理が行われています。この場合、新しく取得した値を InfluxDB に入れる前に、同等の処理を行う必要があります。

この処理については、別途[累積カウンタ値を処理してからInfluxDBに入れる](https://qiita.com/atfujiwara/items/fc6e722de128bc69ae6c)方法を記事にまとめましたので、あわせてご確認ください。