Aqutras Members' Blog

株式会社アキュトラスのメンバーが、技術情報などを楽しく書いています。

sinatra + pgでsorry, too many clients alreadyが出た時の対処法

はじめに

Railsを利用するほどでもないような簡単なAPIサーバを立てたりするときに、Sinatraをよく利用するのですが、アクセスした時に以下のようなエラーが発生しました。

PG::ConnectionBad: FATAL:  sorry, too many clients already
FATAL:  sorry, too many clients already

        api.rb:13:in `initialize'
        api.rb:13:in `new'
        api.rb:13:in `block in <main>'

原因

アプリにはDBとしてPostgreSQLを利用していますが、DBへの接続が上限に達しているようでした。
どうやら、アクセスの度にDBに接続を行い、その接続がずっと残っているままになっているようでした。
原因はすぐに分かったのですが、対処に結構時間がかかってしまいました。

rubyファイルは、以下のように記述していました。

require 'sinatra'
require 'sinatra/reloader'
require 'json'
require 'pg'

class MyApp < Sinatra::Base
  set :bind, '0.0.0.0'
  set :port, 4567

  get '/api/hoge' do
    con = PGconn.connect('localhost', 5432, '', '', 'hoge_db', 'postgres', '')

    res = con.exec("SELECT COUNT(*) FROM foo;")
    res.to_a.to_json
  end
end

問題は、特にこの、リクエスト処理の部分で発生していると思われます。
DBへの接続を行っている con 変数が、消えずに残っているため、DB接続数の上限に達しているようです。

  get '/api/hoge' do
    con = PGconn.connect('localhost', 5432, '', '', 'hoge_db', 'postgres', '')

    res = con.exec("SELECT COUNT(*) FROM foo;")
    res.to_a.to_json
  end

対処法

対処法としては、以下のようにPostgreSQLへのアクセスを行う con をクラス変数として共通化してやることで、コネクションがアクセスの度に増えることがなくなりました。

class MyApp < Sinatra::Base
  @@con ||= PGconn.connect('localhost', 5432, '', '', 'hoge_db', 'postgres', '')
  ...
end

失敗した対処

その1 DBへのコネクションをcloseする

con を生成しているメソッド内で、全て con.close を行い、DB接続を切断することを試みました。
しかし、これでは改善されませんでした。
pry で実行すると、正しく切断できていたので、 sinatra だとダメだったようです。良くわかりませんでした。

その2 インスタンス変数にして初期化する

con をインスタンス変数にし、以下のようなメソッドを定義し、アクセスの度に con が生成されることを防ごうとしました。
しかし、これでも改善されませんでした。
調べてみたところ、Sinatraはアクセスの度にアプリケーションのインスタンスを生成し、リクエストを処理しているらしいです。
そのため、おそらく各インスタンスに @con が存在していしまい、最初のコードと同じ結果になってしまっているのだと思います。
ということは、生成されたインスタンスが削除されずに存在し続けているということでしょうか....これもよく分からない。

参考 Sinatra のスコープについて - 君の瞳はまるでルビー - Ruby 関連まとめサイト

def con
  @con ||= PGconn.connect('localhost', 5432, '', '', 'hoge_db', 'postgres', '')
end

背景と考察

自分用に軽量なアプリを作ったり、書き捨てでアプリ作るときは、Sinatraと、DBアクセスのためにgem pgを利用しているのですが、
今回のエラーには初めて遭遇しました。
PostgreSQLのコネクションの最大数は、デフォルトで100が設定されているようで、1人で開発や運用していると、なかなかこの上限には引っかからないと思います。
サーバの再起動とか、アプリの更新を挟むとリセットはされるので、尚更でしょう。

今回のAPIサーバは、稼働中のあるシステムにAPIを追加するために作成しました。
稼働中のものを触るため、本番に影響は与えないようテストなどを行って追加し、しばらくは何の問題も発生しなかったのですが、
しばらく運用した後、急にこのエラーが発生してシステムが停止していましました。

継続運用や大量アクセスで初めて発生するバグ、しかもサーバ負荷とかではなくコードに原因があるバグだったので、出会えてよかったのかなと思います。
もちろん、事前に意識して防ぐことが出来るのがベストですが。今回のおかげで、今後は同様な自体は防ぐことが出来ると思います。

おわりに

自分で開発するだけだと、アクセス数などの負荷への意識はなかなか身につかないと思いますね。
作ったものは、ぜひ様々な人に使ってもらい、より洗練させていきましょう。

失敗した対処に関して、指摘点などございましたら是非コメント下さい!!