概要
前回 MongoDB Write Concern - Carpe Diem にてWrite Concernについて説明しました。
今回はRead Concernについて説明します。Read Concernはデータの分離性・一貫性・最新性を考慮する際に気をつけるべき設定です。
環境
- MongoDB 3.6+
前提知識
Point-in-time snapshot (MVCC)
でも触れましたが、異なる時間から見たデータベースのデータを参照することによる不整合(読み取りスキュー)を防ぐためにスナップショットという概念があります。
WiredTigerではMVCC(Multi-Version Concurrency Control)を用いてそれを実現しています。
ざっくり説明するとRead/Write操作時にスナップショットデータを取得し、そのスナップショットに対してRead/Writeすることでそれぞれ独立したオペレーションを実現できます。

majority write snapshotの更新
レプリケーションの場合、enableMajorityReadConcern: true(デフォルトtrue)であれば過半数にwriteされたかどうかがスナップショットで管理されます。
そしてそれが更新されたかどうかがRead Concernの判断要素になります。
この更新タイミングは
- Primaryであればdurableになった(=過半数のデータノードからackが返った)時点
- SecondaryであればPrimaryから更新通知を受け取った時点
になります。
具体的なイメージは以下です。

時間軸で表すと
この⑦が完了した時間が公式の時間軸でいうt5にあたります。

上図ではw: "majority"を指定していますが、write concernに関わらず過半数にwriteされればスナップショットは更新されます。
Durable(データ耐久性がある)かどうか
Read Concernではデータがdurableかどうかも判断要素になります。
durableかどうかは構成によってことなり、以下の定義となっています。
| 構成 | durableといえる状態 |
|---|---|
| Standalone | journalファイルに書き込まれたら |
| ReplicaSet | 過半数のvoting members(投票可能なデータノード)のjournalファイルに書き込まれたら |
ref: https://www.mongodb.com/docs/manual/reference/glossary/#std-term-durable
Read Concern
Read Concern Level
Read Concern Levelには以下があります。
- local
- available
- majority
- linearizable
- snapshot
今回は非トランザクションのケースを想定してsnapshot以外についてフォーカスします。
local
localレベルの場合、そのノードにあるデータがそのまま返ります。

上図ではPrimaryはABC全てのデータを持っており、クライアントがアクセスすればABCが返ります(=最新のデータが返る)。
Secondaryもその時点で持っているデータを返します。
時間軸で考えると

先の時間軸で言うと以下です。
| ノード | いつ新しいデータが取得できるか |
|---|---|
| Primary | t0以降 |
| Secondary1 | t1以降 |
| Secondary2 | t2以降 |
メリット
- 最新のデータを早くに取得する事ができる
デメリット
available
availableはシャード構成でなければlocalと同様です。
シャード構成の場合チャンクのマイグレーション下でorphaned documentsも返せますが、データ整合性としては下がります。
majority
majorityではdurableかつ直近w: "majority"で書き込まれたことがsnapshotにも反映されたデータが返ってきます。

上図ではCはまだレプリカセットに伝播していないため、PrimaryにアクセスしてもABが返ります(=最新ではない)。
またSecondary(赤)にはまだデータBすら反映されていないため、アクセスした場合Aが返ります。
時間軸で考えると

先の時間軸で言うと以下です。
| ノード | いつ新しいデータが取得できるか |
|---|---|
| Primary | t3以降 |
| Secondary1 | t5以降 |
| Secondary2 | t6以降 |
t3の時点でPrimaryとSecondary1はdurableなデータを保持できたと言えますが、Secondary1はまだsnapshotの更新ができていないためPrimaryに比べてやや遅れて新しいデータを返すようになります。
このsnapshot更新までの時間は不透明であるため、これを考慮した上でread after write整合性を保ちたい場合はCausal Consistencyという仕組みを利用する必要があります。
メリット
- データ耐久性を持つ(Primaryが落ちても巻き戻らない)
- レプリケーションの反映を待つ分
localよりも同じデータが返りやすい
デメリット
localよりも最新のデータの反映が遅い- アクセスするノードによって異なるデータが返る可能性がある
linearizable
線形化可能なデータ整合性を保証します。
これまでのRead Concernは並行して実行されたwriteは考慮されませんでした(=writeにブロックされない)が、linearizableではそのwriteデータが過半数のノードに反映されるのを待ち、レスポンスにもその結果を含めます。
概念的に処理が直列化されるので、高いデータ一貫性を保証します。
トランザクションの分離レベルで出てくる用語 - Carpe Diem
で説明したSerializableと同様の概念です。
時間軸で考えると

上図のように同時に書き込まれたデータが過半数に反映されるまでブロックされ、ack後にレスポンスが返ります。
メリット
- データが一貫している(Primaryからのみ読むためSecondaryとのズレを考慮しなくていい)
- データ耐久性を持つ(Primaryが落ちても巻き戻らない)
- 最新のデータを読み出せる(直前のwriteも反映されている)
デメリット
- Primaryに限定されるため負荷分散できない
- 直前に並行してwriteが走った場合、線形性を保証するためデータが反映されるまで待つ(=
local、majorityに比べ大幅にレイテンシが増える)
Goでの実装
GoではClientを生成する際に指定することができます。
mongoURI := "mongodb://server:port/" opts := options.Client().ApplyURI(mongoURI). SetWriteConcern(writeconcern.New(writeconcern.WMajority())). SetReadConcern(readconcern.Majority()). SetReadPreference(readpref.SecondaryPreferred()) client, err := mongo.NewClient(opts) if err != nil { panic(err) }
linearizable以外は負荷分散のためRead Preferenceを付ける(デフォルトはPrimary)のが良いでしょう。
まとめ
MongoDBのRead Concernについて説明しました。選定する際は
- 最新のデータを取得したい
- 耐久性のあるデータを取得したい
- 低レイテンシで取得したい
- 負荷分散させたい
これらの軸をトレードオフにRead Concernを指定することになるでしょう。