0

HerokuでSinatra+PhantomJS

ためしに、URLを入れるとaタグのhref属性とxy座標と幅と高さが取れる簡単なアプリを作ってみた。herokuでwebkitが動いててサーバー側でいったんページをレンダリングしてるから、座標とかがわかる。

http://phantom-getlinks.herokuapp.com/


ソースコード https://github.com/shokai/sinatra-heroku-phantomjs

readmeに書いてある通りやればローカルでもHerokuでもすぐ動かせるのでcloneしてみるとよい。


PhantomJSでページをレンダリングしてからaタグの座標とか読む


PhantomJSはプロセス内にwebブラウザ(webkit)を起動して、しかもその中のJavaScriptにアクセスできる
橋本商会 » PhantomJSでwebページ内のJavaScriptを読む


PhantomJSでページに注釈を付けてスクリーンショット撮るやつ – hitode909の日記
を見ていたら、injectJSという関数でPhantomJS内のwebkitにjQueryを読み込ませて好き勝手やってた。

hitodeさんのを参考にして、aタグの位置とかをJSONで吐き出すスクリプトを用意した。


HerokuでPhantomJSとRubyを同時に使えるようにする


まずローカルでSinatraアプリを普通に作る。phantomjsコマンドを呼び出して結果のJSONをブラウザに返すアプリを作る。

それをHerokuで動かすにはbuildpack機能を使う。1つのアプリでRubyとPhantomJSが同時に使えるようになる。

参考:herokuでRubyとphantomjsを使う #Ruby #heroku #PhantomJS – Qiita


create –stack cederの代わりにbuildpackを指定してアプリを作る
% heroku create --buildpack git://github.com/ddollar/heroku-buildpack-multi.git

.buildpacksファイルを作る
https://github.com/stomita/heroku-buildpack-phantomjs.git
https://github.com/heroku/heroku-buildpack-ruby.git
この2行を書く。

あとは普段どおりGemfile, Gemfile.lock, Procfileと.buildpacksをgit addしてcommitして、herokuにgit pushする。

最後に環境変数を設定して、phantomjsコマンドにパスが通って完了
% heroku config:add LD_LIBRARY_PATH=/usr/local/lib:/usr/lib:/lib:/app/vendor/phantomjs/lib
% heroku config:add PATH=bin:vendor/bundle/ruby/1.9.1/bin:/usr/local/bin:/usr/bin:/bin:/app/vendor/phantomjs/bin

ローカルでもHerokuでも問題なく動く。

0

Sinatra用のcometプラグインを作った

追記:RocketIOに統合されました → 橋本商会 » Sinatra RocketIOというプラグイン作った、これでWebSocketとCometが使える

******

作った

Sinatra Comet I/O

インストール

gem install sinatra-cometio



通信を意識せずに、サーバー側からクライアントの関数、クライアント側からサーバーの関数を呼び出せる。


サーバーからクライアントへプッシュする例

サーバー側


Ruby
require 'sinatra'
require 'sinatra/cometio'

CometIO.push :chat, :name => "shokai", :message=> "hello work!!"


クライアント側


HTML
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
<script src="<%= cometio_js %>"></script>

JS
var io = new CometIO().connect();
io.on("chat", function(data){
console.log(data.name + " : " + data.message); // -> "shokai : hello work!!"
});


クライアント→サーバーの例や、新規クライアント接続イベントやエラーイベント等についてもSinatra Comet I/Oに書きました


サンプルとしてHerokuでチャットを作った。 http://cometio-chat.herokuapp.com

チャットの場合同時に300クライアントぐらいしか接続できないっぽい。スケールさせる方法はよくわからない。
少人数で使うちょっとしたツールにリッチなUIを持たせる時などに便利だと思います。


*チャットサンプルですが1台のマシンから2つchrome開いても、交互に通信するみたいです。片方をsafariにするか、chromeをプライベートブラウズモードにすると動きます
色々やってたら大丈夫になりました

0

cometだけどsinatraからサーバープッシュできれば関係ないよね

前に書いたsinatra/streamingでcometするやつを使いやすいようにgemにして、sinatra-cometioというsinatraのpluginを作った。
https://github.com/shokai/sinatra-cometio

