2016.09.14

NoSQLデータベース Riak ~特徴について~


riak

次世代システム研究室の データストア 好きの Y.I. です。

今回も個人的に注目している Basho Technologies, Inc. の分散 NoSQL データベース Riak についてご紹介します。 前回 はノードの追加等の運用が楽という事をメインに詳細させてもらいましたが、今回は Riak KV の特徴についてまとめたいと思います。

■ Riak KV 特徴一覧


  • CAP定理のAPをカバー
  • 高スケーラビリティ
    • ノードの追加・削除が簡単
    • 自動リバランス
    • マスターレスアーキテクチャ
  • 高障害耐性
    • 自動レプリケーション
    • Quorum
    • Hinted Handoff

  • データ衝突の解消について
    • last_write_wins
    • Vector Clocks(+ Siblings)
    • Dotted Version Vectors(DVV)
    • CRDT


■ CAP定理のAPをカバー


CAP 定理とは分散コンピュータシステムにおける情報複製に関する定理であり、 Consistency, Availability, Partition-tolerance を同時に3つの保証をすることを出来ないことを定義しています。DataStore における CAP 定理に関して個人的に一番しっくり来たブログから説明を引用させてもらいます。

原文
  • Consistency means that each client always has the same view of the data.
  • Availability means that all clients can always read and write.
  • Partition tolerance means that the system works well across physical network partitions.

意訳
  • Consistency とは誰もがいつでも同じデータを参照できることである
  • Availability とは誰もがいつでも参照および登録できることである
  • Partition tolerance とはネットワーク分断が起きてもシステムが動作し続けられることである

出典:Nathan Hurst’s Blog | Visual Guide to NoSQL Systems

この内、 Riak は Availability と Partition tolerance をカバーしています。 Consistency に関しては結果整合性となります。どのように対応しているのか、特にデータ衝突時の解消戦略を中心として特徴や機能について触れて行きたいと思います。


■ 高スケーラビリティ


▼ノードの追加・削除が簡単


1ノードの追加や削除が3コマンドで簡単に行えます。下記の自動リバランス機能によりノード追加前に元データをコピーするなど事前準備は不要です。

riak-admin cluster join riak@XXX.XX.X.2
riak-admin cluster plan
riak-admin commit

▼自動リバランス


ノード増減に応じてデータを自動で再配備してくれます。ノード間のデータの偏りも自動で解消してくれます。そのため、アプリケーションで配備先を意識して変更する必要はありません。

▼マスターレスアーキテクチャ


マスターレスアーキテクチャでありクライアントが接続先ノードを意識する必要がないこともあり、アプリケーションの変更なしでノードの増減を行えます。

ノードを増やした分だけリニアに格納可能データ容量も増えますしアクセス性能も上がります。



■ 高障害耐性


▼自動レプリケーション


Riak にデータを登録すると自動で複数のノードにレプリケーションされます。デフォルト設定では3ノードへの複製となっています。複製数の指定は bucket を定義する際に props の n_val の値で出来ます。

▼Quorum


データのレプリケーションの成功判定をするために、過半数や多数決とされる Quorum によりデータ複製の成功を判定します。 Riak においては、 n_val / 2 + 1 です。 n_val = 3 の時は 2つの Vnode にデータを書き込み出来たら成功とします。

▼Hinted Handoff


ノードダウンが発生した時などに隣接ノードが自動で肩代わりしてくれて、サービスの継続が可能です。後にノードを復旧、追加した際に肩代わりしたデータを自動で移行してダウン前の状態に戻します。この機能のことを Hinted Handoff と呼びます。アプリケーションはダウンを意識することなしにサービスを継続可能で、運用者は深夜対応が不要となり翌日にノードを追加すれば良い運用になります。



■ データ衝突の解消について


結果整合性のデータストアでは、レプリケーション遅延やネットワーク分断によるデータの衝突が避けられません。データの衝突に対して Riak では4つの手段が提供されています。各々 Bucket 単位で定義が可能です。

▼last_write_wins


こちらは後勝ち方式です。登録データの timestamp を元に最新のデータを正として採用します。

・設定方法
$ curl -X PUT  -H "Content-Type: application/json" -d '{"props":{"allow_mult":false, "last_write_wins":true}}'  http://XXX.XXX.XX.100:8098/buckets/def_bucket/props

※ bucketの作成時に last_write_wins true を指定します

▼Vector Clocks(+ Siblings)


