参考:【InfluxDB】データの重複(上書き)の避け方 + bitFlyerの板データを効率良く保存
上の記事にて解説しているが、レコードを同時に一括で登録する「バッチ登録」を利用した場合、各レコードが次の条件で「上書き」が起こってしまう。
タイムスタンプが一致
measurementが一致
tag名とtagの値が一致
重要:fieldsに関しては一致・不一致に関わらず上の3条件に合えば上書きされる。上書き時にfieldsのキーが一致した場合は値を上書き、新しいレコードにキーが存在しなかった場合その キー:値 は削除されず残る。
★04/09訂正:私の記憶違いでfieldsのキーを見るという情報が誤っていたため修正。
よほどのことがない限りmeasurementを分けることはないので構造が同じデータをバッチ登録する場合タイムスタンプをズラすかtagの値を変化させる必要がある。
良い。これは公式にも掲載されている手法だ。
参考:How does InfluxDB handle duplicate points?
しかしコレには問題がある。タグはクエリ発行時に効率よく検索できるようにDBのリソースを使う。
つまり、数百といった大量のタグずらしが行われるとDBのデータ登録処理にムダが発生すると考えられる。場合によってはシステムのIOに障害が発生する可能性がある。
せいぜい2,3件のずらしの場合に有効だろう。
レコードを1ナノ秒ズラすことで「別のレコード」と認識させ上書きを回避することができる。
少々強引な手法にもみえるが、こちらも公式に提案されている手法だ。
★ミリ秒やマイクロ秒ででズラせばいいのではとは言ってはいけない。研究なのだ。
InfluxDBはナノ秒単位で登録が可能だが、JavaScriptにナノ秒を扱うコードはない(マイクロ秒は存在する)
Dateオブジェクトはミリ秒扱いで process.hrtime()
はナノ秒だが計測用のコードで日時が取得できない。
``node-microtime
というパッケージを利用すればマイクロ秒が取り扱える
つまり、これを1000倍してやれば一応ナノ秒表記だ。
目的は正確な記録ではなく1ナノ秒単位のズラしであるためこれで十分と考える。
しかし、これにも問題がある
例えばnode-influx公式のこのコード
const date = toNanoDate('1475985480231035677')
桁数は19だ。JavaScriptが扱える整数値の桁数は約16桁。完全にオーバーしている。
Node.jsはこのためにBigIntというのをサポートしている。
console.logすると「1475985480231035677n
」のように語尾にnが付いて表示されるが、数字は変換など必要なくそのまま数字を示している。
コードは BigInt()
という関数に渡してやるだけで変換される。
公式の「NanoDate」ページにはimport {NanoDate} from 'influx'
とあるが全く機能しない。これは正しくES Modulesとして扱われていないなどの初歩的コーディングエラーの問題ではない。本当に存在しないのだ。
余談:ユーザー勘違いしやすいのはNanoDateオブジェクトがあたかもそれだけでシステムからナノセカンド形式の時間を取得しれくれるかのように見えるが実際にはDate型で既に取得された日時をInfluxDB上でナノとして扱うオブジェクトに変換するtoNanoDate()
のみであり、それを元のナノ秒に戻すコードがgetNanoTime()
なのだ。
https://github.com/node-influx/node-influx/issues/334
恐らく世界で彼と私しかこの問題に直面していない(適当)
だれもナノ秒ずらしはやっていないのだろうか?
まず公式のコードでwritePoints
を行うときのタイムスタンプの指定の方法を確認しておく。
//node-influx 公式コードより引用
influx.writePoints([
{
measurement: 'perf',
tags: { host: 'box1.example.com' },
fields: { cpu: getCpuUsage(), mem: getMemUsage() },
//この位置にタイムスタンプを入れる。このサンプル関数はダミー!
timestamp: getLastRecordedTime(),
}
まずmicrotimeをインストールしておこう。
npm i microtime
microtime.now()
を使うことでマイクロ秒での現在時刻が出力されるので1000倍したナノ秒時刻を記録しておく。
//桁数が大きすぎるのでBigIntに変換
const nano_date = BigInt(microtime.now() * 1000)
次にInfluxDBに渡すバッチデータは配列のはずなのでmap
を使ってindex
を引っ張ってくればそれぞれ0からスタートして1ずつプラスされた値が取得できる。
次のコードはbitFlyerの板情報を記録するコードにナノ秒ずらしを入れた例
const asks_data = message.asks.map((asks, index) => {
//補正
const indexed_date = Influx.toNanoDate(nano_date + BigInt(index))
return {
measurement: 'lightning_board_FX_BTC_JPY',
tags: {
type: 'snapshot',
bid_or_ask: 'ask',
},
fields: {
price: asks.price,
size: asks.size
},
timestamp: indexed_date //ここにタイムスタンプ
}
})
補正と書いてあるコードに注目。
nano_date
自体がBigIntなのでIntと足し算ができない。そこでindex
もBigInt化して計算する。
更にtoNanoDateメソッドでnode-influx専用のタイムスタンプ(NanoDate)オブジェクトに変換している。
これは公式の解説にて
if provide a INanoDate as returned from toNanoTime or the results from an Influx query, we'll be able to pull the precise nanosecond timestamp and manipulate it to get the right precision
timestampに指定できる旨が明記されているためエラーのない方法だ。尚、公式の解説ではミリセカンドのままのDateオブジェクトでも良いしナノセカンドを記したstringでも良いらしいが後者はミリセカンドなのかナノセカンドのか明記されていないため危険かと思う。
秒単位で見れば同時刻に複数のデータが上書きされること無く登録されていることが確認できた。
今回シンプルなコードにもかかわらず非常にやっかいだった。
とは言えBigIntの扱い方が分かっただけでも収穫と言えるのではないだろうか。