homeASCIIcasts

266: HTTPストリーミング 

(view original Railscast)

Other translations: En Es

Other formats:

Written by Naomi Fujimoto

今回のエピソードでは、引き続きRails 3.1の最初のベータ版の機能を紹介します。今回はHTTPストリーミングを見ていきましょう。この話題はRuby on Railsブログのポストでも詳しく取り上げられているので、まずはその記事を読むことをお勧めします。ここではRailsアプリケーションでの設定方法と、使用時に発生する潜在的問題について説明します。

HTTPストリーミングを実際に試すため、前回のエピソードで作成した簡単なToDoアプリケーションを使用します。アプリケーションのコントローラでストリーミングを有効にします。デフォルトでは有効になっていないので、アクションごとかコントローラごとに設定します。

ストリーミングを有効にするには、renderを使ってオプションで:stream => trueを渡します。

/app/controllers/projects_controller.rb

def index
  @projects = Project.all
  render :stream => true
end

コントローラ全体でストリーミングを有効にするにはクラスメソッドのstreamを使用します。

/app/controllers/projects_controller.rb

class ProjectsController < ApplicationController
  stream

  def index
    # rest of controller code
  end
end

Rails 3のデフォルトのWebサーバであるWEBrickはストリーミングをサポートしていないので、実際に動作する様子を見るには、Unicornなどのストリーミングをサポートする別のWebサーバに切り替えます。

Unicorn gemはアプリケーションのGemfileに記述されていますが、デフォルトではコメントアウトされています。これを使用するためこの行を非コメント化し、bundleを実行してインストールします。

/Gemfile

# Use unicorn as the web server
gem 'unicorn'

Unicornでストリーミングを使用するために設定を編集します。Railsにはソースコード内にストリーミングに関するコメントがあり、設定の仕方に関する情報が含まれています。必要な作業としては、configディレクトリに設定ファイルを新規作成します。

/config/unicorn.rb

listen 3000, :tcp_nopush => false

次にこの設定ファイルを指定してUnicornを起動します。

$ unicorn_rails --config-file config/unicorn.rb

このコマンドがうまく実行できない場合は、Gemfilegem 'unicorn'の行を非コメント化した後にbundleを実行したことを再度確認してください。それでもまだうまく行かない場合は、コマンドの前にbundle execを付けてみてください。

デフォルトのRailsサーバを使用しているときと同じように、ポート3000でブラウザからアプリケーションを開きます。

Unicornで動作するアプリケーション

ストリーミングをシミュレートする

上のページはストリーミングを使用していますが、その効果はまだよくわかりません。ビュー層で表示されるのに数秒かかるようなものをシミュレートして、レスポンスがチャンク(chunk)単位で返される様子を見てみることにしましょう。これを行う一番簡単な方法は、ビューの最初にsleep呼び出しを追加する方法です。

/app/views/projects/index.html.erb

<% 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-Encodingchunkedに設定されている一方、レスポンスの最初の部分が返されるときにはまだページ全体のサイズがわからないので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の集合に対して繰り返し処理を始めるまではデータベースの呼び出しが行われないようにします。

/app/controllers/projects_controller.rb

def index
  @projects = Project.scoped
end

ストリーミングにともなう潜在的問題

ここまでのところではストリーミングはすばらしい機能のように思われるかもしれませんが、採用を決める前に知っておくべき欠点がいくつかあります。一つ目は、レイアウトとテンプレートのレンダリングの順序が逆になります。通常のRailsのリクエストではアクションのテンプレートがまずレンダリングされて、その後にレイアウトが続きます。ストリーミングのリクエストでは、レイアウトの内容をできるだけ早くレンダリングする必要があります。これは通常はレイアウトにヘッダセクションが含まれているからです。アクションのテンプレートは、レイアウトのyieldコマンドのところで初めてレンダリングされます。

つまりもしテンプレート中のインスタンス変数を設定しようとしても、テンプレートがまだレンダリングされていないのでレイアウトからその変数にはアクセスできません。例えば、indexアクションでインスタンス変数@titleを設定してみます。

/app/views/projects/index.html.erb

<% @title = "Projects "%>
<% sleep 5 %>
<h1>Listing projects</h1>

<table>
<!-- Rest of file omitted. -->

続いてその変数をレイアウトで使用してみます。

/app/views/layouts/application.html.erb

<!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と同じようにふるまいます。これを用いてページのタイトルを設定します。

/app/views/projects/index.html.erb

<% provide :title, "Projects" %>

レイアウトで、content_forのときと同じように、yieldを使用します。

/app/views/layouts/application.html.erb

<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テンプレートで実際には存在しないメソッドの呼び出しを追加します。

/app/views/projects/index.html.erb

<% 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)モードで実行中に表示する標準のエラーページにリダイレクトされます。

開発モードだが本番の500エラーページが表示される

つまり、開発(development)モードでエラーが投げられたときに通常表示されるデバッグ用情報を得ることができないということなので、開発用ログからエラーを探す必要があります。

テンプレート内でセッションやクッキーの情報を設定することも、ストリーミングを使う場合はできません。テンプレートにセッション変数を設定しようとしても、Railsはすでにヘッダ情報をブラウザに送信してしまっているので、テンプレートからヘッダ情報を追加で送ることはできません。この問題は、他にもクッキーや、セッションを利用するフラッシュメッセージにも当てはまります。セッションとクッキーの情報は、ストリーミングを使用しているときはコントローラ内で設定する必要があります。

最後の2つの潜在的問題ですが、まずRailsのHTTPストリーミング機能はRuby fiberを利用しているので、Ruby 1.9を使用する必要があります。また、ストリーミングは一部のミドルウェアと互換性がありません。ミドルウェアがレスポンスを修正する場合、ストリーミングと一緒に動作させることはできません。これで、ストリーミングがデフォルトでは有効になっていない理由がわかったかと思います。ストリーミングを使用する場合に気をつけなくてはいけない潜在的な問題がいくつかあるので、パフォーマンスが最大限に求められるページのみに使用を限定するのがいいでしょう。

ここで紹介した問題があるにも関わらず、ストリーミングは採用を検討する価値があります。特にJavaScriptやCSSファイルをいくつか含んでいるページの場合は、ブラウザがこれらのファイルの読み込みや処理にできるだけ早くとりかかることができるので、エンドユーザのユーザ体験を改善できる可能性があります。