今回はMongoDBのGridFSを少しだけ触ってみました。
This allows us to efficiently store large objects, and in the case of especially large files, such as videos, permits range operations (e.g., fetching only the first N bytes of a file).
MongoDBはデータをBSONと呼ばれる形式で扱っていますが、一つのBSONオブジェクトに対して16MBの制限(v1.7未満のバージョンだと4MB)があるようです。GridFSはMongoDBに巨大なファイルを格納するための仕様で、公式でも上記引用の通り動画像ファイルなどを扱うことを想定しているので、ここでは動画配信サービスのバックエンドの一部に使えるかどうか考えてみたいと思います。
* DRMパッケージやエンコード済み動画像ファイルのみを管理したい (元素材は管理しない)
* タイトルや著作権情報、エンコードプロファイル等のメタデータも併せて管理したい
* プログラムから動画像ファイルのデータを一部だけ取得したい
* 過去に保存したファイルも取得したい (エンコードプロファイル変更前後の比較検証のため)
CPとの契約上、元素材の管理は厳しいはずなので、GridFSでは元素材は保存せずにあくまでテンポラリーストレージとして利用することを想定します。つまり信頼性はあまり気にしないということです(もしファイル破損して修復も難しそうだったら、元素材が保存されているストレージから再度取得すればいい)。
テスト環境は”さくらのVPS 512MB”です。GridFSの検証をするにはディスク容量が少なすぎるのでとりあえずは機能の確認だけします。本当は4GB超のファイルを使って検証すべきなんでしょうが容量的に難しいので。。
mongofiles
GridFSを操作するために mongofiles というコマンドラインツールが付属しています。16MB超の動画像ファイルを準備してテストしてみます(動画像ファイルはffmpegで用意すると楽です)。
[shell]
## 動画像ファイルを準備しておく
$ ls -lh files
合計 854M
-rw-r–r– 1 ryo ryo 97M 5月 8 2010 test1.wmv
-rw-r–r– 1 ryo ryo 158M 2月 12 18:41 test2.mp4
-rw-r–r– 1 ryo ryo 599M 2月 18 22:59 test3.f4v
## ファイルタイプの確認
$ file files/*
files/test1.wmv: Microsoft ASF
files/test2.mp4: ISO Media, MPEG v4 system, version 2
files/test3.f4v: ISO Media
## MongoDBにファイルを保存
## -d オプションでデータベース名を指定、-v オプションで詳細出力
$ mongofiles -v -d video put files/test1.wmv
Sun Mar 25 13:31:16 creating new connection to:127.0.0.1
Sun Mar 25 13:31:16 BackgroundJob starting: ConnectBG
Sun Mar 25 13:31:16 connected connection!
connected to: 127.0.0.1
added file: { _id: ObjectId(‘4f6e9f9413c7c7e974f4da4b’), filename: “files/test1.wmv”, chunkSize: 262144, uploadDate: new Date(1332649881021), md5: “126b430551e2486b84312c8a16c3eba7”, length: 101616791 }
done!
## MongoDBからファイルを取得
$ mongofiles -v -d video get files/test1.wmv
Sun Mar 25 13:34:16 creating new connection to:127.0.0.1
Sun Mar 25 13:34:16 BackgroundJob starting: ConnectBG
Sun Mar 25 13:34:16 connected connection!
connected to: 127.0.0.1
done write to: files/test1.wmv
## MongoDBからファイルを削除
$ mongofiles -v -d video delete files/test1.wmv
Sun Mar 25 13:36:16 creating new connection to:127.0.0.1
Sun Mar 25 13:36:16 BackgroundJob starting: ConnectBG
Sun Mar 25 13:36:16 connected connection!
connected to: 127.0.0.1
done!
[/shell]
MongoDBのログには以下のように出力されていました。ディスクスペースがプリアロケーション済みの場合、600MB程度のファイルだと20秒ほど時間がかかっています。
[shell]
## test1.wmv 97MB
[conn23777] command video.$cmd command: { filemd5: ObjectId(‘4f6ea577c5f3400dcd625147’), root: “fs” } ntoreturn:1 reslen:94 372ms
## test2.mp4 158MB
[conn23890] command video.$cmd command: { filemd5: ObjectId(‘4f6ed4524cb55e30ffb99535’), root: “fs” } ntoreturn:1 reslen:94 898ms
## test3.f4v 599MB
[conn23899] command video.$cmd command: { filemd5: ObjectId(‘4f6ed179b4d62c9b14827ce2’), root: “fs” } ntoreturn:1 reslen:94 19819ms
[/shell]
コレクションの中身は以下のようになっています。
[shell]
$ mongo video
MongoDB shell version: 2.0.1
connecting to: video
> show collections
fs.chunks
fs.files
system.indexes
## fs.files にファイルのメタデータが保存されている。
> db.fs.files.find()
{ “_id” : ObjectId(“4f6ed179b4d62c9b14827ce2”), “filename” : “files/test3.f4v”, “chunkSize” : 262144, “uploadDate” : ISODate(“2012-03-25T08:05:14.504Z”), “md5” : “9cdf035bd66f62b3aafb7cf712fdc22a”, “length” : 627365883 }
{ “_id” : ObjectId(“4f6ed4524cb55e30ffb99535”), “filename” : “files/test2.mp4”, “chunkSize” : 262144, “uploadDate” : ISODate(“2012-03-25T08:16:33.741Z”), “md5” : “617dd8559fc756d0c932756b96605beb”, “length” : 165002988 }
{ “_id” : ObjectId(“4f6f09600a65bb0511338b84”), “filename” : “files/test1.wmv”, “chunkSize” : 262144, “uploadDate” : ISODate(“2012-03-25T12:02:47.450Z”), “md5” : “126b430551e2486b84312c8a16c3eba7”, “length” : 101616791 }
## _id の他に filename フィールドにも自動的にインデックスが貼られる。
> db.fs.files.getIndexes()
[
{
“v” : 1,
“key” : {
“_id” : 1
},
“ns” : “video.fs.files”,
“name” : “_id_”
},
{
“v” : 1,
“key” : {
“filename” : 1
},
“ns” : “video.fs.files”,
“name” : “filename_1”
}
]
[/shell]
自動的に “filename” フィールドにもインデックスを貼ってくれるようです。親切ですね。
プログラムから操作
いろいろな言語のドライバを使ってみましたが、C++ドライバ(doxygen documentation)とPythonドライバ(PyMongo)はGridFS対応も丁寧だったので今回はこれを利用します。
* C++ドライバ
[cpp]
#include
#include
#include
void metaprint(mongo::GridFile& file);
int main(void) { 見てわかるようにかなり高級なインタフェースで操作できます。また、 次はPythonから操作してみます。 import pymongo def gridfs_test(): f = open(‘files/test1.wmv’, ‘r’) except gridfs.errors.NoFile as e: def metaprint(c): if __name__ == ‘__main__’: 使い捨てのバッチなどはPythonドライバを使ってさっと書いてしまうと良いかと思います。 GridFSではファイルをバージョニングしているので、過去に保存したファイルも取得することができます。また上記のPythonの例では、任意のメタデータ(copyright)をアドホックに追加していますが、ここはMongoDBの特性がよく表れているところですね。動画配信サービスでは著作権情報や配信期間などメタデータの管理が大変で、特にエンコードプロファイルは変更/追加頻度が高いのでMySQLなどのRDBMSだと素早い対応が難しいです。MongoDBを使えば手間が少し省けるかもしれません。 動画像ファイル自体のメタデータを取得するために構造解析を行うことはよくあります。ここではGridFSに保存されたmp4ファイル(AVC/H.264 + AAC)がちゃんと読めるか確認しておきます。ここではMP4のMovie Header(mvhd)から timescale と duration の値を取得して動画の尺を求めます。 import pymongo def gridfs_test(): def getduration(mp4): if __name__ == ‘__main__’: 今回はMongoDB GridFSの機能をひととおり触ってみました。実際に動画像ファイルのストレージとして運用する場合は容量も必要になってくるので、複数台でシャーディング構成を組むことになるのでしょうが、もしデータセンターをまたいだ場合にどういった問題が起こるかなども細かく検証していく必要があると思います。時間を作って検証を進めていきたいです。 「Tech Note」ページにもMongoDBセクションを作ったので、これからコツコツ更新していきたいと思います。もし興味があれば覗いていってください。
using namespace std;
try {
mongo::DBClientConnection client;
client.connect(“localhost:27017”);
const string dbname = “video”;
const string filename = “files/test2.mp4”;
mongo::GridFS fs(client, dbname);
// ファイルの取得
mongo::GridFile file = fs.findFile(filename);
metaprint(file);
// ファイルの削除
fs.removeFile(filename);
// ファイルの保存
cout << "---------- Put File" << endl;
mongo::BSONObj retval = fs.storeFile(filename);
cout << retval.toString() << endl;
// ファイルのリスト(全てのドキュメント)を取得
cout << "---------- File List" << endl;
auto_ptr
while(cursor->more()) {
mongo::BSONObj doc = cursor->next();
cout << doc.toString() << endl;
}
} catch(const mongo::ConnectException& e) {
cerr << "connect error" << endl;
cerr << e.getCode() << endl;
cerr << e.what() << endl;
} catch(const mongo::DBException& e) {
cerr << e.getCode() << endl;
cerr << e.what() << endl;
}
return 0;
}
void metaprint(mongo::GridFile& file) {
// メタデータ
// (mongo::GridFile#get~ メソッドはconst指定されてないのでconst参照で渡せない)
using namespace std;
cout << "---------- Meta Data" << endl;
mongo::BSONElement elm = file.getFileField("_id");
cout << "ObjectId: " << elm.OID() << endl;
cout << "File Name: " << file.getFilename() << endl;
cout << "Content Length: " << file.getContentLength() << endl;
cout << "MD5: " << file.getMD5() << endl;
cout << "Upload Date: " << file.getUploadDate().toString() << endl;
cout << "Chunk Size: " << file.getChunkSize() << endl;
cout << "Num Chunks: " << file.getNumChunks() << endl;
}
[/cpp]
* 出力例
———- Meta Data
ObjectId: 4f85970d877c7e26f1f36edb
File Name: files/test2.mp4
Content Length: 165002988
MD5: 617dd8559fc756d0c932756b96605beb
Upload Date: Wed Apr 11 23:37:04 2012
Chunk Size: 262144
Num Chunks: 630
———- Put File
{ _id: ObjectId(‘4f859772c816199f63d12b38’), filename: “files/test2.mp4”, chunkSize: 262144, uploadDate: new Date(1334155126050), md5: “617dd8559fc756d0c932756b96605beb”, length: 165002988 }
———- File List
{ _id: ObjectId(‘4f6ed179b4d62c9b14827ce2’), filename: “files/test3.f4v”, chunkSize: 262144, uploadDate: new Date(1332662714504), md5: “9cdf035bd66f62b3aafb7cf712fdc22a”, length: 627365883 }
{ _id: ObjectId(‘4f85896c0fb072ce90ab4e89’), filename: “files/test1.wmv”, chunkSize: 262144, uploadDate: new Date(1334151540196), md5: “126b430551e2486b84312c8a16c3eba7”, length: 101616791 }
{ _id: ObjectId(‘4f859772c816199f63d12b38’), filename: “files/test2.mp4”, chunkSize: 262144, uploadDate: new Date(1334155126050), md5: “617dd8559fc756d0c932756b96605beb”, length: 165002988 }
mongo::GridFS クラスのメソッドは mongo::DBClientConnection クラスのメソッドのラッパーになっているものが多いので、mongo::BSONObj と mongo::BSONElement クラスの仕様と併せて理解しておくと良いかと思います。あと、戻り値として std::auto_ptr<mongo::DBClientCursor> を返してくるメソッドもあるので注意が必要です。C++ドライバについては以前にも紹介しているのでもし興味があれば参照ください。業務ではこのC++ドライバを使って共有ライブラリ(.so)を作ることが多いです。
* MongoDB C++クライアント
* Pythonドライバ
[python]
#!/bin/env python
# -*- coding: utf-8 -*-
import gridfs
import gridfs.errors
try:
con = pymongo.Connection(‘localhost’, 27017)
db = con.video
fs = gridfs.GridFS(db)
filename = ‘files/test2.mp4’
## ファイルのリストを取得
print fs.list()
## ファイルの存在確認
if fs.exists(filename=filename):
doc = db.fs.files.find_one({‘filename’:filename})
## ファイルの取得 (gridfs.grid_file.GridOut)
# _id で検索
content = fs.get(doc[‘_id’])
# その他メタデータで検索 (Versioning)
# filename で検索, 最初に保存したファイルを取得
content = fs.get_version(filename, 0)
metaprint(content)
## ファイルの削除
fs.delete(doc[‘_id’])
## ファイルの保存
# 任意のメタデータ(copyright)を付与
fs.put(f, filename=’files/test1.wmv’, copyright=’rest-term.com’)
f.close()
## ファイルが存在しない場合
print e
except gridfs.errors.GridFSError as e:
## その他のエラー
print e
## メタデータ
print ‘ObjectId:’,c._id
print ‘File Name:’,c.name
print ‘Content Length:’,c.length
print ‘MD5:’,c.md5
print ‘Upload Date:’,c.upload_date
print ‘Chunk Size:’,c.chunk_size
gridfs_test()
[/python]
* 出力例
[u'files/test1.wmv', u'files/test2.mp4', u'files/test3.f4v']
ObjectId: 4f803eab3adf4b5ee257a7cd
File Name: files/test2.mp4
Content Length: 165002988
MD5: 617dd8559fc756d0c932756b96605beb
Upload Date: 2012-04-07 13:18:50.362000
Chunk Size: 262144
ファイルの中身を読む
[python]
#!/bin/env python
# -*- coding: utf-8 -*-
import gridfs
import gridfs.errors
import os
from binascii import b2a_hex
try:
con = pymongo.Connection(‘localhost’, 27017)
fs = gridfs.GridFS(con.video)
filename = ‘files/test2.mp4’
content = fs.get_version(filename)
(timescale, duration) = getduration(content)
print ‘Movie duration: %s sec’ % (duration/timescale)
except gridfs.errors.NoFile as e:
print e
except gridfs.errors.GridFSError as e:
print e
”’ get mp4 duration from movie header atom (mvhd) ”’
try:
boxsize = int(b2a_hex(mp4.read(4)), 16)
boxtype = mp4.read(4)
if boxtype == ‘moov’:
return getduration(mp4)
elif boxtype == ‘mvhd’:
mp4.seek(12, os.SEEK_CUR)
timescale = int(b2a_hex(mp4.read(4)), 16)
duration = int(b2a_hex(mp4.read(4)), 16)
return (timescale, duration)
mp4.seek(boxsize – 8, os.SEEK_CUR)
return getduration(mp4)
except ValueError as e:
return 0
gridfs_test()
[/python]
動画像ファイル全体を丁寧に解析したい場合はMP4Boxなどの外部プログラムを使った方が楽なんですが、データの一部だけ解析したい場合はプログラムを書いた方が早いのでその辺りは状況に応じて。
* MongoDB – Tech Noteあわせて読む: