概要
MongoDBはデータがどこまで書き込まれたらクライアントにackを返すかという設定ができます。
その設定をWrite Concernといい、メモリまで保存されたのかディスクまで保存されたのか・何台のデータノードにデータが書き込まれたらといった指定が可能です。
データの整合性を考慮する上で理解すべき概念なので図を交えて説明します。
環境
- MongoDB 3.6+
前提知識
メモリとジャーナル
MongoDBではまずメモリに保存され、その後ディスクに保存されます。
journal: falseであればメモリまで保存されたらすぐにacknowledgeが返ります。レイテンシが低い一方でメモリが揮発した場合はデータを失うので耐久性(durability)は低いと言えます。
journal: trueであればディスクまで保存されてからacknowledgeが返ります。ディスクに書き込むためレイテンシは増しますが、耐久性は高いと言えます。

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

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が返ります。

図を見て分かるようにSecondaryにレプリケートされるのを待たずに返すため、もしPrimaryが落ちた場合はデータBは失われます。
時系列で見ると
w: nの数字によってレスポンスが返るタイミングが変わります。

w: 1の場合即時返ることが分かります。
w: majority
majorityの算出方法としては
- arbiterを含む投票可能なノードの過半数
- データノードの数
のどちらか小さい方がmajorityの数として扱われます。
w: majorityの場合、上記の算出方法で求められた数のデータノードに反映されたらクライアントにackを返します。

図ではPrimary、Secondary(青)にデータBが書き込まれてからackが返っています。
Primaryが落ちてもSecondary(青)にデータが残るので耐久性は高いですが、当然ながらレプリケーションの時間分レイテンシが増えます。
時系列で見ると
w: 1の時に比べ、反映を待つのでレイテンシが上がります。

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: falsetrueなら 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>1やw: "majority"の場合複数のデータノードに反映されてからackが返りますが、Primaryに昇格したのがレプリケートされていないSecondaryの場合過去のwriteが反映されない(=ロールバックされる)のでは?という疑問です。
これについてはMongoDBのPrimary ElectionにはCatch Up Timeというものがあり、データが古いSecondaryがPrimaryに昇格した際に他のデータノードから最新のデータをキャッチアップする仕組みがあります。キャッチアップ期間中はクライアントからの書き込みはできません。

キャッチアップが完了しなかった場合は再度Electionが発生するので、w: n>1やw: "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を設定するのが良いでしょう。