プログラム

unicornを使ってみた(1) – 導入

REMPで今までApacheのリバースプロキシを通してthinサーバでAPIを稼働させていたのですが、稼働時間が長くなるとメモリの利用割合が増える状況が続いていたため、どうしようかと悩んでいたところ会社でmizzyさん(@)からunicornだとメモリ利用量が押さえられるという話を伺ったので早速切り替えてみました。

unicornは、Rack 環境をロードした master プロセスが fork して子の worker を作る仕組みになっているため、安定性とメモリ利用量を小さくすることができることが期待されます。
unicornのインストールはgemを使うことで簡単に行えます。

$ sudo gem install unicorn

現在、REMPのAPIサーバはthinで動いているのでアプリケーションのルートにunicornの設定ファイルを書き足すことでunicorn上でアプリをすぐに動かすことができます。
ここでは、

  • Apacheのリバースプロキシから内部の4044番ポートにフォワード
  • unicornのワーカーは4本作る
  • preload_appを有効にして、workerをforkする前のアプリケーションの先読みを有効にする

設定例を書いています。

# unicorn.conf
listen "/tmp/unicorn.sock", :backlog => 64
preload_app true
port = 4044
listen port
worker_processes 4
pid "tmp/pids/unicorn.pid"
stderr_path "log/unicorn_error.log"
stdout_path "log/unicorn_out.log"

この設定ファイルを書き足せば以下のコマンドですぐに動かすことができます。
(-Dでデーモン化)

$ unicorn -c unicorn.conf -D

設定ファイル内のログに正常にログが吐かれていれば、稼働していることを確認することができます。

$ cat log/unicorn_error.log
125.***.***.*** - - [18/Mar/2012 18:56:11] "GET /status/info HTTP/1.1" 200 2 0.0045
125.***.***.*** - - [18/Mar/2012 18:56:11] "GET /status/info HTTP/1.1" 200 2 0.0531

thinサーバをプロセス別に再起動させる方法

自分用メモ。
サーバ上で起動させているThinサーバを再起動させたい場合、

$ thin -C thin.yaml restart

と行うと、yamlでthinのプロセスが常駐する様に設定していた場合、一度全部のプロセスを閉じた後、設定数分のプロセスの起動が行われるため、接続が不可能な状態が一瞬生じます。
せっかく複数プロセスが稼働しているのであれば、一つずつのプロセスで再起動がかけられればよいので、方法を調べたところ --onebyone というオプションを付与することで実現できました。

$ thin -C thin.yaml --onebyone restart

といった具合。
こうすると、以下の様に個々のプロセスの停止と起動が繰り返されます。

$ thin -C thin.yaml --onebyone restart
Stopping server on 0.0.0.0:4040 ...
Sending QUIT signal to process 12835 ...
>> Waiting for 1 connection(s) to finish, can take up to 30 sec, CTRL+C to stop now
>> Exiting!
Starting server on 0.0.0.0:4040 ...
Waiting for server to start ...
Stopping server on 0.0.0.0:4041 ...
Sending QUIT signal to process 12846 ...
Starting server on 0.0.0.0:4041 ...
Waiting for server to start ...
(以下略)

mongo DBのバックアップファイルから特定のドキュメントをリストアする

REMP開発中に誤爆して本番環境上の開発者のプレイリストがうっかり意図しない内容に更新してしまうというケースが生じたため、デイリーでとっているmongo DBのバックアップから復旧させてみました。*1
行いたいのは、特定のユーザのプレイリストのみの復旧なので、単純にmongorestoreする訳にはいかないので、方法として以下の手順で復旧させました。

  1. バックアップファイルを展開する
  2. ローカル環境のmongo DBに一度まるごとリストアする
  3. ローカルにリストアしたmongo DBから該当ユーザのプレイリスト情報(特定のコレクション内の1ドキュメント)をJSON形式で保管する
  4. ローカルで出力したJSONをサーバ側のmongo DBでインポートする

