🌊

ひとりMongoDB University (12月最終週の週末 - レプリカセットへの書き込みとフェイルオーバー)

2020/12/28に公開

この記録は、アドベントカレンダー形式ではじめた、MongoDB Universityの学習コースの記録の続きになります!毎日ではありませんが、ある程度まとまったら上げます。

Chapter 2: Reads and Writes on a Replica Set (動画)

  • レプリカセットにデータを読み書きしてみる
    • まずプライマリノードに接続
    • プライマリノードのnewDBというネームスペースに切り替え
    • データ書き込み(insert発行)をしてみる
# localhost:27011, 27012, 27013 の3台構成
mongo --host "m103-example/m103:27011" -u "m103-admin" -p
"m103-pass" --authenticationDatabase "admin"

脱線

このあたりの動作確認が、やっぱり実機(動作環境)が無いと厳しいので、MongoDBのレプリカセットをざっくりdocker-composeで起動してみることにした。

セカンダリノードから同期を確認する

まずはプライマリに接続してデータ確認

// プライマリに接続
// % docker-compose exec primary mongo admin --port 27011 -u m103-admin -p m103-pass

// Mongo shellから
m103-example:PRIMARY> show databases
admin   0.000GB
config  0.000GB
local   0.001GB
newDB   0.000GB
m103-example:PRIMARY> use newDB
switched to db newDB
m103-example:PRIMARY> show collections
new_collection

// primaryからの読み取りはOK
m103-example:PRIMARY> db.new_collection.find()
{ "_id" : ObjectId("5fe74f088923de3de82dbba3"), "student" : "Matt Javaly", "grade" : "A+" }

レプリカセットに追加した「だけ」のセカンダリに接続

// % docker-compose exec third mongo admin --port 27013 -u m103-admin -p m103-pass

m103-example:SECONDARY> rs.isMaster().ismaster
false

// レプリカセットに追加しただけの状態で、show databasesしてみる
m103-example:SECONDARY> show databases
uncaught exception: Error: listDatabases failed:{
        "topologyVersion" : {
                "processId" : ObjectId("5fe7e85007dbcbaa8fba43f5"),
                "counter" : NumberLong(4)
        },
        "operationTime" : Timestamp(1609034597, 1),
        "ok" : 0,
        "errmsg" : "not master and slaveOk=false",
        "code" : 13435,
        "codeName" : "NotPrimaryNoSecondaryOk",
        "$clusterTime" : {
                "clusterTime" : Timestamp(1609034597, 1),
                "signature" : {
                        "hash" : BinData(0,"WdfyftgXMatY10PKQ8LX+IcNbB0="),
                        "keyId" : NumberLong("6910577760511459332")
                }
        }
} :
_getErrorWithCode@src/mongo/shell/utils.js:25:13
Mongo.prototype.getDBs/<@src/mongo/shell/mongo.js:147:19
Mongo.prototype.getDBs@src/mongo/shell/mongo.js:99:12
shellHelper.show@src/mongo/shell/utils.js:937:13
shellHelper@src/mongo/shell/utils.js:819:15
@(shellhelp2):1:1

"errmsg" : "not master and slaveOk=false" というメッセージが出ており、まだ受け入れ状態になってない。


m103-example:SECONDARY> rs.slaveOk()
WARNING: slaveOk() is deprecated and may be removed in the next major release. Please use secondaryOk() instead.
m103-example:SECONDARY> rs.secondaryOk()
  • rs.secondaryOk() で明示的に設定を有効にする

m103-example:SECONDARY> show databases
admin   0.000GB
config  0.000GB
local   0.001GB
newDB   0.000GB

m103-example:SECONDARY> use newDB
switched to db newDB

// 読み取りしてみる!
m103-example:SECONDARY> db.new_collection.find()
{ "_id" : ObjectId("5fe74f088923de3de82dbba3"), "student" : "Matt Javaly", "grade" : "A+" }

※ rs.slaveOk() は deprecatedで、rs.secondaryOk()を使ってね!といわれたのは、いわゆるmaster/slaveについてセンシティブになってきた関係で。

セカンダリノードが全部落ちてしまったら?

// メンバーの確認
m103-example:PRIMARY> members = rs.status().members
m103-example:PRIMARY> filtered = members.map(member => Object({ name: member.name, health: member.health }));
[
        {
                "name" : "primary:27011",
                "health" : 1
        },
        {
                "name" : "secondary:27012",
                "health" : 1
        },
        {
                "name" : "third:27013",
                "health" : 1
        }
] // 全部ヘルシー
m103-example:PRIMARY>

