dalliはfork safeか?

ふと、dalliがunicornなどのmulti processで動作するアプリケーションで動かしたときにコネクションを張りなおす処理をしないといけないか気になったので、調べてみた。


gist8284358

これを実行してみると、

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/

https://github.com/walf443/rrails/blob/master/Changes.md

適度に実行時間を抑制するrate_throttle

cronなどで統計処理のSQLをたくさん投げたりするときに、mysqlにあまり負荷をかけたくないので、適度にsleepを挟みながら実行させる、という処理を書くときに、perlだと、Sub::Throttleというid:kazuhookuさんのライブラリを使っていたのですが、rubyで同様のライブラリを探すとなかったので、rubyrate_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を読みこみ済みの環境を起動しっぱなしにしておき、既にライブラリ読みこみ状態からコンソールからコマンドを実行することで、ライブラリ読みこみにかかる時間を短縮する、というツールで、非常に実行を高速化することができます。

しばらく使っていたのですが、

  • raills-shのコンソールへ移動して打つのを忘れる
  • zshじゃない
  • RAILS_ENVがtestなコマンドも欲しいと2つのscreenのwindowを消費する
  • rake -Tが動かない

などがあったので、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

既に似たようなものもあるような気もしますが、まずまず便利なのではないかと思います。