お使いのブラウザは最新版ではありません。最新のブラウザでご覧ください。

CNET Japan ブログ

NoSQLの成功は1:10問題にかかっている

2010/09/20 14:57
  • このエントリーをはてなブックマークに追加

ここ2-3年ほど、いわゆる非SQL系データベースがホットな話題になってきています。このムーブメントを総称して「NoSQL (Not-only SQL)」と呼ばれることが多いようです。まるでSQLを否定しているかのような誤解を招きやすい用語ですが、かといってキー・バリュー型データストアや列指向DBを総称できる他の呼び方もないので、このエントリではNoSQLという用語を使うことにします。

OracleやMySQLなどのSQLデータベースが成熟していく一方で、SQLデータベースを特徴づける弱点である柔軟性のなさ、堅牢さと引き換えに犠牲になった更新性能の低さ、スケールアウトの難しさなどから、「何でもかんでもRDB」から「目的に応じた永続化」が模索される流れになってきました。

時を同じくして、キャッシュサーバの世界でも、MemcachedのもつシンプルなAPIの使いやすさが評価される一方、LRUによる追い出しや再起動でデータが失われるという、まさにキャッシュとしての性質自体の弱点から、マイルドな永続化による最低限のデータ保全と平準化されたウォームアップ性能が求められるようになってきました。

NoSQLの流行は、これら二つの流れの結節点に生まれたものであるために、さまざまな誤解を生み、議論を巻き起こしてきました。

さまざまな混乱

たとえば、ちょっと前にブームになったのが「MongoDB is Web Scale」という動画です。内容をかいつまんでいうと、

A:「MongoDBはジョインしないから高性能だしスケールするよ。なんでMySQLなんか使ってるの?」
B:「MongoDBは書き込み時に同期しないから途中でコケたらデータ消えるけど、ほんとにいい設計だとおもう?」
A:「でもそれでベンチマークの性能が出るんだからすげぇだろ」
B:(あんぐり)
B:「もしデータの永続性をそこまで無視するんなら、/dev/nullにでも書けば?めっちゃ速いよ」
A:「もしそれが高速ならそれ使うさ。で、/dev/nullってweb scaleなの?」

というおバカな会話で、その後も「/dev/nullならMySQLのBlackhole使うといいよ!(Dev Null = Unlimited Scale)」とか「書き込み専用(=読み出し不可)で1TBあたり$1という画期的なストレージクラウドが誕生(S4 - Super Simple Storage Service)」とか、もう悪ノリはとどまることを知らず、ネタが続々と出てきました。

公平にいって、MongoDBの実装(書き込み時にはディスクに同期しない)にはもちろん設計上の理由があってそうしているわけで、それはMySQLなどとは異なるトレードオフを選択しているからです。大事なのは、そのトレードオフの意味を正しく理解し、自分の用途に合ってるかどうかを見極めることです。

ただ単にNoSQLの流行をもって「SQLデータベースは死んだ」とか極端なことを言っている人も実際にいますが、たいていは自分のSQLアレルギーの言い逃れに使っているだけで、本当の意味でのNoSQLの普及にとっても有害なので、そういった言説への牽制の意味を込めてこうした動画がつくられ流行したのでしょう。Relational Algebraは、CAP Theoremよりも数学的な含意は深く、より本質的で、つまり応用範囲が広いものです。NoSQLは、SQLを極めてこそ真価を発揮するものだということは、意識しておいて損はないでしょう。

NoSQLの価値は「グローバル変数」と「使いやすさ」にあり

よくNoSQLを持ち上げる理由に「SQLデータベースは性能が低い」というものがあります。

これは、多くの場合、まったくの間違いです。驚くなかれ、何一つディスクに書かないはずのMemcachedでさえ、用途(データサイズ)や使用条件(クライアントライブラリなど)によっては、きちんとチューニングされたMySQLに及ばないケースは珍しくありません。もしデータベースサーバが一台しかない環境なら、そのマシンにはMemcachedを乗せず、ありったけのメモリをMySQLに割り当てるほうが、メモリ容量あたりの性能向上効果は高いでしょう。

では、NoSQLを採用することのメリットとはなんでしょうか?

