レプリケーションの動作やoplogがよく理解できていない方は、本連載のレプリケーションにおける読み取り負荷分散の性能を先に読むことをお勧めします。
レプリケーション遅延
MongoDBのレプリケーションにおいて最もよくある問題はレプリケーションの遅延でしょう。通常セカンダリはロングポーリングという手法を使い、プライマリのoplogが更新されると即座に自身にoplogを適用します。しかし、何かの理由によりoplogを即座に適用できないと、レプリケーション遅延(Replication Lag)になります。
レプリケーション遅延が発生すると、以下のような問題が発生する可能性があるため、放っておくのはよくありません。
- セカンダリ読み込みにより読み込んだデータが古い
- プライマリ故障時に新しいプライマリの選出が遅くなり、書き込み停止時間が延びる
- プライマリ故障時にデータの不整合が発生する
- oplogが溢れるとレプリケーションが停止し、システムの可用性が低下する
レプリケーション遅延が発生する理由として、分かりやすいものは「ネットワークが輻輳し始めた」、「リクエストが増えた」、「セカンダリが稼働しているサーバで、MongoDB以外のプロセスがリソースを消費し始めた」などの外的要因でしょう。これはシステムの状況を観測していれば検知できますし、対策も簡単でしょう。
一方、外的要因は無いのに突如レプリケーション遅延が発生する場合があります。多くの場合、これはoplogの容量が想定より大きくなって処理しきれなくなっていることが原因です。実はクエリの量は必ずしもoplogの量とは一致しないのです。
oplogの肥大化
oplogが肥大化するメカニズムについて説明していきましょう。通常、クエリの内容とoplogの内容は一致しています。例えばMongoDBで以下のinsertを実行したとします。
db.mycol.insert({a:1})
するとoplogには以下のようにaの値を1にするように記載されます。
{ "ts" .... "o" : { "_id" : ObjectId("xxe8"), "a" : 1 } }
次にupdateはどうでしょうか?
db.hoge.updateMany({},{$set : {a:1}})
すると、クエリの数は一つですが、以下の様にoplogにはコレクション内の全ドキュメントに対する更新が1件ずつ記録されました。
{ "ts" ... "o2" : { "_id" : ObjectId("xxe9") }, "o" : { "$set" : { "a" : 1 } } } { "ts" ... "o2" : { "_id" : ObjectId("xxe1") }, "o" : { "$set" : { "a" : 1 } } } { "ts" ... "o2" : { "_id" : ObjectId("xxf8") }, "o" : { "$set" : { "a" : 1 } } } ・・・
これは、MongoDBが複数のドキュメントを整合性をもって更新するトランザクションを提供していないためです。一回のクエリで更新できるのは一つのドキュメントですので、一つの更新クエリは、コレクション内の全てのドキュメントに対する更新クエリに変換されます。これはremoveでも同じことが起きます。このように、単純にクエリの量だけを考えているとoplogの数とは合わないため注意が必要です。
更新や削除は少し考えれば肥大化することがわかりますが、意外なクエリもあります。例えば、配列の先頭を取り出す$popや、配列の要素を検索して削除する$pull等です。これらのクエリは、更新するデータの量とは関係なく配列の全データをoplogに記録します。配列が10MByteあれば10MByte分oplogに記録されます。
実際に動作を見てみましょう。例えば、以下の例では配列から3を検索して削除しています。
db.hoge.insert({ary:[1,2,3,4,5]}) db.hoge.updateMany({},{$pull:{ary:3}})
すると、以下の様にoplogには新しい配列全て([1,2,4,5])が記録されてしまいます。
{ "ts" ... "o2" : { "_id" : ObjectId("xxxx") }, "o" : { "$set" : { "ary" : [ 1, 2, 4, 5 ] } } }
こうなってしまう理由は、oplogが何度適用しても結果が変わらない性質(冪等性)を備えているためです。配列の$popや$pullは複数回実行すると結果が異なってしまうため、冪等な操作にするため新しい配列で上書きするクエリに書き換えています。これを知らずに巨大な配列に対して$popや$pullを利用すると、oplogが予想以上に肥大化し、レプリケーションの遅延を招きます。ただし、配列の末尾に要素を追加する$pushはoplogの肥大化を引き起こしません。$pushは配列の長さと新しい要素だけが記録されます。