プログラム

bit.lyで短縮されたURLのクリック数をRuby(Nokogiri)でスクレイピングする

f:id:hideack:20120504212312p:image

bit.lyで短縮されたURLのクリック状況は短縮されたURLの末尾に'+'をつけると知ることができるので、そこからクリック数をスクレイピングしてみようという話。*1

# bitlycun.rb
require 'nokogiri'
require 'open-uri'
url = "https://bit.ly/#{ARGV[0]}+"
page = open(url)
html = Nokogiri::HTML(page.read, nil, 'UTF-8')
html_a = html.search('//span[@class="count"]')
tmp = html_a[0].children.text
clickcun = tmp.gsub(/(\r\n|\r|\n)/, "")
puts "http://bit.ly/#{ARGV[0]} : click count = #{clickcun}"

スクリプトを実行する際、引数にbit.lyで短縮した際の可変部分(http://bit.ly/***** の ***** ) を第一引数に渡せば現在までのクリック数を取得することができる。

% ruby bitlycun.rb IxW4jJ
http://bit.ly/IxW4jJ : click count = 3

*1:ネタとして探したらあちこちにありそうだったけど…(略

REMP Ver.2.5 リリースしました(してました)

と、twitterではお知らせしたのですが、改めて。
REMP Ver.2.5をリリースしました。*1
見た目上の大きな変更や派手な機能追加は無いのですが、以下の様な点が改善されています。

  • サーバサイドの設定をチューニングして処理の高速化
  • Friendsのログイン状態表示対応
  • デスクトップ通知対応

機能的に新しくなったのはログイン状態表示とデスクトップ通知になります。

ログイン状態表示

REMPの操作画面の左手にfacebookのfriendsが表示されますが、そのリスト表示部分の名前とプレイリスト数の横にログインしていれば緑色のランプアイコンが表示されます。

f:id:hideack:20120408175751p:image

デスクトップ通知

REMPでは、自分のfriendsに対して自分が見つけたYoutube動画を送ることができるのですが、もし誰から動画が送られた瞬間にログインしている状態であればChromeのデスクトップ通知の機能を利用して画面上にポップアップして通知させることができます。ちょうどイメージ的には、メールソフトでメールを受信したときに表示されるNotifyのポップアップの様なイメージです。
また、自分の友達がREMPにログインして操作開始したことも同様にデスクトップ通知で知ることができます。
設定は、画面左下の歯車のアイコンで設定をすることができます。
f:id:hideack:20120408175752p:image

あと、細かいところではブラウザサイズを小さくした際にREMPのプレイヤー上部に表示される再生ボタン等のサイズが絶妙に調整される様になっています。細かいところで@のコダワリが出ています。*2

自分が担当したサーバ側の技術的な部分では、通知系にはPUSHERを利用したWebsocket経由でのフックを使っていたり、ログアウト検出用に独自のお手軽メッセージキューを使ったりしていますが、そのあたりはまたブログに書いていきたいと思います。
(PUSHERの話題は、このあたりを参照)

ここまで読んでREMPを使いたくなった人はこちらから。
(facebookアカウントが必要です)

*1:そういえば、Webアプリなのにバージョン番号つけるの珍しい気がする

*2:毎回さすがと思うところ

unicornを使ってみた(2) – REMPにおけるメモリ利用量の変化

では、実際に現在のREMPの場合において、どの程度メモリ使用量が変化したのかを確認してみました。
Apacheをフロントのリバースプロキシとして、背後にRack用のWebサーバ(Thin or unicorn)を動かすという挙動はかわらないので、Thinとunicornの両方のサーバを立てて、リバースプロキシ(mod_proxy)で均等に複数のプロセスで起動させているthinサーバとunicornにアクセスする様にして3分毎に実メモリ使用量を監視してグラフにしてみました。
mod_proxyの設定は以下の様な形にしています。以下の様に設定することでthinの1プロセスとunicornへ均等にアクセスがされる様にしています。

<IfModule mod_proxy.c>
(中略)
<Proxy balancer://***>
BalancerMember http://127.0.0.1:4040 loadfactor=10  #thinのプロセス(1/2)
BalancerMember http://127.0.0.1:4041 loadfactor=10  #thinのプロセス(2/2)
BalancerMember http://127.0.0.1:4044 loadfactor=20  #unicornのプロセス
</Proxy>
</IfModule>

で、この様な設定にした状態でREMPの本番環境にしてみたところ、得られた各プロセスが消費しているメモリ使用量の変化は以下の様な具合です。
(赤色の線がThinの1プロセス vs 緑色の線がunicornのマスタープロセス+水色の線がunicornのワーカープロセス で比較すればよいのかな。と。)

f:id:hideack:20120318221048p:image

少なくてもREMPのAPIサーバ側のSinatraで実装したアプリをThinの上で稼働させた場合とunicornで稼働させた場合を比較するケースでは、単純に見ると半分程度にメモリ使用量を押さえることができる様です。

もう少し更に検証したいと思います。

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を使って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便利すぎます