2020-04-08

【InfluxDB】Node.js上でナノ秒単位のタイムスタンプを付ける 【node-influx】

そもそもNode.js(JavaScript)上でナノ秒を扱うコードがない。マイクロ秒単位でレコードを計測し、バッチ登録時にナノ秒ずらしで登録する方法を記す。

Article Image

なぜナノ秒で扱う必要があるのか

参考:【InfluxDB】データの重複(上書き)の避け方 + bitFlyerの板データを効率良く保存

上の記事にて解説しているが、レコードを同時に一括で登録する「バッチ登録」を利用した場合、各レコードが次の条件で「上書き」が起こってしまう。

  1. タイムスタンプが一致

  2. measurementが一致

  3. tag名とtagの値が一致

    重要:fieldsに関しては一致・不一致に関わらず上の3条件に合えば上書きされる。上書き時にfieldsのキーが一致した場合は値を上書き、新しいレコードにキーが存在しなかった場合その キー:値 は削除されず残る。

★04/09訂正:私の記憶違いでfieldsのキーを見るという情報が誤っていたため修正。

よほどのことがない限りmeasurementを分けることはないので構造が同じデータをバッチ登録する場合タイムスタンプをズラすかtagの値を変化させる必要がある。

tagずらしではダメなの?

良い。これは公式にも掲載されている手法だ。

参考:How does InfluxDB handle duplicate points?

しかしコレには問題がある。タグはクエリ発行時に効率よく検索できるようにDBのリソースを使う。

つまり、数百といった大量のタグずらしが行われるとDBのデータ登録処理にムダが発生すると考えられる。場合によってはシステムのIOに障害が発生する可能性がある。

せいぜい2,3件のずらしの場合に有効だろう。

そこで1ナノ秒ずらし

レコードを1ナノ秒ズラすことで「別のレコード」と認識させ上書きを回避することができる。

少々強引な手法にもみえるが、こちらも公式に提案されている手法だ。

★ミリ秒やマイクロ秒ででズラせばいいのではとは言ってはいけない。研究なのだ。

JavaScriptではナノ秒を扱うコードがない

InfluxDBはナノ秒単位で登録が可能だが、JavaScriptにナノ秒を扱うコードはない(マイクロ秒は存在する)

Dateオブジェクトはミリ秒扱いで process.hrtime() はナノ秒だが計測用のコードで日時が取得できない。

マイクロ秒なら扱える

``node-microtimeというパッケージを利用すればマイクロ秒が取り扱える

つまり、これを1000倍してやれば一応ナノ秒表記だ。

目的は正確な記録ではなく1ナノ秒単位のズラしであるためこれで十分と考える。

しかし、これにも問題がある

ナノ秒の桁数が多すぎる

例えばnode-influx公式のこのコード

const date = toNanoDate('1475985480231035677')

桁数は19だ。JavaScriptが扱える整数値の桁数は約16桁。完全にオーバーしている。

BigInt型

Node.jsはこのためにBigIntというのをサポートしている。

console.logすると「1475985480231035677n 」のように語尾にnが付いて表示されるが、数字は変換など必要なくそのまま数字を示している。

コードは BigInt()という関数に渡してやるだけで変換される。

使ってみる

注意:node-influx公式のimport文が間違っている

公式の「NanoDate」ページにはimport {NanoDate} from 'influx' とあるが全く機能しない。これは正しくES Modulesとして扱われていないなどの初歩的コーディングエラーの問題ではない。本当に存在しないのだ。

余談:ユーザー勘違いしやすいのはNanoDateオブジェクトがあたかもそれだけでシステムからナノセカンド形式の時間を取得しれくれるかのように見えるが実際にはDate型で既に取得された日時をInfluxDB上でナノとして扱うオブジェクトに変換するtoNanoDate()のみであり、それを元のナノ秒に戻すコードがgetNanoTime()なのだ。

GitHubで指摘されているのに誰も解答していない

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でも良いらしいが後者はミリセカンドなのかナノセカンドのか明記されていないため危険かと思う。

実行結果(Chronograf)

ずらし成功

秒単位で見れば同時刻に複数のデータが上書きされること無く登録されていることが確認できた。

所感

今回シンプルなコードにもかかわらず非常にやっかいだった。

とは言えBigIntの扱い方が分かっただけでも収穫と言えるのではないだろうか。



この記事のタグ

この記事をシェア


謎の技術研究部 (謎技研)