Riak は論理時間により競合解決を行います。 論理時間を使用して値の更新履歴を追跡し、競合する書き込みを検出します。登録データが最新かどうかイベントを辿って判定出来るように Vector Clock が利用されます。データの初期登録時に Vector Clock でタグ付けされ、以降のデータ登録時に Vector Clock を拡張していき、データ更新イベントを辿ることができます。参照時に Vector Clock を元に衝突の解消を行う事ができます。

allow_mult true の bucket において、並列な登録や Vector Clock 未指定により同一オブジェクトが登録された際には、siblings(意訳:兄弟のようなデータ)が生成され複数の登録を区別しながら保持します。アプリケーションから採用する siblings を指定することでデータ衝突を解消できます。

また短期間に1つのオブジェクトを何度も更新すると Vector Clock が爆発的に増える事があります。そのような更新を行うオブジェクトに対して Vector Clock を利用することは推奨されません。

・設定方法
$ curl -X PUT -H "Content-Type: application/json" -d '{"props":{"allow_mult":false, "last_write_wins":false,"dvv_enabled":true}}' http://XXX.XXX.XX.100:8098/buckets/dvv_bucket/props

※ bucketの作成時に dvv_enabled false および last_write_wins true を指定します

・同一オブジェクトに対して何度か更新と参照をしてみます
※ X-Riak-Vclock が Vector Clock になります

$ curl -X PUT http://XXX.XXX.XX.100:8098/buckets/vc_bucket/keys/k1 -d "aaa"
$ http http://XXX.XXX.XX.100:8098/buckets/vc_bucket/keys/k1
HTTP/1.1 200 OK
...
ETag: "5ADYsKNYopALaqNwA1UoYL"
X-Riak-Vclock: a85hYGBgzGDKBVI8Vraf1LN2BJlAhBIZ81gZWnd6XeTLAgA=

aaa

$ curl -X PUT http://XXX.XXX.XX.100:8098/buckets/vc_bucket/keys/k1 -d "bbb"
$ http http://XXX.XXX.XX.100:8098/buckets/vc_bucket/keys/k1
HTTP/1.1 200 OK
...
ETag: "34oeqTct5wtnNtaYFTlxU0"
X-Riak-Vclock: a85hYGBgymDKBVI8Vraf1LN2BJkA2YwZTImMeawMrTu9LvJBpb3C2VZ4NuheY2DwU4JKTwBJZwEA

bbb

$ curl -X PUT http://XXX.XXX.XX.100:8098/buckets/vc_bucket/keys/k1 -d "ccc"
$ http http://XXX.XXX.XX.100:8098/buckets/vc_bucket/keys/k1
HTTP/1.1 200 OK
...
ETag: "4jPODxt8vbqZ697FohIeTE"
X-Riak-Vclock: a85hYGBgymDKBVI8XuFsKzwbdK8xMPgpZTAlMuaxMkzY6XWRDyptZftJPWtHkAmQzQiUZgJKzwRJZwEA

ccc

$ curl -X PUT http://XXX.XXX.XX.100:8098/buckets/vc_bucket/keys/k1 -d "aaa"
$ http http://XXX.XXX.XX.100:8098/buckets/vc_bucket/keys/k1
HTTP/1.1 200 OK
...
ETag: "16Qca5jSpt6cW23ZqNtdb0"
X-Riak-Vclock: a85hYGBgymDKBVI8Vraf1LN2BJkA2YwZTIlMeawMM3d6XeSDSnuFs63wbNC9xsDgpwSVng+SzgIA

aaa



▼Dotted Version Vectors(DVV)


Riak Version 2 以降から利用できます。
Vector Clock をより効率的にしたデータバージョン管理の仕組みによる衝突データ解消の仕組みです。公式Webでは Vector Clocks よりも DVV を強く推奨しています。DVV は Vector Clocks よりも Siblings の増加が少なくなります。

・設定方法
$ curl -X PUT -H "Content-Type: application/json" -d '{"props":{"allow_mult":false, "last_write_wins":false,"dvv_enabled":true}}' http://XXX.XXX.XX.100:8098/buckets/dvv_bucket/props

※ bucketの作成時に dvv_enabled true および last_write_wins true を指定します

