HerokuでRedisを使ったら失敗した

この記事は決してHerokuでRedisを使うんじゃない!という話ではない。

だがHerokuで立てたことによって失敗したことは確かである。

今日、大学の実習課題で制作したWebアプリケーションを聴者の方々、およそ40名に使用させたのだが、しかし僕のHeroku上のSinatraで書かれたpumaはすぐに500を返すだけのサーバーになってしまったのだ。

結果、課題のシステムの背景や設計を細かく練ったにも関わらず、聴者は低評価を付け私はチームメンバーに悔しさを与えることしかできなかった。

一体何が起こったのか

端的に言えばRedisの最大クライアント制限に引っかかったのだ。

Redisのクライアント制限に引っかかると、その後新規にクライアントを作ろうとしても全てエラーとなる。結果 Internal Server Error だ。

Redis Document: http://redis.shibu.jp/admin/config.html#confval-maxclients

以下に絶対に真似してはいけないし失敗するRedisクライアントの生成コードを記述する。

module Whisper
  class App < Sinatra::Base

    ...


    def redis
       @redis ||= Redis.new(:url => (ENV["REDIS_URL"] || 'redis://127.0.0.1:6379'))
    end

    ...


  end
end

Sinatraアプリ上でRedisを呼び出すときにこういう書き方をしていた。

まあAppクラスのインスタンスが残っていれば @redis は再生成されずに、シングルトンのように扱われるし、扱って欲しいというのが僕の考え。

Redisのクライアント上限が20だというのはデプロイ開始時点で承知していたのだ。

しかし実際はこの記述の仕方では毎度インスタンスを生成し続け、一方でRedis側はセッションを最低30秒は保つようにしているため、30秒以内に21回のリクエストが発生すると500となる。

あまりにも貧弱なサーバーコードを書いてしまっていた。

自前である程度実行テストはしていたのだが、短時間で一気にリクエストを送らなければ発生しないため気づかなかった。

しかし、Redisの管理者画面からRedis Clientのコネクション数は確かめられて、殆ど自分か他二人しかアクセスしないページで、不自然に10のクライアントが生成されていることがわかっていても「pumaがスレッディングしているのかな…」と違う推測で理解してしまっていた。

pumaが複数スレッド立てていたとしても最大16個立てていたら1スレッドに対して2つのクライアントを立てるのでそれも落ちるというのに…

ではどうすれば良いのか

Rackサーバーから扱われる上記の Sinatra::Base クラスはリクエスト毎に初期化される。(推測だが)

それもそうだ、でなければ他のリクエストに対しても500を返すようになってしまうだろう。(もしくは何も返さない)

そのため、Sinatra::Base クラスの外側に Redis Client のシングルトンメソッドを設ける。

module ReadCache
  class << self
    def redis
       @redis ||= Redis.new(:url => (ENV["REDIS_URL"] || 'redis://127.0.0.1:6379'))
    end
  end
end

module Whisper
  class App < Sinatra::Base

      ...

      def redis
        return ReadCache.redis
      end

      ...

  end
end

これでHerokuは最大2クライアントしか生成しなくて済むようになった!

違う、そうじゃない

事前にこういった問題を検知するために負荷テストをしよう

これが本題。

結局のところ今回の知見としては「負荷テストをしなさい」という点に尽きる。

特にRedisのクライアント数が小さく制限されているとわかっている時などは重要だ。

後モンスターとコーヒーで脳をブーストしているときも。

正直 Sinatra や HTML / CSS / JS のコードは脳味噌を使わなくてもかける。

多くのプログラムを生成したところでそれはソフトウェアデベロップメントでなく、プログラミングをしただけであって、プログラミングができる=エンジニア とは呼ばない。それはプログラマである。

エンジニアとは…ソフトウェアエンジニアとは、ソフトウェアエンジニアリングの原則に則って、設計から開発、点検、テスト、評価までをする人のことを言うのだ。

Definition: Software engineer - Wikipedia

今回のケースで私は点検はしたがテストを怠ったのだ。ついでにいえば脳味噌を使ってないプログラミングをしてしまった。しかし人間誰しも完璧なものを作ることはできず、完璧に近づける行動しかできない。

そこで、完璧に近づける方法としてテストがあり、今回の負荷テストを行うためのツールとして代表的かつ効果的なのはこれだと思う。

loadimpact.com

というのも、ちゃんと500が出てくれたのだ。私はすっかりこのサービスに心酔してしまった。課金してもいい。

f:id:ebabababa:20170620215948p:plain

500が出ればログが出る。ログが出ればバグがわかる。

$ heroku logs -n 1000 | grep Redis
2017-06-20T10:14:58.371874+00:00 app[web.1]: 2017-06-20 10:14:58 - Redis::CommandError - ERR max number of clients reached:

f:id:ebabababa:20170620220410p:plain

バグを直せば200が出る。先人達が築いた「エラーがきちんと出る」というソフトウェア設計に敬意を表する。

開発物: github.com