Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Write an embedded time-series database in Go

nakabonne
November 13, 2021

Write an embedded time-series database in Go

nakabonne

November 13, 2021
Tweet

More Decks by nakabonne

Other Decks in Programming

Transcript

  1. Write an embedded
    time-series database in Go
    @GoCon 2021 Autumn
    Ryo Nakao (@nakabonne)

    View Slide

  2. 自己紹介
    ● 中尾 涼 (@nakabonne)
    ● 株式会社サイバーエージェント
    ● PipeCD フルタイム開発

    View Slide

  3. Agenda
    ● 時系列データの特徴
    ● tstorageの設計
    ● Goでの実装について

    View Slide

  4. Write an embedded
    time-series database in Go
    Ryo Nakao (@nakabonne)

    View Slide

  5. Write an embedded
    time-series database in Go
    Ryo Nakao (@nakabonne)

    View Slide

  6. Embedded database
    ● ライブラリとして提供されている埋め込み可能なデータベース
    ● 有名なものでLevelDBやRocksDBなどがある

    View Slide

  7. モチベーション
    00

    View Slide

  8. 負荷テストツールのメモリパフォーマンス低下

    View Slide

  9. 負荷テストツールのメモリパフォーマンス低下
    ● 計測結果をSliceにappendしていくだけだった
    ● ディスクも活用したストレージエンジンが欲しい
    ● Go言語からEmbedded databaseとして利用できるものが無い
    ● → 作るしかない

    View Slide

  10. tstorage

    View Slide

  11. 時系列データの特徴
    01

    View Slide

  12. 時系列データ
    ● タイムスタンプ順に並んだ値(データ
    ポイントと呼ぶ)の集まり
    ● 時間の経過とともにデータがどう変化
    するかを見るために使われる
    ● e.g. システムメトリクス、株価データ

    View Slide

  13. 時系列データの特徴(一部)
    ● データ量が膨大
    ● 直近のデータを読み出す
    ● 期間指定して一括で読み出す

    View Slide

  14. データ量が膨大
    ● 一定間隔おきに、永続的にデータが取り込まれる
    ● データ量は線形的に増えていく

    View Slide

  15. 直近のデータを読み出す
    直近のデータの推移を観察するために使われることが多い

    View Slide

  16. 期間を指定して一括で読み出す
    時間の経過と共にどう変化したかを観察するために使われる

    View Slide

  17. 速く書いて
    速く読みたい
    こういった特徴がある中で
    なるべく多くのケースで

    View Slide

  18. できるだけDRAMを使おう

    View Slide

  19. データモデル
    02

    View Slide

  20. ● タイムスタンプでパーティ
    ショニング
    ● 完全に独立した小さなDBを
    追加していく
    ● 最近のデータはヒープ上
    ● ほとんどのケースで読み出
    しが高速

    View Slide

  21. ● (再掲)読み出す時は範囲
    指定して一括で読み出す
    ● タイムスタンプが近いデー
    タが集まっているので、空
    間的局所性を高める

    View Slide

  22. Memory Partitionと
    Disk Partition
    に分ける
    とりあえず

    View Slide

  23. 実装 ~ Memory Partition
    03

    View Slide

  24. Memory Partition
    ● インメモリDB
    ● データロスを防ぐために
    Write Ahead Logに書き
    込む

    View Slide

  25. Logical structure ~ Memory Partition

    View Slide

  26. Logical structure ~ Memory Partition
    とにかくここへの書き込み &読み込みが
    速度パフォーマンスに直結する

    View Slide

  27. How to represent data points
    ● Sliceは基底配列に基づいている
    ● 要素数がcapacityを超えると自動的に基底配列のメモリコピーが
    発生
    ● 際限なくデータが取り込まれるケースでは理論的には要素の追加
    をO(1)で行えるLinked-listベースのデータ構造が良さそう

    View Slide

  28. Benchmarks ~ Slice vs Linked-list

    View Slide

  29. Benchmarks ~ Slice vs Linked-list
    何度実行しても時間計算量の面ではSliceのほうが僅かに勝利

    View Slide

  30. Benchmarks ~ Slice vs Linked-list
    基底配列はRAM上の隣り合った場所に並んでいるため、
    キャッシュの空間的局所性が効く

    View Slide

  31. https://groups.google.com/g/golang-nuts/c/mPKCoYNwsoU/m/OSVy4gbCWwMJ

    View Slide

  32. 速く書くのはいいけど...
    ● メモリパーティションは揮発性ストレージ上で動作
    ● 突然のクラッシュに対応したい

    View Slide

  33. Write Ahead Log (WAL)
    ● 書き込みに先行して操作ログをディスクへ書き込む
    ● リカバリ可能な形式で、なるべく小さくエンコード

    View Slide

  34. WAL format
    op: オペレーションタイプ (Insert, Delete, etc)
    len metric: 後続するメトリック名文字列の長さ
    metric: メトリック名
    timestamp: 最大64ビットの整数値型UNIXタイムスタンプ
    value: 最大64ビットの浮動小数点型の値

    View Slide

  35. WAL format
    op: オペレーションタイプ (Insert, Delete, etc)
    len metric: 後続するメトリック名文字列の長さ
    metric: メトリック名
    timestamp: 最大64ビットの整数値型UNIXタイムスタンプ
    value: 最大64ビットの浮動小数点型の値

    View Slide

  36. Varints (Variable integers)
    ● Protocol Bufferが内部で使用している可変長エンコーディン
    グ手法
    ● 数値の大きさに応じて消費サイズが変わる
    ● Goでは binary.PutVarint が担当

    View Slide

  37. 例) 10を64ビット整数値型として固定長エンコードした場合
    → 必ず8バイト(64ビット)消費してしまう

    View Slide

  38. 例) Varintsで可変長エンコードした場合
    → 1バイト(8ビット)で収まる

    View Slide

  39. 適切な位置で読み出しを終了
    どのように終了位置を特定しているのか?

    View Slide

  40. Varints の仕組み
    先頭ビットが0であればそのバイトで読み出しを停止する

    View Slide

  41. Varints (Variable integers)
    数値型の値に関してはこのように Varints を用いて書き込んでいく

    View Slide

  42. 実装 ~ Disk Partition
    04

    View Slide

  43. Disk Partition
    ● オンディスクDB
    ● 1つのファイルに書き込ま
    れる

    View Slide

  44. File format
    ● 時系列データは不変で、範囲指定
    して一括で読み込むケースがほと
    んど
    ● Metric 毎にデータポイントをまと
    めて圧縮することで局所性↑
    ● mmapによって透過的にキャッ
    シュ

    View Slide

  45. Memory-mapped data
    ● マップされたファイルは []byte としてアクセス可能
    ● なんらかのインデックス構造がないと非効率

    View Slide

  46. Metadata file
    ● パーティション毎にjson形式の
    メタデータファイル
    ● メトリック毎のファイル内バイ
    トオフセットやサイズが保存さ
    れている
    ● ヒープに乗るのはこのメタデー
    タのみ

    View Slide

  47. やりたいこと
    ● 書き込み時のオフセットの
    保存
    ● 読み出し時のオフセットの
    指定
    ● これらを同じインタフェー
    スで行いたい
    ● Goではここでio.Seekerが活

    View Slide

  48. io.Seeker
    ● 次に読み書きする位置を指定する
    ● 現在のオフセットを返してくれるの
    で、取得も指定もこれ一つで可能
    ● ランダムアクセスを許容するバイト
    列であれば基本何でもOK

    View Slide

  49. When flushing
    ● ディスクへ書き込む際に各メトリッ
    クのオフセットを保存する必要
    ● io.Seekerを満たす *os.File を利用
    ● メトリックの書き込みを開始するタ
    イミングでSeek(0, io.SeekCurrent)
    を呼ぶ

    View Slide

  50. When reading
    ● []byte の指定したオフセットから読
    み出したい
    ● io.ReadSeekerを満たす
    *bytes.Reader を利用

    View Slide

  51. 高速に読み出せるようになっ
    たけど...

    View Slide

  52. (再掲)データ量が膨大
    ● Varintsでもかなり圧縮されるが、最低でも1バイト必要
    ● 時系列データの特徴に注目して1バイト未満に圧縮できないか

    View Slide

  53. Encoding
    ● タイムスタンプは単調増加する傾向にある
    ● 差分の差分だけ書き込めば良いのでは?
    ● → Delta-of-delta encoding

    View Slide

  54. Delta-of-delta encoding
    1600000000 1600000015 1600000030

    View Slide

  55. Delta-of-delta encoding
    1600000000 1600000015 1600000030
    ±15 ±15

    View Slide

  56. Delta-of-delta encoding
    1600000000 1600000015 1600000030
    ±15 ±15
    ±0

    View Slide

  57. Delta-of-delta encoding
    1600000000 1600000015 1600000030
    ±15 ±15
    ±0
    3番目以降はこの0 (1bit) だけを書き込んでいく

    View Slide

  58. When reading
    1600000000 1600000015 1600000030
    ±15 ±15
    ±0
    シーケンシャルに読み出してこのdelta-of-deltaを加算していく

    View Slide

  59. Bit単位でストリームに読み書きする
    ● Goの標準ioパッケージの読み書き最小単位はbyte
    ● bit単位で読み書きするためにはビット演算する必要

    View Slide

  60. Bit単位でストリームに読み書きする
    0001000 1
    00010001
    io.Writer.Write()
    byte完成!

    View Slide

  61. Bit単位でストリームに読み書きする
    ● bitはboolで表現

    View Slide

  62. Bit単位でストリームに読み書きする

    View Slide

  63. Bit単位でストリームに読み書きする
    ● 0で敷き詰められた8bitsを
    用意

    View Slide

  64. Bit単位でストリームに読み書きする
    ● 1を書き込む場合は末尾の0
    と1のビット和を取る
    ● そして必要な分だけ左シフ
    トする

    View Slide

  65. 負荷テストツールのパフォーマンスが改善

    View Slide

  66. ● (ja) ゼロから作る時系列データベースエンジン :
    https://zenn.dev/nakabonne/articles/d300838a1500c7
    ● (en) Write a time-series database engine from scratch:
    https://nakabonne.dev/posts/write-tsdb-from-scratch
    ● Repo: https://github.com/nakabonne/tstorage
    More...

    View Slide

  67. Thanks
    Any questions?
    @nakabonne

    View Slide