出典について
この記事はMark Callaghan氏のブログ内の記事、Losing it?(2015/10/11)を翻訳したものです。
何年も前、Google社のMySQLチームのWei Li氏の厚意によって準同期レプリケーションが実装されました。この準同期レプリケーションのユースケースは限られており、コミュニティーはこれが意図した動作をしなかったため失望しました。そのうちに、InnoDBのバイナリログにグループコミットが実装されました。最初はFacebook社のMySQLチームに、そしてMariaDBで改善され、最後にMySQLのアップストリームに取り込まれました。グループコミットによって、いくつか魔法のような機能が追加され、これによりロスレスの準同期が可能となり、私達は必要以上のレプリカを利用することのないロスレスかつ高速なフェールオーバーを自動化してきました。この機能は大変重要です。私は、MariaDB(binlog serverとMaxScaleによる)とMySQL(Fabricとプロキシによる)のソリューションを楽しみにしています。
MongoDBはスケールアウトを簡単に管理できる機能においてMySQLに比べて先行しており、次のリリース(3.2)でいくつか機能が追加され、かつ既存の機能がロバストになります。スケールアウトという名目で犠牲になってきたいくつかの機能が最終的にMongoDBのリリースで利用可能となることを願っています。すなわち、シャード毎のトランザクション、シャード毎のJOIN、シャード毎の一貫性のある読取り(consistent read)、です。
挙動
我々はユーザーが挙動を理解すべきとき、詳細が分からなくなったときに、アルゴリズムを何回も説明します。 トランザクションがいつロストしうるかを開発者が理解するのは重要で、これから2つの質問とそれに対する回答を説明しようと思います。
- 同時実行するセッションに対して見えるコミットによる変更の後、どのような条件でコミットがロストしうるのでしょうか?
- クライアントにコミットが成功したと伝えられた後、どのような条件でコミットがロストしうるでしょうか?
詳細
MySQLとMongoDBで考慮すべき挙動および動きについて次の4つの組合わせをリストアップしました。
- 可視性を損ない、受信確認されたものをロストしうるもの
- MySQL、MongoDB: 非同期レプリケーションを利用した場合
- 可視性を損なうが、受信確認されたものをロストしないもの
- MySQL: 準同期レプリケーションを利用した場合
- MongoDB: majority write concernを利用した場合
- 可視性は損なわないが、受信確認されたものをロストするもの
- 今は無視します
- 可視性を損なわず、 受信確認されたものをロストしないもの
- MySQL: ロスレス準同期レプリケーションを利用した場合
- MongoDB: majority read concernおよびmajority write concernを利用した場合
訳注: 可視性を損なうかどうかについては、前節(挙動)の1番、受信確認したものをロストするかどうかは2番に対応しています。
本編
MySQLは可視性を損なわず同時にコミット確認されたものをロストしない為には1つのソリューションを利用します(ロスレス準同期レプリケーション)。ロスレス準同期レプリケーションでは、最低1台のスレーブからのコミット確認が返ってくるまでマスター上で行ロックを保持します。行ロックを解放しコミットを完了させる前にマスターとレプリカの間のネットワークラウンドトリップがあるためコミットのスループットが低下するでしょう。松信氏によって提唱されたソリューションでは、各データセンタに追加のレプリカを作成するのではなく、バイナリログのアーカイブで受信確認を行います。バイナリログのアーカイブはハードウェアを大量に必要とはしませんし、かつマスターの近くに移動するのが容易だからです。
MongoDBでは2つのソリューションで完全に保護されます。すなわちmajority read concernおよびmajority write concernです。負荷に十分な同時並行度があると仮定すると、majority write concernではコミットが低速にならないという長所があります。MySQLの準同期レプリケーションと同様に各クライアントがコミットする際に遅延の影響が追加で発生することでしょう。またmajority read concernの機能は見えているコミットをロストしないようにします。 しかしながら、自身の書込み動作に対して読取りを行う必要のあるクライアントに対して、待ちが発生するリスクがあります。現段階では、MongoDBが自身の書込みに対する読取りを容易にできるかどうかはわかりません。私は、アプリケーションの性能を気にしているのならmajority read concernをmajority write concernなしで利用したら良いのではないか、と考えています。これはMySQLには無い選択肢です。
性能の差分への影響を理解するのは時間がかかるでしょう。MySQLでは準同期のコミットのプロトコルで連絡するのに時間がかかるようになる為、遅延は書込む側に加えられます。MongoDBではmajority readのスナップショットが伝播するのを待つため、遅延は読取り側に加えられます。
スレーブで永続化するか?
レプリカがマスターに受信応答を返したとき、どのように変更が永続化されているかを理解するのは重要です。これにはいくつかの選択肢があり、私は最近のMySQLあるいはMongoDBのドキュメントを十分に確認できていないので、メモリー内で永続化を超える選択肢があるかどうかは確信を持てません。
- メモリー内で永続化: コミットが受信応答を返す前にレプリカのメモリー内でバッファリングされます。数年前にMySQLで準同期レプリケーションが実装されたときは、これしか選択肢がありませんでした。私は悲観論者で、メモリー 内で永続化するというのは矛盾しているのではないかと考えています。
- ログへの永続化: コミットが受信応答を返す前にログに永続化されます。MySQLでできるという講演がありました。
- レプリカ内でコミットされる: コミットが受信応答を返す前にレプリカに適用されます。マスターに変更がコミットされた直後にレプリカにクエリーが発行された時に、自身の書込みを読取れるという動作を保証します。悲しいかな、これもレプリカが変更を同時に適用する為にマスター同様の多くのスレッドを利用しない限り性能上の遅延を発生させることになります。
本編の詳細
MongoDBのドキュメンテーションはソフトウェアにより提供される機能に対して楽観的である傾向があります。これはコミュニティーが成長し大きくなれば解決されるものと考えています。私はいくつか興味深い発見をしました。ドキュメントに記載されていることと実際の挙動の差分が時と共に減っていくことを望んでいます。
mmapエンジンはデータベース毎あるいはインスタンス毎の書込みロックを、例え書込みの永続化をリクエストされていたとしてもoplogを同期する前に解放します。これは現在ではREAD UNCOMMITTEDと呼ばれていますが、READ NON-DURABLEがよりふさわしいでしょう。読取り一貫性があるもののoplogに変更が永続化される前に他から変更が見えてしまうからです。私はコードを読み本件について書き記し、ドキュメントがそれから更新されたものの、私はまだ編集する必要があると思っています。これはmmapエンジンの問題に過ぎませんし、MongoDBに複数のエンジンがあるということはmmapとWiredTigerを比べ挙動をクリアにする必要があります。
majority write concernの動作については言い過ぎなところがありました。これは受信応答を返したコミットをロストしないことを保証しますが、いくつかのドキュメントではコミットの可視性を損なわないことを保証することが示唆されています。分散システムの検査のエキスパートであるAphyr氏は、彼のCall Me Maybeの連載およびバグレポートでこの問題にハイライトをあてました。私は一部の問題について書き記しましたが、この問題をドキュメンテーションの言い過ぎであることと結びつけませんでした。何年前かにMySQLも準同期レプリケーションのドキュメントを作成した時に似た過ちを犯し、私がバグを申し出てからドキュメントが修正されました。
ドキュメントにConfigサーバーの同期を保つ為に2相コミットが利用されていることが記載されていました。これによってコミットは同じデータをホスティングしている3または5のConfigサーバーに対してオール・オア・ナッシングとなっていそうにみえます。悲しいかな、サーバーが消失した場合は読取り専用になりうるのです。私はコードを読み、2相は次の通りでした。
- ) 全Configサーバーにpingをし、全台が応答したら
- ) 変更を全Configサーバーに送る
全サーバーがOK応答を返さなかった場合、手動介入が必要となります。これは2相コミットではありません。幸運なことに、3.2のリリースでいくらか改善されており、ドキュメントも更新されました。
最後に、Tokutekのレプリカセットのフェイルオーバーに関するすばらしい連載の投稿をご紹介します。概要および1、2、3、4です。 広くは知られていない問題について記載されています。幸いMongoDBの3.2のリリースで改善されているとおもいます。