2012年11月14日水曜日

Node.jsってなんじゃ?(DynamoDBにアクセス)

いままでチャットプログラムをサンプルとして使用していましたが、やりとりしているメッセージは基本的に揮発性のものです。サーバーが止まればなくなってしまう、もしくは参照できなくなるデータです。

今回は、AmazonDynamoDBをメッセージの保存場所としてみます。
node.js でDynamoDBを利用するには、dynodeというモジュールを使用します。

dynodeはDynamoDBへのAPIアクセスをラップしてnodeライクに使うことができます。

では早速触ってみたいと思います。
まずdynodeをインストールします。
# npm install -g dynode

ロードと設定は以下のように行います。
var dynode = require('dynode');
dynode.auth({region:"ap-northeast-1",
                accessKeyId:"xxxxxxxxxxxxxxxxxx",
                secretAccessKey:"yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"});


また、各メソッドは以下のようにコールバック式で行います。
dynode.putItem("chat", {id:id, msg:msg, date:(new Date()).toString()}, function(err, res){
  console.log(err);
});


ここでは、メッセージの保存先として、idというプライマリキーと、dateをレンジキーをもつchatというテーブルを作成します。


サンプルとして以前のチャットプログラムを使用します。

server.js

connectionハンドラで、接続が確率したときにchatテーブルから過去のメッセージをスキャンしてarchiveイベントに乗せてクライアントにemitします。
また、msgハンドラでは、いままで単純にemitしていたところを、dynode.putItemで保存してからemitするようにしました。
var server = require('http').createServer(function(req, res){
  res.writeHead(200, {'Content-Type': 'text/html'});
  res.end('server connected');
});
server.listen(3001);

var dynode = require('dynode');
dynode.auth({region:"ap-northeast-1",
                accessKeyId:"xxxxxxxxxxxxxxxxxx",
                secretAccessKey:"yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"});

var io = require('socket.io').listen(server);
var RedisStore = require('socket.io/lib/stores/redis');
opts = {host:'10.0.0.200', port:6379};
io.set('store', new RedisStore({redisPub:opts, redisSub:opts, redisClient:opts}));

io.sockets.on('connection', function (socket) {

  var id = socket.id;
 //メッセージ履歴をscanします
  dynode.scan("chat", function(err, res){
  //dateでソートします
  res.sort(function(a,b){
     if(a.date < b.date)return -1;
      if(a.date > b.date)return 1;
      return 0;
    });
    //archiveイベントに送信します。
    io.sockets.emit('archive', { id:id, archive: res });
    io.sockets.emit('info', { id:id, msg: 'welcome '+id });
  });

  socket.on('msg', function (msg) {
    io.sockets.emit('msg', {id:id, msg: msg});<
              //保存します。
    dynode.putItem("chat", {id:id, msg:msg, date:(new Date()).toString()}, function(err, res){
        if(err){
            console.log(err);
            console.log(res);
        }
    });
  });
  socket.on('disconnect', function(){
    io.sockets.emit('info', {id:id, msg: 'bye '+id});
  })
});


client.js

クライアント側では、新たにarchiveハンドラを作成し、メッセージをまとめて展開できるようにしておきます。

$(function(){
    var socket = io.connect('http://'+hostname+':3001/');
    socket.on('connect', function() {
      $("#log").html($("#log").html() + "<br />" + 'connected');
      socket.on('info', function (data) {
        $("#log").html($("#log").html() + "<br />" + data.msg);
      });
      socket.on('archive', function(data){
                     //メッセージ履歴を表示します。
      for(var i=0;i<data.archive.length;i++){
      $("#log").html($("#log").html() + "<br />" + "<b>" +       data.archive[i].id + ":</b>" + data.archive[i].msg);
      }
      });

      socket.on('msg', function(data){
        $("#log").html($("#log").html() + "<br />" + "<b>" + data.id + ":</b>" + data.msg);
      });

      $("#send").click(function(){
        var msg = $("#msg").val();
        if(!msg){
          alert("input your message");
          return;
        }
        socket.emit('msg', msg);
      });
    });
  });


これで完了です。それでは動かして見ましょう。
以下のように、メッセージを投稿すると、



以下のようにDynamoに登録されているのがわかります。


ひとしきりメッセージを送ったあと、別のブラウザで接続すると、過去のメッセージが表示されるようになりました。


このように、nodeでもAWSに接続できるモジュールもいくつかあるので、幅が広がりますね。
以上です。

2012年11月12日月曜日

redisってなんじゃ?(FuelPHPの管理画面からSocket.IOで全員にPush)

node.js、とりわけSocket.IOでは、ユーザー同士のリアルタイム通信が簡単にに行えることがわかってきました。
ですが、時には管理画面や、バッチの処理によってユーザーに何か通知をしたいことがあるかもしれません。
今度はredisを利用して、システム管理者とユーザーの間でリアルタイム通信を行なってみます。

以前紹介したFuelPHPもまた、キャッシュストアや、クライアントインターフェースとしてRedisをサポートしています。
そこで、FuelPHPで管理画面をつくり、ボタンを押すとチャット中のユーザーに管理者からのメッセージを表示させたいと思います。

チャットプログラムや構成は前々回の構成のまま、node1, node2という2つのサーバーがredisサーバーにつながってセッション共有されている状態とします。
今回は、以下のように、さらにadminサーバーを追加し、管理画面を置きます。

    +----------+       +---------+
    | admin    |       | redis   |
    |----------| pub   |---------|
    | fuelphp  +------->         |
    |          | sub   | redis   |
    | admin.js <-------+         |
    +-----+----+       +---------+
          |
          +-----------------+
          |      emit       |
    +-----v----+       +----v----+
    | node1    |       | node2   |
    |----------|       |---------|
    | chat.js  |       | chat.js |
    +----------+       +---------+

