KVSのデータ設計が分からん

2015/3/19作成

NoSQLの一つとしてKey-Valueデータベースがあります。NoSQLの文脈で言われるのはビッグデータなどでRDBMSではパフォーマンスが不足する場合が主でしょうか。

それはそれで興味深いのですが、個人的にそんな巨大なデータを扱うことはそんなになくて、逆にシンプルな用途でKVSを使いたいなと思うことがときどきあります。そういう用途ですので、対象もGDBMとかBerkeley DBとかそういったものになります。MySQLサーバを動かすほどの用途ではないけど、ファイルを自分で読み書きするのではちょっと面倒といった用途でしょうか。

ということでKVSを使いたいのですが、これまであんまりつかったことがないのでデータモデリングのやり方がなかなか掴めなくて苦労しています。

Key-Valueのモデリングで一番分かりやすいのは、アプリケーションが全てのキーを知っている場合でしょうか。アプリの設定情報とかが該当すると思います。UNIXのrcファイルであったり、Windowsのレジストリであったりとかそんな感じですね。これはKey-Valueデータベースがそのまま当てはまるので非常に分かりやすい。

次に分かりやすいのは、元のデータ構造がそもそもKey-Valueになっているもの。mixiがユーザ認証処理にKVSを用いて高速化を行ったという記事を見たことがありますが、ユーザ認証ならアカウントというキーに対してパスワード(のハッシュ)というバリューが得られればいいわけなので、データ構造が最初からKey-Valueになっています。これもモデリングしやすい。というかモデリングする必要もない。

分からないのは、たとえばブログの記事を格納したい場合。記事は一つのデータ構造を持っていて、それが複数存在するという形になっています。RDBMSならば簡単に実現できるデータ構造ですが、これをKey-Valueでどのようにモデリングするか。

考えられる方法の一つは、記事にIDを振ってIDをキーにする方法。これなら実現は出来ますが、いくつか問題があります。

まず一つは、全記事を列挙するには全データを取得しないといけない。ブログだったら、最新の記事をトップページに掲載するわけですが、どの記事が最新であるかは全記事を取得して比較しないといけない。トップページを表示するたびに全記事を取得していてはデータベースを使っている意味がない。

解決策としては、最新記事IDを保持するレコードを持っておくというのがありますが、こうするとデータを多重に持つことになるので矛盾が起きる可能性がある。ロジックで矛盾を抑え込むには慎重なアプリ設計と入念なデバッグが必要になって、手軽に使いたいという目的からは遠く離れてしまう。

最新記事だけではなく、記事総数が知りたいといった場合にも全件取得になってしまうのも不格好です。データ件数を取得するAPIがあるKey-Valueデータベース実装もありますが、そうではない場合も多いようです。

バリューに記事のデータ全てを格納してしまうとのはまずいという問題もあります。ブログの場合ならタイトル、投稿日時、本文、コメント、トラックバックなどのデータから構成されますが、これらをくっつけて一つのバリューにしてしまうのはなんとも不格好です。これについては、(id)_title、(id)_posttimeといった風にキーを振ってバリューを分けることで改善は可能ではありますが、今度はタイトルはあるのに本文が行方不明のデータが出てきたりとか別の問題が発生しかねません。

ブログ以外にも、ログ格納に使いたい場合にも苦労しそうな気がします。バリューはログデータをそのまま格納するとして、問題はキーを何にするか。ログだったら発生日時がまず思いつきますが、これは同一日時にイベントが発生したら上書きされてしまうので困ります。実装によっては一つのキーに複数のバリューを持たせることができるそうですが。日時をミリ秒やマイクロ秒まで精度を高めると衝突の可能性は限りなく避けられますが、論理的に衝突の可能性が存在するのは気持ち悪いです。

ということで、Key-Valueデータベースのモデリングってどうするんだろうというので悩んでいます。多分、そんなデータはKey-ValueではなくRDBMSを使えばいいやんというのは正解の一つではあるとは思うんです。軽い用途に使いたい場合でも、MySQLではなくSQLiteという選択肢もありますし。

それはそうなんですが、Key-Valueのモデリングというのも多分ある程度体系化されているんじゃないかと思うんですね。でも調べ方が悪いのか、ネットでも書籍でもなかなか情報が見つからない。その情報があるのなら知りたいなぁと思っている今日この頃です。

(2016/8/22追記)

なんかこの記事が「kvs 設計」で検索されるようになってるようでして、少々びびってます。情報を求めてきている方々には何にも提供できなくてごめんなさい。

