wataメモ

日々のメモをつらつらと書くだけ

nginx1.9.5のHTTP/2の機能を使ってみた

 次世代Web カンファレンスでも話題になっていたHTTP/2でnginxも1.9.5でサポートということで使ってみた。 サポートしたと言ってもnginxのページには以下の注意書きが書かれていた。

  • もしアプリでWAFを使っていて、nginxの前にあるならHTTP/2に対応しているか確認し、指定なければnginxの後ろにしてください
  • HTTP/2のサーバープッシュはこのリリースではまだサポートされません
  • もしssl_prefer_server_ciphersがonに設定されていて、ssl_ciphersブラックリストに乗っているものを使っている場合は、ブラウザはハンドシェイクエラーとなり動きません

サーバプッシュはまだサポートされない模様。 これを使ってみたかったのだが、残念。

試すこと

 せっかく試すので、nginx単体と静的ファイルだけでは面白く無いので、裏にrailsアプリを動かすことにした。

環境準備

OS

 CentOS 6.6を使用。 Vagrantfileを適当に作成してvagrant upを実行。

Vagrantfile

Vagrant.configure(2) do |config|
  config.vm.box = "http2-test"
  config.vm.box_url = "https://github.com/tommy-muehle/puppet-vagrant-boxes/releases/download/1.0.0/centos-6.6-x86_64.box"
  config.vm.network "private_network", ip: "192.168.33.80"
end

パッケージのインストール

 とりあえずこちらも適当にvagran sshしてパッケージをインストール。

sudo yum install -y git readline-devel \
  libxml2-devel libxslt-devel \
  mysql mysql-server mysql-devel \
  nodejs npm ImageMagick ImageMagick-devel

rubyのインストール

 いつものrbenvのお決まり作業。 githubのgistを使って2.2.3をインストール。

curl -L http://git.io/vWmCs | sh

 短縮URLを使ったが、ソースは以下。

gist7f813b7e214a7b9846a3

nginx 1.9.5のインストール

sudo rpm -i http://nginx.org/packages/mainline/centos/6/x86_64/RPMS/nginx-1.9.5-1.el6.ngx.x86_64.rpm

自己証明書作成

 毎度の事なのでコマンドだけ。

openssl genrsa -des3 -out server.key 2048
openssl req -new -key server.key -out server.csr
cp server.key server.key.org
openssl rsa -in server.key.org -out server.key
openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt
# nginxのディレクトリにコピーしておく
sudo cp -ip server.crt server.key /etc/nginx/

nginxのログの設定

 HTTP/2で動いているか確認するためにログ出力変更。 GETのところでもわかるが、最後の「h2:$http2」を追加。

/etc/nginx/nginx.conf

log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                  '$status $body_bytes_sent "$http_referer" '
                  '"$http_user_agent" "$http_x_forwarded_for" '
                  'h2:$http2';

こうすることでHTTP/2の場合は「h2:h2」と出力される。 HTTP/2でない場合は「h2:」となる。

実際のログ

192.168.33.1 - - [21/Oct/2015:04:03:10 +0200] "GET / HTTP/2.0" 404 640 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.71 Safari/537.36" "-" h2:h2

railsアプリ作成

 railsプロジェクト作成。

# nokogiriインストール用
bundle config build.nokogiri --use-system-libraries
# railsのインストール
gem install rails
# プロジェクトディレクトリ作成
mkdir projects
cd projects
# プロジェクト作成
rails new http2 -d mysql -T --skip-bundle

 Gemfileに必要なgemを追加。

Gemfile

# rails 4.2.4とmysql2の0.4.0の相性が良くないのでダウングレード
gem 'mysql2', '~> 0.3.20'

# 以下を追加
gem 'therubyracer'
gem 'unicorn'

 bundle installしてwelcomeコントローラを作成。

# bundle install
bundle --path vendor/bundle
# welcomeコントローラ作成
./bin/rails g controller welcome

app/controllers/welcome_controller.rb

class WelcomeController < ApplicationController
  def index
  end

  def nginx
  end

  # サーバプッシュテスト用(今回はnginxがまだ対応していないので関係ない)
  def push
    response.headers['Link'] = '</assets/14.jpg>; rel=preload'
    render :index
  end
end

まずはrails側で画像ファイルを裁く場合のテスト。

app/views/welcome/index.html.erb

<!DOCTYPE html>
<html>
  <head></head>
  <body>
    <h1>hello http/2!</h1>
    <img src="/assets/14.jpg">
    <img src="/assets/150704-05.jpg">
    <img src="/assets/20150707100135.jpg">
    <img src="/assets/B-U2e0ACcAAAuq6.jpg">
    <img src="/assets/akagami.jpg">
    <img src="/assets/yasashi.jpg">
  </body>
</html>

静的ファイルはnginxで裁く場合のテスト。 nginxの設定で「/img/」はunicornではなくnginxが裁くように設定している。

app/views/welcome/nginx.html.erb

<!DOCTYPE html>
<html>
  <head></head>
  <body>
    <h1>hello http/2!</h1>
    <img src="/img/14.jpg">
    <img src="/img/150704-05.jpg">
    <img src="/img/20150707100135.jpg">
    <img src="/img/B-U2e0ACcAAAuq6.jpg">
    <img src="/img/akagami.jpg">
    <img src="/img/yasashi.jpg">
  </body>