以下、復旧した際のメモをば…。

バックアップファイルを展開する

まず、保管されていたバックアップファイルを展開します。

[ローカル環境]
% tar xvzf rempdb-20120228.tgz
rempdb-20120228/
rempdb-20120228/rempdb/
rempdb-20120228/rempdb/library.bson
(中略)

ローカル環境のmongo DBにリストアする

前段で展開されるバックアップファイルの中身のファイルがJSONであれば、直接テキストファイルなので編集して該当するユーザのコレクションのみの抽出も行えるのですが、BSONのため一度手元のmongo dbに取り込んで処理することにしました。
リストアはmongorestoreコマンドを使えばよいので、リファレンスに従って操作して、

[ローカル環境]
% mongorestore -d rempdb --drop rempdb-20120228/rempdb
connected to: 127.0.0.1
Wed Feb 29 17:43:12 rempdb-20120228/rempdb/library.bson
Wed Feb 29 17:43:12 going into namespace [rempdb.library]
Wed Feb 29 17:43:12 dropping
(中略)

これで手元のmongo dbにバックアップファイルの内容がリストアされました。
(コマンドの末尾のパスはバックアップファイルの展開先)

該当ユーザのプレイリスト(コレクション内のドキュメント)をJSON出力する

サーバ側で復旧させたいユーザのREMPのプレイリストを抽出するためmongoexportコマンドを利用して、該当ドキュメントのJSONファイルを出力させます。
mongoexportコマンドを使うとDBやコレクションを特定し且つ、クエリを与えてヒットしたドキュメントのJSONだけを出力することができます。

[ローカル環境]
% mongoexport -d rempdb -c library -q "{uid:'1234567'}" -o playlist.json
connected to: 127.0.0.1
exported 1 records

中身を見てみると。。。

[ローカル環境]
% cat playlist.json
{ 〜 , "uid" : "1234567" }

と、クエリで抽出されたドキュメントのJSONファイルがそのまま出力されているはずなので、その内容がリストア対象であることを確認します。

サーバ側で稼動しているmongo DBへローカル環境で出力したJSONをリストアする

ローカル環境でまるごとリストアしたDBから出力した特定のドキュメントのJSONをサーバ側でインポートします。
mongoimportコマンドを利用することでJSONファイルをmongo DBにインポートすることができるので、以下の様にコマンドを入力。

[サーバ環境(リストアしたいDB側)]
% mongoimport -d rempdb -c library playlist.json --upsert
connected to: 127.0.0.1
imported 1 objects

これでサーバ上のmongo DBへバックアップファイルから取得した特定のドキュメントのみを復旧させることができました。

上記の方法でリストアできない (あるいは、--upsertオプションが効かない場合)

mongoのバージョンが古い場合、mongoimportコマンドに --upsertオプションが無い場合は、一旦ドキュメントを消してインポートすると良い…のかな。

MongoDB shell version: 1.*.*
connecting to: ***
>db.**.remove({uid:"1234567"})
>

で一度消して、

% mongoimport -d rempdb -c library playlist.json
connected to: 127.0.0.1
imported 1 objects

といった具合。

*1:あ、念のためですが他のユーザの方に影響はありません。開発用のクライアントを操作している際に生じたケースです。

PUSHERでWebhookを行ってみる

PUSHERWebHookができるという記事がブログで紹介されてますよ!と会社で教えてもらったので、試してみました。
先日試してみたwebsocket版の通知とは異なってWebHookなのでユーザが設定する任意のURLに対してPUSHリクエストがあったことをHTTPのPOSTで通知する仕組みになります。
(下の図の赤字で書いたWeb Hookの部分)
f:id:hideack:20120205220755p:image
ですので、PUSH通知できる対象はブラウザ等では無く、Webサーバ上で稼働する任意のアプリケーションが通知対象となります。

