今回は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で用意すると楽です)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
## 動画像ファイルを準備しておく $ 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! |
MongoDBのログには以下のように出力されていました。ディスクスペースがプリアロケーション済みの場合、600MB程度のファイルだと20秒ほど時間がかかっています。
1 2 3 4 5 6 7 8 |
## 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 |
コレクションの中身は以下のようになっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
$ 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" } ] |
自動的に “filename” フィールドにもインデックスを貼ってくれるようです。親切ですね。
プログラムから操作
いろいろな言語のドライバを使ってみましたが、C++ドライバ(doxygen documentation)とPythonドライバ(PyMongo)はGridFS対応も丁寧だったので今回はこれを利用します。
* C++ドライバ
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
#include <iostream> #include <mongo/client/dbclient.h> #include <mongo/client/gridfs.h> void metaprint(mongo::GridFile& file); int main(void) { 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<mongo::DBClientCursor> cursor = fs.list(); 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; } |
* 出力例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
---------- 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ドライバ
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
#!/bin/env python # -*- coding: utf-8 -*- import pymongo import gridfs import gridfs.errors def gridfs_test(): 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']) f = open('files/test1.wmv', 'r') ## ファイルの保存 # 任意のメタデータ(copyright)を付与 fs.put(f, filename='files/test1.wmv', copyright='rest-term.com') f.close() except gridfs.errors.NoFile as e: ## ファイルが存在しない場合 print e except gridfs.errors.GridFSError as e: ## その他のエラー print e def metaprint(c): ## メタデータ 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 if __name__ == '__main__': gridfs_test() |
* 出力例
1 2 3 4 5 6 7 |
[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ドライバを使ってさっと書いてしまうと良いかと思います。
GridFSではファイルをバージョニングしているので、過去に保存したファイルも取得することができます。また上記のPythonの例では、任意のメタデータ(copyright)をアドホックに追加していますが、ここはMongoDBの特性がよく表れているところですね。動画配信サービスでは著作権情報や配信期間などメタデータの管理が大変で、特にエンコードプロファイルは変更/追加頻度が高いのでMySQLなどのRDBMSだと素早い対応が難しいです。MongoDBを使えば手間が少し省けるかもしれません。
ファイルの中身を読む
動画像ファイル自体のメタデータを取得するために構造解析を行うことはよくあります。ここではGridFSに保存されたmp4ファイル(AVC/H.264 + AAC)がちゃんと読めるか確認しておきます。ここではMP4のMovie Header(mvhd)から timescale と duration の値を取得して動画の尺を求めます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
#!/bin/env python # -*- coding: utf-8 -*- import pymongo import gridfs import gridfs.errors import os from binascii import b2a_hex def gridfs_test(): 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 def getduration(mp4): ''' 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 if __name__ == '__main__': gridfs_test() |
動画像ファイル全体を丁寧に解析したい場合はMP4Boxなどの外部プログラムを使った方が楽なんですが、データの一部だけ解析したい場合はプログラムを書いた方が早いのでその辺りは状況に応じて。
今回はMongoDB GridFSの機能をひととおり触ってみました。実際に動画像ファイルのストレージとして運用する場合は容量も必要になってくるので、複数台でシャーディング構成を組むことになるのでしょうが、もしデータセンターをまたいだ場合にどういった問題が起こるかなども細かく検証していく必要があると思います。時間を作って検証を進めていきたいです。
「Tech Note」ページにもMongoDBセクションを作ったので、これからコツコツ更新していきたいと思います。もし興味があれば覗いていってください。
* MongoDB – Tech Note