Carpe Diem

備忘録

MongoDB Write Concern

概要

MongoDBはデータがどこまで書き込まれたらクライアントにackを返すかという設定ができます。
その設定をWrite Concernといい、メモリまで保存されたのかディスクまで保存されたのか・何台のデータノードにデータが書き込まれたらといった指定が可能です。

データの整合性を考慮する上で理解すべき概念なので図を交えて説明します。

環境

  • MongoDB 3.6+

前提知識

メモリとジャーナル

MongoDBではまずメモリに保存され、その後ディスクに保存されます。

journal: falseであればメモリまで保存されたらすぐにacknowledgeが返ります。レイテンシが低い一方でメモリが揮発した場合はデータを失うので耐久性(durability)は低いと言えます。

journal: trueであればディスクまで保存されてからacknowledgeが返ります。ディスクに書き込むためレイテンシは増しますが、耐久性は高いと言えます。

f:id:quoll00:20220314114217p:plain:w400

レプリケーションラグ

Write Concernの必要性を語る上で欠かせないのがレプリケーションにおけるラグです。
Primaryに書き込んだとしてもそれがSecondaryまで反映されるまでには多かれ少なかれラグがあります。

なので以下のようにSecondaryにまだデータ同期されていない状態を常に考慮する必要があります。

f:id:quoll00:20220314112827p:plain

Write Concern

Write Concernのパラメータは主に3つあります。

パラメータ 説明
w write concernの設定値
number/majority/custom tag が指定できる
journal falseならメモリ、trueならディスクに書き込む
wtimeout 無限にブロックされるのを防ぐためのタイムアウト
ミリ秒単位

journalについては前述し、wtimeoutは直感的に理解できるので残りのwについて説明していきます。

w: number

w: nの場合、n台のデータノードに反映されたらackを返します。

図ではw: 1のパターンを示しています。1台なのでPrimaryに書き込まれた時点でクライアントにはackが返ります。

f:id:quoll00:20220314113238p:plain

図を見て分かるようにSecondaryにレプリケートされるのを待たずに返すため、もしPrimaryが落ちた場合はデータBは失われます。

時系列で見ると

w: nの数字によってレスポンスが返るタイミングが変わります。

f:id:quoll00:20220317023303p:plain

w: 1の場合即時返ることが分かります。

w: majority

majorityの算出方法としては

  • arbiterを含む投票可能なノードの過半数
  • データノードの数

どちらか小さい方がmajorityの数として扱われます

w: majorityの場合、上記の算出方法で求められた数のデータノードに反映されたらクライアントにackを返します。

f:id:quoll00:20220314113301p:plain

図ではPrimary、Secondary(青)にデータBが書き込まれてからackが返っています。

Primaryが落ちてもSecondary(青)にデータが残るので耐久性は高いですが、当然ながらレプリケーションの時間分レイテンシが増えます。

時系列で見ると

w: 1の時に比べ、反映を待つのでレイテンシが上がります。

f:id:quoll00:20220317022946p:plain

arbiterがある場合の注意

基本的にデータ耐久性のためmajorityが推奨されますが、P-S-Sの3台構成でなくP-S-Aの3台構成の場合は注意が必要です。

先程の算出方法では

  • arbiterを含む投票可能なノードの過半数=2
  • データノードの数=2

の小さい方なのでmajorityの数は2となりますが、仮にPrimary, Secondaryのどちらかが落ちるとデータノードは1台しかなく、majorityの条件を満たすことができなくなってしまうためです。

なのでP-S-A3台構成の場合はw: 1が推奨されます。

journalが未指定の場合

journalが未指定の場合、構成や設定によって挙動が異なります。

Standalone(1台構成)の場合

w 挙動
w: 1 journal: false (in memory)
w: "majority" journal: true (on disk)

ref: https://docs.mongodb.com/manual/reference/write-concern/#standalone

ReplicaSetの場合

w 挙動
w: number journal: false (in memory)
w: "majority" writeConcernMajorityJournalDefaultがfalseならjournal: false
trueならjournal: true

ref: https://docs.mongodb.com/manual/reference/write-concern/#replica-sets

wtimeoutは必ず設定する

If you do not specify the wtimeout option and the level of write concern is unachievable, the write operation will block indefinitely.

ref: https://docs.mongodb.com/manual/reference/write-concern/#wtimeout

とあるように、wの設定によってはノードのダウンにより実現できないケースがあり、その場合無限にブロックしてしまいます。
なのでwtimeoutは必ず設定するようにしましょう。

レプリケーションラグの大きいSecondaryがPrimary昇格した場合

w: n>1w: "majority"の場合複数のデータノードに反映されてからackが返りますが、Primaryに昇格したのがレプリケートされていないSecondaryの場合過去のwriteが反映されない(=ロールバックされる)のでは?という疑問です。

これについてはMongoDBのPrimary ElectionにはCatch Up Timeというものがあり、データが古いSecondaryがPrimaryに昇格した際に他のデータノードから最新のデータをキャッチアップする仕組みがあります。キャッチアップ期間中はクライアントからの書き込みはできません。

f:id:quoll00:20220317113936p:plain

キャッチアップが完了しなかった場合は再度Electionが発生するので、w: n>1w: "majority"であり、かつjournal: trueであればロールバックは起きません。

Goでの実装

GoではClientを生成する際に指定することができます。

mongoURI := "mongodb://server:port/"
opts := options.Client().ApplyURI(mongoURI).
    SetWriteConcern(writeconcern.New(writeconcern.WMajority()))

client, err := mongo.NewClient(opts)
if err != nil {
    panic(err)
}

まとめ

MongoDBのWrite Concernについて図を交えながらまとめてみました。
レイテンシとデータ耐久性のトレードオフにはなりますが、データのロストは事業的にもリスクになるため基本的にはmajorityを設定するのが良いでしょう。

参考