0

Herokuで同じアプリに2つ目のサーバーを登録する

tiqav-chatを別URLでもう一つ動かした。


ローカルのアプリのディレクトリに移動して、
新しいアプリを作る。(仮に名前をnew_appとする)

% heroku create --stack cedar new_app
Creating new_app... done, stack is cedar
http://new_app.herokuapp.com/ | git@heroku.com:new_app.git


自分が管理してるアプリ一覧を確認。new_appが増えている
% heroku apps


gitにnew_app追加してデプロイ
% git remote add new_app git@heroku.com:new_app.git
% git push new_app master


以後、このローカルgitリポジトリにはサーバーが2つ紐付けられているので、herokuコマンドは–appオプションを付けて対象を指定して実行する。
アドオンを追加したりして1つ目のサーバーと同じ設定にする
% heroku addons:add mongolab:starter --app new_app
% heroku config --app new_app


必要無ければ古いやつを消す
% heroku apps:destroy old_app

0

HerokuでMongoDB+Node.js

HerokuでMongoDB+Sinatraと同じ、1行メモ的なものをNode.js+Express+mongooseで作った。


mongooseはmongoidと同じ雰囲気のQueryチェインが使えて便利。


MongoDBに接続、スキーマ定義

アドオンのMongoLabかMongoHQを入れてたらそっちに接続するようにした。
var mongoose = require('mongoose');
mongoose.connect(process.env.MONGOLAB_URI || process.env.MONGOHQ_URL || 'mongodb://localhost/memo', function(err){
if(err){
console.error(err);
process.exit(1);
}
});

var Schema = mongoose.Schema, ObjectId = Schema.ObjectId;

var MemoSchema = new Schema({
body : {type: String},
created_at : {type: Date, default: Date.now}
});

var Memo = mongoose.model('Memo', MemoSchema);

Memo.latests = function(num){
return this.find().sort('created_at', -1).limit(num);
};

Memo.find_by_id = function(id){
return this.find({'_id':id});
};


読み書き

modelのオブジェクト作ってsaveするとMongoDBに保存される。
var mongoose = require('mongoose');
var Memo = mongoose.model('Memo');

var m = new Memo({body: "はい"});
m.save();

代入しても良い。Schema定義した時にgetterが生成されている。
var m = new Memo();
m.body = "はい";
m.save();


Queryを繋げて書けて良い。
var _ = require('underscore');
var mongoose = require('mongoose');
var Memo = mongoose.model('Memo');

Memo.where(body: /はい/).sort('created_at', -1).limit(30).exec(function(err, docs)){
if(err){
console.error('error');
}
else{
_.each(docs, function(i){
console.log(i.body + ' - ' + i.created_at);
});
}
});


さっきschema定義の時にmodelに付けておいたメソッドを使う
Memo.latests(3).exec(function(err, docs){
if(err) console.error(err);
else{
_.each(docs, function(d){
console.log(d.body + ' - ' + d.created_at);
});
}
});

データの出し入れはcontrollerviewのコードが参考になるかも。
async.parallelで2つのクエリを同時に投げて結果待ちしたりしてみた。なんとなく速そう。


jadeとhamlのちがい

にはまった。

この部分

hamlだとこう書くけど
%ul
- memos.each do |memo|
%li
#{memo.body} -
%a(:href => "/#{memo.id}") #{memo.created_at}


jadeだとliと同じ行に書かないと、下のaタグの中身の変数(memo.created_atとか)が展開されなくなる。
ul
- each memo in memos
li #{memo.body} -
a(href="/#{memo.id}") #{memo.created_at}

0

HerokuのNode.jsでMemcache使えるようにした

memjsというmemcached clientにpull requestが取り込まれた。

HerokuではアドオンのMemcacheMemcachierを使う事になるが、両方共SASLによる認証が入っている。
SASL認証を使うにはバイナリプロトコルが使えるmemcache clientが必要なのだが、node.jsにはバイナリ使えるのmemjsしか無かったのでとりあえず試したけど認証なしでしか動かなかった。

この時にmemcacheとmemcachierのアドオンを入れたり消したりしてたらmemcachierの中の人からメールが来て、相談してたらOpCode 0x21の時にKeyが必要なところが入ってなかったのを見つけて、修正したらうまく動いたのでpull requestして取り込まれた。
これでNodeでHerokuのMemcacheが使えるようになりました。


