BT

最新技術を追い求めるデベロッパのための情報コミュニティ

寄稿

Topics

地域を選ぶ

InfoQ ホームページ アーティクル MongoDBを始めた頃に知っていたら、と思う14のこと

MongoDBを始めた頃に知っていたら、と思う14のこと

原文(投稿日:2018/09/13)へのリンク

私は語るのも恥ずかしいほど長くデータベースに携わっていますが、MongoDBを扱い始めたのは最近のことです。MongoDBを始めた頃に知っていたら、と思うことがいくつかあります。一般的な経験として、データベースはどんなものなのか、何をするものなのか、という先入観は必ずあります。みなさんがMongoDBを簡単に使えれば、という思いで、よくある過ちをいくつか紹介しましょう。

認証なしでMongoDBサーバを立ち上げてしまった

不幸なことにMongoDBは、デフォルトでは認証機能なしでインストールされます。ワークステーションやローカルアクセスだけであれば問題ないのですが、MongoDBは可能な限り多くのメモリを使用するマルチユーザシステムなので、開発作業用であっても、十分なRAMを積んだサーバにインストールした方がずっと便利です。認証機能なしでデフォルトのポート上にインストールするのは、特にクエリ内でJavaScriptを自由に実行できる場合には、問題の元になります(例えば、$whereはインジェクション攻撃の進路になります)。

認証の方法はいくつかありますが、ユーザID/パスワードによる資格確認がインストールも管理も簡単です。LDAPベースの認証が格好いいと思うのでしたら、それもよいでしょう。問題なのはセキュリティなのですから、MongoDBを常に最新にしておくことも必要です。サインインや不正アクセスもログで常にチェックしてください。ポートも変更しておいた方がよいでしょう。

MongoDBの攻撃可能面の対策を忘れていた

MongoDBのセキュリティチェックリストには、ネットワークへの侵入やデータ侵害のリスクを低減するためのアドバイスが紹介されています。開発者はともすればセキュリティを軽視したり、高度なセキュリティは必要ないと考えがちですが、そうではありません — すべてのMongoDBサーバに関係することなのです。具体的には、mapReducegroup、あるいは$whereを使用する特別な理由がない限り、コンフィグファイルで“javascriptEnabled:false”をセットして、JavacSriptを無効にしておくべきです。MongoDBの標準データファイルは暗号化されているので、データファイルへのフルアクセス権を持つ専用ユーザでMongoDBを実行して、オペレーティングシステム自体のファイルアクセス管理を使用することも賢明でしょう。

スキーマの設計に失敗した

MongoDBでは、スキーマは必須ではありません。ですがこれは、必要ないという意味ではありません。実際に、一貫性のあるスキーマを持たずにドキュメントを保存したい場合、保存は極めて高速で簡単なのですが、取得する時は大変なことになります

古典的な記事の“6 Rules of Thumb for MongoDB Schema Design”は一読の価値がありますし、Studio 3Tのようなサードパーティツールの持つSchema Explorer的な機能も、常日頃のスキーマチェック用に準備しておくと役に立ちます。

コレーション(ソート順)のことを忘れていた

これは他の設定ミスよりも、フラストレーションや時間の浪費につながる可能性があります。MongoDBは、デフォルトではバイナリコレーションを使用しますが、これはどのカルチャでも役に立ちません。大文字小文字やアクセントを区別するバイナリコレーションは、あご髭やカフタンや巻き髭と同じように、80年代的な珍しいアナクロニズムと考えられます。今となっては使い道がないのです。実生活では、“motortbike”は“Motorbike”と同じですし、“Britain”と“britain”は同じ場所です。小文字は、大文字の筆記体に相当するものに過ぎません。アクセント付き文字(発音記号)のコレーションについては、私では説明できません。MongoDBでデータベースを生成する時は、アクセントや大文字小文字を区別しない、システムの言語やユーザのカルチャに適したコレーションを使用してください。文字列データによる検索がずっと簡単になります。

大きなドキュメントでコレクションを生成してしまった

