読者です 読者をやめる 読者になる 読者になる

月曜日までに考えておきます

ITネタとゲームネタ中心に興味のあること色々書きます。

PumaでActiveRecordのErrorが出てハマった話

IT ruby

応答に500ms以上掛かる外部APIに依存するサービスを作ってて、Unicornのworker数だと簡単に詰まって死ぬという現象が起きていたのでしばらく前にPumaに置き換えました。

既に Nginx + Unicorn で運用しているサーバのUnicorn部分をPumaに置き換えるのは割と簡単で、これを参考にしたらほぼうまくいきました。(*)

coderwall.com : establishing geek cred since 1305712800

(*) ただし、capistranoset :pty, true にすると capistrano-sidekiqが効かずにsidekiqが動かなくなって死ぬというknown issueを踏んだので注意

それ以降、順当に運用できていたのですが、負荷がかかったときに500エラーが出るという現象はUnicornよりマシになったものの出続けていました。最初はThread数の不足かと思い、増やしたりしたのですがそれでも出続けており、エラー内容は以下のものでした。

ActiveRecord::ConnectionTimeoutError - could not obtain a database connection within 5 seconds.

"Puma ActiveRecord::ConnectionTimeoutError" あたりでググって見たところ、Herokuのこの記事がヒットして回答が書いてありました。

https://devcenter.heroku.com/articles/concurrency-and-database-connections

If you are using the Puma web server we recommend setting the pool value to equal ENV['MAX_THREADS']. When using multiple processes each process will contain its own pool so as long as no worker process has more than ENV['MAX_THREADS'] then this setting should be adequate.

  • Threadごとにコネクションを貼り、コネクションプールが作られる
  • ActiveRecordのコネクションプール数はデフォルトだと5

ということなので、いくらスレッド数を増やしても、Pumaのスレッド数が5以上必要になるケースが発生するとコネクションが貼れなくてエラーになるわけですね。 というわけでこういうふうにdatabase.ymlを書き換えて解決しました。

default: &default
  adapter: mysql2
  username: hoge
  password: fuga
  ...
  pool: 16

UnicornからPumaに置き換えた理由

ちなみに、UnicornからPumaに置き換えた理由ですが、外部依存でリクエストが詰まる環境下だと、Unicornは1Workerで1requestしか処理できないので、Worker数を増やすしか解決策がありません。しかし、Worker数を増やすにはリソース(特にメモリ)が必要になるので、1サーバでそこまでworker数を増やすことができず、遅いリクエストを捌いている間に処理するリクエストより入ってくるリクエストのほうが多くなって、死ぬわけですね。CPUとかぜんぜん余裕があるにもかかわらず。

Pumaは、外部APIやDB待ちのようなIO待ちで何もしていない(CPUに余裕がある)状態であれば別スレッドで他のリクエストを処理することが可能であり、スレッドを増やすのはUnicornのWorker数を増やすよりもリソースを使わないため、待ちの多い多数のリクエストを捌くというのに向いているわけです。