そもそも、なぜ、我々はデータベースを必要としているのでしょうか?データベースがないと、何が困るのでしょうか?

データの保全というのはもちろんですが、実はそんなことよりもっと原始的で、基本的な要求があるはずです。

それは、プログラミングにおける「グローバル変数」としての用途です。

現状の一般的なウェブ・アプリケーションのアーキテクチャでは、たった一個の変数(たとえばアクセスカウンター)を複数のサーバ・インスタンスから共有したい、というような簡単なことでさえ、データベースを経由しなくてはならず、スキーマの追加・変更を伴ないます。このことに業を煮やした経験はないでしょうか?

この問題を解決するために、Memcachedを使っている人たちがいます。しかし、Memcachedはキャッシュですから、いつそのデータが消えてもいいように配慮したコーディングをする必要があります。その結果、アプリケーションコードが複雑になるだけでなく、結局は同時にデータベースにもデータを書くはめになり、それだったらそのままデータベース使ったほうがいいじゃん、ということになりがちです。一見簡単そうなんだけど、実用レベルでは全然気軽じゃなかった、というオチです。

ただ、これら二つのアプローチを比べたときに、より開発フロー的に自然だと感じるのは後者です。あくまで、変数に値をアサインするような気軽さで使いたい。だから、あとは「データが消えないことの保証」の部分をバックエンド側で適当によろしくやってくれたら、それでほとんど満足なんだけどな、と感じることがほとんどではないでしょうか。

私が考えるに、NoSQL的なストレージがフィットする第一の用途はここではないかと思います。

つまり、Memcachedだと「データを書くときにはMemcachedとDBの両方に書く」「データを読むときにはまずMemcachedを読み、なければDBに問い合わせてMemcachedにも書く」という、まるでデータベースの実装がアプリケーション側にはみだしてるような状況で、あまり幸せになれなかったのが、そのへんの面倒をみてくれるエンジンがあれば、ようやく「Memcached的なシンプルな入れ物」をグローバル変数のプライマリストレージとして気軽に使える現実味が出てきたのです。

ちょっと余談ですが、私は今後の世の中の流れとして、いわゆる「キャッシュサーバ」単体の需要はどんどん減っていき、NoSQL的な「ストレージ」への置き換えがどんどん進んでいくと考えています。ソフトウェア開発においては人間の生産性が一番のボトルネックですが、単なる性能改善のためだけのキャッシュ・コンポーネント追加はどちらかというとアプリケーションにとって本質的でないロジックを増やす「必要悪」にすぎないため、ハードウェアの性能向上などとともに、「なくてすむならないほうがいい」という価値観にシフトしていくことでしょう。

「コンピュータサイエンスに難問は二つしかない。それは概念に名前をつけることと、キャッシュの失効である」とは、Phil Karltonの言葉です。

入門用のおすすめはRedis

さて2010年9月現在、この「グローバル変数」的な用途として最適だと私が考えるのは、Redisです。

パンカクでも、ソーシャルゲームプラットフォームのPankiaでユニークユーザの集計など補助的な統計情報にRedisを活用しており、その価値を実感しています。

高性能というのはもちろんですが、それ以上に私が感銘を受けているのは、その設計思想のセンスにあります。

プログラミング言語で変数を使ってちょっとしたデータ構造をつくるとき、もっともよく使われるのが「配列」「ハッシュ」「セット」などだと思いますが、こうした基本的なデータ型をネイティブで用意し、これらのデータに対する様々な操作をO(1)で、かつアトミックに行えるというのは、まさに「グローバル変数」を使っている、という感覚に限りなく近いものがあります。

「グローバル変数Xの値を読み出し、1を足して書き戻す」という処理をA,Bの2つのスレッドで同時に行うと、0が2になるべきところ、1で終わってしまうことがあります。これを競合状態というのですが、アトミックな処理ではこれを「現在の値に1を足せ」という命令として送り、元の数字を読み出すところから足し算を終えるところまでがサーバ側で分割不可能(これ以上分割できない最小単位という「原子(Atom)」本来の意味からきています)な処理として扱われるので、競合状態が発生しません。

