技術概要

この概要では、CouchDB の主なモデルとコンポーネントについて詳しく紹介し、それぞれがどのように動作するのか、また互いにどのように連携するのかについて説明します。

ドキュメント・ストレージ

CouchDB サーバーに収められている名前の付いたデータベースには、ドキュメントが保存されます。各ドキュメントにはデータベース内で一意の名前が付けられ、データベースのドキュメントの読み取りや更新(追加、編集、削除)ができるよう、CouchDB は RESTful HTTP API を提供しています (RESTful については/ wikipedia を参照)。

ドキュメントは CouchDB の基本的なデータ単位で、任意の数のフィールドと添付ファイルから構成されます。また、ドキュメントには、データベース・システムが管理するメタデータも含まれています。ドキュメントのフィールドには一意の名前が付けられ、さまざまなタイプの値(テキスト、数値、ブール値、リストなど)が収められます。テキストのサイズや要素数に制限はありません。

CouchDB のドキュメント更新モデルは、ロックのないオプティミスティックなものです。ドキュメントの編集は、クライアント・アプリケーションがドキュメントを読み込んで変更を加え、その結果をデータベースに保存するという形で行われます。同じドキュメントを編集しているほかのクライアントが先に変更内容を保存した場合、あとからドキュメントを保存しようとしたクライアントは、保存時に編集衝突のエラーが発生したことを知らされます。この場合、更新時の衝突を解決するために、クライアントはドキュメントの最新のバージョンを開き、再度編集を行って、もう一度更新を試みることができます。

ドキュメントの更新(追加、編集、削除)はオール・オア・ナッシング、つまり完全に成功するか、完全に失敗するかのどちらかになります。部分的に保存されたドキュメントや部分的に編集されたドキュメントがデータベースに含まれることはありません。

ACID 特性

CouchDB のファイル・レイアウトとコミットメント・システムは、ACID 特性のすべて、すなわちアトミック性、一貫性、独立性、永続性を備えており (ACID については/ wikipedia を参照)、データベース・ファイルが常に一貫性を保つことを保証します。これはいわゆる「クラッシュオンリー」設計で、シャットダウン・プロセスを経ることなく、単純に CouchDB サーバーを終了させることができるようになっています。

ドキュメントの更新(追加、編集、削除)はシリアライズされますが、例外として BLOB では同時書き込みが行われます。データベースを読み取るアプリケーションは、決してロックアウトされることがなく、書き込みやほかのクライアントの読み込みを待つ必要はありません。任意の数のクライアントが、同時更新によってロックアウトされたり中断されたりすることなく、ドキュメントを読み取ることができ、たとえ読み取りの対象が同一のドキュメントであっても問題ありません。CouchDB の読み取り操作では、MVCC (Multi-Version Concurrency Control) モデルを使用しており (MVCC については / wikipedia を参照)、各クライアントには、読み取り操作の始めから終わりまで、データベースの一貫したスナップショットが見えるようになっています。

ドキュメントに対しては、ドキュメントの名前 (DocID) とシーケンス ID を使って B ツリーによるインデックスが作成されます。データベース・インスタンスに対する更新では、各更新ごとに新しい続き番号が生成されます。シーケンス ID は、あとでデータベース内の変更内容をインクリメンタルに探す場合に使われます。B ツリー・インデックスは、ドキュメントが保存されたり削除されたりしたときに、同時に更新されます。インデックスの更新は、常にファイルの末尾で行われます(追記のみの更新)。

ほとんどのデータベース・システムでは、数多くのテーブルと行にデータを分割して格納しますが、ドキュメントの場合には、データがすでにストレージに適した形でパッケージ化されているという利点があります。ドキュメントがディスクにコミットされると、ドキュメントのフィールドとメタデータがまとめてバッファに入れられますが、この処理はドキュメント単位で一つずつ行われていきます(これが、あとでビューを効率よく作成するのに役立ちます)。

CouchDB のドキュメントが更新されると、すべてのデータおよび関連するインデックスがディスクにフラッシュされ、トランザクションのコミットによってデータベースは常に一貫した状態に保たれます。コミットは次の 2 ステップで行われます。

  1. ドキュメントのデータおよび関連するインデックスがすべて同時にディスクにフラッシュされます。
  2. 更新されたデータベース・ヘッダは、ファイル先頭の 4KB を構成することになる 2 つの連続する同一チャンクとして書き込まれ、これが同時にディスクにフラッシュされます。

