概要
MongoDB Write Concern - Carpe Diem
MongoDB Read Concern - Carpe Diem
でデータの耐久性・一貫性・分離性・最新性などを保証する方法について説明しました。
しかし、これらの設定だけでは
Causal Consistency - Carpe Diem
で紹介した因果関係の一貫性を保証できないケースがあります。
MongoDBではLamport Clockを用いてそれらのケースを保証する仕組みがあり、それをCausal Consistency Sessionといいます。
一貫性が保証できないケース
- write concern: majority
- read concern: majority
に設定すれば多くのケースで問題はありませんが、例えば書き込み直後の読み取り(read after write)の場合
上図のようにレプリケーションラグが大きいSecondaryからreadした場合、書き込んだはずのB
が反映されてません。
これは書き込んでから読み込んだという因果関係(Causal)が一貫していないことになります。
Causal Consistency Session
MongoDBのCausal Consistency Sessionは上記のケースを保証する仕組みです。ただしRead Concern/Write Concernが保証するケースに大きく関わります。
Read Concern / Write ConcernとCausal Consistency Sessionの保証具合
ref: https://docs.mongodb.com/manual/core/causal-consistency-read-write-concerns/
この表の通り、w: "majority"
, r: "majority"
を設定した上でCausal Consistency Sessionを使うと、全ての因果関係の一貫性を保証できます。
検証
では実際にMongoDBのレプリカ構成を組んで動作検証を行ってみます。
環境構築
- MongoDB 5.0.6
- Go 1.18
MongoDBレプリカセット対応
以下のようなdocker-composeを用意します。
version: "3" services: primary: image: mongo restart: always ports: - 27017:27017 command: - --replSet - rs0 - --port - "27017" secondary1: image: mongo restart: always ports: - 27018:27018 command: - --replSet - rs0 - --port - "27018" secondary2: image: mongo restart: always ports: - 27019:27019 command: - --replSet - rs0 - --port - "27019"
初期設定のスクリプトを用意します。
config = { _id: "rs0", members: [ { _id: 0, host: "primary:27017" }, { _id: 1, host: "secondary1:27018" }, { _id: 2, host: "secondary2:27019" }, ], }; rs.initiate(config);
起動し、レプリカセットの設定を行います。
$ docker-compose up -d $ mongo init.js
/etc/hosts
上記docker containerにアクセスできるよう/etc/hosts
もいじっておきます。
## # Host Database # # localhost is used to configure the loopback interface # when the system is booting. Do not change this entry. ## 127.0.0.1 localhost primary secondary1 secondary2 255.255.255.255 broadcasthost ::1 localhost
Go
client生成
- write concern: majority
- read concern: majority
を有効にし、ReadPreferenceをSecondaryにしておきます。
func newClient(ctx context.Context, addr string) (*client, error) { mongoURI := fmt.Sprintf("mongodb://%s/?replicaSet=rs0", addr) opts := options.Client().ApplyURI(mongoURI). SetWriteConcern(writeconcern.New(writeconcern.WMajority())). SetReadConcern(readconcern.Majority()). SetReadPreference(readpref.Secondary()) cli, err := mongo.NewClient(opts) if err != nil { return nil, err } if err := cli.Connect(ctx); err != nil { return nil, err } ctxwt, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() if err := cli.Ping(ctxwt, nil); err != nil { return nil, err } return &client{ cli: cli, db: cli.Database(dbName), }, nil }
read your write部分
insert直後にリスト処理を行います。
func (c *client) readYourWrite(ctx context.Context, data User) ([]User, error) { if _, err := c.db.Collection(col).InsertOne(ctx, data); err != nil { return nil, err } cursor, err := c.db.Collection(col).Find(ctx, bson.M{}) if err != nil { return nil, err } var res []User if err = cursor.All(ctx, &res); err != nil { return nil, err } return res, nil }
Causal Consistency Session
contextとしてCausal Consistency Sessionを渡せるようにします。
func (c *client) withCausalConsistencySession(ctx context.Context) (context.Context, func(), error) { sess, err := c.cli.StartSession(options.Session().SetCausalConsistency(true)) if err != nil { return nil, nil, err } closer := func() { sess.EndSession(context.TODO()) } return mongo.NewSessionContext(ctx, sess), closer, nil }
main()
100回ほどread after write処理を実行して検証します。
もし書き込んだ分のデータが取得できなかった場合ログを吐くようにしておきます。
func main() { ctx := context.TODO() cli, err := newClient(ctx, "primary:27017,secondary1:27018,secondary2:27019") if err != nil { log.Fatal(err) } // ここのsessionの有無で動作が変わるか確認する ctx, closer, err := cli.withCausalConsistencySession(ctx) if err != nil { log.Fatal(err) } defer closer() for i := 0; i < 100; i++ { data := User{ ID: gofakeit.UUID(), Name: gofakeit.Name(), Age: gofakeit.Number(10, 60), } list, err := cli.readYourWrite(ctx, data) if err != nil { log.Fatal(err) } if i+1 != len(list) { fmt.Printf("count: %d, data: %d\n", i+1, len(list)) } } if err := cli.clean(ctx); err != nil { log.Fatal(err) } }
動作確認
ではCausal Consistency Sessionの有無で一貫性の挙動が変わるか確認してみます。
Causal Consistency Sessionじゃない場合
Causal Consistency Sessionの部分をコメントアウトして実行してみます。
causal-consistency $ go run main.go count: 94, data: 93 causal-consistency $ go run main.go count: 90, data: 89 causal-consistency $ go run main.go count: 49, data: 48 causal-consistency $ go run main.go count: 95, data: 94 count: 97, data: 96
何度か実行しましたが100回に0~2回程度一貫性を保証できないケースがありました。
Causal Consistency Sessionの場合
causal-consistency $ go run main.go causal-consistency $ go run main.go causal-consistency $ go run main.go causal-consistency $ go run main.go
データのズレは発生せず、常に一貫性が保証されていました。
サンプルコード
まとめ
Write ConcernやRead Concernを設定することでデータの耐久性や最新性などの保証ができますが、因果関係の一貫性を保証するためには追加でCausal Consistency Sessionを使う必要があると分かりました。