先月、heroku の推しサーバが unicorn から puma に変わったという発表がありました。unicorn だとスロークライアントの影響を受けやすいというのが理由なようです。

もう少し詳しく調べてみましょう。

そもそもスロークライアントってなに

その名の通り遅い回線のクライアントです。3G環境のモバイル端末などが該当します。

「unicorn だとスロークライアントの影響を受けやすい」とは

unicorn はプロセスモデルのサーバであり、blocking I/O モデルを採用しています。つまり、クライアントとの通信中プロセスが専有されるということです。

例えば unicorn がワーカプロセスを3つ立ち上げていて、そこへ通信完了に10分かかるようなスロークライアントが3つ接続されたら…、続くクライアントはスロークライアントの通信が完了するまで実行を待たなければならなくなります。プロセスの数をもっと増やせば対応できますが、それはその分メモリやサーバ台数、最終的には月額のサーバ費用に影響を与えます。

unicorn はなぜそんな設計になっているのか

unicorn は、「ひとつのことをうまくやる」unix哲学に則っており、その上で "バックエンドの" アプリケーションサーバとして作られています。

つまり、nginx などのリバースプロキシを前面にたてるのを前提とした作りになっているわけです。

The Philosophy Behind unicorn

リバースプロキシを使うと、unicorn はレスポンスをリバースプロキシまで届ければよくなります。リバースプロキシはたいてい同一ネットワーク内にあるので、とても早く通信できます。スロークライアントが通信するのはリバースプロキシです。これにより、スロークライアントが unicorn のプロセスを長時間専有するのを防ぐことができます。

nginx & unicorn は、heroku 以外の Rails アプリケーション環境では現時点で一番多い構成なのではないでしょうか。

heroku にはリバースプロキシはないのか

ないようです。ただ、heroku の HTTP Routing のドキュメントを見ると、1MB まではレスポンスをバッファしてくれるようですね。

HTTP Routing | Heroku Dev Center

puma に変更したら全て解決するのか

この発表このへんの文章を見ると、puma にしたらオールオッケーなように読めてしまいますが、そんなことはありません。

この記事 の Multi-threaded blocking I/O の箇所を読むと分かるのですが、確かにスレッドベースのサーバを採用すると、1スレッドがスロークライアントで専有されても別のスレッドが処理を受け持つことができます。ただし、全てのスレッドがスロークライアントと通信したら、結局同じことです。

同じメモリ量でも、プロセスベースのサーバより、スレッドベースのサーバの方が作成できるスレッド数が多いので、スレッドベースのほうがスロークライアントに強いということは言えると思います。

puma にすることによるリスク

puma はスレッドベースのサーバです。ということは、アプリケーションの書き方によっては race condition を引き起こす可能性があります。普通に Rails アプリケーションを書けばスレッドセーフになるはずですが、その辺りをよくわかっていない初心者が安易に手を出すのは危険なのかなと思います。

よくわからない人は Working With Ruby Threads を読みましょう。

unicorn の方が優っていることもあります。この間 ginza.rb で unicorn や puma などの Rack サーバのベンチマーク結果を比較をしました。結果としては、大量アクセス時の処理としては unicorn の方が puma よりも安定している印象でした。

「puma が不安定だ」とは思わないのですが、安易に「herokuが推奨してるからこれからは puma だ! unicorn はオワコン!」とならないようにリスクについても書いてみました。

まとめと雑感

基本的に、リバースプロキシのない heroku を使うのであれば、現時点では puma が適していると思います。しかしその場合はスレッドセーフなコードを書くよう気をつけなければなりません。

また、heroku 以外の環境ではリバースプロキシが使えるため、puma にすることのメリットは(heroku ほどには)ないと言えます。まだまだ nginx & unicorn の天下が続くのではないでしょうか。

リバースプロキシ内蔵のプロセスベース(スレッドベースにもできる)のサーバである raptor (passenger 5) 正式版ががそろそろリリースされます。以前の ginza.rb でベンチマークをとった時は beta2 だったのでまだまだ不安定な印象でしたが、正式版になって安定したら、heroku 環境での鉄板サーバになるかもしれません。

