ももクロスライダー作ったよ。(Redisを使ったアプリをHerokuに置く) 第一弾

f:id:yosuke_furukawa:20120502224348p:image

この前のNode学園でRedisを使ったアプリが多いのと、社内勉強会でRedisの話が出てきたので、試しにRedis使って何かアプリで実践してみようと思いやってみました。

Redisというのは危険なほど速いと噂のインメモリKVSです。memcachedと同じような感じですが、永続化ができること、memcachedよりも高速であることpub/subという通知機能がある事が特徴です。

あと、せっかくだから最近ハマっているももクロの画像を流すアプリでも作ってやれ、という下心もプラスして作りました、「ももクロスライダー」

ももクロスライダー

yosuke-furukawa/momoclo-slider · GitHub

ももクロスライダーの中身は Node.js + Redis + socket.ioになっています。
twitpicから #momoclo の付いた画像を持ってきて、Redisへ一定間隔でセット、せっかくなので、pub/subで新しい画像があったらクライアントへsocket.ioを使って送信までしてみました。特に画像を毎回見に行かなくてもそのまま流しておけば勝手に新しい画像を取ってきてくれるような仕組みになっています。

今回の話はももクロスライダーの中身の話を交えつつ、RedisをHerokuで使う時の注意点を伝えられるといいかなと思います。

■ app.js

/**
 * Module dependencies.
 */
var express = require('express')
  , routes = require('./routes');
var twitpic = require('twitpic').TwitPic;
var redis = require('redis');
var io = require('socket.io');
var redisClient, subscriber;
var port = process.env.PORT || 3000;

// Herokuに置く場合、REDISTOGO_URLがSETされているので、その時は以下のようにする。
if (process.env.REDISTOGO_URL) {
  var rtg   = require("url").parse(process.env.REDISTOGO_URL);
  redisClient = redis.createClient(rtg.port, rtg.hostname);
  subscriber = redis.createClient(rtg.port, rtg.hostname);
  redisClient.auth(rtg.auth.split(":")[1]);
  subscriber.auth(rtg.auth.split(":")[1]);
} else {
  redisClient = redis.createClient();
  subscriber = redis.createClient();
}

var app = module.exports = express.createServer();

// Configuration

redisClient.on("error", function(err) {
  console.log("Error! " + err);
});
subscriber.subscribe('images');

var keyword = 'momoclo';
app.configure(function(){
  app.set('views', __dirname + '/views');
  app.set('view engine', 'jade');
  app.use(express.bodyParser());
  app.use(express.methodOverride());
  app.use(app.router);
  app.use(express.static(__dirname + '/public'));
  // setIntervalで1分間隔でtwitpicを検索。
  setInterval(function() {
  try {
  console.log("keyword = " + keyword);
  twitpic.query('tags/show', {tag: keyword}, function (data){
    if(data) {
      for (var index in data.images) {
        existsAndSet(data, index, keyword);
      }
    }
  });
  } catch (e) {
    console.log(e);
  }

  }, 60000);
  });

app.configure('development', function(){
  app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
});

app.configure('production', function(){
  app.use(express.errorHandler());
});

// Routes
app.get('/', routes.index);
app.get('/img', function(req, res){
  redisClient.hvals("images", function (err, images) {
  console.log('redis connect.');
    if (!images) {
      console.log("images is null");
      getImages(res);
    } else {
      var imageData = [];
      for (var index in images) {
        imageData.push(JSON.parse(images[index]));
      }
      res.json({title: keyword, data: imageData});
    }
  });
});

//存在していたらsetしてpublishします。
function existsAndSet(data, index, keyword) {
 redisClient.hexists("images", data.images[index].short_id, function (err, res) {
    if (err) {
      console.log(err);
    } else if (res == 0) {
      if (data.images[index].message.indexOf(keyword) >= 0) {
        var jsonData = {
          category: 'twitpic',
          short_id: data.images[index].short_id,
          message: data.images[index].message,
          username: data.images[index].user.username,
          width: data.images[index].width,
          height: data.images[index].height
        };
        redisClient.hset("images", data.images[index].short_id, JSON.stringify(jsonData), redis.print);
        redisClient.publish('images', JSON.stringify(jsonData));
      }
    } else {
     //KEY EXISTS, DO NOTHING 
    }
  });
}

function getImages(res) {
  console.log("method getImages begin");
  var imgJsonArray = new Array();
  try {
  twitpic.query('tags/show', {tag: keyword}, function (data) {
    if (data) {
    for (var index in data.images) {
      var jsonData = {
        category: 'twitpic',
        short_id: data.images[index].short_id,
        message: data.images[index].message,
        username: data.images[index].user.username,
        width: data.images[index].width,
        height: data.images[index].height
      };
      imgJsonArray.push(jsonData);
      redisClient.hset("images", data.images[index].short_id, JSON.stringify(jsonData), redis.print);
    }
     res.json(
     { title: 'Express1',
       data: imgJsonArray
     });
    }
  });
  } catch (e) {
    console.log(e);
  }

}

io = io.listen(app);
//subscriberが受信したら、その内容をsocket.ioでemitします。
subscriber.on('message', function(channel, message) {
  console.log('getMessage: ' + message);
  io.sockets.emit('updated', JSON.parse(message));
});

app.listen(port, function(){
  console.log("Express server listening on port %d in %s mode", app.address().port, app.settings.env);
});

Herokuに置くときの注意点はココです。

// Herokuに置く場合、REDISTOGO_URLがSETされているので、その時は以下のようにする。
// Herokuの場合
if (process.env.REDISTOGO_URL) {
  var rtg   = require("url").parse(process.env.REDISTOGO_URL);
  redisClient = redis.createClient(rtg.port, rtg.hostname);
  subscriber = redis.createClient(rtg.port, rtg.hostname);
  redisClient.auth(rtg.auth.split(":")[1]);
  subscriber.auth(rtg.auth.split(":")[1]);
} 
// Herokuじゃない場合
else {
  redisClient = redis.createClient();
  subscriber = redis.createClient();
}

REDISTOGO_URLにauth用の文字列も格納されているのでそれを使ってredisのクライアントの認証を済ませておくことがポイントです。
Redisライブラリによっては面倒なurlのparseをしなくてもいいところがあるみたいですが、ひとまずnode_redisライブラリではこの様にします。

コマンドでは以下のようにしてデプロイします。

$ git init
$ git add .
$ git commit -m "slider appli."
$ heroku create --stack cedar momoclo-slider
$ heroku addons:add redistogo:nano
$ git push heroku master

redistogo:nanoであれば無料で5MBまでですが使えます。

最近では、このRedisをセッション管理に使ってsocket.ioをスケールアウトするのにも使えるみたいですね。
自分も試してみよう。

次回はRedisじゃなくてMongoの場合、twitpicから取得してくるだけの場合でベンチ比較してみようかと思います。