docker-compose stop secondary third してみる (2ノード停止)

// はじめのうちはMongo ShellのプロンプトはPRIMARY表示
m103-example:PRIMARY> filtered = members.map(member => Object({ name: member.name, health: member.health }));
[
        {
                "name" : "primary:27011",
                "health" : 1
        },
        {
                "name" : "secondary:27012",
                "health" : 0
        },
        {
                "name" : "third:27013",
                "health" : 1
        }
]
// 1ノードだけになると、やがてプロンプトはSECONDARYになる
m103-example:SECONDARY> filtered = members.map(member => Object({ name: member.name, health: member.health }));
[
        {
                "name" : "primary:27011",
                "health" : 1
        },
        {
                "name" : "secondary:27012",
                "health" : 0
        },
        {
                "name" : "third:27013",
                "health" : 0
        }
]

プライマリノード1つだけになると、ハートビートでの疎通が切れたのが確認でいた段階で、SECONDARYに降格となります。

読みとり、書き込みともにエラーに。

Chapter 2: Failover and Elections (動画)

  • プライマリノード以外のセカンダリノードがダウンした場合は、プライマリ&セカンダリが1つずつ存在する間はプライマリは問題なく稼働できる

プライマリノードを停止しないといけない状況はどんな時?

  • たとえばローリングアップグレードの場合
    • MongoDBは論理的な一貫性・レプリケーションを採用しているので、バージョンが違ってもレプリケーション可能
    • 基本的にはセカンダリから一時的にレプリカセットから切り離しバージョンを上げて再参加させる
  • 最後に残ったプライマリは、ノードが十分な状態で、まず「セカンダリに降格」させる
    • もとのプライマリノードが降格して、別のセカンダリがプライマリに切り替わったら、元のプライマリを切り離す
    • 元のプライマリをアップゴレードさせてレプリカセットに再参加
    • 必要に応じて新しいプライマリを降格させて、元のプライマリを昇格させるといった流れ

Electionについて

  • 基本はセカンダリノードが投票券を持つ
  • 優先度や重みづけはどれも等しいけれど、設定で変更可能
    • 元のプライマリノードが必ずしも再度プライマリになれるとは限らない

ノード復帰後、フェイルオーバーしてしまった例

  • port: 27011 のノードがプライマリだったが、Electionの結果セカンダリに

  • 設定を調整することで、任意のノードをプライマリにすることが可能

Priorityの調整

  • これも rs.config() でオブジェクトを取得し、調整をおこなって rs.reconfig(..) で反映

実験: セカンダリノードで設定は変更できる?

m103-example:SECONDARY> cfg = rs.conf()
{
        "_id" : "m103-example",
        "version" : 3,
        "term" : 9,
        "protocolVersion" : NumberLong(1),
        "writeConcernMajorityJournalDefault" : true,
        "members" : [
                {
                        "_id" : 0,
                        "host" : "primary:27011",
                        "arbiterOnly" : false,
                        "buildIndexes" : true,
                        "hidden" : false,
                        "priority" : 1,
                        "tags" : {

                        },
                        "slaveDelay" : NumberLong(0),
                        "votes" : 1
                },
                {
                        "_id" : 1,
                        "host" : "secondary:27012",
                        "arbiterOnly" : false,
                        "buildIndexes" : true,
                        "hidden" : false,
                        "priority" : 1,
                        "tags" : {

                        },
                        "slaveDelay" : NumberLong(0),
                        "votes" : 1
                },
                {
                        "_id" : 2,
                        "host" : "third:27013",
                        "arbiterOnly" : false,
                        "buildIndexes" : true,
                        "hidden" : false,
                        "priority" : 1,
                        "tags" : {

                        },
                        "slaveDelay" : NumberLong(0),
                        "votes" : 1
                }
        ],
        "settings" : {
                "chainingAllowed" : true,
                "heartbeatIntervalMillis" : 2000,
                "heartbeatTimeoutSecs" : 10,
                "electionTimeoutMillis" : 10000,
                "catchUpTimeoutMillis" : -1,
                "catchUpTakeoverDelayMillis" : 30000,
                "getLastErrorModes" : {

                },
                "getLastErrorDefaults" : {
                        "w" : 1,
                        "wtimeout" : 0
                },
                "replicaSetId" : ObjectId("5fe74ddcaa210dbfe10ea9e0")
        }
}
m103-example:SECONDARY> cfg.members[0].priority = 2
2
m103-example:SECONDARY> rs.reconfig(cfg)
{
        "topologyVersion" : {
                "processId" : ObjectId("5fe7e84d285fcedf15d21993"),
                "counter" : NumberLong(16)
        },
        "operationTime" : Timestamp(1609044585, 1),
        "ok" : 0,
        "errmsg" : "New config is rejected :: caused by :: replSetReconfig should only be run on a writable PRIMARY. Current state SECONDARY;",
        "code" : 10107,
        "codeName" : "NotWritablePrimary",
        "$clusterTime" : {
                "clusterTime" : Timestamp(1609044585, 1),
                "signature" : {
                        "hash" : BinData(0,"k0uthkumAnwh0OBJq3fZu3Praiw="),
                        "keyId" : NumberLong("6910577760511459332")
                }
        }
}