利用方法はとても簡単でPUSHERのコントロールパネルにWebHooksという項目があるので選択をするとWebHookを有効にするためのチェックボックスとフックする先のURLを指定する欄があるのでURLを指定します。
f:id:hideack:20120205221444p:image

そうすると、PUSHERのAPIを通してWebアプリケーションからPUSH通知が走った場合に、設定したURLへ対してPOSTリクエストが行われます。(同時にWebsocketで接続中のWebブラウザへも通知が行われる)
こうすることによって、フック先のWebアプリケーション側でもPUSH通知が走ったことを検出することができます。
PUSH通知をされた側のWebアプリケーションは、このままでは本当に真のPUSH通知元(=PUSHER)からのPOSTリクエストかどうかを判定することができませんが、PUSHERから通知されたリクエストには、

  • X-Pusher-Key
  • X-Pusher-Signature

という二つのHTTPヘッダが付いていて、PUSH通知内容(POSTリクエストの内容)に対するHMAC SHA256が付いているので、この内容がPUSHER APIの秘密鍵から作られるSHA256のメッセージダイジェストの内容と一致するかを確認することでPUSHERから通知されたことを確認することができます。

以下、ドキュメントに掲載されているサンプルのままですが、読めばシンプルで分かりやすいと思います。

<?php
$app_key = $_SERVER['X-Pusher-Key'];
$webhook_signature = $_SERVER['X-Pusher-Signature'];
$body = http_get_request_body();
$expected_signature = hash_hmac( 'sha256', $body, $app_secret, false );
if($webhook_signature == $expected_signature) {
$payload = json_decode($body);
foreach($payload['events'] as &$event) {
// do something with the event
}
header("Status: 200 OK");
}
else {
header("Status: 401 Not authenticated");
}
?>

参考

Pusher WebHooks Documentation | Pusher
http://pusher.com/docs/webhooks

PUSHERを使ってWebsocket経由のPush通知を行ってみる

Websocket周りを使ってみたいと思っていろいろ試行錯誤をしていたのですが、PUSHERというAPIサービスを利用すると思いの外容易にWebsocketを既存のWebサービスに使えそうなので使ってみました。
(激しい一週遅れ感がありますが…。)

例えば既存のWebアプリにpush実装を行いたいために、通常は稼働中のWebサービスとは別にWebsocketサーバの実装が必要となるのですが、PUSHERを利用するとPush通知を行う部分(Websocketサーバ→ブラウザ間)の実装を代替させることができます。
Push通知を行わせたい部分でWeb側の実装でPUSHERAPIを叩くことで、ブラウザへのWebsocketを通したPushを行わせることができるイメージになっています。
Websocketの接続数が最大20までで1日あたりのメッセージ発行数が10万回以内であれば無料で利用することができます。

f:id:hideack:20120129160326p:image

利用方法

1. アカウントを作成する(無償)