でまあ、今更ながら書きますと、KVSの設計は結局はKey-Valueのついになるものを格納するということだと。言葉にしてしまうと当たり前ですが。なんというか、普段RDBMSを使い慣れているとデータモデリングとしてそっちに引っ張られますけれど、Key-Valueのモデリングも身近にいっぱいあるんですよね。例えば連想配列もそうですし。Key-Valueデータベースとは要するに連想配列がファイルに保存されて永続化されるものだ、と言い切ってもそんなに問題ないような気がする。あんまり自信ないけど。

極論すれば、ファイルシステムもKey-Valueデータベースですよね。ファイル名がKeyで、ファイルがValueで。ある程度の規模のウェブサービスではキャッシュサーバを使いますが、キャッシュもKey-Valueですし。ウェブキャッシュならURLがKeyでコンテンツデータがValueですし、クエリキャッシュならクエリがKeyで結果セットがValueですし。

Key-Valueデータベースが連想配列だとして、上記で論じたデータ矛盾の抑え込みをどうするか。RDBMSでは制約を付けることで矛盾を抑え込むことが出来る。Key-Valueではそこまではできないから、ロジックで頑張って抑え込むしかない。がんばれ、ってわけですな。そう納得するしかないのかな。

データベースとして使うのならデータが矛盾なく永続してもらわないと困るんですが、分析基盤として使うとかなら、そんな保証はなくてもいいですよね。Hadoopとかではそういう使い方だと思うし、CRUDのうちCだけなら矛盾を抑え込むのもそんなに大変ではない。矛盾が起きても、データをクリアして再度構築しなおせばいいって割り切りも可能ですしね。

最後に、上記でなんでブログのデータベースを例に出したかというと、Movable Typeの初期のバージョンではBerkeley DBに対応してたんですね。それってどういうデータモデルになってるんだろう、という疑問が最初にあってこの記事を書いたと。だったら、とっととMovable Typeをインストールして調べてみりゃいいんだけど、それはできてないという体たらく。また調べられたら追記します。はい。

(2017/4/16追記)

ようやくMovable Typeをインストールして試しました。ちなみに、古いバージョンはhttps://movabletype.org/downloads/archives/で配布されています。1.Xからきちんとこうして入手可能な状態にしてくださっているというのはとてもありがたいと思います。

ということで、懐かしい感じがしつつようやくMovable Type 3.38をBerkeley DBでインストールし、テスト記事を投稿して、データベースディレクトリを開いてみて驚愕しました。その驚愕を、是非皆さんも感じてください!