上図のように、fuelphpからredisにpublishしたものを同じadminサーバー上のsocket.ioでsubscribeし、チャットサーバーへブロードキャストするイメージです。
チャットサーバーが直接subscribeすることも可能ですが、そうすると、チャットサーバーの数だけsubscribe→emitが発生して、メッセージが重複してしまいます。subscribe→emit役は1つである必要があるので、adminサーバーに兼任させます。
なので、既存のチャットサーバーには手を加える必要がなく、adminサーバーだけ用意して実装すればいいわけです。

それでは実際に用意してみます。


まず必要なユーザーやライブラリを用意します。
# useradd appadmin
# passwd appadmin
# yum install -y vim git wget php  gcc gcc-c++ make
# curl get.fuelphp.com/oil | sh


FuelPHPの最新版(v1.4)では、タイムゾーンの指定が必至になったのでphp.iniで設定します。
# vim /etc/php.ini
---
date.timezone = Asia/Tokyo
--


次にアプリユーザーのホームディレクトリをApacheからアクセス可能にします。
# chmod 755 /home/appadmin


アプリを作成します。
publishボタンを配置する画面(index)とpublishボタンの押下(publish)の2つのアクションを用意します。
# su - appadmin
$ oil create app
$ cd app
$ oil g controller greeting index publish


次に、DocumentRootをFuelPHPのpublicディレクトリに向けます。
$ exit
# cd /var/www/ 
# mv html html.org
# ln -s /home/appadmin/app/public html


そして、Apacheの設定をシンボリックリンクをたどり、.htaccessを許可するように変更し、起動します。
# vim /etc/httpd/conf/httpd.conf
---

<Directory "/var/www/html">
~略~
    Options FollowSymLinks
~略~
    AllowOverride All
~略~
</Directory>

---
# /etc/init.d/httpd start 


次にFuelPHPのconfig/db.phpおよび、viewとcontrollerを以下のように実装します。

fuel/app/config/db.php
redisの接続先を指定します。チャットサーバーが参照しているのと同じサーバーに接続させるようにします。
$ vim fuel/app/config/db.php
<?php

/**
 * Use this file to override global defaults.
 *
 * See the individual environment DB configs for specific config information.
 */

return array(

    'redis' => array(
        'default' => array(
            'hostname' => '10.0.0.200',
            'port'     => 6379
        )
    ),

);


fuel/app/views/greeting/index.php
Publishボタンを配置します。
$ vim fuel/app/views/greeting/index.php
---
<p>Index</p>

<?php echo Form::open('greeting/publish'); ?>
<?php echo Form::submit('publish', 'publish'); ?>
<?php echo Form::close(); ?>
---


fuel/app/classes/controller/greeting.php
publishアクション内で、Redisクラスをインスタンス化しています。
ここでの引数defaultは、db.phpで設定したラベルを指定します。
そして、publishメソッドを呼び出すことにより、redisサーバーに「greeting」というチャンネルでpublishを行います。
$ vim fuel/app/classes/controller/greeting.php
---
<?php

class Controller_Greeting extends Controller_Template
{

    public function action_index()
    {
        $this->template->title = 'Greeting &raquo; Index';
        $this->template->content = View::forge('greeting/index');
    

    public function action_publish()
    {

        $redis = Redis::instance('default');
        $redis->publish('greeting', 'おはよう諸君!!');

        $this->template->title = 'Greeting &raquo; Publish';
        $this->template->content = View::forge('greeting/publish');
    }
}
---


ここまでで、管理画面自体は完成です。
以下のようにPublishボタンがあるだけのシンプルな画面になっています。




つづいて、同じadminサーバーに、node.jsを入れて動かします。
モジュールはforever, socket.io, redisを入れます。
socket.ioにもredisが含まれているのですが、requireのしやすさやバージョンが新しさなどのため、別途いれておきます。
# cd /usr/local/src
# wget http://nodejs.org/dist/v0.8.14/node-v0.8.14.tar.gz
# tar xzvf node-v0.8.14.tar.gz
# cd node-v0.8.14
# ./configure  
# make
# make install
# curl https://npmjs.org/install.sh | sh
# npm install -g socket.io
# npm install -g forever
# npm install -g redis


これでnodeのインストールができました。
それでは、subscribe→emit用にnodeのスクリプトを実装します。
# su - appadmin
$ mkdir -p /home/appadmin/admin/node
$ cd /home/appadmin/admin/node
$ vim admin.js
---

var server = require('http').createServer(function(req, res){
  res.writeHead(200, {'Content-Type': 'text/html'});
  res.end('server connected');
});
server.listen(3001);

var io = require('socket.io').listen(server);
var RedisStore = require('socket.io/lib/stores/redis');
var opts = {host:'10.0.0.200', port:6379};
io.set('store', new RedisStore({redisPub:opts, redisSub:opts, redisClient:opts}));

var redis = require('redis');
var sub = redis.createClient(opts.port, opts.host);
sub.subscribe('greeting');
sub.on("message", function(channel, message){
  io.sockets.emit('msg', {msg:message});
});

---

ポイントは、チャット用のスクリプトと同じようにredisサーバーをRedisStoreとしてstore登録することと、
それとは別にsubscribe用のredisクライアントを作り、「greeting」チャンネルをsubscribeし、受信したメッセージをチャット参加者全員にemitするようにするところです。

これを起動します。
$ forever start admin.js

それでは、実際に動かしてみましょう。
まず、前回と同様、node1とnode2のチャットウィンドウを開いて、適当にチャットしてみます。


それでは、ここで先程の管理画面でPublishボタンを押してみます。



そして、チャット画面をみてみます。


おお!メッセージが表示されました。
これで管理画面からのコントロールも可能になりました。

以上です。

2012年11月11日日曜日

redisってなんじゃ?(pub/sub編)


redisにはpub/subという機能があります。

これはpublish/subscribeパターンという仕組みの実装で、発行者と購読者という役割でメッセージの配信を行うものです。

では、redisのコンソールを使って試してみたいと思います。

redis1とredis2という2つのホストを使用します。
redis1ではローカルホストでredis-cliに接続します。
redis2ではredis1のredis-clに接続します。

redis1
# redis-cliredis 127.0.0.1:6379>

redis2
# redis-cli -h 10.0.0.200redis 10.0.0.200:6379>

メッセージの購読はチャンネル名を指定します。
redis2でgreetingというチャンネル名でsubscribeコマンドを実行します。

redis2
redis 10.0.0.200:6379> subscribe greeting
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "greeting"
3) (integer) 1

