Cassandra: Cassandra のデータモデル
August 23, 2010
by reciente
0 comments
まずは、公式wikiで日本語訳があるので、そちらの紹介。
次にDiggのエンジニアの人のブログと、その翻訳をしてくれた人のブログの紹介。
WTF is a SuperColumn? An Intro to the Cassandra Data Model
Cassandraデータモデル入門 – Arin Sarkissian
最後にウノラボの記事。
今からはじめるCassandra入門
一番データモデルに関して詳しいのは、Arin Sarkissianさんの記事だね。blogを構築する場合のデータモデル設計例を出してくれているのが大きい。
公式のwikiでも理解出来ないことはないけれど、実例がないからね。
リレーショナルデータベースとの一番の違いは、Columnが固定長じゃない事かな。Key-Value-Storeだから、当然、キーとバリューの関係を次々増やしていける。で、Key-Value-Timestampが1つのColumnなので、自然な流れとして、Columnは増えていく。
更に、リレーショナルという関係がないので、一つのキーを元に複数のバリューを参照したい場合には、SuperColumnという概念が登場する。SuperColumn自体は、通常のColumnとは違い、ハッシュマップでしか無い。つまり、キーを元に複数のColumnを参照する事になる。そして、SuperColumnには、Timestampが存在しない。
そして、ColumnFamilyは、Column, SuperColumnをグルーピングする為の存在。単一のデータ群とみなせるなら、それはColumnFamilyとして纏まる。
最後に、Keyspaceは、データモデルとしては最上位の名前空間といったところかな。リレーショナルデータベースの、データベース名に当たる。
1つのキーで所得したいColumn数が後から増える場合にはこれは非常に便利だ。リレーショナルデータベースの場合は、Tableを追加して、リレーション関係を構築して行うか、ADD COLUMN を行う事になる。どちらにしても、再設計が必要になる。
重要なのが、Nameでソートされる事だろうか。これを利用して、サンプルではエントリーをソートしてる。
ちょっと、Pythonで書いてみた。
エントリー編
from thrift import Thrift
from thrift.transport import TTransport
from thrift.transport import TSocket
from thrift.protocol.TBinaryProtocol import TBinaryProtocolAccelerated
from cassandra import Cassandra
from cassandra.ttypes import *
import time
import pprint
import uuid
def main():
socket = TSocket.TSocket('localhost', 9160)
transport = TTransport.TBufferedTransport(socket)
protocol = TBinaryProtocol.TBinaryProtocolAccelerated(transport)
client = Cassandra.Client(protocol)
pp = pprint.PrettyPrinter(indent=2)
keyspace = 'BloggyApply'
try:
transport.open()
column_path = ColumnPath(column_family='BlogEntries', column='title')
key = '001'
value = '001 title'
timestamp = time.time()
try:
# Insert blog entry.
client.insert(keyspace,
key,
column_path,
value,
timestamp,
ConsistencyLevel.ZERO)
except Thrift.TException, tx:
print "Thrift: %s" % tx.message
column_path = ColumnPath(column_family='TaggedPosts', column=uuid.uuid1().bytes)
key = '__notag__'
value = '001'
timestamp = time.time()
try:
#Insert the data into TaggedPosts
client.insert(keyspace,
key,
column_path,
value,
timestamp,
ConsistencyLevel.ZERO)
#Query for data
column_parent = ColumnParent(column_family='TaggedPosts')
slice_range = SliceRange(start='', finish='')
predicate = SlicePredicate(slice_range=slice_range)
result = client.get_slice(keyspace,
key,
column_parent,
predicate,
ConsistencyLevel.ONE)
pp.pprint(result)
except Thrift.TException, tx:
print "Thrift: %s" % tx.message
except Thrift.TException, tx:
print "Thrift: %s" % tx.message
finally:
transport.close()
if __name__ == '__main__':
main()
コメント編。
from thrift import Thrift
from thrift.transport import TTransport
from thrift.transport import TSocket
from thrift.protocol.TBinaryProtocol import TBinaryProtocolAccelerated
from cassandra import Cassandra
from cassandra.ttypes import *
import time
import pprint
import uuid
def main():
socket = TSocket.TSocket('localhost', 9160)
transport = TTransport.TBufferedTransport(socket)
protocol = TBinaryProtocol.TBinaryProtocolAccelerated(transport)
client = Cassandra.Client(protocol)
pp = pprint.PrettyPrinter(indent=2)
keyspace = 'BloggyApply'
try:
transport.open()
super_column = uuid.uuid1().bytes
values = dict()
values['commenter'] = 'Hoge Mash'
values['email'] = 'hoge@hoge.com'
values['comment'] = 'my comment...'
column_path = ColumnPath(column_family='Comments', super_column=uuid.uuid1().bytes)
super_key = '002'
timestamp = time.time()
try:
# Insert blog entry.
for column in values:
column_path.column = column
client.insert(keyspace,
super_key,
column_path,
values[column],
timestamp,
ConsistencyLevel.ZERO)
except Thrift.TException, tx:
print "Thrift: %s" % tx.message
try:
#Query for data
column_parent = ColumnParent(column_family='Comments')
slice_range = SliceRange(start='', finish='')
predicate = SlicePredicate(slice_range=slice_range)
result = client.get_slice(keyspace,
super_key,
column_parent,
predicate,
ConsistencyLevel.ONE)
pp.pprint(result)
except Thrift.TException, tx:
print "Thrift: %s" % tx.message
except Thrift.TException, tx:
print "Thrift: %s" % tx.message
finally:
transport.close()
if __name__ == '__main__':
main()
これを見て分かると思うけれど、リレーショナルデータベースで既に組まれているシステムを、Cassandraに置き換えるのは、大変な努力が必要だね。データ構造も違うし、プログラム側の処理も全然違うものになってしまう。リレーショナルデータベースみたいに、日付でソートしようと思うだけで、2箇所にデータを入れないといけないという。
CassandraのIOアクセスの速さの理由に、データが常にソートされた状態で保存されている。というのがあるから、ソート順番が決まっているデータにしか使えないっていうのもあるね。ソートをいじるということは、一度データベースを止めないといけない気がする。
そんな訳で、リレーショナルデータベースが得意な部分と、Cassandraが得意な部分を見つけて、うまく混ぜて使うのがいいんじゃないかなと思う。一貫性や、トランザクションを使いたいデータとかはリレーショナルデータベースがいいし、ある程度矛盾や壊れてもいいようなデータで、負荷が高いものは、Cassandraとなるのだろうか。
う~ん、これは本当に使いどころを考えるのが難しいね。サーバプログラムでローカルのメモリ上には持つには大きすぎるような構造体を、Cassandraに持たせてしまうとかだろうか。それでいて、一貫性が緩くても良いもの。
DynamoはAmazonのショッピングカートで使っているっていう話だけれど・・・
後は、単純な集計結果のキャッシュかな。それが、Memcachedを使うよりもCassandraのほうがいい。ってのがあるならいいんだけれど・・・まあ、データ構造が持てる分、複数の集計結果を1度に取得出来るとかはあるか。集計結果がスケールアウトする程の物量になるのか?!っていうのもあるね。
後は、リレーショナルデータベース+Cassandraでデータベース定義を作る事かな。トランザクションが使えないから、MySQLなら、MyISAMで構築していた部分をCassandraに持って行けるなら、特に問題はなさそう。1テーブルが余りにも肥大化しすぎてメモリに乗らないようなデータもCassandra向きかなぁ
2重管理はしたくないから、その辺りが落しどころなのかな・・・Cassandraも持っていて、リレーショナルデータベースも持っている。なんていうデータは作りたくないし。