Carpe Diem

備忘録

MongoDB Read Concern

概要

前回 MongoDB Write Concern - Carpe Diem にてWrite Concernについて説明しました。

今回はRead Concernについて説明します。Read Concernはデータの分離性・一貫性・最新性を考慮する際に気をつけるべき設定です。

環境

  • MongoDB 3.6+

前提知識

Point-in-time snapshot (MVCC)

christina04.hatenablog.com

でも触れましたが、異なる時間から見たデータベースのデータを参照することによる不整合(読み取りスキュー)を防ぐためにスナップショットという概念があります。

WiredTigerではMVCC(Multi-Version Concurrency Control)を用いてそれを実現しています。

ざっくり説明するとRead/Write操作時にスナップショットデータを取得し、そのスナップショットに対してRead/Writeすることでそれぞれ独立したオペレーションを実現できます。

f:id:quoll00:20220315164430p:plain

majority write snapshotの更新

レプリケーションの場合、enableMajorityReadConcern: true(デフォルトtrue)であれば過半数にwriteされたかどうかがスナップショットで管理されます。
そしてそれが更新されたかどうかがRead Concernの判断要素になります。

この更新タイミングは

  • Primaryであればdurableになった(=過半数のデータノードからackが返った)時点
  • SecondaryであればPrimaryから更新通知を受け取った時点

になります。

具体的なイメージは以下です。

f:id:quoll00:20220315175936p:plain

時間軸で表すと

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

f:id:quoll00:20220316002607p:plain

上図では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レベルの場合、そのノードにあるデータがそのまま返ります。

f:id:quoll00:20220315182709p:plain

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

時間軸で考えると

f:id:quoll00:20220316002953p:plain

先の時間軸で言うと以下です。

ノード いつ新しいデータが取得できるか
Primary t0以降
Secondary1 t1以降
Secondary2 t2以降

メリット

  • 最新のデータを早くに取得する事ができる

デメリット

  • レプリケーションの反映を待たないため、アクセスするノードによって異なるデータが返る可能性がある
  • データが巻き戻る(ロールバック)可能性がある

available

availableはシャード構成でなければlocalと同様です。

シャード構成の場合チャンクのマイグレーション下でorphaned documentsも返せますが、データ整合性としては下がります。

majority

majorityではdurableかつ直近w: "majority"で書き込まれたことがsnapshotにも反映されたデータが返ってきます。

f:id:quoll00:20220315183618p:plain

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

時間軸で考えると

f:id:quoll00:20220316002607p:plain

先の時間軸で言うと以下です。

ノード いつ新しいデータが取得できるか
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と同様の概念です。

時間軸で考えると

f:id:quoll00:20220316003719p:plain

上図のように同時に書き込まれたデータが過半数に反映されるまでブロックされ、ack後にレスポンスが返ります。

メリット

  • データが一貫している(Primaryからのみ読むためSecondaryとのズレを考慮しなくていい)
  • データ耐久性を持つ(Primaryが落ちても巻き戻らない)
  • 最新のデータを読み出せる(直前のwriteも反映されている)

デメリット

  • Primaryに限定されるため負荷分散できない
  • 直前に並行してwriteが走った場合、線形性を保証するためデータが反映されるまで待つ(=localmajorityに比べ大幅にレイテンシが増える)

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を指定することになるでしょう。

参考