分散アーキテクチャーにおけるインデックス
Couchbase ServerとRDBMSとの違いとして、メモリーファーストアーキテクチャーがあります。
メモリーファーストアーキテクチャーは、永続化装置および複製(レプリケ)の作成が、アプリケーション(への応答)性能のボトルネックになることを回避するための仕組みです。そのための仕組みとして、Couchbase Serverは、ノード内部でディスクへの書き込みのためのキューを持っています。また、他ノードへの複製(レプリカ)のためのキューメカニズムとして、Data Change Protocol(DCP)と呼ばれる仕組みを持っています。
このDCPは、レプリカのためのみではなく、クラスター内部でデータの変更を他のノードやサービスに反映するために広く利用されます。(Dataサービスにおける)データ変更のインデックスへの反映もその一つです。
インデックス更新から見るCouchbase ServerとRDBとの違い
Couchbase ServerとRDBとの違いをこの観点から見ることができます。データを更新する際には、関係するインデックスの更新が行われるのは、Couchbase ServerとRDBとで共通です。ただし、RDBでは、ユーザーが、いつどのように、データへアクセスしたとしても、データの一貫性が保たれていることを保証するため、クライアントからのデータ更新処理リクエストに対して、成功のステータスが返されるのは、インデックスの更新が完了した後になります。これは、データ一貫性の保証という要件面に加え、RDBがモノリシックなアークテクチャーからなっているという、技術的な面から見ても自然な挙動と言えるかもしれません。一方で、Couchbase Serverは、分散アーキテクチャーという性格を持ち、データの更新処理を司るDataサービスと、インデックスを管理するIndexサービスとは、コンポーネントとして独立しており、異なるサーバーに配置されるのが一般的です。Couchbase Servevrでは、インデックスの更新は、永続化装置への反映や、複製の作成と同様、DCPプロトコルを介した、キュー(非同期)のメカニズムで実現されています。
インデックス更新が非同期であることの影響範囲
RDBにとって、SQLがクライアントにとって共通のインターフェースであるのと異なり、Couchbase Serverでは、N1QLは、クライアントにとって、選択することのできる複数のインターフェースのうちの1つです。さらに言えば、Dataサービスへのキーバリューアクセスが、一次的なインターフェイスであるのに対して、あくまで派生的なインターフェースであると言えます。キーによるアクセスでは、常にデータの同期が保証されます(たとえ、ある新しい更新が、永続化されておらず、ディスクキューに入っている状態だとしても、後続のクライアントのアクセスへは、メモリ内の最新の情報から応答されます)。
さらに、このようなインターフェイスの違いを考慮せず、純粋にアプリケーションのバックエンドとしてのデータベースにとって、データの(インデックスへの)同期が必要な場合と、必要でない場合とを考えることができます。RDBでの開発に慣れている開発者にとって、データベースの中で、データの同期が行われていない可能性を考えることは、違和感があるかもしれません。一方、RDBの開発経験が豊富な方ほど、インデックス定義によるデータ検索時の性能向上と、必然的に伴うデータ更新時の性能劣化との相克に悩まされた経験を持っているのではないかと思います。
まず、ここではインデックスは、あくまでデータの検索に使われることを確認しておきたいと思います。つまり、データがインデックスに同期されていない場合の影響は、あくまで検索結果の違いにのみ関係してきます。ある特定のレコード(ドキュメント)のカラム(フィールド)の値について、データベース内で同期が取れていない(特定時点で、異なる複数の結果を参照しうる)という現象は決して起こりません。そのことを考えれば、影響範囲は非常に限定的であることがわかると思います。また、ここでは非同期、つまり更新の遅延、を問題としていますが、クライアント(アプリケーション)から見た場合、この遅延は、究極的には、ネットワークその他、のあらゆる要因による遅延と区別できないと言えます、この後に触れる、ある1つのケースを除いては。
READ YOUR OWN DATA (自分自身のデータを読む)
複数のリクエストに対して矛盾した結果が返されたことを証明するためには、その矛盾の論拠として、正確な時刻情報を示す必要があります。これは、複数の異なるクライアントからのそれぞれ別の更新と検索リクエストを想定した場合、遅延が余程大きいものでなければ、非常に困難と言えるでしょう(上に触れたように、クライアントとデータベースの間には様々な遅延要因が存在しうると考えると、原因がデータベース内の遅延によるものだと証明することはシステムの内部情報に当たらない限り不可能だと言えます)。言い換えれば、複数のクライアント間の出来事と考えた場合、多少の遅延は事実上の問題となりえないと言えるでしょう。
唯一の例外は、同一のクライアントからのシーケンシャルなリクエストに対して矛盾した結果が返されるケースです。自分で行った更新が、その後の検索結果に反映されていないとしたら(例えばユーザー管理アプリケーション上で、ユーザーを追加した後に、その新規ユーザーが全ユーザーリストに表示されないとしたら)、これは明らかに問題です。
永続化や複製の場合と比べた、インデックスの特殊性
データの永続化、複製作成、インデックス更新は、全て、データ更新とは非同期の処理であるという点で、共通しています。一方、データ永続化および複製作成については、データ更新時に、同期(処理が完了するのを待ってクライアントに成功のステータスが返されること)を強制するオプションが存在しています。これに対して、インデックス更新については、このようなデータ更新時のオプションはありません。
一貫性を保証するためのオプション
データ更新時にインデックス更新の同期を強制するオプションが存在しない代わりに、N1QLクエリ実行時に、インデックス更新状況に対して、リクエストの挙動を変えることのできる複数のオプションがあります。
次のオプションを使用できます。
-
not_bounded:クエリの一貫性を必要とせずに、クエリをすぐに実行します。インデックスの更新に遅延がある場合は、古い結果が返されることがあります。
-
at_plus:インデックスが最新の更新のタイムスタンプまで更新されている事を保証します。インデックスの更新に遅延がある場合は、更新を待ちます。
-
request_plus:インデックスが、クエリリクエストのタイムスタンプまで更新されている事を保証して、クエリを実行します。インデックスの更新状況に遅延がある場合は、更新を待ちます。
デフォルトの整合性はnot_boundedです。
上記のオプション表記は、概念を説明するためのものであり、SDK/プログラム言語によって実際の表記が変わることにご注意ください。以下は、JavaとJavaScriptでのオプションの利用例です。
cluster.query(
"select * from `travel-sample`.inventory.airport",
QueryOptions.queryOptions()
.scanConsistency(QueryScanConsistency.REQUEST_PLUS)
);
const result = await cluster.query(
"select * from `travel-sample`.inventory.airport",
{
scanConsistency: couchbase.QueryScanConsistency.RequestPlus,
});