設定の読み取りはできたけど、変更は反映できない。
(New config is rejected :: caused by :: replSetReconfig should only be run on a writable PRIMARY というメッセージでエラー)

現在のプライマリから操作

// % docker-compose exec third mongo admin --port 27013 -u m103-admin -p m103-pass
m103-example:PRIMARY>
m103-example:PRIMARY> rs.isMaster()
{
        "topologyVersion" : {
                "processId" : ObjectId("5fe80f2751ab41c766ffd924"),
                "counter" : NumberLong(6)
        },
        "hosts" : [
                "primary:27011",
                "secondary:27012",
                "third:27013"
        ],
        "setName" : "m103-example",
        "setVersion" : 3,
        "ismaster" : true,
        "secondary" : false,
        "primary" : "third:27013", // thirdノードが現在のプライマリ
        "me" : "third:27013",
        "electionId" : ObjectId("7fffffff0000000000000009"),
        "lastWrite" : {
                "opTime" : {
                        "ts" : Timestamp(1609044765, 1),
                        "t" : NumberLong(9)
                },
                "lastWriteDate" : ISODate("2020-12-27T04:52:45Z"),
                "majorityOpTime" : {
                        "ts" : Timestamp(1609044765, 1),
                        "t" : NumberLong(9)
                },
                "majorityWriteDate" : ISODate("2020-12-27T04:52:45Z")
        },
        "maxBsonObjectSize" : 16777216,
        "maxMessageSizeBytes" : 48000000,
        "maxWriteBatchSize" : 100000,
        "localTime" : ISODate("2020-12-27T04:52:54.810Z"),
        "logicalSessionTimeoutMinutes" : 30,
        "connectionId" : 24,
        "minWireVersion" : 0,
        "maxWireVersion" : 9,
        "readOnly" : false,
        "ok" : 1,
        "$clusterTime" : {
                "clusterTime" : Timestamp(1609044765, 1),
                "signature" : {
                        "hash" : BinData(0,"msc+xN/ZnH0Y+IGn4hpFpCjVd2w="),
                        "keyId" : NumberLong("6910577760511459332")
                }
        },
        "operationTime" : Timestamp(1609044765, 1)
}

// まず設定のオブジェクト取得
m103-example:PRIMARY> cfg = rs.config()
{
        "_id" : "m103-example",
        "version" : 3,
        "term" : 9,
        "protocolVersion" : NumberLong(1),
        "writeConcernMajorityJournalDefault" : true,
        "members" : [
                {
                        "_id" : 0,
                        "host" : "primary:27011",
                        "arbiterOnly" : false,
                        "buildIndexes" : true,
                        "hidden" : false,
                        "priority" : 1,
                        "tags" : {

                        },
                        "slaveDelay" : NumberLong(0),
                        "votes" : 1
                },
                {
                        "_id" : 1,
                        "host" : "secondary:27012",
                        "arbiterOnly" : false,
                        "buildIndexes" : true,
                        "hidden" : false,
                        "priority" : 1,
                        "tags" : {

                        },
                        "slaveDelay" : NumberLong(0),
                        "votes" : 1
                },
                {
                        "_id" : 2,
                        "host" : "third:27013",
                        "arbiterOnly" : false,
                        "buildIndexes" : true,
                        "hidden" : false,
                        "priority" : 1,
                        "tags" : {

                        },
                        "slaveDelay" : NumberLong(0),
                        "votes" : 1
                }
        ],
        "settings" : {
                "chainingAllowed" : true,
                "heartbeatIntervalMillis" : 2000,
                "heartbeatTimeoutSecs" : 10,
                "electionTimeoutMillis" : 10000,
                "catchUpTimeoutMillis" : -1,
                "catchUpTakeoverDelayMillis" : 30000,
                "getLastErrorModes" : {

                },
                "getLastErrorDefaults" : {
                        "w" : 1,
                        "wtimeout" : 0
                },
                "replicaSetId" : ObjectId("5fe74ddcaa210dbfe10ea9e0")
        }
}