アトミックな数字のインクリメント程度なら多くのNoSQLがサポートしているのですが、Redisは原則すべての操作がアトミックで、たとえば配列に対する追加や取り出しなども一貫してアトミックに行えるので(あるデータベースから別のデータベースへ値を移動するという操作さえアトミックです)、プログラマがロックや競合状態のことを心配することなく気軽にアプリケーションを書けます。これはデータベースを「グローバル変数」的に気軽に使いたい、という観点からすると非常に大きなアドバンテージです。

また、Redisは「限界までシンプルな通信プロトコル」というMemcachedの良いところを継承しており、基本的にTCPの3-way handshake以上のオーバーヘッドやレイテンシは存在せず、実際telnetでの直接対話も可能です。サーバはシングルスレッドでepoll/kqueueを使っているので、CPU的なスケーラビリティはありませんが(CPUがボトルネックになることはないので実用上の問題にはなりません)、同時接続数などI/O面でのスケーラビリティは事実上無限です。実際、2万クライアント以上から同時アクセスしたベンチマークでもほぼ性能劣化なく、90,000qps近くの性能が出たという報告もあります(参考)。

永続化には課題がいっぱい

こんな、いいことずくめのように思えるRedisですが、弱点もあります。それも、用途によってはかなり致命的な弱点です。

それは、まさに「永続化」の部分です。

Redisは、デフォルトで「60秒間に10000回、あるいは5分間に10回、あるいは15分間に1回の書き込みがあればセーブする」というルールで、メモリ状態のデータベースファイルへの書き出しを行ないます。

つまり、データを1件だけ更新したとき、それが実際にデータファイルに書かれるまでに15分の遅延が発生し、その15分以内にサーバがダウンすれば、そのデータは失われることになります。

また「セーブする」とは、まず子プロセスをforkし(つまりバックグラウンドで)、メモリ上にあるすべてのデータをシリアライズして一時ファイルに書き出し、完了したらリネームするという一連の処理のことで、別名「スナップショット」と呼ばれます。Copy on writeが使える環境ならfork自体は瞬時に終わりますが、物理メモリの大半を使うような規模ではメモリのovercommit設定が必須になり、また、巨大なファイル全体を書き出すという処理が割と頻繁に発生することになります。

データサイズが十分に小さい場合なら、これはかなりよくできた仕組みだといえます。ややこしいデフォルトの挙動も、Redisの仕組みをよく理解した上でなら、かなり考えぬかれた設計だと感じられるでしょう。

しかし、ややこしい挙動は、やはり障害分析や運用計画の立案を困難にします。わかりやすさを優先するなら、たとえば「1秒に1件でも更新があればセーブする」というルールにしておけば、失われるデータは1秒以内のものだけであることが保証されているので、運用計画の立てやすさには格段の違いがあります。実際、Redisでも「save 1 1」という設定をすることは可能ですが、これは全体のデータサイズが大きくなると現実的でないことが、上で説明したセーブの手順をみれば理解できるでしょう。

この問題を解決するためにRedis 1.1から新しく追加されたのが、Append Only File (AOF)という記録方式です。これは従来の用語でいうところのwrite-ahead logging (WAL)のようなもので、更新リクエストを受け取るとまずそのリクエストをログに記録しておき、実際の処理をあとで行うことで、(クラッシュ後などの)再起動時には最後のスナップショット時点からこのログをリプレイすることでデータベースを再構築できるようになります。

しかしAOFにはいくつかの明白な欠点があります。

一つめは、これで伝統的なSQLデータベースと同じ性能のボトルネックに片足を突っ込んでしまうこと。AOFを書く処理自体がディスクネックなので、シーケンシャル書き込みとはいえ毎回fsyncすると信じられないぐらい性能が落ちます。「クラウド全盛の昨今、バッテリーバックアップつきのRAIDコントローラを必須とするような構成上の制約を要求する設計は良くない(参考)」というMongoDBの主張に、私は全面的に賛成します(これが、冒頭に述べたMongoDB擁護の要点です)。そこで、Redis 2.0では「1秒ごとにAOFをfsync」がデフォルトに改められています。これはMySQLでいうところのinnodb_flush_log_at_trx_commit=2に相当します。このデフォルトの選択も、モダンなデータベースとしてはバランスが良い選択といえるでしょう。