ステップ 1 の最中に OS がクラッシュしたり停電が発生したりした場合、部分的にフラッシュされた更新は再起動時に無視されます。ステップ 2 の最中(ヘッダのコミット中)に同様のクラッシュが発生した場合、前の同一のヘッダのうち残っている方のコピーが保持され、これまでにコミットされたすべてのデータのコヒーレンシが保証されます。ヘッダ領域を除けば、クラッシュまたは停電後に一貫性のチェックやフィクスアップが必要になることはありません。

圧縮

消費スペースは、適当な時に圧縮を行うことで回復できます。圧縮プロセスでは、あらかじめ決められたスケジュールに従って、またはデータベース・ファイルが一定の消費スペースを超えた時点で、すべてのアクティブなデータを新しいファイルに複製し、古いファイルを破棄します。この間、データベースは常にオンライン状態を保っており、更新や読み取りもすべて問題なく行うことができます。データがすべてコピーされ、ユーザーもすべて新しいファイルに移行した後に、古いファイルは削除されます。

ビュー

ACID 特性はストレージと更新だけに関係するものですが、データを意味のある有益な形で表示する手段も必要です。データを適切に分解してテーブルに格納しなければならない SQL データベースとは異なり、CouchDB のデータは半構造化されたドキュメントに格納されています。CouchDB のドキュメントには柔軟性があり、各ドキュメントは独自に暗黙の構造を持つことができるので、テーブルのスキーマとそこに含まれるデータを双方向にレプリケートするという困難な問題は軽減され、レプリケーション処理に伴う落とし穴も避けることができます。

ただし、単なる出来のよいファイル・サーバーとして使うならともかく、データのストレージと共有を目的とした単純なドキュメント・モデルは、実際に使えるアプリケーションを構築するための基盤としてはあまりに単純過ぎて、ユーザーの要望や期待に十分応えることはできません。データをさまざまな方法で料理し、結果を表示できるようにする必要があります。必要なのは、分割してテーブルに格納されているわけではないデータを対象に、フィルタリングやグループ化、レポート作成を可能にする手段です。

ビュー・モデル

構造化されていないデータと半構造化されたデータに構造を持たせるという問題に対処するため、CouchDB にはビュー・モデルというものが組み込まれています。ビューは、データベース内のドキュメントを集約したり、レポートを作成したりするための手段です。ビューは、データベース・ドキュメントを対象とした集約、結合、およびレポート作成時に、オンデマンドで生成されます。ビューは動的に生成され、元のドキュメントは変更されないので、必要なら同じデータを対象にいくつでもビューを作成することができます。

ビューの定義は完全に仮想的なもので、現在のデータベース・インスタンスのドキュメントを表示するだけなので、ビューとビューが表示するデータは分離されており、ビューもレプリケーションの対象にすることができます。CouchDB のビューは、特別なデザイン・ドキュメントの中で定義されていて、通常のドキュメント同様、データベース・インスタンス間でレプリケートできます。このため、CouchDB ではデータをレプリケートするだけでなく、アプリケーションの設計全体をレプリケートできます。

JavaScript によるビュー関数

ビューは、MapReduce システムの Map 部分として機能する JavaScript 関数で定義します (MapReduceシステムについては/ wikipedia を参照)。ビュー関数は CouchDB ドキュメントを引数に取り、ビューで表示するデータを選び出すのに必要な処理を行います。単一のドキュメントに基づくビューに複数の行を追加することもできますし、まったく行を付け加えないことも可能です。

ビュー・インデックス

ビューはデータベースの実際のドキュメント・コンテンツを動的に表現したもので、CouchDB では有益なデータのビューを簡単に作成できます。ただし数十万、数百万といった数のドキュメントから構成されるデータベースのビューを作成する作業は、時間もかかればリソースも消費するので、毎回ゼロからそうしたビューを作成することは望ましくありません。

ビューの問い合わせを高速化するため、ビュー・エンジンはビュー・インデックスを保持し、データベースに対する変更を反映させるために、インデックスをインクリメンタルに更新します。CouchDB のコアデザインは、ビューとそのインデックスを効率的かつインクリメンタルに作成する必要性を重視して、そのことを中心に最適化されています。

