Advanced @doryokujin 第6回 MongoDB 勉強会 2011/09/24 #1
Jan 15, 2015
Advanced
@doryokujin第6回 MongoDB 勉強会 2011/09/24
#1
・井上 敬浩(26歳)
・twitter: doryokujin
・データマイニングエンジニア
・MongoDB JP 代表
・NoSQLやHadoopに興味
・マラソン2時間33分
自己紹介
・[ニュース] InfoWorldが2011年のベストオープンソース賞「Bossies」を発表、MongoDB、Hadoop、LibreOfficeなどが受賞
・[ニュース] MongoDB 2.0 正式リリース
・[コミュニティ] 11月15日(火)「第7回MongoDB勉強会」に ServerDensity の @davidmytton 氏が来日・講演
・[コミュニティ] 1月第3週に10genのエンジニアを招待して「MongoDB Conference in Japan#2」を開催予定!
最近のニュース
1. スキーマデザイン
2. パフォーマンス
3. データ保全・バックアップ
アジェンダ
参考図書
・MongoDB on the Cloud
・MongoDB with Web Application
・MongoDB for Data Processing
・Sharding に関する Tips
#2 へ続く
今回書けなかった事
・1-1. Reference / Embedded
・1-2. オブジェクト増加性
・1-3. Template / Preallocate
・1-4. _id にどの型を用いるべきか
・1-5. ドキュメント内のフィールド順序
1. スキーマデザイン
・1-1. Reference / Embedded
・1-2. オブジェクト増加性
・1-3. Template / Preallocate
・1-4. _id にどの型を用いるべきか
・1-5. ドキュメント内のフィールド順序
1. スキーマデザイン
Collection “product”:
{
"_id" : productId1,
"name" : name1,
"price" : price1,
"desc" : description1
},
{
"_id" : productId2,
…
Collection “order”:
{
"_id" : orderId,
"user" : userInfo,
"items" : [
productId1,
productId2,
productId3
]
}
Reference Document
Collection “product”:
{
"_id" : productId1,
"name" : name1,
"price" : price1,
"desc" : description1
}
{
"_id" : productId2,
…
Collection “order”:
{
"_id" : orderId,
"user" : userInfo,
"items" : [
productId1,
productId2,
productId3
]
}
> db.product.findOne(“_id”:productId1)
> db.product.findOne(“_id”:productId2)
> db.product.findOne(“_id”:productId3)
シングルクエリでドキュ
メント情報全取得は不可
Reference Document
Embedded DocumentCollection “order”:
{
"_id" : orderId,
"user" : userInfo,
"items" : [
{
"_id" : productId1,
"name" : name1,
"price" : price1
}, {
"_id" : productId1,
"name" : name1,
"price" : price2
},...
] }
Embedded DocumentCollection “order”:
{
"_id" : orderId,
"user" : userInfo,
"items" : [
{
"_id" : productId1,
"name" : name1,
"price" : price1
}, {
"_id" : productId1,
"name" : name1,
"price" : price2
},...
] }
シングルクエリで全てのorder情報を取得可能
collectionの中から特定のnameや条件以上の
priceを持つorderも特定可能
> db.order.find({ items :{
$elemMatch: { name: name1 }
} } )
> db.order.find({ items :{
$elemMatch: { price:{ $gt: 3000 } }
} } )
Embedded: [] or {} ?Collection “order”:
{
"_id" : orderId,
"user" : userInfo,
"items" : [
{
"_id" : productId1,
"name" : name1,
"price" : price1
}, {
"_id" : productId1,
"name" : name1,
"price" : price2
},...
] }
Collection “performance_rating”:
{
"_id" : "employeeId",
"name" : "name",
"post" : "section chief",
"abilities" : {
"skillA" : 20,
"skillC" : 12,
"skillE" : 18,
"skillF" : 24,
"skillG" : 23
}
}
Embedded: [] or {} ?Collection “order”:
{
"_id" : orderId,
"user" : userInfo,
"items" : [
{
"_id" : productId1,
"name" : name1,
"price" : price1
}, {
"_id" : productId1,
"name" : name1,
"price" : price2
},...
] }
Collection “performance_rating”:
{
"_id" : "employeeId",
"name" : "name",
"post" : "section chief",
"abilities" : {
"skillA" : 20,
"skillC" : 12,
"skillE" : 18,
"skillF" : 24,
"skillG" : 23
}
}
・等価なオブジェクトが増えていく場合・$elemMatch オペレータを使用
・オブジェクトは1つでフィールドのみが 増加する場合・”abilities.skillA”といったドット記法で内部フィールドにアクセス
Embedded Document
[デメリット]
・16MB/ドキュメント制約
・product 情報が更新された場合が非常に厄介(例えば期間限定セールなど)
・階層固定:order 情報から product 情報は辿れるが、product 情報からorder情報は辿れない
Reference or Embedded ?[1] パフォーマンス:シングルクエリで完結する方が高速
※ MongoDB は DBRef の機能自身を備えていない。つまりEmbedded オブジェクトの情報を取得する場合は普通にクエリを実行しなければならない(ドライバが Wrap してくれる)
[2] 更新コスト:Embedded されるオブジェクトにどれくらいの変更可能性があるか、あれば大量の更新処理が必要
[3] オブジェクト増加性:Embedded されるオブジェクト数が挿入後不変出なければ必ず Reference を使う
※ 先の例では order 作成時にproductの数も決まるので不変
・1-1. Reference / Embedded
・1-2. オブジェクト増加性
・1-3. Template / Preallocate
・1-4. _id にどの型を用いるべきか
・1-5. ドキュメント内のフィールド順序
1. スキーマデザイン
オブジェクト増加性Collection “blog”:
{
"_id" : blogId,
"user" : userInfo,
"tags" : [tag1, tag2, tag3]
"comment": [{
"_id" : commentId1,
"user" : userInfo1,
"contents" : contents1
}, {
"_id" : commentId2,
"user" : userInfo2,
"contents" : contents2
},...
] }
comment オブジェクトはユーザーの投稿によりドキュメント挿入後も増加し続ける
・16MB/document 制約に抵触する危険性・更新がIn-Placeで行われなくなり非効率
オブジェクト増加性Collection “blog”:
{
"_id" : blogId,
"user" : userInfo,
"tags" : [tag1, tag2, tag3]
"comment" : [
commentId1,
commentId3,
commentId5
],...
},
{
Collection “comment”:
{
"_id" : commentId,
"user" : userInfo,
"contents" : contents1,
"lastUpdate" : datetime,
...
},...
{
"_id" : commentId5,
"user" : userInfo,
"contents" : contents1,
"lastUpdate" : datetime,
...
}
> db.blog.update({“_id”:blogId},
{$push: { “comment”: commentId5 }}
})
> db.comment.insert({
“_id”:commentId5,
“user”:...
})
・Embedded Document の場合、Update のパフォーマンス的にも、1回挿入した後にできるだけドキュメントのサイズが増えないように工夫する
・In-Place Update:新規ドキュメント挿入時には、必ずpadding(デフォルト1MB)が設けられる。新しくオブジェクトが追加される場合でも、”In-Place”で高速にUpdateが可能。paddingを超えるサイズ増加はドキュメントの領域移動が起こる
・必ず全てのUpdateが ”In-Place” になるように
(続)オブジェクト増加性
・padding のサイズはデフォルトは1MB
・その後コレクション内のドキュメントサイズの増加傾向を見ながら自動で padding サイズを拡張してくれる。現在の padding サイズは以下のコマンドで確認可能
padding の確認
> db.coll.stats()
{ ...
#1で無ければドキュメントの移動が起きている
"paddingFactor" : 1.4099999999940787,
...
}MongoDB paddingFactor implications
・1-1. Reference / Embedded
・1-2. オブジェクト増加性
・1-3. Template / Preallocate
・1-4. _id にどの型を用いるべきか
・1-5. ドキュメント内のフィールド順序
1. スキーマデザイン
Template DocumentCollection “access_history”:
{
"_id" : pageId,
"start" : time,
"visits" : {
"minutes" : [
[num0, num1, ..., num59],
[num0, num1, ..., num59],
[num0, num1, ..., num59],
[num0, num1, ..., num59],
[num0, num1, ..., num59],
[num0, num1, ..., num59]
],
"hours" : [num0, ..., num5] }
}
ページごとに、“start”の時間から6時間の間の毎分のvisitor数を記録するケース
“minites”は[]からどんどんとオブジェクト(配列・要素)が追加される
しかし“minites”と”hours”は全ての配列要素を0で初期化しておける
Template DocumentCollection “access_history”:
{
"_id" : pageId,
"start" : time,
"visits" : {
"minutes" : [
[0,0, ..., 0],
[0,0, ..., 0],
[0,0, ..., 0],
[0,0, ..., 0],
[0,0, ..., 0],
[0,0, ..., 0]
],
"hours" : [0, ..., 0] }
}
事前に初期化したオブジェクトで満たしておくことで、ドキュメントサイズの後からの増加
を防げる
Template DocumentCollection “access_history”:
{
"_id" : pageId,
"start" : time,
"visits" : {
"minutes" : [
[0,0, ..., 0],
[3,0, ..., 0],
[0,0, ..., 0],
[0,0, ..., 0],
[0,0, ..., 0],
[0,0, ..., 0]
],
"hours" : [0, ..., 0] }
}
> db.pages.update({ "_id" : pageId, "start" : thisHour},{ "$inc": {"visits.minutes.1.0": 3}})
$incオペレータなどはオブジェクトのサイズを増加させない
・オブジェクトの増加を避けられない場合でも、意図的に大きな padding を持たせておくことで、できる限り In-Place Update を可能にする:
Preallocate Document
> collection.insert(
{ "_id" : 123,
/* other fields */,
"garbage" : someLongString})
> collection.update(
{ "_id": 123 },
{ $unset: { "garbage" : 1} })
十分に長い文字列を持ったフィールドで多めの領域を事前に確保しておく
insert後すぐにgabageフィールドを削除
・1-1. Reference / Embedded
・1-2. オブジェクト増加性
・1-3. Template / Preallocate
・1-4. _id にどの型を用いるべきか
・1-5. ドキュメント内のフィールド順序
1. スキーマデザイン
・ObjectId = BSON( [4byte timestamp]
+ [3byte hash(hostname)]
+ [2byte pid]
+ [3byte inc] )
・ObjectId は決して重複せず、単調増加。できる限りObjectIdを使用
・数億件のデータで自前でユニーク性を保証できる _id を生成するのは困難 (重複する _id の insert は失敗する)
_id にどの型を用いるべきか
[例外]
[1] 値を int 型で持てる場合:インデックスサイズの節約
[2] リトライを行う場合:insert 処理が途中で失敗した場合は対象のデータ集合全体を remove して再 insert が必要
・しかし時間等に依らず常に同じ値を持つ _id にしておけば remove 不要で、再実行すれば既に insert してあるデータは失敗(無視)し、残りの insert だけを実行する事ができる。ただし Fire-and-Forget insert にしておく
_id にどの型を用いるべきか
# 例:(集計後の)ユーザー行動ログ
{
"_id" : BinData(5,"iiOI5R6P+dbut19O1mS1lA=="),
"reduce_id" : 0,
"user_id" : 00000,
"date" : 20110727,
"action_type" : "a{Make}",
"key_depth" : 1,
"action_detail" : {
"kakejiku" : 2,
"kaizokunojyuu" : 1,
"matoryo-sika" : 1,
"syokudai" : 1
}
}
・_id = user_id + date + timeMilliSec + action_type
でユニークな_idを取得可能。この文字列をハッシュ化してBinary変換する
# Pythonbson.Binary( hashlib.md5( str(unique_id) ).digest(), bson.binary.MD5_SUBTYPE)
_id にどの型を用いるべきか
[indexサイズの考慮]
・_id (その他インデックスフィールド)に使用できる現実的な型は int, ObjectId, base64
How to save 200% RAM by selecting the right key data type for #MongoDB
_id にどの型を用いるべきか
・1-1. Reference / Embedded
・1-2. オブジェクト増加性
・1-3. Template / Preallocate
・1-4. _id にどの型を用いるべきか
・1-5. ドキュメント内のフィールド順序
1. スキーマデザイン
{
"_id" : id,
"name" : username,
"email" : email,
"phone" : phone,
"twitter" : username,
"facebook" : username,
"linkedin" : username,
"google+" : number,
"street" : street
"city" : city,
"state" : state,
"zip" : zip,
"fax" : number
}
> db.profile.find({ “fax”: number })
常に各ドキュメントを上から順に、”fax”フィールドが無いかスキャンしている
ドキュメント内のフィールド順序
ドキュメント内のフィールド順序
・ドキュメント内の順序は非常に重要
・頻繁に検索されるフィールドは先頭に近くあるべき
[対策1]:順序付き連想配列 <-- Pythonならば”SON” オブジェクトを使う。
[対策2]:サブオブジェクトを使用。subKey のフィールドには ”key.subKey” で直接アクセスできる
※ 両方を併用する
{
"_id" : id,
"name" : username,
“online”: {
"email" : email,
"twitter" : username,
...
},
”address”: {
"street" : street
"city" : city,
...
},
“telephone”: {
"phone" : phone,
"fax" : number
}
> db.profile.find({ “telephone.fax”: number })
上から順にスキャンせず、直接”telephone”フィールド内のスキャンを行える
・2-1. Query Match 順序
・2-2. Index
2. パフォーマンス
・2-1. Query Match 順序
・2-2. Index
2. パフォーマンス
[AND(OR)-Queries]
・次の2つのクエリは検索速度が異なる
Query Match 順序
# Aにマッチする集合の中でBにマッチさせ、さらにその集合の中でCにマッチする集合を表示
> db.coll.find({“A”: a, “B”: b, “C”: c})# Cにマッチする集合の中でBにマッチさせ、さらにその集合の中でAにマッチする集合を表示
> db.coll.find({“C”: c, “B”: b, “A”: a})
# Aの補集合の中でBにマッチさせ、その補集合の中でCにマッチする集合の和を表示
> db.coll.find($or: [“A”: a, “B”: b, “C”: c])# Cの補集合の中でBにマッチさせ、その補集合の中でAにマッチする集合の和を表示
> db.coll.find($or: [“C”: c, “B”: b, “A”: a])
[AND-Queries]
[worse] Aの集合が大きい場合。続くB、Cはその大きな集合から検索される
[better] Cの集合が最も小さい場合。先に検索されれば、続くB、Aは小さな集合内から高速に検索される
Query Match 順序
AB B
C
A A
C
BBA
C C
[OR-Queries]
[worse] Cの補集合(青領域)が大きい場合。続くB、Cはその大きな補集合から検索される
[better] Aの補集合(青領域)が最も小さい場合。続くB、Aは小さな集合内から高速に検索される
Query Match 順序
C
A AB
AB
C
B
C
B
C
A
・2-1. Query Match 順序
・2-2. Index
2. パフォーマンス
・インデックスを使用しているフィールド群に限定してデータを抽出する場合は、実質インデックスツリーのスキャンだけでデータの高速検索が可能
Covered Index
> db.coll.ensureIndex({ A: 1 })
> db.coll.ensureIndex({ B: 1 })
> db.coll.ensureIndex({ C: 1 })
# find の第2引数で “Select A,B,C” のように特定のフィールドのみを取得する
> db.coll.find({ A: a, B: b, C: c },
{ _id: -1, A: 1, B: 1, C: 1 })
・インデックスを作成しているフィールドのみを条件・抽出 (_idは必ず取得項目から除外) 対象にしたクエリには Covered Index が自動的に適用される
・MongoDBはインデックスと、直近にアクセスされたドキュメントをメモリに持っておく
・「インデックスサイズ」 < 「メモリサイズ」となるように常に心がけるのは非常に重要(しかし現実は厳しい)
→ できるだけインデックスを作成しない
→ インデックスの型に注意(配列などは論外。文字列も避ける)
→ Sharding (Chunk間ではインデックスは共有される)
→ ver.2.0 (25% smaller and 25% faster。ただし再作成の必要)
Index について
・3-1. Replica Set
・3-2. Journaling
・3-3. getlasterror
・3-4. Repair / Comapct
3. データ保全・バックアップ
・3-1. Replica Set
・3-2. Journaling
・3-3. getlasterror
・3-4. Repair / Comapct
3. データ保全・バックアップ
Replica Set メンバータイプType 特徴
High Priority フェイルオーバー時、Primaryになりやすい (例: priority=1.0)
Low Priority フェイルオーバー時、Primaryになりにくい (例: priority=0.5)
Slave Delay Primaryと遅延同期(例: slaveDelay=300)
Back Up Primaryには決してなり得ない。Readは可能 (例: priority=0.0)
Hidden Primaryには決してなり得ず、Readも不可能(例: hidden=true)
Arbiter 自身はデータを持たず、フェールオーバー時の投票のみ参加(例: arbiterOnly=true)
Replica Set メンバータイプType
High Priority
Low Priority
Slave Delay
Back Up
Hidden
Arbiter
・サーバーにばらつきがある場合
・ユーザーの人為的操作ミスからでデータを守りたい場合
・journaling専用バックアップとして使用する場合。インデックスも作成する必要なし:buildIndexes=false
・ReplicaSet最低構成台数3を満たすためのダミーとして。AWSではSingle Instance に使用可能
Replica Set 構成例
Primary
Secondary
Slave Delay
Back Up --journal
Secondary
DC1 DC2
・人為的ミスから復帰させるためにSlave Delayを用意するのは有効
・メンバーの中で最低1つはjournalingを有効に
Replica Set 構成例
Secondary
Shard1 on AWS
Arbiter
Primary
Large Instance
Large Instance
MicroInstance
・Micro Instance ではメモリ不足・Small Instance は32bitなので不可・Large Instance を使用する・ただしArbiterは実データを持たないのでSmallで十分
--journal
[1] フェイルオーバーでは、新しい Primary が必ずしも最新の Oplog を保持しているとは限らない。
[2] Secondary の方が新しい Oplog を保持している可能性もある
[3] その場合は Primary の Oplog に全て統一するために、全 Secondary の Oplog の差分は捨てられる
※しかし、捨てられたオペレーションは手動でロールバックすることができる。これによってできる限り元 Primary の保持していたデータに近づける
Replica Set Rollback
[4] フェイルオーバー後、dbpathのディレクトリ以下の ”rollback” ディレクトリの中に、差分の oplog 情報を含むbsonファイルが作成される
[5] bsondump コマンドによりロールバックを行う:
Replica Set Rollback
$ ls /dbpath/rollback
dbname.collname.2011-08-14T18-27-14.0.bson
$ bsondump dbname.collname.2011-08-14T18-27-14.0.bson
{ "_id" : ... }
{ "_id" : ... }
{ "_id" : ... }
Wed Aug 15 13:33:32 3 objects found
・3-1. Replica Set
・3-2. Journaling
・3-3. getlasterror
・3-4. Repair / Comapct
3. データ保全・バックアップ
・ver.2.0 で標準機能に
・さらにフラッシュタイミング(デフォルト100ms)が任意に調整可能に
[journalingとは]
・データ書き込み前にオペレーションを journal ログに先行して保存 (WAL)
・journal ログは --dbpath 以下の journal/ サブディレクトリ以下にある
・古い journal ファイルは1GBごとにローテートされ、古いログは削除
Journaling
[0] クリーンシャットダウン時には journal サブディレクトリは削除
[1] サーバーダウン時には journal サブディレクトリはそのまま残る
[2] mongod は起動時に journal サブディレクトリが無いかチェック
[3] あればそのディレクトリ内の journal ログからダウン時に未実行だったオペレーションを実行する(クライアントは完了まで待機)
・journal ログへの書き込みは任意 (> v.2.0、デフォルト100ms)
・書き込みは Group Commits を行って高速に行われる
Journaling Process
[メリット]
・データの安全性:実際の書き込みオペレーションが適用前にダウンしても journal ログに記述されたオペレーションは保証される
・Repair 不要:不意なシャットダウンからの復帰は lock ファイルの削除、Repair コマンドが必要であったがそのどちらのオペレーションが不要に。起動時に自動リカバリしてくれる (journaling process)
・Journaling は必ず使用すべき
Journaling
[デメリット]
[1] パフォーマンス低下:常にJournalログへの書き込みが行われるので全体のパフォーマンスが低下する(30%程度悪化?)
[2] 重複書き込み:Replica SetもオペレーションをOplog Collectionに保存する。Journalingもオペレーションを保存する。毎回のオペの書き込みが重複 (3重) するコストは大きい
Journaling
[対策][1] journal サブディレクトリをシンボリックリンクで別ディスクへ逃がす。SSD(64GBで十分)に逃がせば効率的
[2] Replica Set メンバーの中で最低 1 台のメンバー(BackUpタイプ) のみjournaling を有効にしておく
Journaling
Primary
Secondary
Back Up --journal
DC1 DC2
・DC1のPのダウン時にはSがPになるが、その際にはロールバックが必要な場合も。DC2のダウン時には再起動してリカバリして同期をすれば良い
・DC1全体のダウン時にはDC2のバックアップをMasterとして起動することでデータをリカバリできる。
・3-1. Replica Set
・3-2. Journaling
・3-3. getlasterror
・3-4. Repair / Comapct
3. データ保全・バックアップ
getlasterror コマンド・デフォルトの Insert は ”Fire-and-Forget”
・書き込み完了を確認してからリターンを返すためにはgetlasterror コマンドを併用
・ドライバからはオプションで指定可能:Pythonなら “safe=True”、JavaならWriteConcernオブジェクト
> db.collname.insert({_id:...})
# 直前の書き込みが完了してからリターンを返す> db.getLastError()
# or
> db.runCommand("getlasterror")
getlasterror オプション
[fsync] ※ journaling を適用していない場合・全データがフラッシュされるまで待機
[j] ※ journaling を適用している場合・journal commit が完了するまで待機
> db.runCommand({ getlasterror: 1, fsync: true })
{ "err" : null, "n" : 0, "fsyncFiles" : 2, "ok" : 1 }
> db.runCommand({ getlasterror: 1, j: true })
getlasterror オプション[w]・Replica Set の n 台のメンバーに対して書き込みが完了するまで待機
・w を使用する場合は必ず timeout を設定する
[majority]
・過半数のノードへの書き込みが完了するまで待機
> db.getLastError(2, 5000) // w=2, timeout 5000ms
> db.getLastError("majority")
・3-1. Replica Set
・3-2. Journaling
・3-3. getlasterror
・3-4. Repair / Comapct
3. データ保全・バックアップ
・MongoDB は remove() や drop() によって削除したドキュメントやコレクションの領域をシステムに解放しない
・MongoDB が空き領域を管理し、後に追加されるデータをそこに入れようとする → 実際は効率良く行われない
・この機能によってデータ削除を繰り返すと実データの割に占有するディスク領域が大きくなっていく
・Repair、Compact (ver2.0) コマンドでその無駄な領域を開放できる
Removeの繰り返しによる断片化
・Repair コマンドは、DB のダウンから復帰の際にも使用する。不良データチェックとデフラグの役割を果たす
・全データスキャンを行うため、非常に時間がかかる。できれば使いたくないコマンド
・さらに現在のディスクサイズと同じサイズの空き領域が必要
・内部的には空き領域に mongoimport/mongoexport によって隙間なくデータを書き込んでいく
Repair コマンドについて
・Repair コマンドはサーバー単位。Replica Set ではSecondary に降格させて順次バックグラウンドで行う。Sharding 環境でも shard 個々に行う必要がある。
・Compact コマンドはコレクション単位で行える。ただしこれも Secondary に降格させて行う
・Compact コマンドは2倍のディスクを消費せず、かつ高速
※ Compact コマンドはドキュメントの padding をも消去する。オブジェクト増加性を持ったコレクションにはパフォーマンスに悪影響
Comapct コマンドについて
・スキーマデザインの問題は完全にケースに依存、ただできるだけ “Single Query” で実行出来るデザインであるべき
・インデックスのサイズには常に細心の注意を
・MongoDB を実際に運用する際にはデータ保全のための施策とバックアップを行っておく事が重要
まとめ
・MongoDB JPでは「MongoDB Conference in Japan#2」のスポンサーを募集しています
・また会場を提供して頂ける方、スタッフをして頂ける方を広く募集しております
・MongoDB の導入を検討されている企業さんも気軽に相談して頂ければ
・mr.stoicman[at]gmail.com か @doryokujin まで宜しくお願いします
募集
ありがとうございました