取り急ぎこのブログ(heroku & lokka)を passenger 5.0.0.rc2 にしてみました。このブログは特にアクセス数が多いわけではないので高負荷時の安定性などはわかりませんが、とりあえず普通に動きますね。

自分用のメモ。RubyMotion 3.3。

前提

  • RubyMotion では Java のクラスを Ruby で継承できる
  • ただ、その際はコンストラクタだけは Java で書かないとダメ

単純にコンストラクタを書く用途以外に、「現行のRubyMotionでうまく動かない箇所をJavaで書いて何とか動かす」用途としても使える(バッドノウハウ)。

落とし穴のレシピ

  • コンストラクタでインスタンス変数に値を格納する
  • 当然、Java の方で変数宣言する

この時、Ruby 側でも attr_accessor などを使ってインスタンス変数にアクセスするメソッドを生やすとハマる。なぜか Java の方で入れた値を Ruby 側で見ることができず、 nil になる。

解決策

attr_accessor を消せば OK

第19回目の ginza.rb ミートアップを開催しました。

Ginza.rb 第19回 だれが一番?Railsアプリサーバ徒競走!&Ruby2.2について話そう - Ginza.rb | Doorkeeper

raptor と rhebok のパフォーマンスをみる

第17回ミートアップ で、Rack サーバの比較をしましたが、その時はまだ raptor の実装が公開されていませんでした。

今回は raptor(Passenger5) のベンチマークがとれるようになったので、ベンチを取ってみました。さらに、unicorn の2倍早いという rhebok というサーバも登場したので、それも一緒に。

@y_yagi さんが、第17回に利用したengine yardさんのアプリを使ってベンチをとってくれました。多謝!

結果としては、比較対象としてベンチをとった unicorn と Passenger4 が安定しているなという印象。rhebok や raptor はまだ不安定な気がします…。

あとは、raptor の sleep(sleep 1 してから render するアプリ)の結果の爆速ぶりが異常。多分HTTPサーバのレベルでキャッシュをして、それを返しているのだと思いますが、現時点でキャッシュを返す条件のドキュメントがないので詳細がわからず。

続いては raptor のキャッシュ機能、Turbocaching についてのブログ記事があったのでその内容についてシェア。

Researching a potential new form of HTTP caching optimization - Phusion Blog

簡単にまとめると、Cookieの"一部の"値を利用して、ユーザ種別を判別し、ユーザに応じたキャッシュを返すようにして高速化を図っています。Varnish などのキャッシュ−サーバにも似たような機能があるのですが、Cookie全体の値を利用するため、サードパーティーCookieを利用したアプリなどでは想定するような高速化はできないとのこと。

この機能を使うと、

  • 大多数の人が同じ画面をみるアプリ(ブログなど)
  • 一部だけがユーザ個別のアプリ(ログインユーザの名前の表示部分だけが異なるアプリなど)

の高速化が見込めます。詳細は元記事を参照してください。 たぶんこの記事以外にもTurbocachingの機能があると思うので、それに期待しています。

Ruby 2.2.0 について話す

Ruby 2.2.0 の関連の記事を読みました。

個人的には

{"hoge": "fuga"}

という書き方ができるようになったのが嬉しいです。

次回

次回は2/17(火)です。お題はコーディング規約です。いろんな会社の規約を見ながらワイワイ話しましょう!

Pow などを利用して開発を行う際に、pry-remote は欠かせないツールです。

binding.pry_remote でブレークポイントを設定し、コンソールで pry-remote コマンドを使うと pry のようにデバッグができます。

しかし、pry-byebug の 2.0.0 になってから、pry-remote が利用できなくなってしまいました。pry-byebug の仕様変更が原因のようです。Issue を見る限り、対応には時間がかかりそうなので次のようにして対応しました。

gem 'pry-byebug', '1.3.3'