ビューとその関数は特別な「デザイン」ドキュメントの中で定義され、デザイン・ドキュメントには一意の名前を持つ関数を任意の数だけ含めることができます。ユーザーがビューを開いて、ビュー・インデックスが自動的に更新されると、同じデザイン・ドキュメント内のすべてのビューが一つのグループとして扱われ、これらのビュー・インデックスが作成されます。

ビュー・ビルダは、データベースのシーケンス ID を使って、ビュー・グループがデータベースと同じ最新の状態になっているかどうかを調べます。最新の状態になっていない場合、ビュー・エンジンは、最後のリフレッシュ以降に変更されたすべてのデータベース・ドキュメントを(パックされた順序に従って)調べます。ドキュメントはディスク・ファイル内に配置された順序に従って読み取られるため、ディスク・ヘッドのシーク頻度とそれに伴うペナルティを減らすことができます。

ビューがリフレッシュされている最中でも、ビューを読み取ったり、クエリーを実行したりすることができます。あるクライアントが時間をかけて大規模なビューの内容を読み取っている場合、別のクライアントは同時に同じビューを開くことができ、その場合でもリフレッシュは可能で、最初のクライアントの処理が妨げられることはありません。この点は、同時に読み取るクライアントの数がいくつでも変わりありません。これらのクライアントは、ほかのクライアントに対してインデックスがリフレッシュされている最中でも、ビューに対する読み取りとクエリーを実行することができ、ビューを読み取っているどのクライアントにも問題は発生しません。

ドキュメントを調べる際、もし前回の行の値が存在していれば、これらの値はビュー・インデックスから削除されます。ドキュメントがビューの関数で選択される場合、関数の処理結果は新しい行としてビューに挿入されます。

ビュー・インデックスの変更がディスクに書き込まれる場合、更新処理は常にファイルの末尾に対して行われ、コミット中のディスク・ヘッドのシークにかかる時間を減らすとともに、クラッシュや停電が発生した場合でもインデックスの破損を確実に防ぎます。ビュー・インデックスを更新している最中にクラッシュが発生した場合には、不完全なインデックスの更新処理は破棄され、前回コミットされた状態からインクリメンタルにインデックスが再作成されます。

セキュリティと有効性確認

ドキュメントの読み取りと更新ができるユーザーを制限するために、CouchDB ではシンプルな読み取りアクセスと更新の有効性確認のモデルを提供しており、これを拡張することで独自のセキュリティ・モデルを実装できるようにしています。

管理者アクセス

CouchDB のデータベース・インスタンスには管理者アカウントがあります。管理者アカウントでは、ほかの管理者アカウントを作成したり、デザイン・ドキュメントを更新したりできます。デザイン・ドキュメントは、ビューの定義とその他の特別な式に加え、通常のフィールドと BLOB を含む特別なドキュメントです。

読み取りアクセス

ドキュメントの内容を保護するため、CouchDB のドキュメントは読み取りユーザーのリストを保持することができます。これはドキュメントの読み取りを許可された読み取りユーザーの名前のリストで、省略可能です。読み取りユーザーのリストがある場合、保護されたドキュメントを見ることができるのは、リストに記述されたユーザーだけです。

ユーザーがデータベースにアクセスすると、そのユーザーのアカウント情報(名前とパスワード)を使って、そのユーザーに対応する読み取りユーザーの名前が動的に調べられます。ユーザーのアカウント情報は JavaScript 関数への入力となり、この関数が当該ユーザーに対応するユーザーの名前のリストを返します。ユーザーのアカウント情報が間違っている場合にはエラーを返します。

読み取りアクセスのリストによってドキュメントを保護する場合、ドキュメントを読み取ろうとするすべてのユーザーをリストに記述する必要があります。読み取りユーザーのリストは、ビューに対しても同様に適用されます。ユーザーが読み取りを許可されていないドキュメントはビューから動的に除外され、読み取りを許可されていないユーザーは、ドキュメントの行と抽出された情報を見ることができません。

更新の有効性確認

ドキュメントがディスクに書き込まれるときに、JavaScript の関数を使って、セキュリティとデータの有効性確認の両方について動的にドキュメントの有効性を確認することができます。ドキュメントが定められた有効性確認の基準をすべてパスすれば、更新を続行することができます。有効性確認が失敗すると、更新は中止され、ユーザー・クライアントはエラー・レスポンスを受け取ります。

