はじめまして。 小関と申します。 Rails + MySQLを使っていて、長時間DBへのアクセスが無い場合に Railsアプリケーションが反応しなくなる問題を調べています。 MongrelのFAQでそれらしい現象が報告されているのですが、 http://mongrel.rubyforge.org/faq.html > Q: Mongrel stops working if it's left alone for a long time. ここに載っている対策は以下の点で正しくないのではないかと 考えています。 * ソースを読むと、ActiveRecord::Base.verification_timeout は 接続確認を「行わない」期間を表す値に見える。 * RailsとMySQL間の接続のタイムアウトは、interactive_timeoutで はなく、wait_timeoutで設定するのでは。 * 少なくとも手元の環境では問題が解決しなかった。 いろいろ試した結果、Railsを使わずに、以下の手順で原因と 思われる症状を再現できました。 ---------------------------------------------------------------- 1. TCPSocketを、SO_KEEPALIVEソケットオプションをオンで作成。 2. 別のスレッドを起動する。このスレッドはsleepしているだけでOK。 3. サーバから接続をcloseする。 4. クライアントはcloseせず、CLOSE_WAITが消えるまで待つ。 5. その後2回TCPSocket#flushを実行。 6. 1回目のflushはBroken pipeの例外が発生する。 7. 2度目flushの実行中にスレッドが停止したままになる。 ---------------------------------------------------------------- 上記2.で、別のスレッドを起動しなかった場合は再現しませんでした。 でした。また、4.のCLOSE_WAITが消える前でも、再現しませんでした。 Railsでは、3.がwait_timeoutの切断に相当し、5.の2回のflushが mysql.rbドライバのCOM_STATISTICS/COM_QUIT送信に相当します。 Rubyのソースにプリント文を入れてどこで止まっているのか確認した ところ、io.cのio_fflushから呼ばれる、 eval.c rb_thread_fd_writable(fd)内の、 rb_thread_schedule()で停止しているようでした。 http://www.tamacom.com/tour/lang/ruby/S/43.html#L346 http://www.tamacom.com/tour/lang/ruby/S/35.html#L10795 あまりよくわかっていませんが、 curr_thread->wait_for = WAIT_SELECT; が満たされないので、flushを実行中のスレッドに制御が移らなく なるのではないかと思います。 これはRuby本体の問題のような気がするのですが、 どうでしょうか? 以下具体的な再現の手順です。 ■サーバ (sv.rb) - TCPServerのサンプルほぼそのままです。 --------------------------------------------------------- #! /usr/bin/env ruby require "socket" port = ARGV[0] puts "server[#{$$}]: #{port}" puts Time.new.to_s gs = TCPServer.open(port) while true Thread.start(gs.accept) do |s| print(s, " is accepted\n") while s.gets puts $_ end print(s, " is gone\n") s.close end end --------------------------------------------------------- ■クライアント (cl.rb) --------------------------------------------------------- #! /usr/bin/env ruby require "socket" def start_another_thread @th = Thread.new do while true do sleep 5 end end puts "Thread started: #{@th}" end def connect(host, port) STDOUT.sync = true @sock = TCPSocket.open(host, port) @sock.setsockopt Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true end def flush begin @sock.sync = false @sock.write("client[#{$$}]: write " + Time.new.to_s + "\n") @sock.sync = true puts "--- BEGIN flush" @sock.flush puts "--- END flush" rescue => e puts "rescue: #{e}" end end Signal.trap(:QUIT) do 5.times { do_flush } end # # main # (host,port,wait) = ARGV wait = wait.to_i wait = 150 if wait <= 0 puts "client[#{$$}]: #{host}:#{port} wait:#{wait}sec" puts Time.new.to_s if 3 < ARGV.length start_another_thread end connect(host, port) flush while true print "--- sleep " i = 0 (wait / 10).times do print "#{i += 1}." sleep 10 end puts 5.times { flush } end @sock.close --------------------------------------------------------- ■ 再現の手順 0. ターミナルを3つ開く。 1. サーバを10080番ポートで起動。 $ ./sv.rb 10080 2. クライアントを起動。 $ ./cl.rb localhost 10080 7800 1 7800秒後にflushする。末尾の1は別スレッドを起動する指定。 3. netstatのFIN_WAITとrubyを監視。 $ watch -n1 'netstat -nap 2> /dev/null | grep "\(FIN_WAIT\|ruby\)"' 4. 実行中のサーバをC-cで止める。 5. FIN_WAIT2が約1分後に消えるのを確認する。 6. CLOSE_WAITが約2時間後に消えるのを確認する。 7. クライアントが7800秒後に以下を出力して止まる。 --- BEGIN flush rescue: Broken pipe --- BEGIN flush cl.rbの起動オプションで末尾の1を指定しなかった場合は、 別のスレッドが起動せず、5回繰り返しBroken pipeが表示される。 --- BEGIN flush rescue: Broken pipe --- BEGIN flush rescue: Broken pipe --- BEGIN flush rescue: Broken pipe --- BEGIN flush rescue: Broken pipe --- BEGIN flush rescue: Broken pipe CLOSE_WAITが消えるまでの2時間の待ち時間は、以下の 設定で短縮できる(システム全体の設定が変わるので注意が必要)。 # echo 30 > /proc/sys/net/ipv4/tcp_keepalive_time # echo 20 > /proc/sys/net/ipv4/tcp_keepalive_intvl cl.rbの起動オプションは、7800の代わりに150を指定する。 150秒で再現する。 ■環境 VMWare上のFedora FC5 # ruby --version ruby 1.8.6 (2007-03-13 patchlevel 0) [i686-linux] 以上よろしくお願いいたします。 -- koseki - http://d.hatena.ne.jp/koseki2/