PUSHER(http://pusher.com/ ) のトップページでアカウントが作成できるのでアカウントを作成します。(メールアドレスが必要)

f:id:hideack:20120129151530j:image

2. APIキーを取得する

アカウントを作成するとAPIを利用するためのAPIキーが1つ表示されるので、表示されているアプリケーションID(app_id), APIアクセス用のkeyとtokenを控えます。

3. クライアント側の実装 (Webサーバから渡されるHTMLの実装)

Webアプリ側がWebsocketを通じたPush通知を受けるための実装を行います。
サンプルは、先程のAPIキーが表示される画面(API access)の中段に"Pastable HTML code"として例が掲載されているので、参考にして既存のWebアプリに組み込みます。(PUSHERの動作を確認したければ、そのままコピーすればOK)
以下の様な具合。

<html><head>
  <title>Pusher Test</title>
  <script src="http://js.pusher.com/1.11/pusher.min.js" type="text/javascript"></script>
  <script type="text/javascript">
    // Enable pusher logging - don't include this in production
    Pusher.log = function(message) {
      if (window.console && window.console.log) window.console.log(message);
    };
    // Flash fallback logging - don't include this in production
    WEB_SOCKET_DEBUG = true;
    var pusher = new Pusher('****'); // ←API取得時に控えたAPI keyを書く。
    var channel = pusher.subscribe('test_channel');
    channel.bind('test_event', function(data) {
      alert(data);
    });
  </script></head><body>
<p>pusher API test</p></body></html>

4. Push通知側の実装

Push通知側の実装(=PUSHERのREST APIを叩く)は、PUSHERでRESTのAPIを叩くためのライブラリ(http://pusher.com/docs/rest_libraries ) が各種言語用に用意されているので、これを利用すれば簡単に実装することができます。
以下、RubyPHPの場合の例。
PHPの場合は、

<?php
require('../lib/php/Pusher.php');
require('pusher-setting.php');          // pusher settings
$pusher = new Pusher(PUSHER_KEY, PUSHER_SECRET, PUSHER_APPID);
$pusher->trigger('test_channel', 'test_event', 'hello pussher!!');
printf("trigger called.");

Rubyの場合は、

require 'rubygems'
require 'pusher'
require 'eventmachine'
require 'em-http-request'
require './pusher-setting.rb'
Pusher.app_id = PUSHER_APPID
Pusher.key = PUSHER_ID
Pusher.secret = PUSHER_SECRET
EM.run {
deferrable = Pusher['test_channel'].trigger_async('test_event', 'Hello, pussher!! via Ruby')
deferrable.callback {
puts "success!"
EM.stop
}
deferrable.errback { |error|
puts error
EM.stop
}
}

となりますね。

5. 動作確認

3.で作成したHTMLをブラウザで読み込んだ状態で、4.で作成したPush通知(呼び出し側)のプログラムを実行すると、
f:id:hideack:20120129155411p:image

といった具合でPush通知されるのが確認できると思います。

6. APIの利用状況の確認

PUSHERにログインすればAPIの呼び出し状況を確認可能です。
f:id:hideack:20120129160518p:image

なかなか簡単に利用することができるので、早速試してみたいな…。

Rubyでmemcachedを使う

REMPAPI周りでキャッシュを使いたくなったので、memcachedを使おうと思ったのだけどRubyで使う場合のクライアントを知らなかったので簡単にまとめ。
Ruby用のmemcacheクライアントはいくつかある様なのだけど、dalliというクライアントが一般的な様なので使ってみた。

インストールは簡単。gemコマンドでインストールできる。

$ sudo gem install dalli
Fetching: dalli-1.1.4.gem (100%)
Successfully installed dalli-1.1.4
1 gem installed
Installing ri documentation for dalli-1.1.4...
Installing RDoc documentation for dalli-1.1.4...

使い方も比較的シンプル。
irbで試すと以下の様な形で利用できる。
(localhostの11211番ポートでmemcachedサーバが常駐している状態)

irb(main):001:0> require 'dalli'
=> true
irb(main):002:0> dc = Dalli::Client.new('localhost:11211')
=> #0}, @ring=nil>
irb(main):003:0> dc.set("hoge", "foo")
=> true
irb(main):004:0> dc.get("hoge")
=> "foo"
irb(main):005:0> dc.get("hoge2")
=> nil

REMPは、sinatraで稼働させているのでその場合は以下の様な書き方をしておけばよいかと思う。

# (中略)
configure do
cache = Dalli::Client.new('localhost:11211', :expires_in => 3600 * 24)  # キャッシュ寿命24時間
set :cache, cache
end
get '/foo' do
tmp = setting.cache.get(params['id'])
val = 0
if tmp.nil?
val = db.getValue()   # DBからデータ取得
else
val = tmp  # memcachedのキャッシュを利用
end
val
end

意外と手軽にできたな。
ちょっとRubyのmemcachedクライアントがいくつかあったので迷ったけど、これで大丈夫そう。

REMP(レンプ)開発に参加してますよ

そういえば、REMP(レンプ)というYoutube動画を連続再生できるWebアプリの開発に参加してますー*1

f:id:hideack:20111211132100p:image

Mashup Award 7 にも応募したりしましたよ。
動作環境はgoogle chromeがインストールされているPCでfacebookアカウントがあれば利用することができます。

REMPの特徴

  • Youtubeの動画で自分が好きなものをリストにして登録できます
    • 手順としては、動画検索→自分が好きな動画をリストに登録
  • 作成したプレイリストを連続再生したり、繰り返し再生できます
    • REMPのコア機能。かなり便利。
  • facebookで繋がっている友達と自分が作ったプレイリストをシェアできます
    • 難しくなくて単に友達は自分のプレイリストを見ることができるということです
  • facebookで繋がっている友達に自分が気に入った動画を紹介することができます

詳しくは紹介ページ http://remp.rockf.es/hello/ を参照。
難しい説明はさておいて、使ってもらうとわかりやすいかと思います!

いきさつなど

きっかけは、会社の開発合宿(ペパボお産合宿)で一緒のチームになった@Chromeアプリ版のREMPを紹介してもらって、そのときにWebアプリとしてtwitterfacebookの連携機能を作りたい。という話を聞いたのが始まり。
じゃ、facebook連携周りを一緒に作りましょうか。という話になって、お手伝いした次第。
初めてsinatraを作ってAPI側のロジック実装したり、データストアにmonogo dbを使ったりと初めて使う技術ばかりでしたが、なんとか動かせましたよ。
(RubyでWebアプリは初めて書きました…。いままで検証用のスクリプト程度しか使ったことがなかったので。)
実装的には@がフロント側、私がバックエンドという役割分担で、打ち合わせは会社の昼休みにご飯を食べながら、あるいは開発が業務時間外や週末だったりしたのでfresh meetingで仕様を詰めながら実装を進めました。*2
多少、このあたりでの技術的な話をこのあたり(SinatraアプリをApache2のmod_proxy_balancerを通してThinで動かす(1) - Thinの設定)で書いていたりします。
MA7の応募が近づいてきて、今度は紹介ページとかがいるぞ。デザインどうすんだ?って話になったときにデザイナーの@参加!で、素敵な紹介ページができたのでした。

使ってみたい場合

*1:このエントリ早く上げようとしてたのだけど、遅れに遅れた…orz

*2:fresh meeting便利すぎます

Solrの検索クエリーとMySQLの検索クエリーの対応

Solrで検索クエリーを作るときにどうしてもSQLクエリが先に出てしまうので対応をメモ。
クエリーパラメータでいうとqですね。

検索式 MySQL*1 Solr
一致検索 WHERE title="foo" title:foo
全件 (where無し) *:*
AND WHERE title="foo" AND price=200 title:foo AND price:200
OR WHERE title="foo" OR price=200 title:foo OR price:200
NOT WHERE title="foo" AND price<>200 title:foo NOT price:200
範囲指定(未満) WHERE price < 200 price:{* TO 200}
範囲指定(以下) WHERE price <= 200 price:[* TO 200]
範囲指定 WHERE price > 10 AND price < 200 price:{10 TO 200}
先頭一致検索 WHERE title LIKE "foo*" title:foo*

*1:というか、SQLか。

SinatraアプリをApache2のmod_proxy_balancerを通してThinで動かす(2) -mod_proxy_balancerの設定

続けて、Apache2の設定。ここでは、既にmod_proxyが入っているものとします。
フロントのApache2から複数起動しているThinのサーバに振るためにmod_proxy_balancerを入れます。
Ubuntuであれば、

$ sudo a2enmod proxy_balancer

で、mod_proxy_balancerが有効になるので、続けてconfファイルを編集。

$ sudo vi /etc/apache2/mods-available/proxy.conf

中身は以下の様な形。


ProxyRequests Off

ProxyPass /foo balancer://foo
ProxyPassReverse /api balancer://foo


BalancerMember http://127.0.0.1:4040 loadfactor=10
BalancerMember http://127.0.0.1:4041 loadfactor=10
BalancerMember http://127.0.0.1:4042 loadfactor=10
BalancerMember http://127.0.0.1:4043 loadfactor=10
BalancerMember http://127.0.0.1:4044 loadfactor=10

これで、自身のホスト内の4040番〜4044番ポートで動いているThinサーバにフォワードされる様になります。
設定を有効にするためにApacheを再起動します。

$ sudo /etc/init.d/apache2 restart

これで、apacheサーバの ~/foo にアクセスされたものがThinサーバにフォワードされます。
開発用にwebrickで動かしていたものと比べて体感的にも大分変わりました。

SinatraアプリをApache2のmod_proxy_balancerを通してThinで動かす(1) – Thinの設定

今、WebアプリケーションのAPIサーバをRubyフレームワークSinatraでつくっていて、これを本格的に動かすときにWebサーバをどうしよう。ということになって、Thinが良いらしいという情報を得たので導入してみた。
現状、動いているApacheサーバがあるので、reverse proxyを通して特定のエントリポイントに対してアクセスがあったもののみをフォワードし、Thinを通してSinatraアプリにつなぐことを目標にします。

f:id:hideack:20111103165746p:image

インストールは非常に簡単。

% gem install thin

これで準備OK。簡単ですね。
次にアプリケーションを呼び出すためのファイルを作ります。
ここでは、ファイル名をconfig.ruとします。内容的には以下の様な形。
ここでrequireされているapp.rbはSinatraアプリ本体です。

# config.ru
require 'rubygems'
require 'sinatra'
require './app.rb'
run Sinatra::Application

単純に呼び出してるだけですね。
続けて、Thin本体の設定ファイルを作ります。
まずは中身。

# thin.yaml
timeout: 30
log: log/thin.log
environment: production
servers: 5
daemonize: true
rackup: config.ru
port: 4040

おおよそ設定見てもらったままなのですが、上記の設定でThinサーバのデーモンプロセスが5本起動されます。
利用されるポート番号は4040〜4044までとなります。
続けて、ログファイルの保管先とプロセス管理用のファイル保管されるディレクトリを予め作っておきます。

$ mkdir log
$ mkdir tmp

これでThin本体の準備完了。実際に動かしてみます。

$ thin start -C thin.yaml
Starting server on 0.0.0.0:4040 ...
Starting server on 0.0.0.0:4041 ...
Starting server on 0.0.0.0:4042 ...
Starting server on 0.0.0.0:4043 ...
Starting server on 0.0.0.0:4044 ...

起動完了。試しに動作を確認したければ、wget等で起動したいずれかのポートに対してwgetを叩けば挙動を確認できますね。
止める場合は、$ thin stop -C thin.yaml でOKです。(restartも)
tmpディレクトリには、起動中のプロセスのpidファイルが、logディレクトリにはそれぞれのThinサーバへのアクセスログが記録されます。

$ ls -l tmp/pids/
total 40

  • rw-r--r-- 1 hideack staff 5 11 3 16:30 thin.4040.pid
  • rw-r--r-- 1 hideack staff 5 11 3 16:30 thin.4041.pid
  • rw-r--r-- 1 hideack staff 5 11 3 16:30 thin.4042.pid
  • rw-r--r-- 1 hideack staff 5 11 3 16:30 thin.4043.pid
  • rw-r--r-- 1 hideack staff 5 11 3 16:30 thin.4044.pid

$ ls -l log/
total 40

  • rw-r--r-- 1 hideack staff 585 11 3 16:30 thin.4040.log
  • rw-r--r-- 1 hideack staff 360 11 3 16:30 thin.4041.log
  • rw-r--r-- 1 hideack staff 360 11 3 16:30 thin.4042.log
  • rw-r--r-- 1 hideack staff 360 11 3 16:30 thin.4043.log
  • rw-r--r-- 1 hideack staff 360 11 3 16:30 thin.4044.log