#266 HTTP Streaming
- Download:
- source codeProject Files in Zip (90.8 KB)
- mp4Full Size H.264 Video (17.3 MB)
- m4vSmaller H.264 Video (10.1 MB)
- webmFull Size VP8 Video (11.1 MB)
- ogvFull Size Theora Video (24 MB)
今回のエピソードでは、引き続きRails 3.1の最初のベータ版の機能を紹介します。今回はHTTPストリーミングを見ていきましょう。この話題はRuby on Railsブログのポストでも詳しく取り上げられているので、まずはその記事を読むことをお勧めします。ここではRailsアプリケーションでの設定方法と、使用時に発生する潜在的問題について説明します。
HTTPストリーミングを実際に試すため、前回のエピソードで作成した簡単なToDoアプリケーションを使用します。アプリケーションのコントローラでストリーミングを有効にします。デフォルトでは有効になっていないので、アクションごとかコントローラごとに設定します。
ストリーミングを有効にするには、renderを使ってオプションで:stream => true
を渡します。
def index @projects = Project.all render :stream => true end
コントローラ全体でストリーミングを有効にするにはクラスメソッドのstream
を使用します。
class ProjectsController < ApplicationController stream def index # rest of controller code end end
Rails 3のデフォルトのWebサーバであるWEBrickはストリーミングをサポートしていないので、実際に動作する様子を見るには、Unicornなどのストリーミングをサポートする別のWebサーバに切り替えます。
Unicorn gemはアプリケーションのGemfile
に記述されていますが、デフォルトではコメントアウトされています。これを使用するためこの行を非コメント化し、bundleを実行してインストールします。
# Use unicorn as the web server gem 'unicorn'
Unicornでストリーミングを使用するために設定を編集します。Railsにはソースコード内にストリーミングに関するコメントがあり、設定の仕方に関する情報が含まれています。必要な作業としては、config
ディレクトリに設定ファイルを新規作成します。
listen 3000, :tcp_nopush => false
次にこの設定ファイルを指定してUnicornを起動します。
$ unicorn_rails --config-file config/unicorn.rb
このコマンドがうまく実行できない場合は、Gemfile
のgem 'unicorn'
の行を非コメント化した後にbundle
を実行したことを再度確認してください。それでもまだうまく行かない場合は、コマンドの前にbundle exec
を付けてみてください。
デフォルトのRailsサーバを使用しているときと同じように、ポート3000でブラウザからアプリケーションを開きます。
ストリーミングをシミュレートする
上のページはストリーミングを使用していますが、その効果はまだよくわかりません。ビュー層で表示されるのに数秒かかるようなものをシミュレートして、レスポンスがチャンク(chunk)単位で返される様子を見てみることにしましょう。これを行う一番簡単な方法は、ビューの最初にsleep
呼び出しを追加する方法です。
<% sleep 5 %> <h1>Listing projects</h1> <table> <tr> <th>Name</th> <th></th> <th></th> <th></th> </tr> <% @projects.each do |project| %> <tr> <td><%= project.name %></td> <td><%= link_to 'Show', project %></td> <td><%= link_to 'Edit', edit_project_path(project) %></td> <td><%= link_to 'Destroy', project, confirm: 'Are you sure?', method: :delete %></td> </tr> <% end %> </table> <br /> <%= link_to 'New Project', new_project_path %>
curl
を使ってこのページを取り込むことでチャンク単位の(chunked)レスポンスを見ることができます。アプリケーションのトップページを指定すると、すぐにレスポンスの一部が返されます。
$ curl -i localhost:3000 HTTP/1.1 200 OK Date: Wed, 18 May 2011 08:18:56 GMT Status: 200 OK Connection: close Cache-control: no-cache Transfer-Encoding: chunked Content-Type: text/html; charset=utf-8 X-UA-Compatible: IE=Edge X-Runtime: 0.023745 <!DOCTYPE html> <html> <head> <title>Todo</title> <link href="/assets/application.css" media="screen" rel="stylesheet" type="text/css" /> <script src="/assets/application.js" type="text/javascript"></script> <meta content="authenticity_token" name="csrf-param" /> <meta content="0eBxvhbMH6HA8ocRLw06uNnmh7zqWo5dGSeFIA8sfj8=" name="csrf-token" /> </head> <body>
その後数秒たってから残りの部分が返されます。上記の結果のヘッダ情報で、Transfer-Encoding
はchunked
に設定されている一方、レスポンスの最初の部分が返されるときにはまだページ全体のサイズがわからないのでContent-Length
ヘッダはありません。
<h1>Listing projects</h1> <table> <tr> <th>Name</th> <th></th> <th></th> <th></th> </tr> <tr> <td>Housework</td> <td><a href="/projects/1">Show</a></td> <td><a href="/projects/1/edit">Edit</a></td> <td><a href="/projects/1" data-confirm="Are you sure?" data-method="delete" rel="nofollow">Destroy</a></td> </tr> </table> <br /> <a href="/projects/new">New Project</a> </body> </html>
ストリーミングを有効にしなかった場合、サーバからレスポンスが返ってくるまでに5秒の遅れが発生します。ページのヘッダセクションがすぐに返されることで、ブラウザがこの部分の処理を開始して、参照されるべきJavaScriptやCSSファイルを読み込みながら、サーバからページの残りの部分が返されるのを待ちます。
このページをブラウザで見ると、表示が開始されるのに5秒かかるのですが、メインページの残りが送信されるのを待っている間に裏でJavaScriptとCSSファイルを取得しています。
ストリーミングの効果を最大限利用するためには、できるだけ多くの処理をビュー層に移して、サーバができるだけ早くページのストリーミングを開始できるようにします。indexアクションではProject.all
を使用して、ページに表示するprojectを取得しています。サーバは、このコマンドが実行されるまでこのページのストリーミングを開始することができません。そこでこれを、必要時にロードを行うscoped
などに置き換えることで、ビュー層がprojectの集合に対して繰り返し処理を始めるまではデータベースの呼び出しが行われないようにします。
def index @projects = Project.scoped end
ストリーミングにともなう潜在的問題
ここまでのところではストリーミングはすばらしい機能のように思われるかもしれませんが、採用を決める前に知っておくべき欠点がいくつかあります。一つ目は、レイアウトとテンプレートのレンダリングの順序が逆になります。通常のRailsのリクエストではアクションのテンプレートがまずレンダリングされて、その後にレイアウトが続きます。ストリーミングのリクエストでは、レイアウトの内容をできるだけ早くレンダリングする必要があります。これは通常はレイアウトにヘッダセクションが含まれているからです。アクションのテンプレートは、レイアウトのyield
コマンドのところで初めてレンダリングされます。
つまりもしテンプレート中のインスタンス変数を設定しようとしても、テンプレートがまだレンダリングされていないのでレイアウトからその変数にはアクセスできません。例えば、index
アクションでインスタンス変数@title
を設定してみます。
<% @title = "Projects "%> <% sleep 5 %> <h1>Listing projects</h1> <table> <!-- Rest of file omitted. -->
続いてその変数をレイアウトで使用してみます。
<!DOCTYPE html> <html> <head> <title><%= @title %></title> <%= stylesheet_link_tag "application" %> <%= javascript_include_tag "application" %> <%= csrf_meta_tags %> </head> <body> <%= yield %> </body> </html>
curlを使ってページを取得してhead
セクションを見てみるとtitle
要素は空です。レイアウトがテンプレートの前にレンダリングされるので、レイアウトファイルから読み込まれる時にはまだ@title
変数が設定されていないのです。ストリーミングを使用しないリクエストでは、テンプレートが最初に読み込まれるのでこのようなことは起こりません。
<head> <title></title> <link href="/assets/application.css" media="screen" rel="stylesheet" type="text/css" /> <script src="/assets/application.js" type="text/javascript"></script> <meta content="authenticity_token" name="csrf-param" /> <meta content="x0CtIY+0vEbfkh6gohZp/WdOd0ZanobQHZT8+HUC/OE=" name="csrf-token" /> </head>
テンプレートからレイアウトに情報を渡す正しい方法はcontent_for
を使うやり方ですが、これもうまく動作しません。試しにcurl
を使ってページを見てみると、出力はtitle要素の直前で止まってしまいます。
この問題は、content_for
のしくみに原因があります。同じ項目に対してcontent_for
を複数回呼び出している場合、Railsはそれを一つに連結(concatenate)します。そのためRailsは、最初にcontent_for :title
が現れたときに、ページの下の方に同じ呼び出しがもうないということを知ることができません。
Rails 3.1にはprovide
という新しいメソッドがあり、値を連結(concatenate)しないことを除いては、これがまさにcontent_for
と同じようにふるまいます。これを用いてページのタイトルを設定します。
<% provide :title, "Projects" %>
レイアウトで、content_for
のときと同じように、yield
を使用します。
<title><%= yield :title %></title>
ページを見てみると、title
要素には設定したタイトルが入っています。
<!DOCTYPE html> <html> <head> <title>Projects</title> <link href="/assets/application.css" media="screen" rel="stylesheet" type="text/css" /> <script src="/assets/application.js" type="text/javascript"></script> <meta content="authenticity_token" name="csrf-param" /> <meta content="NzdFt92dDBSXRRgFR0pRZRizirN87Qb5CVdqgGEAvTU=" name="csrf-token" /> </head> <!-- rest of page -->
ストリーミングを利用するときに注意すべきもうひとつの問題は、例外が発生したときの動作です。例として、index
テンプレートで実際には存在しないメソッドの呼び出しを追加します。
<% provide :title, "Projects" %> <% fall_over %> <% sleep 5 %> <h1>Listing projects</h1> <!-- rest of page -->
ページを見てみると、関心を引く出力が表示されています。
$ curl -i http://localhost:3000/ (header information omitted) <!DOCTYPE html> <html> <head> <title>Projects</title> <link href="/assets/application.css" media="screen" rel="stylesheet" type="text/css" /> <script src="/assets/application.js" type="text/javascript"></script> <meta content="authenticity_token" name="csrf-param" /> <meta content="rhFAQuK2s5Rxi6jjC3jA12k07GjD75VeWlbsyf47bLc=" name="csrf-token" /> </head> <body> "><script type="text/javascript">window.location = "/500.html"</script></html>
例外が発生すると、Railsはブラウザに対して500.html
ページにリダイレクトさせるJavaScriptコードを含むscript要素を送り返します。このページをブラウザで見てみると、Railsアプリケーションが本運用(production)モードで実行中に表示する標準のエラーページにリダイレクトされます。
つまり、開発(development)モードでエラーが投げられたときに通常表示されるデバッグ用情報を得ることができないということなので、開発用ログからエラーを探す必要があります。
テンプレート内でセッションやクッキーの情報を設定することも、ストリーミングを使う場合はできません。テンプレートにセッション変数を設定しようとしても、Railsはすでにヘッダ情報をブラウザに送信してしまっているので、テンプレートからヘッダ情報を追加で送ることはできません。この問題は、他にもクッキーや、セッションを利用するフラッシュメッセージにも当てはまります。セッションとクッキーの情報は、ストリーミングを使用しているときはコントローラ内で設定する必要があります。
最後の2つの潜在的問題ですが、まずRailsのHTTPストリーミング機能はRuby fiberを利用しているので、Ruby 1.9を使用する必要があります。また、ストリーミングは一部のミドルウェアと互換性がありません。ミドルウェアがレスポンスを修正する場合、ストリーミングと一緒に動作させることはできません。これで、ストリーミングがデフォルトでは有効になっていない理由がわかったかと思います。ストリーミングを使用する場合に気をつけなくてはいけない潜在的な問題がいくつかあるので、パフォーマンスが最大限に求められるページのみに使用を限定するのがいいでしょう。
ここで紹介した問題があるにも関わらず、ストリーミングは採用を検討する価値があります。特にJavaScriptやCSSファイルをいくつか含んでいるページの場合は、ブラウザがこれらのファイルの読み込みや処理にできるだけ早くとりかかることができるので、エンドユーザのユーザ体験を改善できる可能性があります。