・DVV設定の確認
$ curl http://XXX.XXX.XX.100:8098/buckets/dvv_bucket/props | jq

  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   466  100   466    0     0   155k      0 --:--:-- --:--:-- --:--:--  227k
{
  "props": {
    "allow_mult": false,
    "basic_quorum": false,
    "big_vclock": 50,
    "chash_keyfun": {
      "mod": "riak_core_util",
      "fun": "chash_std_keyfun"
    },
    "dvv_enabled": true,   
    "dw": "quorum",
    "last_write_wins": false,
    "linkfun": {
      "mod": "riak_kv_wm_link_walker",
      "fun": "mapreduce_linkfun"
    },
    "n_val": 3,
    "name": "dvv_bucket",
    "notfound_ok": true,
    "old_vclock": 86400,
    "postcommit": [],
    "pr": 0,
    "precommit": [],
    "pw": 0,
    "r": "quorum",
    "rw": "quorum",
    "small_vclock": 50,
    "w": "quorum",
    "write_once": false,
    "young_vclock": 20
  }
}
※ dvv_enabled true, last_write_wins false, allow_mult false になっていることが確認できます

・同一オブジェクトに対して何度か更新と参照をしてみます
$ curl -X PUT http://XXX.XXX.XX.100:8098/buckets/dvv_bucket/keys/k1 -d "aaa"
$ curl -X PUT http://XXX.XXX.XX.100:8098/buckets/dvv_bucket/keys/k1 -d "bbb"
$ curl -X PUT http://XXX.XXX.XX.100:8098/buckets/dvv_bucket/keys/k1 -d "ccc"

$ http http://XXX.XXX.XX.100:8098/buckets/dvv_bucket/keys/k1
HTTP/1.1 200 OK
...
ETag: "6Bk4BaJ3AzSTFWfGAFY3aR"
X-Riak-Vclock: a85hYGBgymDKBVI8XuFsKzwb9A4wMPgpZTAlMuaxMkxq87rIB5VebLJ47vopAkeBbEagNBNMOgsA

ccc

$ curl -X PUT http://XXX.XXX.XX.100:8098/buckets/dvv_bucket/keys/k1 -d "aaa"
$ http http://XXX.XXX.XX.100:8098/buckets/dvv_bucket/keys/k1
HTTP/1.1 200 OK
...
ETag: "iJ5OfundchsnoqH9DJsel"
X-Riak-Vclock: a85hYGBgymDKBVI8XuFsKzwb9A4wMPgpZTAlMuaxMkxq87rIB5VebLJ47vopAkeBbEagNDNQ2rwDKJ0FAA==

aaa

$ curl -X PUT http://XXX.XXX.XX.100:8098/buckets/dvv_bucket/keys/k1 -d "aaa"
$ curl -X PUT http://XXX.XXX.XX.100:8098/buckets/dvv_bucket/keys/k1 -d "bbb"
$ http http://XXX.XXX.XX.100:8098/buckets/dvv_bucket/keys/k1
HTTP/1.1 200 OK
...
ETag: "46TQKlx980TRwNo1wKim1G"
X-Riak-Vclock: a85hYGBgymDKBVI8i00Wz10/ReAokM2YwZTInMfKYN7hdZEPKu0VzrbCs0HvAAODnxJU2hUknQUA

bbb


▼CRDT(Conflict-Free Replicated Data Types)

Riak Version 2 以降から利用できます。
データが衝突しても収束可能なデータタイプで Counters, Sets, Flags, Registers, Maps の5種類があります。書き込みが成功したデータに関して、消失することがありません。また各々定められた収束方法にもとづいてデータ衝突が Riak 内で解消されるため、アプリケーションでデータ解消する必要がありません。

Counters

increment/decrement可能なカウンターを提供するデータタイプです。データ衝突時の解消方法は、全ての inc/dec を再現して正しい値を提供します。

Sets

ユニークな値のコレクションを提供するデータタイプです。データ衝突時の解消方法は、値の追加や削除が並列に行われた際に、追加を勝ちとして値を提供します。

Flags

true/false や enable/disable などの値のを提供するデータタイプです。 Flags 単独でデータを登録することは出来ず Maps 内に登録可能です。データ衝突時の解消方法は、trueやenableを勝ちとして値を提供します。

Registers

文字列をはじめとしたバイナリーデータを提供するデータタイプです。データ衝突時の解消方法は、 timestamp を元とした最新の値を勝ちとして値を提供します。

Maps

key/value 形式で他のデータタイプを保持可能なコレクションを提供するデータタイプです。データ衝突時の解消方法は、追加、更新や削除が並列に行われた際に、追加や更新を勝ちとして値を提供します。

今回は Sets を使ったデータ登録、参照を行ってみます。

・riak ノードへログイン
$ docker exec -it riak_coordinator_1 bash 

※ 前回に docker image で構築した環境へログインします

・sets という名前の set data typeを作成
$ riak-admin bucket-type create sets '{"props":{"datatype":"set"}}'
sets created

・sets をアクティベート
$ riak-admin bucket-type activate sets