// 順番を確認。元のプライマリノードは0番目(primary:27011)
m103-example:PRIMARY> cfg.members
[
        {
                "_id" : 0,
                "host" : "primary:27011",
                "arbiterOnly" : false,
                "buildIndexes" : true,
                "hidden" : false,
                "priority" : 1,
                "tags" : {

                },
                "slaveDelay" : NumberLong(0),
                "votes" : 1
        },
        {
                "_id" : 1,
                "host" : "secondary:27012",
                "arbiterOnly" : false,
                "buildIndexes" : true,
                "hidden" : false,
                "priority" : 1,
                "tags" : {

                },
                "slaveDelay" : NumberLong(0),
                "votes" : 1
        },
        {
                "_id" : 2,
                "host" : "third:27013",
                "arbiterOnly" : false,
                "buildIndexes" : true,
                "hidden" : false,
                "priority" : 1,
                "tags" : {

                },
                "slaveDelay" : NumberLong(0),
                "votes" : 1
        }
]

// もとのプライマリの優先度を2に変更
m103-example:PRIMARY> cfg.members[0].priority = 2
2

// 設定を反映
m103-example:PRIMARY> rs.reconfig(cfg)
{
        "ok" : 1,
        "$clusterTime" : {
                "clusterTime" : Timestamp(1609044872, 1),
                "signature" : {
                        "hash" : BinData(0,"7fC3ABsyq+vD3uGnTNzJtDC2+jY="),
                        "keyId" : NumberLong("6910577760511459332")
                }
        },
        "operationTime" : Timestamp(1609044872, 1)
}

// 現在のプライマリノードを降格させる  (third:27013)
m103-example:PRIMARY> rs.stepDown()
{
        "ok" : 1,
        "$clusterTime" : {
                "clusterTime" : Timestamp(1609044872, 1),
                "signature" : {
                        "hash" : BinData(0,"7fC3ABsyq+vD3uGnTNzJtDC2+jY="),
                        "keyId" : NumberLong("6910577760511459332")
                }
        },
        "operationTime" : Timestamp(1609044872, 1)
}

// わりと即座にプロンプトが切り替わった!!
m103-example:SECONDARY> rs.isMaster()
{
        "topologyVersion" : {
                "processId" : ObjectId("5fe80f2751ab41c766ffd924"),
                "counter" : NumberLong(10)
        },
        "hosts" : [
                "primary:27011",
                "secondary:27012",
                "third:27013"
        ],
        "setName" : "m103-example",
        "setVersion" : 4,
        "ismaster" : false,
        "secondary" : true,
        "primary" : "primary:27011", // 切り替わった!
        "me" : "third:27013",
        "lastWrite" : {
                "opTime" : {
                        "ts" : Timestamp(1609044883, 2),
                        "t" : NumberLong(10)
                },
                "lastWriteDate" : ISODate("2020-12-27T04:54:43Z"),
                "majorityOpTime" : {
                        "ts" : Timestamp(1609044883, 2),
                        "t" : NumberLong(10)
                },
                "majorityWriteDate" : ISODate("2020-12-27T04:54:43Z")
        },
        "maxBsonObjectSize" : 16777216,
        "maxMessageSizeBytes" : 48000000,
        "maxWriteBatchSize" : 100000,
        "localTime" : ISODate("2020-12-27T04:54:52.096Z"),
        "logicalSessionTimeoutMinutes" : 30,
        "connectionId" : 24,
        "minWireVersion" : 0,
        "maxWireVersion" : 9,
        "readOnly" : false,
        "ok" : 1,
        "$clusterTime" : {
                "clusterTime" : Timestamp(1609044883, 2),
                "signature" : {
                        "hash" : BinData(0,"J10IVbVflLCoqPpPfTRBFunYbQw="),
                        "keyId" : NumberLong("6910577760511459332")
                }
        },
        "operationTime" : Timestamp(1609044883, 2)
}