$ ls -la
total 512
drwxrwxrwx. 2 root   root     4096 Apr 17 08:00 .
drwxr-xr-x. 4 root   root     4096 Apr 17 07:54 ..
-rw-r--r--. 1 root   root     1967 Apr 17 08:00 a
-rw-rw-rw-. 1 apache apache   8192 Apr 17 07:58 author.created_on.idx
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:58 author.created_on.idx.lock
-rw-rw-rw-. 1 apache apache   8192 Apr 17 07:58 author.db
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:58 author.db.lock
-rw-rw-rw-. 1 apache apache   8192 Apr 17 07:58 author.email.idx
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:58 author.email.idx.lock
-rw-rw-rw-. 1 apache apache   8192 Apr 17 07:58 author.name.idx
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:58 author.name.idx.lock
-rw-rw-rw-. 1 apache apache   8192 Apr 17 07:58 author.type.idx
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:58 author.type.idx.lock
-rw-rw-rw-. 1 apache apache   8192 Apr 17 07:59 blog.children_modified_on.idx
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:58 blog.children_modified_on.idx.lock
-rw-rw-rw-. 1 apache apache  12288 Apr 17 07:59 blog.db
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:58 blog.db.lock
-rw-rw-rw-. 1 apache apache   8192 Apr 17 07:58 blog.name.idx
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:58 blog.name.idx.lock
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:58 category.db.lock
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:58 comment.db.lock
-rw-rw-rw-. 1 apache apache   8192 Apr 17 07:58 config.db
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:58 config.db.lock
-rw-rw-rw-. 1 apache apache   8192 Apr 17 07:59 entry.author_id.idx
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:59 entry.author_id.idx.lock
-rw-rw-rw-. 1 apache apache   8192 Apr 17 07:59 entry.basename.idx
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:59 entry.basename.idx.lock
-rw-rw-rw-. 1 apache apache   8192 Apr 17 07:59 entry.blog_id.idx
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:59 entry.blog_id.idx.lock
-rw-rw-rw-. 1 apache apache   8192 Apr 17 07:59 entry.created_on.idx
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:59 entry.created_on.idx.lock
-rw-rw-rw-. 1 apache apache   8192 Apr 17 07:59 entry.db
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:58 entry.db.lock
-rw-rw-rw-. 1 apache apache   8192 Apr 17 07:59 entry.modified_on.idx
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:59 entry.modified_on.idx.lock
-rw-rw-rw-. 1 apache apache   8192 Apr 17 07:59 entry.status.idx
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:59 entry.status.idx.lock
-rw-rw-rw-. 1 apache apache   8192 Apr 17 07:59 entry.week_number.idx
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:59 entry.week_number.idx.lock
-rw-rw-rw-. 1 apache apache  12288 Apr 17 07:59 ids.db
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:58 ids.db.lock
-rw-rw-rw-. 1 apache apache   8192 Apr 17 07:59 log.blog_id.idx
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:58 log.blog_id.idx.lock
-rw-rw-rw-. 1 apache apache   8192 Apr 17 07:59 log.class.idx
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:58 log.class.idx.lock
-rw-rw-rw-. 1 apache apache   8192 Apr 17 07:59 log.created_on.idx
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:58 log.created_on.idx.lock
-rw-rw-rw-. 1 apache apache   8192 Apr 17 07:59 log.db
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:58 log.db.lock
-rw-rw-rw-. 1 apache apache   8192 Apr 17 07:59 log.level.idx
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:58 log.level.idx.lock
-rw-rw-rw-. 1 apache apache   8192 Apr 17 07:58 permission.author_id.idx
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:58 permission.author_id.idx.lock
-rw-rw-rw-. 1 apache apache   8192 Apr 17 07:58 permission.blog_id.idx
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:58 permission.blog_id.idx.lock
-rw-rw-rw-. 1 apache apache   8192 Apr 17 07:59 permission.db
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:58 permission.db.lock
-rw-rw-rw-. 1 apache apache   8192 Apr 17 07:58 permission.role_mask.idx
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:58 permission.role_mask.idx.lock
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:59 placement.db.lock
-rw-rw-rw-. 1 apache apache   8192 Apr 17 07:58 session.db
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:58 session.db.lock
-rw-rw-rw-. 1 apache apache   8192 Apr 17 07:58 session.kind.idx
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:58 session.kind.idx.lock
-rw-rw-rw-. 1 apache apache   8192 Apr 17 07:58 session.start.idx
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:58 session.start.idx.lock
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:59 tag.db.lock
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:58 tbping.db.lock
-rw-rw-rw-. 1 apache apache   8192 Apr 17 07:58 template.blog_id.idx
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:58 template.blog_id.idx.lock
-rw-rw-rw-. 1 apache apache   8192 Apr 17 07:58 template.build_dynamic.idx
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:58 template.build_dynamic.idx.lock
-rw-rw-rw-. 1 apache apache 143360 Apr 17 07:58 template.db
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:58 template.db.lock
-rw-rw-rw-. 1 apache apache   8192 Apr 17 07:58 templatemap.archive_type.idx
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:58 templatemap.archive_type.idx.lock
-rw-rw-rw-. 1 apache apache   8192 Apr 17 07:58 templatemap.blog_id.idx
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:58 templatemap.blog_id.idx.lock
-rw-rw-rw-. 1 apache apache   8192 Apr 17 07:58 templatemap.db
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:58 templatemap.db.lock
-rw-rw-rw-. 1 apache apache   8192 Apr 17 07:58 templatemap.is_preferred.idx
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:58 templatemap.is_preferred.idx.lock
-rw-rw-rw-. 1 apache apache   8192 Apr 17 07:58 templatemap.template_id.idx
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:58 templatemap.template_id.idx.lock
-rw-rw-rw-. 1 apache apache   8192 Apr 17 07:58 template.name.idx
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:58 template.name.idx.lock
-rw-rw-rw-. 1 apache apache   8192 Apr 17 07:58 template.type.idx
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:58 template.type.idx.lock
-rw-rw-rw-. 1 apache apache   8192 Apr 17 07:59 trackback.blog_id.idx
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:59 trackback.blog_id.idx.lock
-rw-rw-rw-. 1 apache apache   8192 Apr 17 07:59 trackback.category_id.idx
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:59 trackback.category_id.idx.lock
-rw-rw-rw-. 1 apache apache   8192 Apr 17 07:59 trackback.created_on.idx
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:59 trackback.created_on.idx.lock
-rw-rw-rw-. 1 apache apache   8192 Apr 17 07:59 trackback.db
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:59 trackback.db.lock
-rw-rw-rw-. 1 apache apache   8192 Apr 17 07:59 trackback.entry_id.idx
-rw-rw-rw-. 1 apache apache      0 Apr 17 07:59 trackback.entry_id.idx.lock

そうきたか!(笑)。正直、これは全く想定してませんでした。んでも、まあ確かにそうなるわね。おいちゃん、一本取られたよ。