しかしAOFの欠点はこれだけにとどまりません。更新の激しいアクセスカウンターのような用途だと、1,2,3,....と同じ数バイトだけが書き変わっていくだけにもかかわらず、ログファイルはバカバカしいほど膨大になります。また、どんどん大きくなっていくログファイルを刈りこむBGREWRITEAOFというコマンドの発行タイミングはユーザ任せとなっており、これまた運用計画が立てづらい。MySQLなら、WALのサイズは設定で固定(innodb_log_file_sizeとinnodb_log_files_in_group)でき、そのサイズを超えそうになったら自動的にバッファプールの内容をデータベースファイルにフラッシュしてくれるのですが、Redisにはそういった機構もないので、突発的なアクセス急増でdisk full、のような悪夢もありえます。しかも、AOFを書いてる最中にクラッシュした場合にはAOF自体が部分的に壊れてることもあるので、それの修復も必要です。

おそらく我々が本当に求めているものは「データベース全体のスナップショット(更新頻度の増加に強いがデータサイズの巨大化に弱い)」と「あらゆる更新を逐一記録する追記処理(データサイズの巨大化に強いが更新頻度の増加に弱い)」の中間的なものでしょう。おそらく、「1秒に1度、更新のあった部分だけをデータベースファイルに書き戻す」ぐらいの堅牢性がちょうどいい。

Redisはまだ勢いよく発展している途上にあるので、これらの問題もいずれ解決されていくでしょうけど、今採用するなら、私のおすすめは「なるべく大きくなりにくいデータ(たとえば1GB程度まで)だけを扱い、メモリに余裕をもたせ、AOFは使わずにデフォルトの永続化ルール(スナップショット)を使う」です。これなら、Redisのおいしい部分だけを味わえるでしょう。

最大の課題は実メモリサイズを超えたデータの取り扱い

さて、ここにいたって、ようやく表題の「1:10問題」(one-ten problemと勝手に命名)です。

これは、「1GBの物理メモリで10GBのデータを扱うこと」を意味しています。

実のところ、現存するNoSQLデータベースは、(私の知る限りでは)例外なく、データの全部が物理メモリ上に乗っかった状態で稼働することを前提としており、実メモリを超えたデータをどうやって扱うのか?という質問をすると、ほぼ例外なく「クラスタリング」「シャーディング」「スケールアウト」という回答がかえってきます。つまり、「メモリが足りなくなったら、メモリを足せばいいじゃないの」というわけです。これは言い逃れとしてはよくできていますが、現実には「できません」といってるのと同じです。

現実世界のアプリでは、大抵の場合に「参照の局所性」が存在し、またハードディスクよりもメモリのほうが容量あたりの単価は圧倒的に高価なので、そのミックスには何らかのトレードオフやスイートスポットが存在することは明らかです。SSDがこの問題を根本的に解決してしまう可能性も皆無ではありませんが、廉価なVPSなどのクラウドで利用できるようになるまでには、数年以上のタイムラグがあることでしょう。性能がマシン台数に比例してしかスケールしないなら、それはただのブルートフォースです(しかも平均故障間隔はどんどん悪化します)。コンピュータサイエンスの世界では、アルゴリズム的なアプローチで、O(N)ではなくO(logN)やもっと低いオーダーでソフトウェア的に問題を解けてこそブレイクスルーなのです。

Redisでは、実メモリを超える巨大なサイズのデータを扱うために、2.0からVirtual Memory (VM)という仕組みを導入しました。これは、swapファイルを使って参照頻度の低いページをディスクに追い出す仕組みです。ただし、ディスクに追い出せるのはバリューのほうだけで、キーのほうはオンメモリでないといけないという制約があります。(ちなみに、このほかの場面でも、なるべくキーの数を少なく、キー長も短くしておくのはRedisのチューニングにおける大原則です。ハッシュ型のデータを使うなら、zipmapを検討しましょう)

