dalliはfork safeか?
ふと、dalliがunicornなどのmulti processで動作するアプリケーションで動かしたときにコネクションを張りなおす処理をしないといけないか気になったので、調べてみた。
これを実行してみると、
true 1 I, [2014-01-07T00:27:41.396287 #9290] INFO -- : localhost:11211 failed (count: 0) I, [2014-01-07T00:27:41.396622 #9291] INFO -- : localhost:11211 failed (count: 0) I, [2014-01-07T00:27:41.396905 #9292] INFO -- : localhost:11211 failed (count: 0) I, [2014-01-07T00:27:41.397327 #9293] INFO -- : localhost:11211 failed (count: 0) I, [2014-01-07T00:27:41.397726 #9294] INFO -- : localhost:11211 failed (count: 0) I, [2014-01-07T00:27:41.398108 #9295] INFO -- : localhost:11211 failed (count: 0) I, [2014-01-07T00:27:41.398495 #9296] INFO -- : localhost:11211 failed (count: 0) I, [2014-01-07T00:27:41.399108 #9297] INFO -- : localhost:11211 failed (count: 0) I, [2014-01-07T00:27:41.399440 #9298] INFO -- : localhost:11211 failed (count: 0) I, [2014-01-07T00:27:41.399961 #9299] INFO -- : localhost:11211 failed (count: 0) [9290, 0, 1] [9292, 0, 1] [9291, 0, 1] [9290, 1, 1] [9293, 0, 1]
こんな感じになり、forkした直後のログで失敗した、というエラーメッセージが出るが、その後、再接続し、通信できているようだ。
dalliのソースコード(acb1ff3afd4d)をpidでgrepすると、
# lib/dalli/server.rb def verify_state failure!(RuntimeError.new('Already writing to socket')) if @inprogress failure!(RuntimeError.new('Cannot share client between multiple processes')) if @pid && @pid != Process.pid end
という処理が出てきて、newしたときと、pidが変わっているかをチェックしていることがわかる。
このverify_stateはクライアントがリクエストしようとしたときには必ず通過するようになっているので、これでpidの変更を検出しているようだ。
failureの中では、
def failure!(exception) message = "#{hostname}:#{port} failed (count: #{@fail_count}) #{exception.class}: #{exception.message}" Dalli.logger.info { message } @fail_count += 1 if @fail_count >= options[:socket_max_failures] down! else close sleep(options[:socket_failure_delay]) if options[:socket_failure_delay] raise Dalli::NetworkError, "Socket operation failed, retrying..." end end
のようになっていて、失敗回数を保持するカウンタを1あげつつ、例外を吐いている。
ここで発生させた例外は、
# lib/dalli/client.rb # Chokepoint method for instrumentation def perform(*all_args, &blk) return blk.call if blk op, key, *args = *all_args key = key.to_s key = validate_key(key) begin server = ring.server_for_key(key) ret = server.request(op, key, *args) ret rescue NetworkError => e Dalli.logger.debug { e.inspect } Dalli.logger.debug { "retrying request with new server" } retry end end
serverを呼びだしている元の、performメソッドで、rescueされて、retryするようだ。
rrails v1.0.0 released.
以前公開した、rails/rakeコマンドを超早くするrrailsですが、
http://d.hatena.ne.jp/walf443/20120427/1335482420
あれから、色々な方に使っていただいているようで、パッチをいくつかもらったりしてupdateしたりしています。
最近quark-zjuさんという方からたくさんpatchをいただいて、今までできなかったことができるようになった & 今までと非互換なところがでてきた、ということで紹介します。
rails console/serverができるように
今まで子プロセスへPTYの引きつぎをしていなかったため、rails console/serverなどが実行しても、こちらの入力をrrailsへ教えることができなかったのですが、できるようになりました。
個人的にはrails consoleあんまり使わないので対応しなくてもいいかなーと思って放置していたところですが、使えるとまぁ便利かなとは思います。
rrails-serverコマンドは非推奨になり、rrails startで起動するように
guard-rrailsつかっているとあまり関係ないですが、rrails-serverコマンドはそのうちなくなると思われるので、rrails startを使うようにしてください。
TCPSocketではなく、UNIXDomainSocketをデフォルトで使うように
rrailsとの通信は今までTCPでの通信でしたが、デフォルトではUnixDomainSocketを使うようになりました。
これにより、複数のプロジェクトでのrrailsの立ちあげが楽になると思われます。
一方で、rrailsを実行する際に、対象のプロジェクトのディレクトリへcdしないと適切に実行できなくなっています。
まとめ
使い方が色々かわっている(なるべくは互換性残すようにはがんばったつもりだけど...)ので、今まで使っていた方はあらためてREADMEを読みなおすことを推奨します。
http://walf443.github.com/rrails/
適度に実行時間を抑制するrate_throttle
cronなどで統計処理のSQLをたくさん投げたりするときに、mysqlにあまり負荷をかけたくないので、適度にsleepを挟みながら実行させる、という処理を書くときに、perlだと、Sub::Throttleというid:kazuhookuさんのライブラリを使っていたのですが、rubyで同様のライブラリを探すとなかったので、rubyへrate_throttleという名前で移植しました。(本当はProc::Throttleにするつもりだったのですが、既にあったので....)
require 'rate_throttle' 1000.times do # limit block to 10% workload. RateThrottle.throttle(0.1) do # do something. end end
こんな感じで書くと、ブロックを実行した後にその実行時間が10%になるようにsleepを入れてくれます。ブロック内の実行時間が1秒であれば、sleepは9秒で、合計10秒になる、という塩梅です。
crawlerとかを書いているときに適度にsleepを挟むといったときなどにも使えるのではないでしょうか。
参考: http://labs.cybozu.co.jp/blog/kazuho/archives/2008/08/sub-throttle.php
fork時のrand()の挙動
perlでforkしたときに、srand呼ばないと親プロセスとseedの値が同じになってしまうので、子プロセスの間でrand値が変わらなくてハマる、という経験があったのだけど、rubyの場合だとそのようなことはないようだ。
# perl use strict; use warnings; warn rand(); for ( 1..10 ) { fork() or next; warn "$$: " . rand(); exit; } wait() for (1..10);
# ruby rand() 10.times do Process.fork do p [$$, rand()] end end Process.waitall
実行結果はこのような感じ
$ perl hoge.pl 0.272763864210905 at hoge.pl line 4. 61802: 0.107602762381674 at hoge.pl line 9. 61803: 0.107602762381674 at hoge.pl line 9. 61804: 0.107602762381674 at hoge.pl line 9. 61805: 0.107602762381674 at hoge.pl line 9. 61806: 0.107602762381674 at hoge.pl line 9. 61808: 0.107602762381674 at hoge.pl line 9. 61810: 0.107602762381674 at hoge.pl line 9. 61811: 0.107602762381674 at hoge.pl line 9. 61812: 0.107602762381674 at hoge.pl line 9. 61813: 0.107602762381674 at hoge.pl line 9. $ ruby hoge.rb [62124, 0.20627116612114527] [62125, 0.333383116761385] [62126, 0.23762467129788523] [62127, 0.14602078653671557] [62128, 0.5977205787316395] [62129, 0.4148314943310857] [62130, 0.05280638343461841] [62131, 0.008602114330672928] [62132, 0.3500050758109612] [62133, 0.6088914568482737]
rackのアクセスログをfluentdへ投げるミドルウェア
railsのアクセスログは、とても機械的に解析しづらく、運用するにはあまりよろしくない。そこで、Rack::CommonLoggerを使ってアクセスログを別途とったりしようとしていたのですが、(一般的にはNew Rericとかをつかうのが普通なんでしょうか?)
ログのローテーションとか考え出すと、あまり良い方法が思いつかない、という関係で、
- syslogへ投げる
- fluentdへ投げる
というアイディアを思いついたのですが、syslogだと機械解析しづらいフォーマットになってしまったりあまり詳しい人が意外といなかったりする関係で、流行っているfluentdへ投げちゃえと思いRack::CommonLogger::Fluentというrackのミドルウェアを書いてみた次第。
https://github.com/walf443/rack-common_logger-fluent
まだ作り始めで、仕様をどうしようと悩んでいるのだけど、わりと需要はありそうな気がしていて、できるだけ使われるものにしたいのと、あとから後方互換性を崩すということはなるべくしたくないので、ご意見を募集中
ログのフォーマット
Rack:CommonLogger::Fluentでは、RackのリクエストからHashを構築し、それをFluent::Loggerを使い、fluentdへ投げる、という感じになっています。
- "content_length": レスポンスのCONTENT_LENGTHヘッダの値。Integerにするか、Stringにするか悩んでいる。Intergerがよいのかな
- "http_status": レスポンスのHTTP STATUS CODE。これもIntegerにするかStringにするか悩んでいる。Integerがよいのかな
- "accessed_at": 日付はどのフォーマットの文字列にするべきか?
- ヘッダが空だった場合の扱い。nil or ""で悩んでいる。どちらかというとnilかなー?
UNIXSocketに対応してないライブラリを無理矢理対応させる
プロトコルは同じなので、既にあるクライアントライブラリを使いたいのだけれど、そのクライアントライブラリが想定しているサーバーは、unixsocketには対応していない場合、クライアントライブラリにUnixSocketに対応させるpatchを送ってもacceptはされないでしょう。
とはいえ、patchを保持しつづけるのもしんどいので、うまくやれないかなーと思って作ってみた
https://github.com/walf443/unix_socket_hack
require 'unix_socket_hack' UNIXSocketHack.apply({ 'unixsocket:9999' => '/path/to/unix.sock' }) sock = TCPSocket.new('unixsocket', 9999) #=> UNIXSocket # example using memcache-client. require 'memcache' memd = MemCache.new(['unixsocket:9999'])
unixsoket:9999へつなぎにいくと、UNIXSocketが返るようになるpatchをTCPSocketへ当てている。
もちろん、クライアントライブラリなどや、アプリケーションが実行する環境によっては相性が悪い可能性はありそうので、使わないにこしたことはないと思われる。
railsコマンドの実行を超早くするrrails
最近はお仕事で久々にがっつりrubyを書いてます。rails難しいです。
それはさておき、railsコマンドの実行が遅いのがつらいので、色々探していたら、jugyoさんのrails-shを見つけました。
rails-shはscalaのsbtとかみたいに、railsを読みこみ済みの環境を起動しっぱなしにしておき、既にライブラリ読みこみ状態からコンソールからコマンドを実行することで、ライブラリ読みこみにかかる時間を短縮する、というツールで、非常に実行を高速化することができます。
しばらく使っていたのですが、
などがあったので、TCPでコマンドを送り、それ経由でrails/rakeコマンドを実行させるrrailsというやつを書いてみました。
使い方は簡単で、
bundle exec rrails start
とrrailsのサーバーを起動しておき、
rrails -- rails ....
rrails -- rake -T
などと、rails/rakeなどをそのまま送り実行します。
ずっとコマンドが動きっぱなしのrails console/severなどは、動かなかったりします。
guardからrails-serverを起動することでwindowを節約しつつ、development/testを一気に起動することができます。(guardの起動が遅くなるのが欠点ですが。。。
https://github.com/walf443/guard-rrails
bundle guard init rrails
bundle exec guard
既に似たようなものもあるような気もしますが、まずまず便利なのではないかと思います。