Chapter 2: Failover and Elections (クイズ)

Problem

Which of the following is true about elections?

こたえ

  • Nodes with priority 0 cannot be elected primary.
  • Nodes with higher priority are more likely to be elected primary.

Chapter 2: Write Concerns: Part 1 / Part 2 (動画)

Ref: https://docs.mongodb.com/manual/reference/write-concern/

Write concern describes the level of acknowledgment requested from MongoDB for write operations to a standalone mongod or to replica sets or to sharded clusters. In sharded clusters, mongos instances will pass the write concern on to the shards.

Write concern (書き込み確認) は、MongoDBに対して書き込み操作を行う際に、どのレベルで書き込み操作を伝播するかというのを設定するために利用します。単一ノードやレプリカセット、あるいはクラスタ構成のノードに対して、どのようなレベルで書き込みを伝播させるか、ということです。

ちょっと難しい...けど、動画を見てそれとなく理解。

  • Durability (耐障害性)と、書込みの伝播(各ノードへの反映)は、トレードオフの関係になる

  • ノード数が多い場合、確実に伝播させたほうが耐障害性は高くなるが、ノードへの書込みが完了したことを確認するまでの待ち時間があるため、クライアントからの要求に答えるまでに時間がかかってしまう

  • Write Concern

    • 0: アプリケーションの書き込み要求を受け付けたら、即クライアントに結果を返す
      • プライマリやセカンダリへの書き込みが完了したかどうかは問わない
      • プライマリノードに接続できたかどうかでしか失敗or成功を返さず、接続後の書き込み要求が出されたあとはなにも返事はしない
    • 1: MongoDBのデフォルト。
      • プライマリノードに書き込みがあったことを確認するだけ
      • セカンダリへの結果は問わない
    • 2以上: 書き込みの合計がプライマリとセカンダリノードの数であることを指定
      • 2であれば、プライマリ+もう一台
      • 3であれば、プライマリ+セカンダリ2台

ノードが増えるたびに、WriteConcernの数を更新、調整するのは手間がかかるので、こういう場合は "majority" を指定しておくと、書き込み確認のノード数が過半数以上になるように自動で調整される

Write Concernのオプション

数値、もしくはmajorityでの書き込みノード数の担保以外にも、オプションがある。

  • j Option

    • true: 書き込みに当たって、ジャーナルログへのコミットまで担保する
    • false: メモリ上に書き込み内容が伝播された時点でOKとする
  • wtimeout

    • 書き込みまでのタイムアウトを指定する

Chapter 2: Write Concerns (クイズ)

Problem

Consider a 3-member replica set, where one secondary is offline. Which of the following write concern levels can still return successfully?
3ノードでのレプリカセット構成で、セカンダリが1つオフラインになってしまった。
どのWriteConcernのレベルなら、この状態で書き込み操作があった場合に「成功」と返しますか?

こたえ

  • majority

(0, 1, 2 以外では 2/3 で過半数ということでmajority)

Chapter 2: Lab - Writes with Failovers (練習問題)

Evaluate the effect of using a write concern with a replica set where one node has failed.
Consider a 3-node replica set with only 2 healthy nodes, that receives the following insert() operation:

use payroll
db.employees.insert(
  { "name": "Aditya", "salary_USD": 50000 },
  { "writeConcern": { "w": 3, "wtimeout": 1000 } }
)

レプリカセット構成で、ノード数3のうち、1ノードに障害があった場合。
上記のinsert操作で何が起こりますか?

  • When a writeConcernError occurs, the document is still written to the healthy nodes.
    • writeConcernErrorが返っても、正常に動いているノードには書き込みが行われる
  • The unhealthy node will have the inserted document when it is brought back online.
    • 問題のあったノードが復帰したら、書き込みが伝播される

wtimeoutの指定が無い場合の答えを間違っちゃった!!!

If wtimeout is not specified, the write operation will be retried for an indefinite amount of time until the writeConcern is successful. If the writeConcern is impossible, like in this example, it may never return anything to the client.

タイムアウトの指定が無い場合は、writeConcernの件数を満たすまでリトライし続けます!
writeConcernが完了とならない場合、クライアントにはなにも結果を返しません。(失敗か成功かどうかも返せません)

ここまでの進捗

毎日はレポしませんが、Chapter2はあと少し。
練習問題で間違えてしまいました。。。(赤いです)

Discussion