memjsの使い方はこんなの。
まだREADMEがしっかり書かれていないのでflagとかが何だかわからないけど、テストコード見たらだいたい使い方もわかってbugfixもできた。

// npm install memjs

var memjs = require('memjs');
var cache = new memjs.Client.create(null, {expires: 10}); // 10sec expire

// set value
cache.set('name', 'shokai', function(err, val){
if(err) console.error('set error');
else{

// get value
cache.get('name', function(err, val, flag){
if(err) console.error('get error');
else console.log('get : '+ (val ? val.toString() : null));
});

// wait 11 sec, then get value
setTimeout(function(){
console.log('wait 3 sec');
cache.get('name', function(err, val, flag){
if(err) console.error('get error');
else console.log('get : '+ (val ? val.toString() : null));
});
}, 11000);
}
});

2バイト以上の文字はencodeURI/decodeURIを通して使う。

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で運用する場合は動くコードなのか知らない。調べてない。

0

HerokuでMongoDB+Sinatra

試しにメモ帳作った。
http://shokai-memo.herokuapp.com

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


MongoHQMongoLabを使う。

MongoHQはHerokuが管理してるっぽくて16MBまで無料。MongoLabはObjectLabs Corporationがやってて240MBまで無料らしい。
MongoLabの方が明らかに得だけど、容量を2GBまで増やすとMongoHQの方が安くなる。


Herokuのプロジェクト作る

git init
heroku create --stack cedar


MongoDBのアドオンを有効にする

とりあえず無料プランで、どちらかをaddする
heroku addons:add mongolab:starter
heroku addons:add mongohq:free

するとMONGOLAB_URIかMONGOHQ_URLという環境変数が増えるので、確かめる
heroku config


RubyではMongoid2.4をMongoのラッパーとして使うのが良いと思う。
Herokuにデプロイしたら環境変数にMongoDBのアドレスとパスワード等が入った状態でSinatraが起動するので、読み込ませる
Mongoid.configure do |conf|
conf.from_hash {'uri' => ENV['MONGOLAB_URI'] || ENV['MONGOHQ_URL']}
end


最終的にこうなった。
mongoidは普通mongoid.ymlから設定を読む。
MongoLabやMongoHQの接続設定が環境変数にあったらそっちを使うし、無ければmongoid.ymlを読んでローカルのMongoDBに接続する。
Mongoid.logger.level =
case ENV['RACK_ENV']
when 'production'
Logger::WARN
else
Logger::DEBUG
end

Mongoid.configure do |conf|
h = {'uri' => ENV['MONGOLAB_URI'] || ENV['MONGOHQ_URL']}
unless h['uri']
yaml = YAML.load(open(File.expand_path 'mongoid.yml', File.dirname(__FILE__)).read)
h = yaml[ ENV['RACK_ENV'] || 'development' ]
end
conf.from_hash h
end



メモ帳作る

Memo modelを定義する
class Memo
include Mongoid::Document
field :created_at, :type => Time, :default => lambda{ Time.now }
field :body, :type => String, :default => ""

def self.find_by_id(id)
self.where(:_id => id).first
end

def self.latests(num=10)
self.all.desc(:created_at).limit(num)
end

def to_s
"#{body} - #{created_at}"
end
end


適当にSinatraで書いて完成。
get '/' do
mems = Memo.latests(100).map{|m|
"<p>#{Rack::Utils.escape_html m.body} - <a href='/#{m.id}'>#{m.created_at}<a><p>"
}.join('')

"<html><form method='POST' action='/'><input type='text' name='body' size=70 /><input type='submit' /></form>#{mems}</html>"
end

post '/' do
m = Memo.new :body => params[:body]
m.save!
redirect '/'
end

get '/:id' do
m = Memo.find_by_id params[:id]
halt 404, 'not found' unless m
"<html><p><a href='/'>top</a></p><p>#{Rack::Utils.escape_html m.to_s}</p></html>"
end


ローカルのRubyからHerokuのMongoDBに接続する


手元で起動したSinatraも、環境変数さえセットされていればHerokuのMongoDBに接続される
heroku config --shell | ruby -lane 'puts "export "+$_'
でてきたMONGOLAB_URIとかをターミナルにコピペすれば環境変数をセットできる。


管理画面

https://api.heroku.com/myappsからアプリを選んで、右上のaddonsからMongoLab/MongoHQの管理画面に行ける。