・sets のstatusを確認
$ riak-admin bucket-type status sets
sets is active

young_vclock: 20
w: quorum
small_vclock: 50
rw: quorum
r: quorum
pw: 0
precommit: []
pr: 0
postcommit: []
old_vclock: 86400
notfound_ok: true
n_val: 3
linkfun: {modfun,riak_kv_wm_link_walker,mapreduce_linkfun}
last_write_wins: false
dw: quorum
dvv_enabled: true
chash_keyfun: {riak_core_util,chash_std_keyfun}
big_vclock: 50
basic_quorum: false
allow_mult: true
datatype: set
active: true
...

以上で set data type を利用する準備が完了しました。引き続き set data type を使った bucket の作成と値の登録を行います。

・bucket 作成およびデータ登録
$ curl -X POST http://XXX.XXX.XX.100:8098/types/sets/buckets/set_bucket/datatypes/key1 -H 'content-type: application/json' -d '{"add":"aaa"}'

※ typesの後に作成した sets を指定します
※ add で登録するデータを指定します

・作成結果の表示
$ http http://XXX.XXX.XX.100:8098/types/sets/buckets/set_bucket/datatypes/key1
HTTP/1.1 200 OK
Content-Encoding: gzip
Content-Length: 94
Content-Type: application/json
Date: Mon, 12 Sep 2016 03:51:25 GMT
Server: MochiWeb/1.1 WebMachine/1.10.8 (that head fake, tho)
Vary: Accept-Encoding

{
    "context": "g2wAAAABaAJtAAAADD1QNj5kWTpjAADDUWEBag==",
    "type": "set",
    "value": [
        "aaa"
    ]
}
※ type が sets で データ aaa が登録されていることが確認できます

・ 同じkey1の set に bbb を追加してみます
$ curl -X POST http://XXX.XXX.XX.100:8098/types/sets/buckets/set_bucket/datatypes/key1 -H 'content-type: application/json' -d '{"add":"bbb"}'

・追加結果の確認
$ http http://XXX.XXX.XX.100:8098/types/sets/buckets/set_bucket/datatypes/key1
HTTP/1.1 200 OK
Content-Encoding: gzip
Content-Length: 99
Content-Type: application/json
Date: Mon, 12 Sep 2016 04:00:41 GMT
Server: MochiWeb/1.1 WebMachine/1.10.8 (that head fake, tho)
Vary: Accept-Encoding

{
    "context": "g2wAAAABaAJtAAAADD1QNj5kWTpjAADDUWECag==",
    "type": "set",
    "value": [
        "aaa",
        "bbb"
    ]
}
※ value に aaa と bbb が登録されていることを確認できます

・値 aaa を削除してみます
$ curl -X POST http://XXX.XXX.XX.100:8098/types/sets/buckets/set_bucket/datatypes/key1 -H 'content-type: application/json' -d '{"remove":"aaa"}'
※ remove を指定します

・削除結果の確認
$ http http://XXX.XXX.XX.100:8098/types/sets/buckets/set_bucket/datatypes/key1
HTTP/1.1 200 OK
Content-Encoding: gzip
Content-Length: 94
Content-Type: application/json
Date: Mon, 12 Sep 2016 04:00:53 GMT
Server: MochiWeb/1.1 WebMachine/1.10.8 (that head fake, tho)
Vary: Accept-Encoding

{
    "context": "g2wAAAABaAJtAAAADD1QNj5kWTpjAADDUWECag==",
    "type": "set",
    "value": [
        "bbb"
    ]
}
※ value から aaa が削除されていることを確認できます


■ 参考



■ 最後に


今回は Riak がサービス継続しやすいことや複数の競合解決手段をもっていることを紹介させてもらいました。(恥ずかしながら)CAP定理のAPをカバーするデータストアは後勝ち方式による結果整合性しかない思い込んでいたので、今回 Riak を調べているうちに VCC や CRDT など衝突解消プランがあることの知識を得られました。ただ簡単なオペレーションを通して、Vector Clock と Dotted Version Vectors の違いを明示的に確認することはできませんでした。内部的なデータバーションの持ち方なので外から確認することはできないのでしょうか。今後に新しい発見があれば追記していきたいと思います。個人的にはデータのロストがないことやアプリケーションで衝突の解消を指定する必要がないので CRDT による運用が一番有用な気がしました。


次世代システム研究室では、グループ全体のインテグレーションを支援してくれるアーキテクトを募集しています。アプリケーション開発の方、次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ 募集職種一覧 からご応募をお願いします。

皆さんのご応募をお待ちしています。