</html>

 ルーティングを設定。

config/routes.rb

root 'welcome#index'
get 'nginx' => 'welcome#nginx'
get 'push'  => 'welcome#push'

unicornの設定、起動

 unicornタスクを作成。

./bin/rails g task unicorn

 いつものごとく設定。 環境(RACK_ENV)は環境変数を利用するようにしてある。

lib/tasks/unicorn.rake

namespace :unicorn do
  ##
  # Tasks
  ##
  desc "Start unicorn for development env."
  task(:start) {
    config = Rails.root.join('config', 'unicorn.rb')
    sh "bundle exec unicorn_rails -c #{config} -D"
  }

  desc "Stop unicorn"
  task(:stop) { unicorn_signal :QUIT }

  desc "Restart unicorn with USR2"
  task(:restart) { unicorn_signal :USR2 }

  desc "Increment number of worker processes"
  task(:increment) { unicorn_signal :TTIN }

  desc "Decrement number of worker processes"
  task(:decrement) { unicorn_signal :TTOU }

  desc "Unicorn pstree (depends on pstree command)"
  task(:pstree) do
    sh "pstree '#{unicorn_pid}'"
  end

  def unicorn_signal signal
    Process.kill signal, unicorn_pid
  end

  def unicorn_pid
    begin
      File.read("/tmp/unicorn.pid").to_i
    rescue Errno::ENOENT
      raise "Unicorn doesn't seem to be running"
    end
  end
end

 unicornの起動。

./bin/rake unicorn:start

nginxとunicornの連携

 HTTP/2テスト用に以下のnginx設定ファイルを追加。

/etc/nginx/conf.d/http2.conf

upstream unicorn {
  server unix:/tmp/unicorn.sock fail_timeout=0;
}
server {
    listen  443 ssl http2;
    ssl_prefer_server_ciphers on;
    ssl_ciphers AESGCM:HIGH:!aNULL:!MD5;

    server_name localhost;
    ssl_certificate     /etc/nginx/server.crt;
    ssl_certificate_key /etc/nginx/server.key;

    # /img/は静的に対処する
    location /img/ {
      root /var/www/html;
    }

    try_files $uri/index.html $uri @unicorn;
    location @unicorn {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_redirect off;
        proxy_pass http://unicorn;
    }
}

 HTTP/2がオフの状態(HTTP)を試すためにdefault.confを修正。

/etc/nginx/conf.d/default.conf

erver {
    listen       80;
    server_name  localhost;

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

    try_files $uri @unicorn;
    location @unicorn {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_redirect off;
        proxy_pass http://unicorn;
    }
}

 nginxの起動。

sudo service nginx start

実際に試してみる

 後はブラウザ(Chrome)からアクセスして、Networkを見てみる。

HTTP

 見事に同時に6コネクションでブロックして終わってから残りをアクセスする感じになっている。 結果的には一番遅い。

f:id:wata_htn:20151021183356p:plain

HTTP/2(すべてrails)

 静的ファイルのリクエストがすべて同時に開始されている。 HTTPでの処理より早い。

f:id:wata_htn:20151021183935p:plain

HTTP/2(画像はnginx)

 同じく静的ファイルのリクエストがすべて同時に開始されている。 今回の結果としてはrailsを通すより早くなっている。 (想定通りの結果)

f:id:wata_htn:20151021183942p:plain

リクエスト、レスポンスヘッダについて

 HTTP/2はヘッダ周りの仕様も変わっているので確認すると以下のようになっていた。

HTTP

 HTTPはいつもどおりヘッダは大文字から始まっている。

リクエスト

f:id:wata_htn:20151021190531p:plain

レスポンス

f:id:wata_htn:20151021190553p:plain

HTTP/2

 ヘッダ名は小文字になっていて一部のヘッダは「:method:」とかになっている。 これはStatic Tableに定義されている物だ。 よく使うものは番号を振っておいて、データ転送量を減らす目論見。 この辺の考えはmod_ajpでも取り入れられていた。

リクエスト

f:id:wata_htn:20151021190519p:plain

レスポンス

f:id:wata_htn:20151021190546p:plain

まとめ

 nginxや他のサーバもHTTP/2を実装してきている。 どこまで既存のアプリがそこまで対処なしで移行出来るかが今後のポイントとなる。 ブラウザにキャッシュされているリソースをサーバプッシュで送るのは無駄なので サーバプッシュ周りが実装されたら、nginxやアプリ側でどう対応していくのかのベストプラクティスが必要。 h2oのようにCookieでブラウザのキャッシュのやり取りをして最適化するという手法もあるようだ。 Ruby界隈ではRackの対応やRailsでassets周りがHTTP/2に殆ど意識せずに移行出来るなら、HTTP/2は広まっていくだろう。 この辺の腰が重いと「HTTP/1.1で良くない?」という流れにもなる。

 サーバプッシュ周りで素晴らしい使い方が発見され、HTTP/2が流行ることを期待したい。