こうすることによって、redis2はredis1のgreetingというチャンネルのメッセージの待ち受けが始まります。

続いて、redis1でメッセージを発行します。
greetingというチャンネルで「Hello World!」という内容のメッセージを発行します。

redis1
redis 127.0.0.1:6379> publish greeting "Hello World!"
(integer) 1
redis 127.0.0.1:6379>

すると即座にredis2のクライアントにHello Worldというメッセージが受信されたことがわかります。

redis2
redis 10.0.0.200:6379> subscribe greeting
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "greeting"
3) (integer) 1
1) "message"
2) "greeting"
3) "Hello World!"

このように、redisでは他のサーバーのredisクライアントとメッセージのやりとりが可能です。
クライアントコンソールでは効能がわかりにくいので、次回はアプリケーションレベルで試してみたいと思います。

以上です。

2012年11月10日土曜日

Node.jsってなんじゃ?(redisでSocket.IOをスケール)


前回は、redisをインストールして生でつかってみました。
今回はnode.jsでredisを利用してみたいと思います。

マルチユーザーのサーバーでのプッシュ配信はSocket.IOが定番ですが、
サーバーが増えた時にある問題が生じます。
例えばサーバーを2つに増やして、サーバーAでブロードキャストしても
サーバーBのクライアントでは受信できないのです。

以前の記事で作成したチャットプログラムを例にしてみます。

サーバー側のjs
$ cat /home/appadmin/chat/node/chat.js
var server = require('http').createServer(function(req, res){
  res.writeHead(200, {'Content-Type': 'text/html'});
  res.end('server connected');
});
server.listen(3001);

var io = require('socket.io').listen(server);
io.sockets.on('connection', function (socket) {
  socket.emit('info', { msg: 'welcome' });
  socket.on('msg', function (msg) {
    io.sockets.emit('msg', {msg: msg});
  });
  socket.on('disconnect', function(){
    socket.emit('info', {msg: 'bye'});
  });
});


クライアント側のjs

$ cat /home/appadmin/chat/public/assets/js/client.js
$(function(){
    var socket = io.connect('http://'+location.hostname+':3001/');
    socket.on('connect', function() {
      $("#log").html($("#log").html() + "<br />" + 'connected');
      socket.on('info', function (data) {
        $("#log").html($("#log").html() + "<br />" + data.msg);
      });
      socket.on('msg', function(data){
        $("#log").html($("#log").html() + "<br />" + "<b>" + data.msg + "</b>");
      });
      $("#send").click(function(){
        var msg = $("#msg").val();
        if(!msg){
          alert("input your message");
          return;
        }
        socket.emit('msg', msg);
      });
    });
  });


画面
$ cat /home/appadmin/chat/public/index.html
<!DOCTYPE html>
<html>
<head>
     <meta charset="UTF-8">
     <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js" type="text/javascript" charset="utf-8"></script>
     <script type="text/javascript">
     $(function(){
         function load(){
             $.getScript("assets/js/client.js");
         }
         $.getScript("http://" + location.hostname + ":3001/socket.io/socket.io.js", function(){
             load();
         });
     });
     </script>
     <title>Node A</title>
</head>
<body>
     <input id="msg" type="text" style="width:400px;"></input>
     <input id="send" type="button" value="send" /></br >
     <div id="log" style="width:400px;height:400px;overflow:auto;border:1px solid #000000;"></div>
</html>


サーバー側のjsをforeverで起動します。
$ forever start chat.js

これをサーバーAとします。
同じ内容を別のサーバーBに配置し、同じようにnodeを起動します。

2つのサーバーにアクセスすると、以下のように同じ画面が表示されます。




サーバーAで「a」と入力します。
サーバー側では接続した全ユーザーに投稿内容をブロードキャストし、ユーザーの画面に「a」が表示されます。
しかし、サーバーBには何も表示されません。
おなじようにサーバーBで、「b」と投稿してもサーバーAに接続した画面にはなにも表示されません。
接続がサーバーAとサーバーBで共有されていないためです。

ここで登場するのがredisです。
Socket.IOではデフォルトで接続情報をローカルメモリに保存しています。
これをMemoryStoreと呼びますが、Socket.IOにはRedisStoreというredisに接続情報を保存するオプションも存在します。
このオプションを選択することで、分散されたnodeサーバーが同じredisサーバーを参照し
各nodeの接続情報を共有することができます。


それではサーバー側のjsを修正してRedisStoreを使ってみます。
接続するredisは前回設定したredisサーバーにします。

var server = require('http').createServer(function(req, res){
  res.writeHead(200, {'Content-Type': 'text/html'});
  res.end('server connected');
});
server.listen(3001);

var io = require('socket.io').listen(server);

//RedisStoreを読み込みます
var RedisStore = require('socket.io/lib/stores/redis');
//redisサーバーの接続先情報を定義します
opts = {host:'10.0.0.200', port:6379};
//storeをRedisStoreにし、redisPub, redisSub, redisClientをredisサーバーに向けます
io.set('store', new RedisStore({redisPub:opts, redisSub:opts, redisClient:opts}));

io.sockets.on('connection', function (socket) {
  socket.emit('info', { msg: 'welcome' });
  socket.on('msg', function (msg) {
    io.sockets.emit('msg', {msg: msg});
  });

  socket.on('disconnect', function(){
    socket.emit('info', {msg: 'bye'});
  });
});


これで再起動します。
$ forever restart chat.js


再度2つの画面をリロードして、サーバーAの画面に「a」サーバーBの画面に「b」と入力してみます。




おお、両方の画面に「a」「b」が表示されました!
これで台数が増えてもすべてのユーザーが同じ空間でコミュニケーションすることができます。

以上です。

2012年11月9日金曜日

redisってなんじゃ?


