昨年、私はSquareで色々なデータベースの面倒を見る中で、こんなことをしてきた。
- データベースのパフォーマンス問題の調査と解決
- 新しいアプリケーションの、データモデルのデザインやシャーディング戦略の立案
- データベース新製品の評価と運用化
最初は必要に迫られてやっていた私は、それから徐々に、データベースの虜になっていった。データベースを学ぶことは、コンピュータサイエンスのあらゆるトピックを横断的に扱うことであり、その理論や実装は、洗練され、かつチャレンジングでもある。
しかし、こういった感覚は他のみんなにも一般的なわけではないということも分かってきた。私の同僚や友人の多くにとっては、データベースは魔法のブラックボックスであり、踏み込むには複雑で恐ろしい世界のようだ。私はそんな状況を変えたかった。
データベースについて議論するとき、分散システムに関しても無視することはできないだろう。多くのモダンなデータベースは分散されており、それは暗黙的に始めからそうなっていること(分散クラスタデータベース)もあるし、外部の仕組みを使うこと(アプリケーションレベルのシャーディングで、複数のDBに接続している1つのアプリケーション)もある。
この記事は、データベースと分散システムへの私からの愛の告白であり、我々のような日常的にデータベースを扱っているアプリケーション開発者に対しても書いている。我々は、サーバサイドアプリケーションのほとんどをJavaやPython、Rubyで作っている。ここでは、以下の項目について取り上げたい。
- 異なるデータベースの比較と検証
- データベースをどのように理解し、どのように活用するか
- データベースがどのように動作するのか、高いレベルでの理解
まず最初に、データベースとは何か?
ここでは、将来の読み出しに備えてデータを受け入れ、それを永続化するあらゆるソフトウェアのことを、データベースと定義しよう。これは、伝統的なRDBMSもNoSQLデータベースも、Apache ZookeeperやKafkaのようなシステムも含んでいる。
CAP定理
CAP定理。これは、チューリングの停止性問題やP≠NP問題から続く、私の大好きな解決不可能問題だ。CAP定理は、あらゆる分散システムはCP(一貫性と分断耐性)またはAP(可用性と分断耐性)、あるいはそれらの範囲のどこかの、いずれかしか満たさないということを表している。その結果、一貫性と可用性の間には興味深いトレードオフがあらわになってくる。
CAP定理については、いくつか重要な勘違いがある。
- 伝統的な「3つのうちのどれか2つ」という議論は意味がない。そもそも分断耐性は、「分断が起こっている間に実行された処理の振る舞いは、定義されていない」という意味なので、分断耐性を放棄することはできない。そういった場合、データベースは本当の意味では一貫性がない状態だからだ([1]を参照)。
- デフォルトでは、CAP定理の限界が見える所まで行きはしない。世の中には、一貫性もないし、可用性もないし、分断耐性もないデータベースもたくさんある。CAP定理の限界に達するには、注意深いデザインと実装が必要になる。
分散システム
上に書いたように、モダンなデータベースは全て何らかの形で分散されている。その理由は主に以下の2つだ。
- 複数台のマシンにスケールするため。複数のノードにデータを保存し、処理する。
- 可用性向上のため。データベースがSPOFにならないようにする。
この2つのゴールは非常に近いところになる。通常、マシンの数を増やしてスケールして行くと、マシンの故障に遭遇する確率が上がるので、可用性にはネガティブな影響を与えてしまう。一方で、スケーラビリティを実現するには、高可用性が必須条件であるとも言える。
正確さと効率
正確さと効率はどちらも重要で、分散データベースではこの2つも密接に関連している。
正確さはどんなソフトウェアでも重要だが、特にデータベースにおいては根本を成すことだ。それは、(1) データベースはデータを永続化するので、不正なデータは再起動後も残ってしまう。(2) データベースはソフトウェアスタックにおいて信頼の根幹となるものと見なされている。という2点による。
データベースが正しい状態にあるとは、どういうことだろうか?分散データベースの多くには、特有の一貫性に関する動作が小さな文字で書かれているものだ。ここにトレードオフを考える余地がある。一般論で言えば、厳格な一貫性を取れば、効率性と可用性にかけるコストの点で、アプリケーションのコードは簡単になる。
理論はさておき、正確さに対する実装と操作の難しさはどうしても考えなければならないことだ。分散システムというものは、本質的に複雑なものだ。Paxosのようなアルゴリズムは、理解するのも、正しく実装するのも難しい事で有名だ。システムが複雑になればなるほど、分かりにくい障害のパターンが現実のものになってくる。RedisやElasticSearchのようなシステムは、それらの分散システムの自明でないデザインに苦しめられている。
これらのトレードオフの他に、上で見たような難しさ故に、効率は重要である。低レベルのプログラミングを知る程、いかに多くの物理的な操作が行われているかを私は知った。多くの場合、効率的であることはすなわち複雑さを減らし、システム全体をシンプルにしてくれる。分散システムは、低レベルのプログラミングよりも難しいというその事実によって、私はさらに効率を追い求めようとするようになった。同じ負荷に対してなら、より少ないマシンで済むデータベースを私は喜んで選ぶだろう。
もう1つ最後に言っておきたいのは、マシン間の協調動作の計算実行時のオーバーヘッドやレイテンシは、非常に大きいものだという点だ。つまり結論としては、部品が少なければ少ない程よいという事になる。
コードから更なるパフォーマンスと効率をひねり出すには、以下のような低レベルの抽象化層にまで理解を広げる必要がある。
- メモリアロケータやガベージコレクタ [2]
- ファイルシステムスケジューラやIOデバイスの特性
- カーネル設定
- システムコールの実装の詳細(fork, execv, malloc)
これらの間で起きるあらゆる不調和は、パフォーマンスの劣化を引き起こす可能性になる。カーネルハッカーにまではならなくてもよいが、これらのコンポーネントがどう関連しているか、高いレベルでの概要は把握しておく必要がある。
アプリケーションを強力にする
プログラミング言語にもそれぞれの長短がある様に、データベースもそれぞれがユニークな特徴がある。それらを完全に理解する事が重要だ。そうすれば、エラーが発生しがちな複雑な処理のほとんどをデータベースに任せて、効率的で洗練されたアプリケーションの実装ができるだろう。
伝統的なRDBMSは、厳しいパフォーマンス上あるいは可用性の制約がないのであれば、妥当な選択肢だろう。ACID特性は非常に強力だし、ツールも充実している。RDBMSのシャーディングは骨が折れるが、既に広く理解されている作業だ。MySQLとPostgreSQLが、その選択肢として一般的だろう。
全文検索エンジンを使えば、高度なインデクシングと検索の機能が手に入る。これらのシステムがどのように統合されるかという結果整合的動作は、検索というものがそもそも曖昧なところを持つ処理であるが故に、あまり問題にならないだろう。Luceneとその派生(SolrやElasticSearch)がよく使われている。
メッセージキューやイベント処理システムは、正確かつ効率的な実装が難しいタイプのコードを減らすのに役立つ。KafkaやStorm、Spark SQL、RabbitMQ、Redisなどが人気だ。
リージョンをまたいだレプリケーションが可能なデータベースは、リージョン間のフェイルオーバが可能だし、高可用性を簡単に実現できる。まだあまり多くのオープンソースソフトウェアの選択肢はないが、Cassandraが最も成熟しているだろう。
コンセンサスアルゴリズム、リーダー選出、分散ロックといった仕組みは、実装もテストも難しい。自分で作るのはやめて、Zookeeperやetcd、ライブラリとしてのRaftを使おう。
細かい話に入っていこう。データベースは本質的にleaky abstraction(漏れのある抽象化)だ。データベースは後ろに複雑さを隠した状態でうまく動いてくれる。しかし、制限を無視してしまうと、窮鼠猫を噛む事態に陥る事もある。重要なのは以下の事だ。
- データベースがどのように正確さを保証するのかを理解する事。処理の失敗とはどういう意味か[3]?どういった処理が一貫性があり、どういった処理が一貫性がないというのか?
- データがどのように永続化され、どのように取り出されるのかを理解する事。どんな処理が効率的で、どんな処理が非効率なのか?クエリプランナはあるのか、処理ごとに詳しい統計が取られているか?
- シャーディングとクラスタのトポロジ。クラスタにどのようにデータが分散されているのかを理解する事。そのシャーディング戦略では、データは均一に分散されているのか、あるいはホットスポットがあるのか?
- データモデリングパターンとアンチパターン
運用上の問題
一度ソフトウェアスタックの一部となってしまえば、データベースは24時間365日休む事なく動作し続ける事になる。そのため、それ特有の運用上の問題が出てくる。
データベースを運用する事は、大海原の真ん中でヨットを操るのに似ている。問題に遭遇したら、嵐のど真ん中にいようが、データベースを沈没させる事なく修理しなければならない。そのため、データベースは以下の機能を持っていなければならない。
- システムの内部を調べ、監視する方法
- システムを維持し、管理する手がかり
- レプリケーション、バックアップ、リストアの方法。データを失う事は恐ろしく重大な問題だ。マシンはいつかは壊れる。データベースはステートフルであり、コードをデプロイし直すだけでよいものではない。
全部、動作中にやることだ。正直言って、どんなデータベースもその意味では欠点を持っていると言える。完全に動作するようになるまでにいじくり回した設定を適用するのは、難しい挑戦になるだろう。多くの操作は、データベース全体にわたるミューテックスあるいは追加のシステムリソース、さらには再起動が必要になってしまう。例としては以下のようなものがある。
- MySQLの5.6より前のバージョンでは、カラムの追加時はフルテーブルロックが必須だった。ただ、pt-online-schema-changeのような素晴らしいハックツールのおかげで、この問題は軽減されてはいる。MySQLは最新バージョンではオンラインのスキーママイグレーションがサポートされている。
- Cassandraは簡単にノードの追加、削減、修復ができる。ただし、これらの操作時はシステムの負荷が上がってしまい、より大きなキャパシティが必要だ。
もう一つ重要なのは、データベースは簡単にはリプレイスできないという事だ。同じデータベース内でデータをマイグレーションする作業ですら、シンプルには行かない。他のデータベースにマイグレートするのはもっと難しい[4]。可能であればの話だが。アプリケーション側のコードはもっとあっさりと公開できるし、簡単に切り戻し可能だ。データはコードよりも長く生き残る。データスキーマや保存されたデータそのものは、普通、複数のアプリケーション間で共有される。従って、最初に選択するデータベースシステムとそれに対応したデータモデルは、非常に重要になる。
最後に、データベースはそのうち壊れるだろう。どのプラットフォームやどのIaasを使っていようと、障害は避けられない。
- データを壊したり消したりしてしまうアプリケーションのバグ[5]
- データベースの安全性や一貫性保証の仕組みの勘違いから、書き込みが失われる
- データモデルとデータベースのミスマッチ。例えば、1つのシャードに収まりきらないデータセットにしてしまう、結果整合性データベースでCAS操作(コンペア・アンド・スワップ)ができると思い込む、など
- 運用上の障害。マシンのクラッシュ、HDDの故障、OSアップグレードなど
- ネットワーク分断[6]
- Thundering herd(巨大な群れ)。あるシステムで障害が起きると、他のシステムにも波及する
- 最悪なのが「急なスローダウン」「レイテンシのランダムな悪化」「1日1回の散発的なエラー」「このレコードがないといけないのにどこかへ行ってしまった」
これらに対して、これさえあればよいという解決策はない。データベースを運用していくテクニックは、そのまま高いSLAのシステムを運用していく事に他ならない。しかし、いくつかのヒントはある。
- アプリケーション開発者は、制限と障害時の挙動を理解する事
- 弾力性のあるアプリケーションを書く事。複数のデータセンタへのデプロイや自動フェイルオーバを実装する
- 様々な障害シナリオを知りそのリカバリ方法を理解したオペレーションエンジニアチームをコンポーネントごとに作る(サイトリライアビリティエンジニアやDBA)
付け加えると、Squareでは、いけてるオンラインデータストレージ(ODS)チームという、これらの問題を取り除くチームがいる。
基本的な構成要素
データベースが提供する抽象化は、本当に魔法のようだ。データの取り込み、検索、レプリケーション、フェイルオーバと言った機能が全部同じパッケージになっているのだ。そういった機能に慣れてしまっているかもしれないが、基本的な構成要素は学んでおく必要がある。一般的なパターンや、あらゆるデータベースで共通に存在するコンポーネントといったものだ。
まず、データの取り出しは以下のように分けられる。
- キーと値の検索(ハッシュテーブル)
- 範囲検索(ツリーやLSM)
- ファイルオフセット検索(Kafka、HDFS)
結局のところ、SQLやインデックス、結合などといった可愛いお飾り的なものは、コンピュータは理解できない。高レベルな操作は、マシンが実行可能なものに変換する必要がある。
永続化のためのデータ構造としては、Bツリーやハッシュテーブル、ログ構造マージツリー(LSMツリー)が広く使われている。特殊な検索(例えば地理空間情報の検索)を必要とする場合意外は、多くの場合このどれかを使ってデータが保存されている。LSMツリーは、モダンな選択肢としてよく使われており、素晴らしい書き込みとそこそこの読み出し性能を持つ事から、BigTableやHBase、Cassandra、LevelDB、RocksDBに採用されている。
その他にも、色々なシステムで使われている一般的なパターンやアルゴリズムがある。Paxos、Raft、コンシステントハッシング、クォーラム読み込み・書き込み、ハッシュ(マークル)ツリー、ベクタクロックなどは、基本的な構成要素になっている。
まとめ
この記事では、各トピックについてシンプルだが高レベルな概要を述べたつもりだ。ここではカバーし切れなかったたくさんのトピックがある。ワークフロー(OLAP、OLTP、バッチ処理など)ごとの最適化法や、データベースのUX(クエリ言語、転送プロトコル、クライアントライブラリなど)なども、同じくらい重要な事だ。逐次一貫性、read-your-own-writes、at-least-once deliveryなどの様々な一貫性の動作の関係性も、非常に面白い話題だろう。
データベースを考えるにあたって最高なのは、それが非常に成熟した抽象化の仕組みである事だ。何でもやってくれて、アプリケーション開発者は何も考えなくても簡単にデータを保存したり取り出したりできる。これは間違いなく祝福すべき事ではあるが、ひとたび足を踏み入れてみれば、その高度な技術は、確実に学ぶ価値のあるものなのである。
私は、もっと多くの人がデータベースに魅了される事を願っている。そして、そんな人達のために、この記事が役立ちますように。
参考文献と脚注
- [1] 分断耐性を犠牲にできない http://codahale.com/you-cant-sacrifice-partition-tolerance/ Aphy'rs Japsen氏の記事も入門にはちょうどいい http://aphyr.com/tags/Jepsen
- [2] モダンなデータベースは、ファイルシステムアクセスを高速化させるために、OSのファイルシステムキャッシュをかなり利用している。使用していないメモリは自動的にキャッシュとして使われる。そういったシステムにおける推奨構成である、メモリの90%をどこにも割り当てない設定は、慣れてない人から見ると不思議に映るだろう。
- [3] Dynamoライクなクォーラムベースのシステムでよくある落とし穴は、書き込みが失敗しても何の情報もない事だろう。内部のレプリカに対して時間内に書き込みが終わらなければ、書き込みは失敗したとクライアントに通知される。そのため、失敗した書き込みというのは実はかなりの確率で成功している。最悪の場合、last-write-win戦略やシステムクロックがずれている時には、未来の書き込みが上書きされてしまう事も起こり得る。
- [4] データベース間のマイグレーションでは、複数のデータベースから同時に読み出しと書き込みを行う。データのオーナーシップ(いわゆる「真実を語る者」)がどこにあるのかがはっきりしなくなり、新旧システム間でデータが一致しなくなってしまう可能性がある。
- [5] 多くのスタートアップのインフラにおいては、アプリケーションのバグが、信頼性や可用性を損なう最も強力な敵だろう。スケーリングやパフォーマンスの問題は予想がつくし、それほど多くのサーバを持っていないうちは、ハードウェア障害はそれほど多くない。一方で、新しいコードは毎日のようにデプロイされる。
- [6] "The Network is Reliable"から。ネットワーク分断は思っている以上によくある事だ。そもそも、高レイテンシ、ネットワーク分断、GCによる停止、マシンの障害といった事象は、全てコネクションのスローダウンという形で現れるので、切り分けのしようがない。こういった問題はElasticSearchではよくある話だ。ノードが長いGCによって停止してしまうと、クラスタ全体がダウンしたと判断され、データを積極的にリシャッフルしようとし、問題が連鎖してしまう。
複数のデータベース
あるシステムが複数のデータベースとやり取りし始めると、そのシステムは結果整合になる。二相コミット(2PC)を実装したとしても、全く同時に複数のデータベースを更新するのは不可能だ。これは、「組み合わせたアトミックな操作は、アトミックではない」というのと同じ事だ。
データの削除
どんな分散システムでも、データの削除は難しく危険だ。データはデータベース自身やアプリケーションによってあちこちに複製されている。きちんとした調整ができないと、削除されたデータがレプリケーションで書き戻される事もあり得る。これに対するよくある戦略としては、削除を表すためにtombstoneレコードを書き込むことだ。しかしこれにも問題がある。
- tombstone自体もディスク容量を必要とする。ディスク容量を空けるには、tombstoneが有効期限切れである必要がある。tombstoneが完全にレプリケーションされる前に有効期限切れになって削除されてしまうと、削除されたレコードもレプリケーションされ復活してしまう。
- 将来書き込まれるデータを古いtombstoneで消せてしまう。これはシャレでdoomstone(訳注、スカイランダーズという人気ゲームのキャラクター)と呼ばれている。その用語自体は笑えるが、これは現実の問題だ。