MongoDBは16MBまでの大容量ドキュメントをコレクションに収容可能ですし、GridFSは16MB以上のドキュメントを扱えるように設計されています。ですが、可能であるということは、それが望ましいという意味とは限りません。SQLテーブルの行と同じように、MongoDBでも、個々のドキュメントを数キロバイトのサイズに保つと最も効果的です。大きなドキュメントは、パフォーマンス上の問題を発生させます。

大きな配列でドキュメントを作成してしまった

ドキュメントには配列を含めることができますが、配列の要素数は4桁未満にすることがベストです。要素が頻繁に追加され、格納されているドキュメントを超えると、ディスク上のロケーションを変更する必要が生じて、結果的にすべてのインデックスを更新しなければなりません。大きな配列を持つドキュメントでは、別々のインデックスエントリが配列の全要素に存在するため、インデックス書き換えが何回も実施されることになります。同じようなインデックス更新は、ドキュメントの挿入や削除でも発生します。

この問題を最小限に抑えるため、MongoDBでは、ドキュメントが拡張するためのスペースとして“パディングファクタ”を持っています。

配列にインデックスを作成しなければ、この問題を回避できると思うかも知れませんが、残念ながら、インデックスがなければ、また別の問題に突き当たる可能性があります。ドキュメントが最初から最後までスキャンされるため、配列の最後に行くほど要素検索に時間が掛かるようになるのです。そのようなドキュメントを扱うオペレーションのほとんどが遅くなります

集約でのステージ順が重要であることを忘れていた

クエリオプティマイザを備えたデータベースシステムでは、記述されたクエリは、取得の方法ではなく、何が欲しいのかを記述したものになります。レストランの注文と同じです — 普通は料理を注文するのであって、詳しい方法をコックに注文することはありません。

MongoDBでは、調理方法をコックに教えます。例えば、$matchや$projectを使ってパイプラインのできるだけ早い段階でデータを絞り、そのデータに一度だけソートを実行し、それから目的とするルックアップが行われるようにする必要があります。不要な作業を取り除くクエリオプティマイザがあると、ステージを最適に並べ替えて、Joinのタイプを選択することで、あなたの意図が台無しになるかも知れません。MongoDBはコントロール性では優れていますが、利便性の面でコストがあります。

Studio 3Tのようなツールは、正しいMongoDB集約クエリの作成を簡単にしてくれます。Aggregation Editor機能を使用すれば、パイプライン演算子を1ステージずつ適用して、各ステージのインプットとアウトプットを検証することで、デバッグが容易になります。

高速書き込みを使用した

永続性(durability)が低い場合、MongoDBを高速書き込みに設定してはいけません。この“ファイア・アンド・フォーゲット”モードでは、実際に書き込みを行う前にコマンドが返ってくるので、書き込みが速いように見える一方、データがディスクに書き込まれる前にシステムがクラッシュすると、データが消失して一貫性が損なわれるリスクがあります。幸いにも64-bit MongoDBでは、ジャーナリングが有効です。

MMAPv1とWiredTigerストレージエンジンは、どちらもジャーナリングを使ってこれを防止しますが、WiredTigerではジャーナリングがオフになっていても、リカバリ時には、一貫性のある最後のチェックポイントでリストアすることが可能です。

ジャーナリングは、リカバリ時にデータベースが一貫性のある状態であることを保証すると同時に、ジャーナルが書かれた時点までのすべてのデータを保持します。ジャーナルの書き込み間隔は、実行時オプションのcommitIntervalMsを使って設定できます。

書き込みを間違いなく実施するためには、コンフィギュレーションファイルでジャーナルが有効(storage.journal.enabled)であることを確認してください。コミットのインターバルが、データを紛失する場合の間隔に相当します。

インデックスなしでソートした

検索や集計ではデータの並べ替えるがよくありますが、結果をフィルタリングして、ソートするデータの量を減らした上で、最後のステージに1回だけ行うようにできれば好都合です。その場合においても、ソートをカバーするインデックスは必要です。単一インデックスでも複合インデックスでも構いません。
適当なインデックスが利用できなければ、MongoDBはインデックスなしで ソートを実行しようとしますが、ソート操作には全ドキュメントの合計サイズが32MBまでという制限があります。MongoDBがこの制限に達すると、エラーが生成されるか、場合によっては単に空のレコードセットが返されます