thin等のEventMachine上のサーバーで動く。Apache+Passenger等では動かない。


チャットを作った例 http://cometio-chat.herokuapp.com


インストール

gem install sinatra-cometio


なかなか簡単に使えるようになっている。socket.ioを参考にした。

サーバー側
onのイベント名でクライアント側からのアレを受け取って、クライアント側のイベント名を指定して送り返す
require 'sinatra'
require 'sinatra/cometio'

## echo
CometIO.on :chat do |data|
puts "#{data.name} : #{data.message}"
self.push :chat, data # server --> client
end

require ‘sinatra-cometio’すると /cometio 以下にcomet用のrouteやjsファイルが生成されるようになっている。


クライアント側
jQueryが必要。
cometio.jsを読み込むんだけど、cometio.jsのURLはcometio_jsというヘルパー関数が返してくれる
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
<script src="<%= cometio_js %>"></script>


on(イベント名, callback)でサーバーから来たデータを受け取る。push(イベント名, データ)でサーバーに送る。
var io = new CometIO().connect();

// client <-- server
io.on("chat", function(data){
consle.log(data.name+" : "+data.message);
});

// client --> server
io.push("chat", {name: "shokai", message: "hello"}); // 送信ボタン押したイベントとかから呼び出す


ちゃんと測ってないけど500ぐらいぶら下がってしばらくすると接続エラーが出始めた。300ぐらいなら問題ない。
大してメモリも食わないのでforkしておけばいいのではないか

まだsessionがないので、つないでいるクライアント全員に送るしかない、なんとかする

sinatra/streamingでcomet (long polling)する はコメントを受け付けていません。

sinatra/streamingでcomet (long polling)する

良い感じのgem sinatra-cometioを作ったのでこっちを使うと良いと思います




チャットを作ってみた。
http://shokai-comet-chat.herokuapp.com/
Chrome/Safari/Firefox/Android/iPod touchで動いた。


sinatra/streaming

sinatra-contribにsinatra/streamingというのがあった。eventmachineを使っているサーバー(thinなど)で動くらしい。herokuでも動いた。

Sinatra::Streaming (part of Sinatra::Contrib)


本気で使うには厳しいしnode.jsを使えばいいと思うけど、Rubyでちょっとしたツールを作るのには便利そう。
一応同時に1000接続までは耐えた。ただ、Cometだとクライアント側とサーバー側両方で1分ぐらいでタイムアウトさせて接続しなおしたりするわけだから、タイミングが悪かったり接続が残ったりする事があると思うので少なめに見積もったほうがいいと思う。
実装は楽だし、ポートを1つしか使わないのでイントラ内で使うようなちょっとしたツールにリッチなUI付けたい時とかに便利そう。
arduino_firmataをwebからゴリゴリ触れるとか。



getやpostの中でstreamブロックを宣言すると、任意のタイミングでレスポンスを返せる。IOっぽく扱える。

getするとpostでデータが入るまで待たされる簡単な例
https://gist.github.com/3989831
#!/usr/bin/env ruby
require 'rubygems'
require 'sinatra'
require 'thin'
require 'sinatra/streaming'

@@comet = Hash.new

post '/comet/:channel' do
channel = params['channel']
data = params[:data]
@@comet[channel] = data
data
end

get '/comet/:channel' do
channel = params['channel']
stream do |s|
data = nil
loop do
break if (data = @@comet.delete channel)
sleep 1
end
s.write data
s.flush
end
end

getすると待たされる
% curl http://localhost:4567/comet/test

postするとgetの方に”hello”がでる
% curl -d 'data=hello' http://localhost:4567/comet/test


chat


チャット
http://shokai-comet-chat.herokuapp.com/


ソースコード https://github.com/shokai/sinatra-comet-chat


/chatにgetするとlong pollingで待たされるので
https://github.com/shokai/sinatra-comet-chat/blob/master/controllers/comet.rb


ajaxしてレスポンスを待つ。受信したらすぐajaxしなおす。
https://github.com/shokai/sinatra-comet-chat/blob/master/public/js/chat.js

このchatには同時に1000接続までなら動いた。1200ぐらい接続したらthinごと死亡した。


