MongoDB: Data Model Design
📝 MongoDB のドキュメントを箇条書きしていく。随時更新。
Env / Versions
- MongoDB v3.6
Embedded Data Models (埋め込みモデル)
Example:
{ _id: <ObjectId1>, username: "123xyz", contact: { phone: "123-456-7890", email: "xyz@example.com" }, access: { level: 5, group: "dev" } }
- 非正規化モデル
- より少ないクエリとアップデートで済む
- 次のような時に使用する
- エンティティ間に「包含」関係がある。1対1の関係
- エンティティ間に1対多の関係がある。子ドキュメントは常に親ドキュメントのコンテキストで表現される
- 🙆♀️ Strengths
- 単一のデータベースから関連するデータを取得するのと同じように、参照操作のパフォーマンスを向上させる
- 1回のアトミックな更新操作で関連するデータを更新することができる
- 🤦♀️ Weaknesses
- 関連データをドキュメントに埋め込むと、作成後にドキュメントサイズが大きくなる
- MMAPv1ストレージエンジンを使用すると、ドキュメントの増大が書き込みのパフォーマンスに影響を与え、データの断片化を招く可能性がある
- 📝 ドキュメントのサイズは最大で 16 megabytes
Normalized Data Models (参照モデル)
Example:
// user document { _id: <ObjectId1>, username: "123xyz" } // contact document { _id: <ObjectId2>, user_id: <ObjectId1>, phone: "123-456-7890", email: "xyz@example.com" } // access document { _id: <ObjectId3>, user_id: <ObjectId1>, level: 5, group: "dev" }
- 正規化モデル
- 次のような時に使用する
- embedded による読み込みのパフォーマンスのメリットがデータが重複してしまうデメリットを上回らないとき
- より複雑な多対多の関係を表現するとき
- 大きな階層化されたデータセットをモデル化するとき
- 参照モデルは埋め込みモデルよりもフレキシブル。
- ただし、クライアント側アプリケーションは、参照モデルを解決するためにフォローアップクエリを発行する必要がある。
- 📝ドキュメント間の関連性の解決はクライアント側が担う(JOINとかないので、複数クエリの発行が必要)
- 正規化されたデータモデルでは、サーバーへのより多くのラウンドトリップが必要になる可能性がある
Links
MongoDB : Replica Set の参照設定
📝MongoDB のドキュメントを箇条書きしていく。随時更新
Env / Versions
- MongoDB v3.6
参照の優先度設定
- ⚠️ Primary から Secondary への非同期レプリケーションには遅延が発生するため、古いデータが返される可能性がある
参照のモード
- 各 MongoDB ドライバーは以下の5つのモードをサポートしている
primary
- Primary からのみ参照する
- Primary が使用できない場合はエラーになる
- デフォルト
primaryPreferred
- Primary から参照するが使用できない場合は Secondary から参照する
secondary
- Secondary からのみ参照する
- 使用可能な Secondary が無い場合はエラーになる
secondaryPreferred
- Secondary から参照するが使用可能な Secondary が1台もない場合は Primary から参照する
nearest
- ネットワークレイテンシが最も低いメンバーから参照する
- Primary, Secondary 問わない
サーバ選択アルゴリズム
- サーバーの選択は操作ごとに1回発生し、参照設定と
localThresholdMS
の設定によって決められる - サーバ選択アルゴリズムは各クライアントで実装されている
secondary
またはsecondaryPreferred
モードを指定した場合- 最も低いレイテンシ + 15msec 以内のサーバを「使用可能」とする
- 15msec はデフォルト値であり変更可能
- 「使用可能」なサーバ群からランダムで接続先が決定される
Links
🍣 Itamae & Serverspec ことはじめ
Itamae を使って Vagrant 環境を構築してみます。 また、構築した環境は Serverspec でテストします。
今回のサンプルコードは GitHub にアップしました。
Env
- Mac OS X El Capitan 10.11.4
- ruby 2.3.1
- rake 11.1.2
- itamae 1.9.6
- serverspec 2.34.0
- Vagrant 1.8.1
Vagrant セットアップ
まずは Vagrant の設定を。
$ mkdir workdir; cd workdir $ vagrant init
CentOS7 で試してみたことがあったので、今回は Ubuntu でいきます。
$ vagrant init ubuntu/trusty64; vagrant up --provider virtualbox
ref: https://git.io/vrsCY
起動した Vagrant にログインできることを確認。
$ vagrant ssh
Itamae で Nginx をインストール
Itamae も Serverspec も Ruby の Gem で公開されています。 Bundler を使ってインストールします。
$ bundle init $ vim Gemfile $ # diff + gem 'itamae' + gem 'serverspec' $ bundle install --path vendor/bundle --jobs 4
ref: https://git.io/vrsB4
Itamae には決まったディレクトリ構成はありませんが、推奨の構成があります。Best Practice · itamae-kitchen/itamae Wiki 今回はこの構成で進めていきます。
$ bundle exec itamae init
$ tree
.
├── Gemfile
├── Gemfile.lock
├── Vagrantfile
├── cookbooks
└── roles
手始めに Nginx のインストールレシピを書いてみます。
$ vim cookbooks/nginx/default.rb $ vim roles/web.rb
ref: https://git.io/vrsB4
このレシピを Vagrant に反映します。
$ # .ssh/config に Vagrant のホスト設定を追加 $ vagrant ssh-config --host localhost.ubuntu >> ~/.ssh/config $ $ # Itamae を実行 $ itamae ssh --vagrant --host localhost.ubuntu roles/web.rb INFO : Starting Itamae... INFO : Recipe: /your/workdir/roles/web.rb INFO : Recipe: /your/workdir/cookbooks/nginx/default.rb
Vagrant 上で起動を確認
$ vagrant ssh
vagrant@localhost:~$ service status nginx
status: unrecognized service
Serverspec で Nginx 周りをテスト
さて、せっかくサーバのセットアップがコード化できたので、設定の確認作業もコードで管理したいところです。
Serverspec でテストコードを書いていきます。 Serverspec も Itamae 同様にディレクトリをどう構成するするかは自由ですがコマンドでテンプレートを作ることができます。 こちらもデフォルトの構成で進めます。
serverspec-init コマンドを使って対話式でテンプレートを作成します。
$ bundle exec serverspec-init Select OS type: 1) UN*X 2) Windows Select number: 1 Select a backend type: 1) SSH 2) Exec (local) Select number: 1 Vagrant instance y/n: y Auto-configure Vagrant from Vagrantfile? y/n: n Input vagrant instance name: localhost.ubuntu + spec/ + spec/localhost.ubuntu/ + spec/localhost.ubuntu/sample_spec.rb + spec/spec_helper.rb + Rakefile + .rspec
ref: https://git.io/vrs7b
spec 配下に作成されたディレクトリはテスト対象のホスト名です。 テストコードを対象ホスト単位でまとめるのか、ロール(web や db など)でまとめるのかは自由です。 Rakefile のコードを修正することで自由に編成できます。
Serverspecのサイトにあるサンプルが参考になります。 ref: Serverspec - Advanced Tips
Nginx のインストールとサービス設定、そして 80 ポートを Listen してるかのテストをします。
ref: https://git.io/vrs7A
テストは Rake で実行します。Rake タスクは動的に定義されます。(Rakefile 参照) 定義された Rake タスクは rake -T で確認できます。
$ rake -T rake spec:localhost.ubuntu # Run serverspec tests to localhost.ubuntu
テストを実行します。
$ rake spec:localhost.ubuntu /Users/keisuke/.rbenv/versions/2.3.1/bin/ruby -I/Users/keisuke/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/rspec-support-3.4.1/lib:/Users/keisuke/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/rspec-core-3.4.4/lib /Users/keisuke/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/rspec-core-3.4.4/exe/rspec --pattern spec/localhost.ubuntu/\*_spec.rb Package "nginx" should be installed Service "nginx" should be enabled should be running Port "80" should be listening Finished in 0.58268 seconds (files took 5.66 seconds to load) 4 examples, 0 failures
テスト成功です :)
実際にはテストコードを共通化したり、複数ホストにテストを同時実行したりと、もっとカスタマイズすることになるでしょう。 その辺も実装してみたので、また今度まとめたいですね。
おわり
たまにしかやらないからこそ、コード化できていると良いですね。仕事でも導入しましたが、インフラの変更履歴も共有でき、何よりテストが自動化できたのには安心感が増します。
とりあえず、初めの1歩でした :P
👀 MongoDB 3.x インデックス生成前後のexplain()結果を読む
はじめに
MongoDB 3.0 から explain() の出力結果が変わり、読み解くのに時間がかかってしまいました。 今回はインデックスの生成前後で explain() の結果がどう変わるかを確認してみます。
環境
- Mac OS X 10.10.3
- MongoDB 3.0.2
- MongoDB storage engine: mmapv1 (default)
サンプルデータの準備
DBを用意
$ mongo use sample_db switched to db sample_db
サンプルデータ追加
とりあえず10万件のドキュメントを生成しておきます。
> for (var i=0; i < 100000; i++) { ... db.items.insert({ name: 'item_' + i, price: 100 + i }) ... } > db.items.count() 100000
インデックスが無い状態
itemsコレクションから119円の商品を探すクエリを実行します。
> db.items.find({price: 119}).explain("executionStats") { "queryPlanner": { "plannerVersion": 1, "namespace": "sample_db.items", "indexFilterSet": false, "parsedQuery": { "price": { "$eq": 119 } }, "winningPlan": { "stage": "COLLSCAN", "filter": { "price": { "$eq": 119 } }, "direction": "forward" }, "rejectedPlans": [] }, "executionStats": { "executionSuccess": true, "nReturned": 1, "executionTimeMillis": 59, "totalKeysExamined": 0, "totalDocsExamined": 100000, "executionStages": { "stage": "COLLSCAN", "filter": { "price": { "$eq": 119 } }, "nReturned": 1, "executionTimeMillisEstimate": 20, "works": 100002, "advanced": 1, "needTime": 100000, "needFetch": 0, "saveState": 781, "restoreState": 781, "isEOF": 1, "invalidates": 0, "direction": "forward", "docsExamined": 100000 } }, "serverInfo": { "host": "MacBook-Pro.local", "port": 27017, "version": "3.0.2", "gitVersion": "nogitversion" }, "ok": 1 }
まずは queryPlannner.winningPlan
に着目します。"stage": "COLLSCAN"
となっています。
これは MongoDB 2.x の時の BasicCursor
に相当します。
インデックスを使わずに全走査してることがわかります。
※ COLLSCAN => COLLECTION SCAN
実際に走査対象となったドキュメント数やクエリにかかった時間は
queryPlannner.executionStats
を確認します。
key | description | value |
---|---|---|
"nReturned": 1 | 見つかったドキュメント数 | 1 |
"executionTimeMillis": 59 | 実行時間 | 59 msec |
"totalKeysExamined": 0 | 検索したインデックス数 | 0 |
"totalDocsExamined": 100000 | 検索したドキュメント数 | 100000 |
totalDocsExamined
からも全ドキュメントが検索対象だったことがわかりますね
※ インデックス生成してないので当たり前ですが
インデックス生成後
それではインデックスを生成してみます。
> db.items.createIndex({ price: 1 }) { "createdCollectionAutomatically" : false, "numIndexesBefore" : 1, "numIndexesAfter" : 2, "ok" : 1 } > db.items.getIndexes() [ { "v" : 1, "key" : { "_id" : 1 }, "name" : "_id_", "ns" : "sample_db.items" }, { "v" : 1, "key" : { "price" : 1 }, "name" : "price_1", "ns" : "sample_db.items" } ]
price_1
という名前の priceフィールド昇順 のインデックスができました。
追記
db.collection.ensureIndex() は MongoDB 3.0.0 で deprecated と なりましたので db.collection.createIndex() に書き直しました。 参考: db.collection.ensureIndex()
再度、items コレクションから119円の商品を探すクエリを実行します。
> db.items.find({price: 119}).explain("executionStats") { "queryPlanner": { "plannerVersion": 1, "namespace": "sample_db.items", "indexFilterSet": false, "parsedQuery": { "price": { "$eq": 119 } }, "winningPlan": { "stage": "FETCH", "inputStage": { "stage": "IXSCAN", "keyPattern": { "price": 1 }, "indexName": "price_1", "isMultiKey": false, "direction": "forward", "indexBounds": { "price": [ "[119.0, 119.0]" ] } } }, "rejectedPlans": [] }, "executionStats": { "executionSuccess": true, "nReturned": 1, "executionTimeMillis": 13, "totalKeysExamined": 1, "totalDocsExamined": 1, "executionStages": { "stage": "FETCH", "nReturned": 1, "executionTimeMillisEstimate": 0, "works": 2, "advanced": 1, "needTime": 0, "needFetch": 0, "saveState": 0, "restoreState": 0, "isEOF": 1, "invalidates": 0, "docsExamined": 1, "alreadyHasObj": 0, "inputStage": { "stage": "IXSCAN", "nReturned": 1, "executionTimeMillisEstimate": 0, "works": 2, "advanced": 1, "needTime": 0, "needFetch": 0, "saveState": 0, "restoreState": 0, "isEOF": 1, "invalidates": 0, "keyPattern": { "price": 1 }, "indexName": "price_1", "isMultiKey": false, "direction": "forward", "indexBounds": { "price": [ "[119.0, 119.0]" ] }, "keysExamined": 1, "dupsTested": 0, "dupsDropped": 0, "seenInvalidated": 0, "matchTested": 0 } } }, "serverInfo": { "host": "MacBook-Pro.local", "port": 27017, "version": "3.0.2", "gitVersion": "nogitversion" }, "ok": 1 }
queryPlannner.winningPlan
を確認します。 "stage": "IXSCAN"
に変わりました。
これは MongoDB 2.x の時の BtreeCursor
に相当します。
INDEXを使用していることがわかります :)
※ IXSCAN => INDEX SCAN
ここでまた、 queryPlannner.executionStats
を確認します。
key | description | value |
---|---|---|
"nReturned": 1 | 見つかったドキュメント数 | 1 |
"executionTimeMillis": 13 | 実行時間 | 13 msec |
"totalKeysExamined": 1 | 検索したインデックス数 | 1 |
"totalDocsExamined": 1 | 検索したドキュメント数 | 1 |
おわりに
最低限見ておきたい項目について、インデックスの生成前後での差分を見てみました。 実際の運用においては1コレクションに1インデックスという事は少なく、複数のインデックスや 複合キーインデックスが生成されていると思います。
そうすると今回見ていった項目以外にも rejectedPlans
で採用されなかったインデックスを確認したり
ソート指定した場合のインデックスの使われ方を確認したりといった事が必要となるでしょう。
それぞれの項目の説明についてはまた別途まとめましょうか。