ここをOSのスワップまかせにしてない理由としては、作者のブログによれば、OSのページはチャンクが大きすぎて(4KB)、断片化の激しいRedisのメモリ空間では「たった10%のよく使われるデータのために4KBのページ全体をスワップアウトできない」ということがよく起こるので、もっと圧倒的に小さい単位をページとして扱うようにしている、ということのようです。(これについては、隣接するデータを物理的にも隣接するようにがんばって配置してページサイズは大きくしたほうがメモリのバースト転送の恩恵で全体最適になるような気もするのですが)

まぁそのことの是非自体はさておくとしても、このVMの仕組みだと、既存のスナップショットやAOFに加えて、さらにディスクI/Oを必要とするコンポーネントが増えることになります。しかも、VMはあくまでswapであり、再起動したら消えてしまうもの、つまり永続化のための処理ではないので、平たくいうと「無駄なディスクI/O」ということになります。せっかくディスクに書いてるのに!

ディスクI/Oは、いまのコンピューティングリソースの中でもっとも遅く、したがってもっとも気を使わなければいけない希少な資源であり、無駄に消費できる余裕はないので、個人的にこのVMの仕組みは好きになれません。

ここでもやはり温故知新、参考になるのはInnoDBのバッファプールの仕組みでしょう。swapの仕組みと永続化の仕組みを統合し、同じ一回のディスクI/Oで「メモリ不足の緩和」「データの保全」という二つの目的を同時に達成する。InnoDBでは、1GBのメモリで10GBのデータベースを扱うような構成は、ごく一般的です。もちろん、最高の性能を目指すなら、すべてオンメモリにあったほうがよいのは間違いありませんが、実メモリをこえたら極端に性能が落ちてしまうNoSQL系のデータベースに比べ、InnoDBの場合には(参照の局所性があれば)性能の劣化がゆるやかです。コストパフォーマンス面での優位性はいうまでもありません。さらにMySQL 5.1のInnoDB Pluginでは、少々のCPUオーバーヘッドと引き換えにバッファプール上でのページを圧縮しておけるBarracudaというフォーマットも採用されており、メモリ対データサイズ比の改善はさらに先をいっています。

運用計画において、もっとも嫌われるのは、ある閾値をこえたら急激に遅くなるようなコンポーネントでしょう。ちょっとした予想外のアクセス集中があっても、とりあえず許容可能でgracefulな性能のデグレでやりすごしつつ、別の方法を検討する時間をかせぐ、というのが計画的なスケーラビリティ設計というものでしょう。それができるマージンがほとんどないというのも、NoSQLの採用が進まない理由のひとつ、という気がします。

実際、PankiaでRedisを使う上でも、この問題があるために、リーダーボード(Sorted Set型がピッタリなのに)などのすごい勢いで巨大化していくデータに適用できる見通しが立ちません。

InnoDBのバッファプールのような仕組みは、OSの仮想記憶の再発明になりかねない巨大なコンポーネントであり、実装の難易度も高く、したがって開発の時間もかかるため、優先順位が下がってしまうのも仕方がないでしょう。しかし、この「1:10問題」こそ、NoSQLが一皮むけるための最後の試金石になるだろう、と個人的には考えています。実績のあるRDBMSが、ほぼ例外なく自前の共有バッファとページングの仕組みを持っているのには、歴史的な事情とかではない、ちゃんとした合理的な理由があるのです。そして、細かく制御可能なデータファイルへの書き出し手段があれば、先のAOFのような「無限に肥大化するWAL」問題も、InnoDBのように自動でうまく解決できるようになるでしょう。

RedisのVMの仕組みは、かなり近いところまで来ているようにも思えますし、MongoDBもv1.8でsingle server durabilityをサポートするとの発表がありました。そして、永続化の堅牢さという意味では一歩先をいくKyoto Cabinet / Kyoto Tycoonが、この「1:10問題」にどのようにアプローチしてくるのか。

ますますNoSQLの世界が楽しみになってきました。

Elva / 表白

※このエントリは CNET Japan ブロガーにより投稿されたものです。朝日インタラクティブ および CNET Japan 編集部の見解・意向を示すものではありません。
運営事務局に問題を報告

最新ブログエントリー