有効性確認のための式には、ユーザーのアカウント情報と更新されたドキュメントの両方が入力として渡されるので、これらの情報を利用すれば、ユーザーにドキュメントを更新する権限があるかどうかを確認する独自のセキュリティ・モデルを実装することができます。

「作成者のみ」がドキュメントを更新できるというベーシックなモデルは、簡単に実装できます。この場合、ドキュメントを更新するユーザーが、そのドキュメントの “author” フィールドに記述されているかどうかを調べれば、ドキュメント更新の有効性を確認することができます。もっと動的なモデル、たとえば独立したユーザー・アカウント・プロファイルで権限の設定を調べるといったモデルなども実装できます。

更新の有効性確認は、直接更新する場合とレプリケートされた更新の場合の両方に適用され、共有された分散システムでのセキュリティとデータの有効性確認が保証されるようになっています。

分散更新とレプリケーション

CouchDB はピア・ベースの分散データベース・システムです。CouchDB では、接続が切断されている場合でも複数のユーザーやサーバーが同じ共有データにアクセスして更新することができ、変更内容はあとで双方向にレプリケートされます。

CouchDB のドキュメント・ストレージ、ビュー、およびセキュリティの各モデルは、互いに連携することで、本当の意味での双方向レプリケーションが効率的で信頼性の高いものになるように設計されています。ドキュメントと設計の両方をレプリケーションの対象とすることができるので、(アプリケーションの設計、ロジック、およびデータを含む)データベース・アプリケーション全体を、たとえばラップトップ・コンピュータにレプリケートしてオフラインで使用したり、接続が低速だったり切断されやすいためにデータの共有が困難なリモート・オフィスにあるサーバーにレプリケートしたりすることができます。

レプリケーション・プロセスはインクリメンタルです。データベース・レベルで言うと、レプリケーションは前回のレプリケーション以降に更新されたドキュメントだけを対象にします。そして、更新された各ドキュメントのうち、変更されているフィールドと BLOB だけをネットワーク経由でレプリケートします。レプリケーションがいずれかの段階で失敗した場合、たとえばネットワークの障害やクラッシュなどで失敗した場合、次のレプリケーションでは、前回処理できなかったドキュメントから処理を再開します。

部分的なレプリカを作成、保持することもできます。レプリケーションは JavaScript の関数でフィルタリングできるので、特定のドキュメントや指定した条件を満たすドキュメントだけをレプリケートすることができます。この機能を使えば、ユーザーは大規模な共有データベース・アプリケーションのサブセットを取り出してオフラインで使用することができます。その場合も、アプリケーションおよび取り出したサブセットのデータを対象に、通常どおり処理を行うことができます。

衝突

分散編集システムはどれもそうですが、衝突の検出と管理が重要な課題となります。CouchDB のストレージ・システムは、衝突を例外的な状態ではなく、通常の状態と捉えます。衝突の処理モデルはシンプルかつ「非破壊的」で、単一ドキュメントのセマンティクスを保持しながら、非集中型の衝突解決を可能にしています。

CouchDB では、互いに衝突するドキュメントが同時にデータベース内にいくつあってもよいことになっています。データベースの各インスタンスが、どのドキュメントが「承認」されたドキュメントか、どのドキュメントが衝突するドキュメントかを確定的に判断します。ビューに表示できるのは「承認」されたドキュメントだけですが、「却下」された衝突ドキュメントについても、データベースの圧縮の際に削除されるかパージされない限り、該当するドキュメントにアクセスすることができ、データベース内に保持されます。衝突文書も通常のドキュメントであることに変わりはないので、通常のドキュメント同様レプリケートされ、通常のドキュメントと同じセキュリティおよび有効性確認の規則に従います。

分散編集で衝突が発生すると、すべてのデータベース・レプリカにおいて、同一の「承認された」リビジョンが見えるようになり、どのレプリカでも衝突を解決することができます。衝突の解決は手動で行うこともできれば、データや衝突の性質に応じて、自動化されたエージェントによって解決することもできます。CouchDB では、非集中型の衝突解決を可能にしながらも、単一ドキュメント・データベースのセマンティクスを堅持します。

