読者です 読者をやめる 読者になる 読者になる

MongoDB の Capped Collection を作成する

クロスワープの大鷲です。

MODD では一部のログの管理に MongoDB を利用しています。
そういえば MongoDB 3.0 がリリースされていましたね。移行するかどうかはわかりませんが…。

Capped Collection

MongoDB には Capped Collection という機能が存在します。
コレクションというのは RDB で言うところのテーブルのようなものです。
Capped Collection とは、コレクションに対し、サイズや要素数に上限を設ける機能です。
上限を超えた場合、古い要素から順番に消えていきます。

ログを(半)永久的に保存しようとすれば、一定の期間ごとにコレクション名を変えてローテーションすることになるでしょう*1
MODD にも、そのように管理されているログも存在します。
しかし、一部のログではローテーションされておらず、固定のコレクションに保存されますので、定期的に消さなければ、コレクションのサイズが無限に膨れ上がってしまいます。
キャップを設定しておけば、自動で削除処理をしてくれますので、(古いログは捨ててもよいというポリシーなら)Capped Collection はログに向いていると言えるでしょう。

というわけで、今回は Capped Collection を作ってみましょうという記事です。
キャップを設定し忘れて、既にログが記録されているコレクションに、既存のデータを維持したままキャップを設定するという想定です(データを維持したままとは言っても、キャップを超えた古いデータは消えます)。

Capped Collection を作成するには、いくつかの方法があります。

mongo シェルを使う

いずれの方法を採るにせよ、mongo シェルを使います。
(Windows では)MongoDB のインストール ディレクトリにある mongo.exe がそれです。
コマンド プロンプトから(何もオプションを付けずに)起動すると、以下のような表示になります*2

C:\Program Files\MongoDB 2.6 Standard\bin>mongo
MongoDB shell version: 2.6.5
connecting to: test
>

"connection to: test" と出ていますので、今は test データベースに接続している状態です。
目的のデータベースに接続するには、use コマンドを使います。

> use sample
switched to db sample
>

これで sample データベースに接続できました。

"db" というのは、常に現在接続しているデータベースを指します。
例えば db.createCollection() メソッドは、現在接続しているデータベースにコレクションを作成するわけです。

シェルで "db" とだけ打つと、現在のデータベースを確認できます。

> db
sample
> 

なお、この時点では、存在しないデータベース名を指定することも可能です。
データを書きこむ時に自動的に作成されます。

余談を言いますと、社内では MongoVUE という GUI の管理ツールを使っているのですが、MongoVUE にはシェルコマンドを叩く機能は無いようなのです。

convertToCapped コマンドを使う

読んで字のごとく、既存の(Capped でない)コレクションを Capped Collection に変換するコマンドです。
コマンドを実行するには、db.runCommand() メソッドを使います。

db.runCommand({
  convertToCapped: "log",
  size: 104857600 // 100MB
})

size パラメーターは必須です。

このコマンドは内部的には

  1. 新しい Capped Collection を作成し
  2. 元のコレクションからデータをコピーして
  3. 元のコレクションを削除し
  4. 新しいコレクションの名前を元のコレクションと同じにする

という操作をすることにより、あたかも、既存のコレクションが Capped Collection に変換されたように見えるということのようです。

このコマンドの注意点は、操作が完了するまで、対象のコレクションはロックされ、操作を受け付けなくなるということです。
移行したい既存のデータのサイズが大きい場合には注意が必要でしょう。

cloneCollectionAsCapped コマンドを使う

新しい Capped Collection を作成し、既存のコレクションからデータをコピーします。

db.runCommand({
  cloneCollectionAsCapped: "log",
  toCollection: "log_capped",
  size: 104857600 // 100MB
})

size パラメーターは必須です。
toCollection に指定する名前のコレクションが既に存在しているとエラーになります。

db.createCollection() メソッドのオプションを指定する