redisはCで書かれたインメモリKVSでで、データセットすべてをメモリ内にもつため動作は爆速です。

また、指定回数更新が発生すると、データのスナップショットをディスクに書き込むため、プロセスが落ちてもデータを保持することができ永続化も可能です。

今回はredisをちょっと触ってみたいと思います。
さっそくインストールしてみます。
$ cd /usr/local/src
$ wget http://redis.googlecode.com/files/redis-2.6.3.tar.gz
$ tar xzf redis-2.6.3.tar.gz
$ cd redis-2.6.3
# make
# make install

インストールはこれで完了です。/usr/local/binにredisのコマンド群がインストールされました。

続いてredisを起動してみます。
# redis-server
[3042] 08 Nov 16:32:25.609 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf
[3042] 08 Nov 16:32:25.609 # Unable to set the max number of files limit to 10032 (Operation not permitted), setting the max clients configuration to 992.
[3042] 08 Nov 16:32:25.610 # Warning: 32 bit instance detected but no memory limit set. Setting 3 GB maxmemory limit with 'noeviction' policy now.
                _._                                              
           _.-``__ ''-._                                          
      _.-``    `.  `_.  ''-._           Redis 2.6.3 (00000000/0) 32 bit
  .-`` .-```.  ```\/    _.,_ ''-._                                
 (    '      ,       .-`  | `,    )     Running in stand alone mode
 |`-._`-...-` __...-.``-._|'` _.-'|     Port: 6379
 |    `-._   `._    /     _.-'    |     PID: 3042
  `-._    `-._  `-./  _.-'    _.-'                                
 |`-._`-._    `-.__.-'    _.-'_.-'|                              
 |    `-._`-._        _.-'_.-'    |           http://redis.io    
  `-._    `-._`-.__.-'_.-'    _.-'                                
 |`-._`-._    `-.__.-'    _.-'_.-'|                              
 |    `-._`-._        _.-'_.-'    |                              
  `-._    `-._`-.__.-'_.-'    _.-'                                
      `-._    `-.__.-'    _.-'                                    
          `-._        _.-'                                        
              `-.__.-'                                            

[3042] 08 Nov 16:32:25.610 # Server started, Redis version 2.6.3
[3042] 08 Nov 16:32:25.610 # WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect.
[3042] 08 Nov 16:32:25.610 * The server is now ready to accept connections on port 6379

このようなコンソールログが出力され、起動されたことを示します。
リッスンポートは6379がデフォルトのようです。


また、ログの内容をみると、confファイルを指定して起動する方法も記載されているので、その様にしてみます。
まずCtrl+Cでサーバーをストップします。
そして、/etc/redis/ディレクトリを作成し、解凍したredis-2.6.3にあるredis.confを、その下に配置します。
また、データファイルの置き場として/var/lib/redisを作成しておきます。
# mkdir /etc/redis /var/lib/redis
$ cd /usr/local/src/redis-2.6.3/
$ cp redis.conf /etc/redis/

そして設定ファイルの場所を指定して再起動します。
# redis-server /etc/redis/redis.conf


ここで、同じサーバーの別のコンソールからこのサーバーへクライアント接続してみます。
$ /usr/local/src/redis-2.6.3/src
$ redis-cli
redis 127.0.0.1:6379>


このようにredisのクライアントが立上がり、コマンドを待ち受けます。

値の保持と取得はset,getで行うようです。
redis 127.0.0.1:6379> set greeting "Hello World!"
OK
redis 127.0.0.1:6379> get greeting
"Hello World!"
問題なくセットされているようです。


次に、redisサーバーを落としてみます。
[3042] 08 Nov 16:32:25.610 * The server is now ready to accept connections on port 6379
^C


再起動します。
# redis-server /etc/redis/redis.conf
redis 127.0.0.1:6379> get greeting
(nil)

永続化されていないようです。
redisはディスクに書き込む条件を設定でき、さきほどの設定ファイルをみると
save 900 1
save 300 10
save 60 10000
となっています。

ドキュメントの日本語訳をみると
このような設定がされると、次のようなタイミングで保存します:
もし最低1回、キーの変更が発生すると、900秒(15分)後
もし最低10回、キーの変更が発生すると、300秒(5分)後
もし最低10,000回、キーの変更が発生すると、60秒後

となっています。
ここではわかりやすいように、変更があったら1秒後に保存してみます。
また、ログファイル出力やデータファイルの場所も指定しておきます。
# vim /etc/redis/redis.conf
---
~略~
#logfile stdout
logfile /var/log/redis.log
~略~
save 1 1
#save 900 1
#save 300 10
#save 60 10000
~略~
---


そして再起動します。
^C
# redis-server /etc/redis/redis.conf


再びコンソールで値をセットします。
redis 127.0.0.1:6379> set greeting "Hello Word!"
OK
redis 127.0.0.1:6379> get greeting
"Hello World!"


そして、サーバーを一度落としてから再度起動してみます。
^C
# redis-server /etc/redis/redis.conf


再度コンソールで値を取得すると、、、
redis 127.0.0.1:6379> get greeting
"Hello World!"

今度は取得することができました。
データがメモリからディスクに保存されていたことがわかります。
また、起動してしまえばデータはメモリ上に再配置されているので、パフォーマンスが落ちることはありません。

ここまででも良いのですが、redisには起動スクリプトがありません。
起動スクリプトをつくって起動してみます。
あらかじめredis-serverは落としておきます。
まず、redisをデーモン化できるように設定します。
# vim /etc/redis/redis.conf
---
~略~
#daemonize no
daemonize yes
~略~
---


スクリプトファイルをgist公開している人がいたので、流用させていただきました。
ただし、redisの場所は/usr/local/sbinではなく/usr/local/binなのでその部分は変更しています。
#vim /etc/init.d/redis-server
---

#!/bin/sh
#
# redis - this script starts and stops the redis-server daemon
#
# chkconfig: - 85 15
# description: Redis is a persistent key-value database
# processname: redis-server
# config: /etc/redis/redis.conf
# config: /etc/sysconfig/redis
# pidfile: /var/run/redis.pid

# Source function library.
. /etc/rc.d/init.d/functions

# Source networking configuration.
. /etc/sysconfig/network

# Check that networking is up.
[ "$NETWORKING" = "no" ] && exit 0

redis="/usr/local/bin/redis-server"
prog=$(basename $redis)

REDIS_CONF_FILE="/etc/redis/redis.conf"

[ -f /etc/sysconfig/redis ] && . /etc/sysconfig/redis

lockfile=/var/lock/subsys/redis

start() {
    [ -x $redis ] || exit 5
    [ -f $REDIS_CONF_FILE ] || exit 6
    echo -n $"Starting $prog: "
    daemon $redis $REDIS_CONF_FILE
    retval=$?
    echo
    [ $retval -eq 0 ] && touch $lockfile
    return $retval
}

stop() {
    echo -n $"Stopping $prog: "
    killproc $prog -QUIT
    retval=$?
    echo
    [ $retval -eq 0 ] && rm -f $lockfile
    return $retval
}

restart() {
    stop
    start
}

reload() {
    echo -n $"Reloading $prog: "
    killproc $redis -HUP
    RETVAL=$?
    echo
}

force_reload() {
    restart
}

rh_status() {
    status $prog
}

rh_status_q() {
    rh_status >/dev/null 2>&1
}

case "$1" in
    start)
        rh_status_q && exit 0
        $1
        ;;
    stop)
        rh_status_q || exit 0
        $1
        ;;
    restart|configtest)
        $1
        ;;
    reload)
        rh_status_q || exit 7
        $1
        ;;
    force-reload)
        force_reload
        ;;
    status)
        rh_status
        ;;
    condrestart|try-restart)
        rh_status_q || exit 0
;;
    *)
        echo $"Usage: $0 {start|stop|status|restart|condrestart|try-restart|reload|force-reload}"
        exit 2
esac
---


続いて、自動起動設定します。
# chkconfig --add redis-server
# chkconfig redis-server on
# /etc/init.d/redis-server start
redis-server を起動中:                                     [  OK  ]
# ps -ax | grep redis
Warning: bad syntax, perhaps a bogus '-'? See /usr/share/doc/procps-3.2.8/FAQ
 5494 ?        Ssl    0:00 /usr/local/bin/redis-server /etc/redis/redis.conf
 5499 pts/2    S+     0:00 grep redis


うまく起動しているようです。
ここで別コンソールのredis-cliを起動してみます。
$ redis-cli
redis 127.0.0.1:6379> get greeting
"Hello World!"

成功しました。
これで起動時にredisを立ち上げることができました。

次回はこのredisをアプリケーションから使ってみたいと思います。
以上です。

2012年11月7日水曜日

Fluentdってなんじゃ?(Mongo&Node編 ブラウザでtail)

前回までで、fluentdを利用してApacheのログをmongoDBに保存することができました。

構成はこのようになっていました。
  +----------------------------+   +------------------------+
  | web server (10.0.0.8)      |   | mongo server(10.0.0.16)|
  |----------------------------|   |------------------------|
  |          fluentd           |   |                        |
  |                            |   |                        |
  | +---------+     +--------+ |   |      +----------+      |
  | | input   |     | output | |   |      |  fluent  |      |
  | |---------|+--> |--------| +--------->|----------|      |
  | |  tail   |     |  mongo | |   |      |   test   |      |
  | +---------+     +--------+ |   |      +----------+      |
  +----------------------------+   +------------------------+

今回はnode.jsを使用して、mongoDBに保存されたログをブラウザでリアルタイムにtailしてみます。
構成は以下のとおりです。
  +----------------------------+   +-------------------------+
  | web server (10.0.0.8)      |   | mongo server(10.0.0.16) |
  |----------------------------|   |-------------------------|
  |          fluentd           |   |         mongod          |
  |                            |   |                         |
  | +---------+     +--------+ |   |      +----------+       |
  | | input   |     | output | |   |      |  fluent  |       |
  | |---------|+--> |--------|+---------->|----------|       |
  | |  tail   |     |  mongo | |   |      |   logs   |       |
  | +---------+     +--------+ |   |      +-----+----+       |
  +----------------------------+   +------------|------------+
                                                |
                                 +--------------+
                                 | +-------------------------+
                                 | | node server(10.0.0.100) |
                                 | |-------------------------|
                                 | |         node.js         |
                                 | |                         |
                                 | |      +-----------+      |
                                 +------->| mongoose  |      |
                                   |      |-----------|      |
                                   |      | socket.io +----------> Brower
                                   |      +-----------+      |
                                   +-------------------------+


今回、web serverとmongo serverは、ほとんど前回の設定のままですが、
新規nodeサーバーでmongodbにアクセスするmongooseはデフォルトで複数系の名前のコレクションにアクセスするようになっています。
そこで、webサーバーのtd-agentのmongo outputプラグインの書き出し先をlogsという名前に変更しておきます。
また、リアルタイムに表示させたいのでflush_intervalを0sに設定しておきます。
<source>
  type tail
  format apache
  path /var/log/httpd/access_log
  tag apache.access
</source>

<match apache.access="apache.access">
  type mongo
  flush_interval 0s
  database fluent
  collection logs

  host 10.0.0.16
  user memorycraft
  password *******
</match>


それでは、nodeサーバーを設定します。
新規にEC2インスタンスをたちあげてnodeとhttpdをインストールします。
# yum install httpd -y
# /etc/init.d/httpd start

# yum install -y wget
# cd /usr/local/src/
# wget http://nodejs.org/dist/v0.8.14/node-v0.8.14.tar.gz
# tar xzvf node-v0.8.14.tar.gz 
# cd node-v0.8.14
# ./configure
# make
# make install


次にnpmをインストールしてからsocket.io, log, forever, mongooseをnpmインストールします。
# curl https://npmjs.org/install.sh | sh
# npm install -g socket.io
# npm install -g forever


nodeを動かすためのユーザーを作成します
# useradd appadmin
# passwd appadmin
# chmod 755 /home/appadmin
# su - appadmin


ノードモジュールへのパスを通すために環境変数を設定します。
$ vi ~/.bash_profile 
$ source ~/.bash_profile 
---
export NODE_PATH=/usr/local/lib/node_modules
---

$ source ~/.bash_profile


ここで、nodeサーバーとHTML用のコンテンツ置き場を用意します。
ディレクトリ構成は以下のとおりです。
cd ~/nodetest
tree
.
|-- node
|   |-- logs
|   |   `-- app.log
|   `-- server.js (nodeサーバー)
`-- public
    |-- assets
    |   |-- css
    |   |-- img
    |   `-- js
    |       `-- client.js (nodeクライアント)
    `-- index.html (tail用の画面)


また、publicをhttpdのDocumentRootにするため、以下のように設定します。
$ su -
# cd /var/www
# mv html html.org
# ln -s /home/appadmin/nodetest/public html
# vi /etc/httpd/conf/httpd/conf
---
~略~
<directory html="html" var="var" www="www">
    Options FollowSymLinks
    AllowOverride All
~略~
---
</directory>

# /etc/init.d/httpd restart


そして、httpdとnodeに接続させるため、SecurityGroupを以下のように設定します。



次にサーバーとコンテンツを実装してみます。 それぞれの内容は以下の通りです。

node/server.js
//3001番でlistenします
var server = require('http').createServer(function(req, res){
  res.writeHead(200, {'Content-Type': 'text/html'});
  res.end('server connected');
});
server.listen(3001);

//logsコレクションのスキーマ定義をしておきます。
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var AccessSchema = new Schema({
    host: String,
    user: String,
    method: String,
    path: String,
    code: Number,
    size: Number,
    referer: String,
    agent: String,
    time : Date
});
mongoose.model('log', AccessSchema);

//fluentデータベースに接続します。
mongoose.connect('mongodb://10.0.0.16/fluent');
var Access = mongoose.model('log');

//socket.ioを起動します。
var io = require('socket.io').listen(server);
io.sockets.on('connection', function (socket) {
    /**
     * クライアント接続時に、初期データとして、
     * 保存されているlogsコレクションをすべてemitします。
     */
    Access.find({}).sort({time:1}).exec(function(e, docs){
        if(e){
        }
        else{
            socket.emit('init', docs);
        }
    });
});

/** 
 * 1.5秒おきに最終取得ログの日時以降のログを取得して
 * 全員にブロードキャストし、最終日時を保存します。
 */
var last = new Date();
setInterval(function(){
    Access.find({}).where('time').gt(last).sort({time:1}).exec(function(e, docs){
        if(e){
        }
        else{
            for(var i=0; i<docs.length; i++){
                if(last < docs[i].time){
                    last = docs[i].time;
                }
            }
            io.sockets.emit('log', docs);
        }
    });
}, 1500);


public/assets/js/client.js
$(function(){
  //接続します
  var socket = io.connect('http://' + location.hostname + ':3001/');
  socket.on('connect', function() {
   /**
     * 接続成功時に初期データを時系列降順で表示させます
     */
    socket.on('init', function(data){
        $("#log").empty();
        for(var i=0; i<data.length; i++){
            row = data[i];
            $("#log").prepend(row.host+" "+row.time+" "+row.path+"<br/>");
        }
    });

    /**
     * 随時送られてくるデータは一番上に表示させます
     */
    socket.on('log', function(data){
        for(var i=0; i<data.length; i++){
            row = data[i];
            $("#log").prepend(row.host+" "+row.time+" "+row.path+"<br/>");
        }
    });

  });
});


public/index.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
<script type="text/javascript">
$(function(){
    function load(){
        $.getScript("assets/js/client.js");
    }
    $.getScript("http://" + location.hostname + ":3001/socket.io/socket.io.js", function(){
        load();
    });
});
</script>
<body>
    <div id="content">
        <h1>ログ</h1>
        <div id="log" width="500px" height="100%">
        </div>
    </div>
</body>
</html>


早速動かしてみます。
$ cd ~/nodetest/node
$ forever start server.js

これでnodeサーバーが起動しました
それでは、ブラウザを見てみます。



初期ログが表示されています。うまく動いているようです。
そこで、fluentdのtail対象サーバーであるwebサーバーへアクセスをしてみます。
わかりやすくするために、何度かリロードしておきます。


そして、さきほどのnodeサーバーの画面をみると、、、


おお!ログが追加で現れました!
これであるサーバーのログデータを別のサーバーでリアルタイムに表示させることができました。

以上です。

2012年11月5日月曜日

Fluentdってなんじゃ?(MongoDB編)


今回は、fluentdを利用して、Apacheのログを別サーバーのMongoDBに保存してみようと思います。

前回のlogサーバー(10.0.0.16)にmongoDBをホストし、webサーバー(10.0.0.8)のtd-agentから直接mongoに保存します。

まずはlogサーバーにmongoDBをインストールします。
yumリポジトリとして以下を追加してからyumインストールを行います。
# vi /etc/yum.repos.d/10gen.repo]
---
[10gen]
name=10gen Repository
baseurl=http://downloads-distro.mongodb.org/repo/redhat/os/i686
gpgcheck=0
---

# yum install mongo-10gen mongo-10gen-server -y

mongoのインストールは完了しました。
起動しておきます。
# /etc/init.d/mongod start


続いてwebサーバー側にmongoプラグインを設定します。
mongo Output Pluginはビルトインではなく、td-agent用のrubygemでインストールして使用するため、以下のようにインストールを行います。
# /usr/lib/fluent/ruby/bin/fluent-gem install fluent-plugin-mongo
Fetching: fluent-plugin-mongo-0.6.10.gem (100%)
Successfully installed fluent-plugin-mongo-0.6.10
1 gem installed
Installing ri documentation for fluent-plugin-mongo-0.6.10...
Installing RDoc documentation for fluent-plugin-mongo-0.6.10...


設定ファイルには、以下のように出力先をtype mongoを設定します。これは、

source
 apacheフォーマットの/var/log/httpd/access_logファイルをtailし、apache.accessというタグをつける

match
  apache.accessタグの入力があったら、10.0.0.16のmongoDBのfluentデータベースへmemorycraftユーザーで接続し、hogeコレクションとしてデータを登録する

という意味になります。
# vim /etc/td-agent/td-agent.conf

---

<source>
  type tail
  format apache
  path /var/log/httpd/access_log
  tag apache.access
</source>

<match apache.access>
  type mongo
  database fluent
  collection hoge
  host 10.0.0.16
  user memorycraft
  password ******
</match>

---


この時点ではwebサーバーからlogサーバーへmongoの接続ポートが閉じているので、まだ接続できません。
EC2で、mongoのデフォルトポートの27017番を開放します。




ここで一度webサーバー側のtd-agentを再起動しておきます。
# /etc/init.d/td-agent restart


この時点でmongoに接続すると、fluentデータベースはできているようです。
ここで、認証用にユーザーを作成します。
# mongo
MongoDB shell version: 2.2.1
connecting to: test

> use fluent
switched to db fluent
> db.addUser("memorycraft", "*****")
{
 "_id" : ObjectId("5096e46f1b4da85be896fb1e"),
 "user" : "memorycraft",
 "readOnly" : false,
 "pwd" : "2922d9ce24a959b1f359f7ba2d044017"
}

これで認証が設定されたので、webサーバー側のtd-agentを改めて再起動します。
# /etc/init.d/td-agent restart


ここで、mongoシェルに接続してみるとfluentというDBにhogeというコレクションができていることがわかります。
# mongo
MongoDB shell version: 2.2.1
connecting to: test
> use fluent
switched to db fluent
> show collections
system.indexes
system.users
hoge


中身を見てみると、、、
> db.hoge.find()
{ "_id" : ObjectId("5096e4e5a3441113d4000001"), "host" : "219.117.233.241", "user" : "-", "method" : "GET", "path" : "/", "code" : "403", "size" : "5039", "referer" : "-", "agent" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.1 Safari/537.17", "time" : ISODate("2012-11-04T21:57:09Z") }
{ "_id" : ObjectId("5096e4e5a3441113d4000002"), "host" : "219.117.233.241", "user" : "-", "method" : "GET", "path" : "/icons/poweredby.png", "code" : "304", "size" : "-", "referer" : "http://54.249.23.246/", "agent" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.1 Safari/537.17", "time" : ISODate("2012-11-04T21:57:10Z") }
{ "_id" : ObjectId("5096e4e5a3441113d4000003"), "host" : "219.117.233.241", "user" : "-", "method" : "GET", "path" : "/icons/apache_pb.gif", "code" : "304", "size" : "-", "referer" : "http://54.249.23.246/", "agent" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.1 Safari/537.17", "time" : ISODate("2012-11-04T21:57:10Z") }
{ "_id" : ObjectId("5096e4e5a3441113d4000004"), "host" : "219.117.233.241", "user" : "-", "method" : "GET", "path" : "/favicon.ico", "code" : "404", "size" : "288", "referer" : "-", "agent" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.1 Safari/537.17", "time" : ISODate("2012-11-04T21:57:10Z") }



おおー。データが入っています。
たとえば、pathが"/"のデータのみのログも簡単に抽出できるようになりました!
> db.hoge.find({path:"/"})
{ "_id" : ObjectId("5096e4e5a3441113d4000001"), "host" : "219.117.233.241", "user" : "-", "method" : "GET", "path" : "/", "code" : "403", "size" : "5039", "referer" : "-", "agent" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.1 Safari/537.17", "time" : ISODate("2012-11-04T21:57:09Z") }


このようにfluentdを用いると、ログの集約や集計がデイリーやマンスリーではなく準リアルタイムで行えるようになります。とても便利ですね。

今回はここまで。

Fluentdってなんじゃ?(Apache編)

前回は同じサーバー内でコマンドからファイル出力を行いました。
内容としては以下のような流れです。

  +--------------------------------------+
  | server                               |
  +--------------------------------------+
  |                fluentd               |
  |                                      |
  | +-------------+       +------------+ |
  | |    input    |       |   output   | |
  | |-------------| +---> |------------| |
  | |command(tcp) |       |    file    | |
  | +-------------+       +------------+ |
  +--------------------------------------+

今回はApacheのログを別のサーバーにファイル転送してみたいと思います。
以下のようなイメージです。
  +--------------------------------------+   +--------------------------------------+
  | web server (10.0.0.8)                |   | log server (10.0.0.16)               |
  +--------------------------------------+   |--------------------------------------|
  |                fluentd               |   |               fluentd                |
  |                                      |   |                                      |
  | +-------------+       +------------+ |   | +------------+       +-------------+ |
  | |    input    |       |   output   | |   | |   input    |       |    output   | |
  | |-------------| +---> |------------| +---> |------------| +---> |-------------| |
  | |    tail     |       |    tcp     | |   | |   tcp      |       |    file     | |
  | +-------------+       +------------+ |   | +------------+       +-------------+ |
  +--------------------------------------+   +--------------------------------------+


まず、送信側のwebサーバーでは、以下のようにアパッチのログをapache.accessというタグでtailし、tcp送信するように設定します。

source

 ログのフォーマット指定でapacheを指定することでapacheのログ構造をJSON化できるようになります。
そのほかにはsyslogのフォーマットがプリセットで指定できますが、その他アプリのログなどは名前付きキャプチャの正規表現で指定することにより、任意のプロパティ名でJSON化できます。

match

 matchディレクティブで設定するforwardは、Bufferd Outputプラグインというタイプで、デフォルトではメモリにデータをバッファし60秒でflushします。
つまり60秒に一回まとめて送信を行います。今回はわかりやすくリアルタイムで行うよう、flush_intervalを0sとし、0秒でflushするように設定してみます。
また、serverディレクティブで送信先のサーバーのIPを指定します。 特に指定しない場合、ポートは24224番が使用されます。

# vim /etc/td-agent/td-agent.conf
---
<source>
  type tail
  format apache
  path /var/log/httpd/access_log
  tag apache.access
</source>

<match apache.access>
  type forward
  flush_interval 0s
  <server>
    host 10.0.0.16
  </server>
</match>
---

# /etc/init.d/td-agent restart


受信側のサーバーにもtd-agentをインストールし、以下のように、tcpを受信しファイルに出力するように設定します。

source
 webサーバーのtcpから受信するためforward Input Pluginを指定します。

match
 apache.accessタグのデータを/var/log/fluent/apache_access_log***に書き出します。

# mkdir /var/log/fluent
# chown td-agent:td-agent /var/log/fluent
# vim /etc/td-agent/td-agent.conf
---
<source>
  type forward  
</source>

<match apache.access>
  type file
  path /var/log/fluent/apache_access_log
</match>
---

# /etc/init.d/td-agent start

ここで、webサーバーとlogサーバーの間にFWなどがある場合、fluentdのポートを開放する必要があります。 今回はEC2を使用しているので、Security Groupの設定でTCPの24224番を開放します。また、データ転送にはTCPを使用しますが、死活監視にはUDPが使われるので、両方を開放しておきます。



webサーバーのコンテンツにブラウザからアクセスしてみます。



すると、logサーバー側の/var/log/fluent/apache_access_log.xxxというファイルにJSONが記録されているのがわかります。
tail -1000f /var/log/fluent/apache_access_log.20121105.b4cdb099dbbaa1e42
---
2012-11-05T05:20:11+09:00 apache.access {"host":"219.117.233.241","user":"-","method":"GET","path":"/","code":"403","size":"5039","referer":"-","agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.1 Safari/537.17"}
2012-11-05T05:20:11+09:00 apache.access {"host":"219.117.233.241","user":"-","method":"GET","path":"/icons/poweredby.png","code":"304","size":"-","referer":"http://54.249.23.246/","agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.1 Safari/537.17"}
2012-11-05T05:20:11+09:00 apache.access {"host":"219.117.233.241","user":"-","method":"GET","path":"/icons/apache_pb.gif","code":"304","size":"-","referer":"http://54.249.23.246/","agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.1 Safari/537.17"}
2012-11-05T05:20:11+09:00 apache.access {"host":"219.117.233.241","user":"-","method":"GET","path":"/favicon.ico","code":"404","size":"288","referer":"-","agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.1 Safari/537.17"}

これで、リモートのサーバーにログを送ることができました。 また、このようにApacheのログがオブジェクト化されることで、後々集計集約しやすくなりました。

次回は、ログをDBに送り、抽出してみたいと思います。
以上です。

Fluentdってなんじゃ?

今回はfluentdについて触れてみたいと思います。

普段サーバーの運用をしていて、規模が大きくなりサーバーが分散されるとWebサーバーやアプリケーションのログが各サーバーに散在することになり、ユーザーからの問い合わせや集計などのためにログをかき集めたりする必要が出てきます。
fluentdはイベントログをJSONフォーマットで転送して一箇所に集約するためのツールです。

集計などを行う場合、Apacheなどの生ログはテキストデータなのですが、fluentdを経由しJSON化することで、ログのデータをオブジェクトとして扱うことが出来るので、DBに入れてフィルタをかけて集計なども簡単に行えます。

また、fluentdは入力元となるログの種類や出力先の指定をプラグイン形式で様々なフォーマットに対応して、それぞれを組み合わせることで数多くの用途に答えることができます。
例えば以下のような入出力ができます。

入力元:
  • fluent apiを利用してアプリケーションから直接出力
  • 既存ログのtail
  • TCPソケットからのデータ
  • コマンドの実行結果
出力先:
  • ファイルに書き出み
  • 各種DBへ保存
  • SQSでキューにメッセージ送信
  • S3へ保存
  • Growlで通知
  • メール送信
  • Twitterへ投稿

こうみるとPlaggerを思い出しますね。

fluentdはruby環境が必要ですが、TreasureDataというデータウェアハウスがtd-agentというruby環境込みのstableパッケージを提供しています。td-agentを使用すると、ruby環境がない場合や既存のruby環境を壊したくない場合にとても有用です。今回はtd-agentを利用してみます。

まずはtd-agentをインストールします。
TreasureDataのyumリポジトリを登録してから、yumでインストールを行います。
# vi /etc/yum.repos.d/td.repo
---
[treasuredata]
name=TreasureData
baseurl=http://packages.treasure-data.com/redhat/$basearch
gpgcheck=0

---

# yum install td-agent -y
インストールはこれで完了です。

次に、設定ファイルを見てみます。

設定ファイルはシンプルで、source定義で指定した入力を、マッチしたmatch定義の設定で出力します。
まずは、flluentdのQuickStartにあるように付属のflluent-catを使用して、擬似的なTCPからの入力をローカルのファイルに出力するように設定します。
#vi /etc/td-agent/td-agent.conf

---
<source>
  type forward
  tag test
</source>

<match test>
  type file
  path /var/log/fluent/test
</match>
---

これは、

source
 forward Input Pluginを使用してtcpからの入力にtestというタグをつける

match test 
 file Output Pluginを使用して、タグが「test」にマッチした入力があれば、/var/log/fluent/test*** というファイルに受け取ったデータを出力する

という意味の設定になります。 設定を記述したら起動します。
#/etc/init.d/td-agent start
そしてfluent-catで以下のようにコマンドからJSONを送ってみます。
その際、引数として、タグとして設定した「test」を渡します。
# echo '{"msg":"Hello World!"}' | /usr/lib/fluent/ruby/bin/fluent-cat test
すると、以下のようなファイルが生成され、時間+タグ+JSONの形式で出力されていることがわかります。
# tail -1000f /var/log/fluent/test.20121105.b4cdae0705726cb91
2012-11-05T01:40:32+09:00 test {"msg":"Hello World!"}


ここまででは、ただファイルに入力したのと変わらないので、便利さがわかりづらいと思います。
次回はログをリモートのサーバーに転送してみたいと思います。
以上です。