FirefoxとChromeは、同じマシンで2つウィンドウを開いてチャットすると交互にしか受信できない。

Safariは問題ない。Chromeはシークレットモードだと両方受信できるらしい。

0

HerokuのSinatraにバックグラウンドワーカーを詰め込んで節約

Webアプリと同じプロセスにworker入れてお金が節約できる。


Webアプリは “リクエスト来る→サーバーで処理→レスポンス返す” というのを繰り返すわけだが、サーバーでの処理に時間がかかる場合にそこを別のプロセスに任せて、先にレスポンスを返しておいて、あとで結果は取りに来てよ、という実装をする事がある。


時間がかかる処理は2つに大別できる。

  1. 動画をエンコードするとか。CPU負荷が高くて時間がかかるのでWebサーバーとは別の場所で動かしたい
  2. Twitter APIを10回ぐらい使った結果をまとめて返すとか。CPU負荷は低いけどIO待ちが長い
2の方について、HerokuのRuby環境で安く上げる方法をまとめる。


手法

HerokuのcedarスタックでRuby使う時はwebサーバーとしてThinが起動する。
ThinはEventMachineの中で動いてるので、EM::defer等が使える。
Herokuは1プロセス目は無料、2プロセス目を起動させると課金されるが、EventMachineでworkerをWebアプリのプロセス内に同居させればお金がかからなくなる。


非同期処理

例えば、ユーザー登録されたらメール返す処理の場合
post '/regist_user' do
mail_addr = params[:mail]
send_mail(mail_addr, 'hello!!') ## メール送信する処理
redirect '/' ## トップページに戻す
end
これだとメールが送信されるまでレスポンスを返せないので、ブラウザが固まる。
また、Sinatra+Thinを1プロセスしか起動していない場合、1リクエスト/レスポンス処理するまで次のリクエストが処理できないので、
メール送信されるまで他の人からのアクセスを全員待たせる事になる。


重い処理をEM::deferで囲むだけで、そこは別スレッドで処理される。すぐにレスポンスが返るので、みんな待たずに済む。
post '/regist_user' do
mail_addr = params[:mail]
EM::defer do
send_mail(mail_addr, 'hello!!') ## メール送信する処理
end
redirect '/' ## すぐレスポンス返す
end


たとえばTwitter APIを連続使用する時は1秒間隔を空けろとか指定があるけど、そういうのをSinatraのプロセス内で行うならEM::deferでやってしまうのが良いと思う。
お金払ってdelayed_jobを使わなくても、EM::deferで囲むだけで非同期になるので便利。
CPUに負荷がかかるタイプの処理は素直にお金払って別のプロセス起動したほうがいいと思う。


ジョブキュー

処理時間が長い仕事が大量にある時、仕事のリストを作っておいて、順番に処理していくという手法がある。
普通はDB等に仕事を保存しておいて、Webアプリとは別のプロセスが順番に処理し、結果をDBに入れておくとかするけど
これもWebアプリに内蔵させられる。

config.ru でSinatraアプリを起動した後にEM::deferを書いておくと、Webアプリとは別スレッドでずっと動き続けるループが作れる。
require 'sinatra'
require 'eventmachine'
## (略)

run Sinatra::Application

EM::defer do
loop do
sleep 5
next if @@jobs.empty?
job = @@jobs.shift ## ジョブ1つ取り出す
## job処理する
end
end


起動しといたスレッドにジョブ追加する
post '/add_job' do
@@jobs.push '仕事'
end
キューにはDBとかgearmanとか使ったほうが良いと思う。


注意点

しばらくアクセスが無いと、プロセスがkillされる。クリティカルな処理は大量にキューに入れて処理しない方が良い。
メモリ使用量も注意するべき。Dyno(プロセス)ひとつあたり512MB割り当てられていて、1.5GB超えたら再起動すると書かれている。

実際はアクセスが無いと長くても2,3時間以内にはkillされてる気がするので、この方法あんまりアテにしない方が良いと思う。


あと、この「Sinatraにワーカー埋め込む」というのはEventMachine内で動くWebサーバー(ThinやWebrick)だけで使える方法なので、
自分のサーバーでUnicornやApache+Passengerで運用する場合は動くコードなのか知らない。調べてない。