衝突の管理は、接続が切断された複数のユーザーやエージェントが同じ衝突の解決を試みた場合でも機能します。解決された衝突によってさらに衝突が増えた場合、システムはこれらの衝突も同じように受け入れ、各マシン上でどのドキュメントが「承認」されたものかを判断し、単一ドキュメントのセマンティクスを維持します。

アプリケーション

CouchDB のベーシックなレプリケーション・モデルを使えば、従来の多くのシングル・サーバー・データベース・アプリケーションをそのまま分散アプリケーションにすることができ、特別な作業もほとんど不要です。CouchDB のレプリケーションは、ベーシックなデータベース・アプリケーションですぐ使えるように設計されていますが、拡張可能なので、さらに高度で機能の豊富な利用にも対応することができます。

非常にわずかなでデータベース作業で、粒度のあるセキュリティ設定や完全なリビジョン履歴を持った分散ドキュメント管理アプリケーションを作成することができます。ドキュメントに対する更新は、フィールドと BLOB のインクリメンタル・レプリケーションを利用することで実装でき、レプリケートされた更新も、実際の編集上の差異 (“diffs”) にほぼ匹敵する効率的でインクリメンタルなものとなります。

CouchDB のレプリケーション・モデルは、ほかの分散更新モデルで使うために変更することができます。ストレージ・エンジンを強化して、複数のドキュメントを更新するトランザクションが可能になるようにすれば、上流のサーバーに対してレプリケーションを行う場合に、Subversion のような「オール・オア・ナッシング」のアトミックなコミット、すなわち一つでもドキュメントの衝突や有効性確認の失敗があれば全体の更新も失敗として処理することができます。この場合、Subversion の場合と同様、「プル型」レプリケーションを行ってローカルに衝突を発生させ、マージを行った後、上流のサーバーに再度レプリケーションを行うことで、衝突を解決することができます。

実装

CouchDB は、関数型並行プログラミング言語・開発プラットフォームである Erlang OTP プラットフォーム上に構築されています。Erlang は、リアルタイムの電気通信アプリケーションのために開発されたもので、信頼性と可用性を非常に重視しています。

Erlang は、構文とセマンティクスの両面で、C や Java といった従来のプログラミング言語とは一線を画しています。Erlang は、並行性を実現するためにライトウェイトな「プロセス」とメッセージ・パッシングを使っており、共有状態を持つスレッドを作成せず、すべてのデータは不変です。Erlang の堅牢な並行性は、データベース・サーバーに最適です。

CouchDB は、コンセプト・モデルにおいても、実際の Erlang による実装においても、ロックを使わない並行性を実現するよう設計されています。ボトルネックを減らし、ロックを避けることで、重い負荷がかかっている場合でもシステム全体が想定どおりに動作するようになっています。CouchDB では、多数のクライアントが変更をレプリケートしたり、ドキュメントを開いて更新したり、ビューのクエリーを行ったりすることができます。ビュー・インデックスは、ほかのクライアントに対しても同時にリフレッシュされ、その場合もロックは不要です。

可用性をさらに高めて、より多くのユーザーが並行して使えるようにするため、CouchDB は「何も共有しない」クラスタリングを実現するよう設計されています。「何も共有しない」クラスタでは、各マシンは独立しており、クラスタを構成する相手とデータのレプリケートを行います。そのため、個々のサーバーで障害が発生してもゼロ・ダウンタイムを実現することができます。また、再起動時も一貫性のチェックとフィクスアップが不要なので、クラスタ全体が(データセンター内の停電などで)障害を起こした場合でも、再起動後ただちに CouchDB 分散システム全体が利用可能になります。

CouchDB は、分散ドキュメント・データベース・システムを実現するという一貫したビジョンのもとにゼロから構築されています。CouchDB では、従来と同じモデルとデータベースをベースにして、その上に分散機能を付加するという面倒な方法は取らず、白紙の状態から熟慮を重ねて設計、計画、実装を行っています。CouchDB は、ドキュメント、ビュー、セキュリティ、およびレプリケーションの各モデルをはじめ、専用クエリー言語、効率的で堅牢なディスク・レイアウト、Erlang プラットフォームの持つ並行性と高信頼性といったすべての要素をよく考えて統合しているため、信頼性の高い効率的なシステムになっています。