サポートインデックスのないルックアップ

ルックアップ(lookup)は、SQL joinと同じような機能を実行します。十分なパフォーマンスを得るには、外部キーとして使用するキー値にインデックスが必要です。このことはexplan()では報告されないため、明確ではありません。これらのインデックスは、$matchおよび$sortパイプライン演算子がパイプラインの開始時に発生した場合に、explain()が記録するインデックスに追加されるものです。集約パイプラインのどのステージでもインデックスを使用することができます

マルチアップデートを使用しなかった

既存のドキュメントの一部ないし全体の更新や、既存ドキュメント全体の置き換えには、db.collection.update()メソッドを使って、それぞれのアップデートパラメータを指定します。クエリ条件に一致する全ドキュメントを更新するようにマルチパラメータをセットしない限り、コレクションないの全ドキュメントが実行対象にならないということは、必ずしも明確ではありません。

ハッシュオブジェクト内のキー順序の重要性を忘れていた

JSONでは、オブジェクトは0個以上の名前と値のペアによる、順序のないコレクションで構成されます。名前は文字列で、値は文字列、数値、ブール値、ヌル、オブジェクト、あるいは配列です。

残念ながらBSONでは、検索時に順序が重要な意味を持ちます。埋込みオブジェクト内のキーの順序は、MongoDBでは意味を持っています。例えば‘{ firstname: "Phil", surname: "factor" }’と‘{ surname: "factor", firstname: "Phil" }’は一致しません。これはつまり、名前と値のペアを確実に見つけるためには、ドキュメント内での順序を維持する必要がある、ということです。

‘nul’と‘undefined’を混同した

公式のJSON標準(ECMA-404, Section 5)によれば、‘undefined’はJSONでは有効ではないのですが、現実にはJavaScriptで使用されています。さらにBSONでは“非推奨”である上、$nullに変換されるのですが、これは必ずしも満足できる解決策ではありません。MongoDBでは‘undefined’を使用しないでください

$sort()を使わずに$limit()を使用した

MongoDBで開発を行なっているとき、クエリや集約から返される結果のサンプルだけで十分な場合がよくあります。$limit()がこの目的で使用できますが、最初に$sortを使用しない場合には、コードの最終バージョンには置かないでください。結果の順序を保証することができないため、データを確実に“ページ”することができないからです。結果の先頭レコードは、ソートを行った方法によって異なります。確実な動作のためには、クエリないし集約は“決定論的”、すなわち、実行するたびに同じ結果が得られなくてはなりません。$sortを使わない$limitは決定論的でないので、後になって追跡の難しいバグを発生させる可能性があります。

結論

MongoDBに失望を感じるのは、RDBMSなど違うタイプのデータベースと直接比較してしまうか、あるいは何か特別な期待を持って導入しようとする場合に限られます。オレンジとフォークを比較するようなもので、データベースシステムにはそれぞれの目的があります。それぞれの違いを認識し、理解することが最善の方法です。MongoDBの開発者たちに対して、RDBMSの流儀を余儀なくさせるような道を迫る圧力があるのは残念なことです。データの整合性を保証したり、データシステムを障害や悪用に強くするというような、古い問題を新しい、興味深い方法で解決することを今後も期待しています。

MongoDBがバージョン4.0でACIDトランザクションを導入したことは、重要な改善を革新的な方法で導入した好例です。マルチドキュメント、マルチステートメントのトランザクションがアトミックになりました。ロックの取得やトランザクションの期限切れの時間を調整したり、アイソレーションレベルを変更することも可能です。

著者について

Phil Factor氏(訴追を避けるため匿名)、別名Database Moleは、データベース中心のアプリケーションに40年近いキャリアを持っています。かつては1980年初頭のある展示会で、激怒したBill Gates氏に怒鳴りつけられていたこともありましたが、自身のキャリアを通じて匿名を頑なに守り続けてきました。

この記事に星をつける

おすすめ度
スタイル

BT