unicornはconfで
timeout 20
とかやっとくと、20秒以上かかったらそのworkerが殺される。それはいい。問題はその殺され方にあって、タイムアウトしたunicorn workerはmasterにKILLシグナルで強制的に殺される。KILLで殺されてしまうと、worker側でtrapして安全に終了処理をすることができない。
一番困るのは、Railsのloggerは(production環境のデフォルトだと)リクエストが終了するまでバッファリングしているので、リクエストの途中でKILLされてしまうとloggerがflushされない。つまり、unicornのタイムアウト時には、リクエストのログは一切production.logには出力されない。異常時のログが出ないとか、まじで困ると思うんだけど、みんなどうしてんだろ。
これに対処するためにはunicornのコードに手を入れるのが手っ取り早い。方法はいくつかあると思うけど、タイムアウトでunicorn workerを殺すときのシグナルをconfで指定できるようにしてみた。
Commit d0b24198af57383d1c09f7be79c7e59c161bcfe5 to cookpad/unicorn - GitHub
timeout 20 timeout_signal :INT
このtimeout_signalというのが今回unicornに追加したオプション。これを指定するとKILL以外のシグナルでworkerを殺すようになる(デフォルトはKILL)。たとえばINTならこんなふうにworker側でtrapできる。
after_fork do |server, worker| trap (:INT) do Rails.logger.error "Unicorn Timeout (trapped SIGINT)" Rails.logger.flush Process.kill :KILL, $$ end end
workerがINTをtrapしたら、Rails.loggerをflush(バッファに溜まっているログを吐かせる)してから、自分自身をKILLする。これでとりあえず目的は果たせた。
応用編:タイムアウト時に実行していたSQLをログに出す
タイムアウト時のログを出すだけじゃなくて、何をやっていたときにタイムアウトしたかをちゃんとログに出力したい。タイムアウトしがちなのはやはりDBのスロークエリだろう。というわけで、unicornタイムアウト時に実行していたSQLをログに出すようにしてみる。
これも色々やり方はありそうだけど、今回はmysql2_adapterにモンキーパッチをあてることにする。
::ActiveRecord::ConnectionAdapters::Mysql2Adapter.class_eval do def execute_with_timeout_logging(sql, name = nil) result = nil @@current_query_info = {:sql => sql, :start_time => Time.now} result = execute_without_timeout_logging(sql, name) @@current_query_info = nil result end alias_method_chain :execute, :timeout_logging def self.current_query_info unless defined?(@@current_query_info) @@current_query_info = nil end @@current_query_info end end
で、さっきのunicornのconfに、ログの処理を追加する。
trap(:INT) do info = ActiveRecord::ConnectionAdapters::Mysql2adapter.current_query_info if info ms = ((Time.now - info[:start_time])*1000).to_i ::Rails.logger.error "Query Aborted (#{ms}ms) #{info[:sql]}" end Rails.logger.error "Unicorn Timeout (trapped SIGINT)" Rails.logger.flush Process.kill :KILL, $$ end
これでタイムアウトしたときに実行していたSQLがproduction.logに出るようになる。
おわりに
今回はunicornにtimeout_signalオプションを実装した。unicornは多くの部分がrubyで書かれているので、自分で簡単にいじれるから助かる。このtimeout_signalオプションをしばらくINTで運用してみてるけど、うまく動いているので、ちゃんとドキュメントとtestを書いて本家にpush requestしたいと思ってる。
注意しなくちゃいけないのは、KILL以外のシグナルでworkerを殺すようにすると、暴走したworkerがちゃんと死んでくれないという状況が想定される。暴走してるworkerを見つけてはKILLするcronを書くなり、そういう運用があわせて必要になると思う。