db.createCollection() メソッドは、新しい空のコレクションを作成するメソッドです。
このメソッドで Capped Collection を作成するには、以下のように、capped、size、max パラメーターを指定します。
size パラメーターはコレクションのサイズ(バイト単位)、max はコレクションの最大要素数です。

db.createCollection("log_capped", {
  capped: true,
  size: 104857600 // 100MB
})

size パラメーターは必須で、max パラメーターはオプションです(3 つの方法のうち、max パラメーターを指定できるのはこの方法だけです)。
max を省略した場合は 9,223,372,036,854,775,807 になります。この値は何かと言うと、2 の 63 乗 - 1(16 進数では 7fff ffff ffff ffff)で、符号付き 64 ビット整数の最大値です。

既存のデータをコピーする

db.createCollection() メソッドでは空のコレクションを作成することしかできないので、別途、既存のデータをコピーする必要があります。
コレクションをコピーするには、collection.copyTo() メソッドを使います。

db.log.copyTo("log_capped")

コピー先が Capped Collection の場合、溢れたデータは古い方から自動的に削除されます。

コレクションを削除 or リネームする

convertToCapped コマンドを使った場合は自動的にやってくれるのですが、db.createCollection() メソッドを使った場合では、新しいコレクションは元のコレクションとは違う名前で作られています。
このままでは書きこむプログラムの修正が必要になってしまうので、新しい Capped Collection を、元のコレクションと同じ名前にしましょう。
元のコレクションは削除してしまうか、別の名前で退避しておきます。

コレクションの削除には collection.drop() メソッド、リネームには collection.renameCollection() メソッドを利用できます。

db.log.drop()
db.log_capped.renameCollection("log")

もしくは

db.log.renameCollection("log_backup")
db.log_capped.renameCollection("log")

分解してみる

こうしてみると、convertToCapped コマンドは一発で全部やってくれるのに対し、convertToCapped コマンドが内部的に行う動作を手動で実行することもできるということがわかります*3

db.runCommand({
  convertToCapped: "log",
  size: 104857600
})

というコマンドを、他の方法の組み合わせで実現することを考えてみます。
組み合わせ方は何通りかあります。

  1. 既存のデータを退避して
  2. 元のコレクションを削除して
  3. 退避したデータから新しい Capped Collection を作成する
db.log.copyTo("log_temp")
db.log.drop()
db.runCommand({
  cloneCollectionAsCapped: "log_temp",
  toCollection: "log",
  size: 104857600
})

とか、

  1. データを引き継ぎつつ新しい Capped Collection を一時的な名前で作成して
  2. 元のコレクションを削除して
  3. 一時的な名前を元の名前に変える
db.runCommand({
  cloneCollectionAsCapped: "log",
  toCollection: "log_temp",
  size: 104857600
})
db.log.drop()
db.log_temp.renameCollection("log")

とか。

cloneCollectionAsCapped コマンドは createCollection() メソッドと copyTo() メソッドの組み合わせで同じことが実現できます。
renameCollection() は createCollection()、copyTo()、drop() に分解することもできますね*4

convertToCapped コマンドは楽でいいですが、もし何か問題が起きてコレクションが消えてしまったら…という不安もあります*5
念のため、元のコレクションを削除するのではなく名前を変えて残しておきたいとか、コレクションをロックする時間を最小限にするために操作を細分化したいとか、要求に応じて採るべき方法は異なってきます。
あるいは、要素数で制限したい場合、max パラメーターを指定できるのは createCollection() だけですので、cloneCollectionAsCapped や convertToCapped は使えません。

最適な方法を探してみてください。

*1:シャーディングという、データを複数のサーバーに分散させる機能もありますが、ログに使うには大げさかもしれません

*2:バージョンや設定によって最初に出てくるメッセージは異なるようです

*3:結果の等価性にのみ着目しており、実際に内部でこのような実装になっているという意味ではありません。

*4:あらかじめ明示的に createCollection() しておかないと、元が Capped Collection だった場合に、新しいコレクションが Capped Collection になりません。

*5:特に根拠はないんですけど…。