NoSQLの中には「常に最新の値が見える」とは限らない代わりに、書き込みや読み取りを複数ノードに分散させることで高いスループットとスケーラビリティを実現しているものがあります。この「最新ではないかもしれないが、最終的には揃う」性質が 結果整合性(Eventual Consistency) です。本記事では、強整合性との違い、結果整合性が必要になる理由、それを総称した BASE特性、アプリケーションに応じて整合性の強さを切り替えられる 調整可能整合性(W+R>N)、そして結果整合性で起こる典型的な問題と対策までを整理します。
本記事における「NoSQL」の範囲
NoSQLは大きくキーバリュー型・ワイドカラム型・ドキュメント型・グラフ型の4種類に分類され、整合性モデルはプロダクトごとに異なります。一括りに「NoSQL = 結果整合性」とは言えません。
| 本記事の扱い | 整合性モデル | 代表プロダクト | 特徴 |
|---|---|---|---|
| 対象 | 結果整合性(BASE特性) | Cassandra, DynamoDB, Riak, Couchbase(XDCR), MongoDB(標準設定) | マルチマスター/非同期レプリケーションでスケールアウト志向 |
| 対象外 | 強整合性(CAP定理上のCP分類) | Redis(Sentinel/Cluster), HBase, MongoDB(writeConcern=majority) | 分断時は過半数を取れない側を停止して整合性を守る |
| 対象外 | 強整合性(ACIDトランザクション) | Neo4j などのグラフDB | シャーディングはできないが、トランザクション保証は強い |
本記事で扱うのは1行目の「マルチマスターまたは非同期レプリケーションを採用してスケールアウトを志向し、結果整合性(BASE特性)を採用するNoSQL」です。NoSQLの4分類で言うとキーバリュー型(DynamoDB・Riak)・ワイドカラム型(Cassandra)・ドキュメント型(Couchbase、MongoDB の標準設定)に該当するプロダクト群が対象です。グラフ型(Neo4j など)はACIDトランザクションを持つため対象外で、同じキーバリュー/ワイドカラム/ドキュメント型のなかでも分断時に整合性を優先するCP系の設定(Redis Sentinel/Cluster、HBase、MongoDB の writeConcern=majority など)も対象外とします。
NoSQLの4分類そのものについては『NoSQLの4分類を整理する: KVS・ワイドカラム・ドキュメント・グラフの本質的な違い』を、CAP定理によるCP/AP/CAの分類については『CAP定理(CA/CP/AP)とPACELC定理(PA/EL・PA/EC・PC/EC)による分散データベース分類』を、ACIDトランザクションそのものについては『ACIDトランザクションの4特性』を参考にしてください。
強整合性と結果整合性の違い
整合性には大きく2つのモデルがあります。
- 強整合性(Strong Consistency)はクラスタ内のどのノードから読んでも、書き込み完了後は必ず最新の値が返ってくるモデル
- 結果整合性(Eventual Consistency)は書き込み直後の一定時間は古い値が返ることもあるが、十分時間が経てば全レプリカが最新値に揃うモデル
RDB(OLTP)は分散トランザクションで全レプリカに同期的に書き込むため、強整合性が前提です。一方、NoSQLの多くは性能とスケーラビリティを優先し、結果整合性を選択しています。
なぜNoSQL(キーバリュー型・ワイドカラム型・ドキュメント型)は結果整合性なのか
NoSQL(キーバリュー型・ワイドカラム型・ドキュメント型)は性能とスケーラビリティを上げるために、複数ノードへのレプリケーションを前提としています。書き込みを1ノードで受けて他レプリカへ非同期で伝搬する、あるいは複数ノードで同時に書き込みを受け付ける(マルチマスター)構成です。この構成では強整合性は保てません。
1. レプリケーションの伝搬遅延で古いデータを読む
書き込みを受けたノードから他レプリカへ伝搬する間に時間差があるため、伝搬完了前のレプリカに読みに行くと古い値が返ってきます。
- アプリがノードAに「Cを挿入」する書き込みを送る
- ノードAから他レプリカ(ノードB)へ伝搬中で、ノードBにはまだ届いていない
- その間にアプリがノードBに件数を問い合わせると、Cが追加される前の古い件数が返ってくる
このズレは一時的で、レプリケーション完了後には全レプリカが最新化されます。「あるタイミングではクラスタ全体で整合性が取れていなくても、最終的には整合性が取れる」ことから「結果整合性」と呼ばれます。
2. マルチマスターでは書き込み競合が起きる
マルチマスターレプリケーションでは複数ノードで書き込みを同時に受けるため、同じデータに対して競合する書き込みが発生します。
- アプリAがノードAに「Cを挿入」する
- アプリBがノードBに「Dを挿入」する
- 同じデータに対して矛盾する書き込みが同時に発生し、どちらを「正」とするか判定が必要になる
どちらを「正」とするかはプロダクトの実装や設定次第ですが(タイムスタンプの新しい方を採用、ベクタークロック、CRDT、アプリ側で解決など)、一時的に間違ったクラスタの状態が見えることは避けられません。これも結果整合性です。
結果整合性の設計思想(BASE特性)
このような結果整合性を前提とするNoSQLの思想は、RDB(OLTP)のACID特性になぞらえてBASE特性と呼ばれます。
- B(Basically Available)はアプリケーションは基本的にどんな時でも動く(可用性優先)
- S(Soft state)は常に整合性を保っている必要はなく、中間状態を許容する
- E(Eventual consistency)は結果として整合性が取れる状態に至る
ACIDが「正しさを最優先」する設計思想なのに対し、BASEは「動き続けることを最優先」する設計思想です。両者は単純な優劣ではなく、用途に応じた選択軸です。
| 観点 | ACID(RDB / OLTP) | BASE(NoSQL のキーバリュー型・ワイドカラム型・ドキュメント型) |
|---|---|---|
| 整合性 | 強整合性 | 結果整合性 |
| 可用性 | 厳格な保証よりも整合性優先 | 基本的に常に応答する |
| スケーラビリティ | 分散トランザクションで頭打ちになりやすい | 線形スケールしやすい |
| 想定ワークロード | 銀行取引・在庫など正確性が要求されるOLTP | ビッグデータ・SNS・ログ・キャッシュなど |
結果整合性のNoSQLでも強整合性は得られる(調整可能整合性)
「結果整合性のNoSQLでは強整合性は絶対に得られない」というのは誤解です。調整可能整合性(Tunable Consistency)とは、書き込み時・読み取り時に何ノードに確認を取るかを操作ごとに指定することで、結果整合性と強整合性のあいだを連続的に切り替えられる仕組みのことです。Cassandra や DynamoDB などが採用しています。
各プロダクトの整合性オプション・トランザクション対応範囲の個別整理は『主要NoSQLプロダクトリファレンス: Redis・Cassandra・DynamoDB・MongoDB・Neo4j を中心に』を、ユースケース起点で候補プロダクトを逆引きする整理は『ユースケース別NoSQL選定ガイド: 「こういうときはどのNoSQL?」を逆引きする』を参照してください。
強整合性を実現する条件(W + R > N)
レプリケーション係数(同じデータを何ノードに持つか)をN、書き込み時の確認ノード数をW、読み取り時の確認ノード数をRとすると、以下が成り立ちます。
W + R > N を満たせば、書き込みと読み取りで参照するノード集合が必ず1ノード以上重なるため、最新の書き込みを必ず読み取れます(= 強整合性)。
直感的にはこういうことです。N台のレプリカがあり、書き込みでW台に確認し、読み取りでR台に確認するとき、W + R が N より多いなら、書き込み先のノード集合と読み取り先のノード集合は必ず1台以上重なります(N台しかないクラスタで合計N台より多く選んでいるので、どこかは被るしかないため)。その重なったノードには最新の書き込み値が入っているので、読み取り側は必ず最新値を取得できます。
たとえばN=3, W=2, R=2(2 + 2 > 3)の場合を見てみます。
| ノードA | ノードB | ノードC | |
|---|---|---|---|
| 書き込み先(W=2) | ● | ● | |
| 読み取り先(R=2) | ● | ● |
書き込み先・読み取り先の組み合わせをどう選んでも、3台しかないノードから2台ずつ選ぶ以上、必ず1台以上重なります(この例ではノードB)。重なったノードには最新の書き込み値が入っているため、読み取り側は最新値を取得できます。これはQUORUM(過半数)と呼ばれる典型的な設定です。
WとRの指定方法(整合性レベル)
調整可能整合性を実装したプロダクトでは、各操作に対して W=2, R=2 のように直接数値を指定するのではなく、ONE / QUORUM / ALL のような名前付きの設定値から選ぶ形を取るのが一般的です。この、書き込み・読み取りそれぞれで「何ノードに確認を取るか」を指定する設定値の総称が 整合性レベル(Consistency Level) です。
「整合性レベル」という用語自体や選択肢の粒度はプロダクトごとに異なり(DynamoDB なら ConsistentRead、MongoDB なら writeConcern / readConcern など)、この用語を最も明確に持ち出しているのは Cassandra です。代表例として Cassandra の整合性レベルを見てみます。Cassandra では操作ごとに以下のような整合性レベルを指定できます。
| 整合性レベル | 意味 | 特徴 |
|---|---|---|
ONE | 1ノードに書き込み/1ノードから読み取り | 速い・整合性は弱い |
QUORUM | 過半数(N=3 なら 2)に確認 | 整合性と性能のバランス |
ALL | 全ノードに確認 | 整合性は最も強い・1ノード障害で失敗 |
LOCAL_QUORUM | ローカルDC内の過半数に確認 | DC間トラフィックを発生させずQUORUM相当 |
EACH_QUORUM | 各DCで過半数に確認 | DC間の整合性を維持 |
ANY | (書き込みのみ)任意のノード、ダウン時はヒンテッド・ハンドオフ | 可用性最大・整合性最小 |
整合性レベルは SELECT / INSERT / UPDATE / DELETE の操作ごと、リクエストごとに指定できます。これにより「強整合性 vs 結果整合性」を全体で1つ選ぶのではなく、データやユースケースごとに使い分けられます。
- 重要なトランザクション(決済・在庫更新)は
QUORUMやALLで強整合性を取る - 多少のズレが許される更新(ソーシャルメディアの「いいね」、ログ)は
ONEで速度優先で取る
結果整合性で起こる典型的な問題と対策
結果整合性を選ぶと、アプリケーション側で以下のような事態に対応する必要があります。
1. 自分の書き込みが直後に読めない(read-your-writes 問題)
ユーザーが投稿してすぐ画面をリロードすると、自分の投稿が表示されない、というパターンです。書き込んだノードと読み取ったノードが別で、まだ伝搬していない状態が原因です。
対策の例として以下があります。
- 書き込んだノードからの読み取りに固定する(読み取り先をピン留めする)
- 該当の読み取りだけ整合性レベルを
QUORUMに上げる - アプリ側で書き込み直後はローカルキャッシュから表示する
2. リロードすると新しい情報→古い情報の順で見える(monotonic reads 問題)
最初のリクエストで最新化されたレプリカにアクセスし、次のリクエストでまだ伝搬していないレプリカにアクセスすると、時系列が逆転して見えます。
対策の例として以下があります。
- 同一セッション内では同じノードに固定する(セッションスティッキー)
- 整合性レベルを上げる
3. 書き込みの競合
マルチマスター構成で同じキーに対して複数の書き込みが起きた場合、どれを採用するかをプロダクト・アプリ側で決める必要があります。レプリケーション3方式(シングルリーダー / マルチリーダー / リーダーレス)と競合解決戦略の整理は『レプリケーション戦略: シングルリーダー・マルチリーダー・リーダーレスの使い分け』を参照してください。
対策の例として以下があります。
- ラスト・ライト・ウィン(最新タイムスタンプ採用)
- ベクタークロックで因果関係を判定
- CRDT(Conflict-free Replicated Data Type)で自動マージ
- アプリ側で複数バージョンを返してユーザーに選ばせる
まとめ
- 強整合性はどのノードから読んでも最新値が返るモデル、結果整合性は一時的に古い値が返ることを許容し最終的に揃うモデル
- NoSQLのうちキーバリュー型・ワイドカラム型・ドキュメント型は性能・スケーラビリティを優先するためレプリケーション前提で動き、結果整合性を採用することが多い(グラフ型はACIDトランザクション系で別物)
- BASE特性(Basically Available / Soft state / Eventual consistency)はACIDの対義概念で、可用性優先・最終的に整合性が取れる、という設計思想を表す
- 調整可能整合性により「結果整合性 ⇄ 強整合性」を操作単位で切り替えられる。W + R > N を満たせば強整合性を実現できる
- 結果整合性を選ぶ場合、read-your-writes / monotonic reads / 書き込み競合などへの設計上の対策が必要