要約
- nodejsで画像生成するなら、サーバーサイドのCanvasと言う手段があるよ
- hapijsだとpathの作成が楽だよ
- redisにバイナリ保存するときはoption設定がいるよ
ことのはじまり
みなさんどうやって大量のデータを人の目で確認するでしょうか。
テーブル生成して目視? なるほど、いいですね。ですがすべてのユーザが眼grepを得意とする人ではありません。
データがある程度増えてくると、チャートや画像の方がずっと分かりやすいことがよくあります。
今回はnodejsで画像生成ついでにhapiフレームワークとredisでのキャッシュを試してみました。
画像生成Server
最初はクライアントサイドでcanvasを使うと言う手も考えました。
ブラウザ上で表示が制御できればいいんじゃないかと言うのもあったのですが、操作が増えるのが嫌煙されていたので却下。
しかし対象の画像をすべて表示しようと思うと1pageあたり100-300枚ほどあり、全部をブラウザでと言うのは現実的でないためサーバで画像生成を行い後述するように一部はキャッシュをすると言う方式を取りました。
幸いなことにcanvas moduleが存在し、クライアントサイドでやるのとほとんど変わらずに書くことができます。
Canvas Install
CanvasはNative moduleなのでbuildするためにライブラリやVSのインストールが必要です。
本家解説にリンク先が貼ってあるので参照してください。
今回はWindowsを想定して進めます。
- Python2.7系をインストールする
- Visual Studio 2013をinstallする
- GTK 2をインストールする
-
C:\GTK
など適当なフォルダにzipを解凍 - pathを通す
- libjpeg-turboをインストールする
-
npm install canvas
が成功することを確認する
OSとnodejs(たぶん正確にはV8)のversionがあっていればビルド済みのバイナリモジュールを使い回すことができるので、
複数立てるときには一台でビルドしてそれをコピーするのが楽かもしれません。
Canvasをつかう
使い方はブラウザと同じでContextを生成して描画を行い、pngかjpegをbuffer/DataURLで書き出して使います。
Method | description |
---|---|
pngStream() | png形式でStream書き出し |
jpegStream(opt) | jpeg形式でStream書き出し |
toBuffer() | png形式で同期書き出し |
toBuffer(callback(err, buf)) | png形式で非同期書き出し |
toDataURL(['image/png']) | DataURLで同期書き出し |
toDataURL('image/png', [opt], callback(err, img)) | DataURLで非同期書き出し |
描画例
const Canvas = require('canvas');
// Context生成
let image = Canvas.image;
let canvas = new Canvas(200,200);
let ctx = canvas.getContext('2d');
// 文字描画
ctx.font = "30px Lato"
ctx.rotate(.1);
ctx.fillText('Canvas', 50, 100);
// ライン描画
let te = ctx.measureText('Canvas');
ctx.stroke.style = 'rgba(0,0,0,0.7)'
ctx.beginPath();
ctx.lineTo(50,102);
ctx.lineTo(50 + te.width, 102);
ctx.stroke();
// 書き出し
canvas.toBuffer((err, buf)=>{
fs.writeFile("image.png", buf);
})
hapijs
httpサーバーといえばexpressがとても作りやすいですが今回はhapijsを選択。
なぜ選んだかというと
でhapi-swaggerの存在が気になったからと言うものですが実際、
- routingの管理がしやすい
- joiをつかったparam/queryのバリデーションが楽
と言うあたりが嬉しい感がある。
しかし移行するにはpluginも使い方も大きく変わるので設定方法など学ぶことが多く
まだ把握しきれていないので語れません。
とりあえずIDとnumberで1URL=1画像生成になるように設定。
const lib = require('../lib/image');
server.route({
method: 'GET',
path: '/image/{id}/{num}',
// config にtagsとvalidateを設定。swaggerにも反映される
config: {
tags: ["api", "image"],
validate: {
params: {
id: Joi.string().regex(/^[a-zA-Z0-9]{3,20}$/),
num: Joi.number().integer().min(10).max(1000),
}
}
}
handler: (req, rep)=>{
lib(req.params,(err, buf)~>{
rep(buf).type('image/png');
})
}
});
redisでキャッシュ
画像生成サーバはできましたが本番環境の実測では生成に1枚あたり100-200ms程度かかっており、1page(200枚前後)で10-20sec...常時これでは使い物になりません。生成サーバーを並列にして単位時間あたりの生成数を増やし、生成後はredisに一定時間キャッシュするようにします。
使い方云々はほかを見てもらうとして躓いた点
- 書き込んだバイナリが壊れている
- redisが死んだらAppも死ぬ
に対する対応についてだけ話します。パッケージはnode redisを使います。
バイナリが壊れる
redis-cliで見たら存在するのにnodejsで読み出してみたら壊れているというのにハマりました。
実際data.lengthを見ると小さくなっています。これは先人が記録を残してくれていました。
node redisでバイナリを読み書きする場合はoptionの設定に detect_buffers
を追加し、keyのbufferで入力するとBufferのデータ形式がとれました。ちなみにreturn_buffers
をtrueにすればすべてバイナリで返ってくるようです。
const redis = require('redis');
const client = redis.createClient({detect_buffers:true}); // detect_buffesをtrueにする
// client.setex("key", exipre, data) // 文字列や数字などは文字列でよい
client.setex(new Buffer("key"), exipre, data) // keyをBufferに変換
redisが死んだらAppも落ちる
単純にcllientを作っただけだとredisが死んだときに一緒に落ちます。
道連れにされるよりは遅くても生成できる方がマシなので対応。
一つはClient.on('error',(err))
を設定しておくこと。リスナーがいないと例外が飛んで落ちます。
もう一つはgetの前に生存確認を挟んで、死んでる場合はキャッシュ周りをスキップします。
client.on("error", function (err) {
console.error("Error " + err);
});
if(!client.connected) return fn(cb); // 未接続ならcacheの確認をせずに生成して返すなど
client.get(key,(err, buf)=>{
// hoeghoge
})
さいごに
hapijsは気が効いていいよという話でした。
まだ使いこなせてないので(特にログ周りのケアが...)しばらく実験が必要。
今回のリポジトリとherokuを用意しているので下記を参照してください。
今回初めてやってみましたがherokuのデプロイってすごく楽ですね!
redisはタダで用意する方法がすぐに見つからなかったので未適用。