NginxによるHTTP/2 事始め

freeeでアプリケーションエンジニアをやっています @jpshadowapps です。Twitter上では「ビール飲みたい」「35億欲しい」と発作的に呟く ~~老害~~ オジさんをやっています。

この記事は freee Engineers Advent Calendar 2015 の9日目です。

# サーバーサイドで出来る事って何だろう？
フロントエンドの開発プロセスや構成がとてつもないスピードで変化を続けている昨今、私のようなサーバーサイドがメインの ~~老害~~ オジさんはそのスピードに若干圧倒され白目を剥きつつ動向を追いかけている状態です。

そうしたフロントエンドの技術刷新も（特に）負債返却・開発品質とスピードの向上 という意味で重要な事ではありますが、最終的にデプロイされたアプリケーションが「正確で、かつストレス無く高速に」動く事も重要です。動く＝ユーザーに届く、と読み替えても頂いても良いと思います。freeeでは端末インストール型アプリを凌駕するスピード感と使い心地をクラウド上のWebシステムで実現しようとしています。ただアプリの実装だけ頑張ってどこまで辿り着けるのかは正直未知数な部分が多いです（それがやり甲斐の1つでもありますが）。もちろんアプリの実装自体も改善の余地はまだまだ多いのですが、サーバーサイド、特に通信技術のようなインフラ視点からも課題解決策を検討しておくべきと思っています。その候補の1つとなり得るのがHTTP/2ではないかと思っています。

特にここ1年程で、HTTP/2という言葉を目にする機会が増えてきました。とは言え私自身はそもそも何が出来るのか（出来ないのか）がよく分かっていない状態で、

* 何だか速そう
* 何だか軽そう
* 何だか安全そう

という漠然としたイメージだけを抱いた状態でした。

そんな折に[9月にリリースされた Nginx ver 1.9.5からHTTP/2がサポートされ始め](https://www.nginx.com/blog/nginx-1-9-5/)、私のようなアプリエンジニアが「環境を作ってまずは動かしてみる」事への敷居が下がったように思えます。HTTP/2そのものに関してはまだ学び始めたばかりのペーペーですので、HTTP/2の詳細については日本語の分かりやすい文献・記事が多数あるのでそちらにお任せしますが、「アプリ開発者が環境を作ってまずは試してみるという状況が出来つつあるこの時期に一度試しておこう」というのがこの記事を書いた動機です。

# HTTP/1.1 => HTTP/2 のおさらい

## たくさん接続するHTTP/1.1、1本の接続を使い回すHTTP/2

HTTPはステートレスなプロトコルで状態を持ちません。という事は必要な情報のやり取りは都度接続して行わなければなりません。そしてそのやり取りは1つずつ行う事しか出来ません。これを愚直にやるのは非効率なのでブラウザはリクエスト先のドメインに対して同時に複数の接続を試みるのですが、これでは情報のやり取りが非効率な上に通信先のサーバーに負荷を掛ける事になります（Domain Shardingのようなテクニックはその回避策の1つですね）。
HTTP/2では1つの接続内でチャネルによる多重処理を実現しており、これにより接続数を減らし並行性向上やサーバ負荷低減を実現しようとしています。

## 愚直に同じヘッダを送り続けるHTTP/1.1、ヘッダを圧縮するHTTP/2
先程「ステートレスなプロトコル」「都度情報をやり取り」と書きましたが、HTTP/1系はそのやり取りの最中に同じ内容のヘッダを何度も送信し続けます。ブラウザのデベロッパーツールでデバッグした経験のある方はよくご存知かと思いますが、時々「ヘッダ多っ！」と感じるようなサイトを開いた経験があるのではないでしょうか？HTTP/2では HPACK という圧縮技術を用いてヘッダを圧縮し、加えて前回送信したヘッダとの差分のみを送信する手法を取っています。これにより通信負荷の軽減を実現しようとしています。

## テキストでやり取りするHTTP/1.1、バイナリでやり取りするHTTP/2
HTTP/2ではメッセージをバイナリでやり取りする事でメッセージをコンパクト化し、送受信の効率化を図ろうとしています。
（個人的には「デバッグするの大変そう」という感想も頭をよぎりましたが、、）

# Nginx 1.9.5 からのHTTP/2対応
先程も書きましたがNginxでは[9月にリリースされたver 1.9.5からHTTP/2がサポートされ始めました](https://www.nginx.com/blog/nginx-1-9-5/)。リンク先で紹介されているようにsslを有効化した上で`server`ディレクティブに`http2`という記述を追記しオレオレ証明書を置くだけでHTTP/2で動かす事が出来ます。

# 動かしてみましょう

## 使用した環境
* Ubuntu 14.10
    * Mac上でVM稼働（Virtualbox & Vagrant）
* Ruby 2.2系
* Rails 4.2系
* Unicorn 5.0.1
* Nginx 1.9.7

[検証環境として使用したVM](https://github.com/goldeneggg/vagrant-nginx-http2/blob/master/Vagrantfile)

## Nginxのセットアップ
現時点の最新verである 1.9.7 をtarballからインストールします。http2を有効化したい場合、sslを有効化するオプション`--with-http_ssl_module`とhttp2を有効化する`--with-http_v2_module`オプションを指定します。

```bash
$ ./configure --with-http_ssl_module --with-http_v2_module --prefix=/usr/local/nginx

$ make
$ sudo make install
```

インストールしたらupstartのデーモンとして登録して起動します

```bash
$ sudo vi /etc/init/nginx.conf
```
```
# nginx

description "nginx http daemon"
author "George Shammas <georgyo@gmail.com>"

start on (filesystem and net-device-up IFACE=lo)
stop on runlevel [!2345]

env DAEMON=/usr/local/nginx/sbin/nginx
env PID=/var/run/nginx.pid

expect fork
respawn

respawn limit 10 5
#oom never

pre-start script
        $DAEMON -t
        if [ $? -ne 0 ]
                then exit $?
        fi
end script

exec $DAEMON
```
```bash
$ sudo initctl start nginx
```


## `nginx.conf`

通信は全てSSLで行われます。`server`ディレクティブにはhttpsの設定を記載します。証明書は適当なオレオレ証明書を作って配置します。
http2を有効化するには`server` -> `listen`ディレクティブに`http2`という記述を追加します。
※`ssl_ciphers`と`ssl_prefer_server_ciphers`に関しては [HTTP/2から見えるTLS事情 - あどけない話](http://d.hatena.ne.jp/kazu-yamamoto/20141219/1418976024)を参考にさせてもらいました。


```bash
$ sudo vi /usr/local/nginx/conf/nginx.conf
```
```nginx:nginx.conf
http {
    
    # (略)
    
    server {
        listen       443 ssl http2 default_server;
        server_name  _;

        ssl_certificate      /usr/local/nginx/server.crt;
        ssl_certificate_key  /usr/local/nginx/server.key;

        ssl_session_cache    shared:SSL:1m;
        ssl_session_timeout  5m;

        ssl_ciphers AESGCM:HIGH:!aNULL:!MD5;
        ssl_prefer_server_ciphers  on;

        location / {
            root   html;
            index  index.html index.htm;
        }

        access_log /usr/local/nginx/logs/app_huga_access.log;
        error_log /usr/local/nginx/logs/app_huga_error.log;
    }
```

## chrome拡張 "HTTP/2 and SPDY indicator" の導入
chromeはHTTP/2に対応しています。[HTTP/2 and SPDY indicator](https://chrome.google.com/webstore/detail/http2-and-spdy-indicator/mpbpobfflnpcgagjijhmgnchggcjblin?hl=ja) という拡張機能を導入すると、HTTP/2で動いているサイトにアクセスした際にURLバーの右側に青い稲妻が表示されるようになります。何故青い稲妻なのかは謎のままです。

<img width="897" alt="青い稲妻.png" src="https://qiita-image-store.s3.amazonaws.com/0/4516/3f82c8bf-e51a-9675-2ae9-a693a1761f78.png">


## 動かしてみよう
例えば複数サイトの画像にアクセスするページを用意して繋いでみます。同じドメインへのリクエストが（ほぼ）同時に行われます。

<img width="1243" alt="page.ssl.http2.png" src="https://qiita-image-store.s3.amazonaws.com/0/4516/ec419bcd-aa53-1eb4-d69d-6a8377565003.png">

nginxのアクセスログの抜粋です。"HTTP/2.0" と出力されます

```
192.168.56.1 - - [04/Dec/2015:20:49:38 +0900] "GET /img/hoge.png HTTP/2.0" 304 174 "https://app_server/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.73 Safari/537.36"
192.168.56.1 - - [04/Dec/2015:20:49:38 +0900] "GET /img/huga.png HTTP/2.0" 304 174 "https://app_server/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.73 Safari/537.36"
```

# Railsアプリを動かしてみよう

## 構成
Rails + Unicorn + Nginx with HTTP/2という構成です。
連携の仕組みはnot HTTP/2と同じですので、HTTP/2独自の設定は不要です。所謂よく見かける構成です。
Web VMの任意のディレクトリに対象のrailsアプリを置いて起動します。
※今回vagrantでVMを構築していますが、ホストPCとの共有ディレクトリである`/vagrant`はファイルI/Oの発生するオペレーションが遅かったりパーミッション絡みでハマったりするので避けます。

```
+------------------------------------------+
|                    Web VM                |
| +-------+  +-------+   +------+          |
| | nginx +-->unicorn+---> rails|          |
| +-------+  +-------+   +---+--+          |
|                            |             |
+------------------------------------------+
                             |
                   +-------------------+
                   |         |         |
                   |     +---v--+      |
                   |     |mysql |      |
                   |     +------+      |
                   |                   |
                   |       DB VM       |
                   +-------------------+
```

## unicorn

Gemfileにunicornを（無ければ）追加し、対象のアプリに`config/unicorn.rb`を作成します

```rb:Gemfile
gem 'unicorn'
```
```rb:config/unicorn.rb
worker_processes Integer(ENV["WEB_CONCURRENCY"] || 2)
timeout 30
preload_app true

listen "/tmp/unicorn_app.sock"
pid "/tmp/unicorn_app.pid"

stderr_path File.expand_path('log/unicorn_app.log', ENV['RAILS_ROOT'])
stdout_path File.expand_path('log/unicorn_app.log', ENV['RAILS_ROOT'])

before_fork do |server, worker|
  Signal.trap 'TERM' do
    puts 'Unicorn master intercepting TERM and sending myself QUIT instead'
    Process.kill 'QUIT', Process.pid
  end

  defined?(ActiveRecord::Base) and ActiveRecord::Base.connection.disconnect!
end

after_fork do |server, worker|
  Signal.trap 'TERM' do
    puts 'Unicorn worker intercepting TERM and doing nothing. Wait for master to send QUIT'
  end

  defined?(ActiveRecord::Base) and ActiveRecord::Base.establish_connection
end
```

## `nginx.conf`

unicornとの連携部分の設定もhttpの場合と変わりません。`proxy_pass`で参照されるバックエンドを`upstream`ディレクティブに定義し、proxyしたいパス（例えば `/`）へのアクセスを`location`ディレクティブでproxyします。
※unicorn + nginxの設定例は[unicorn/nginx.conf at master · defunkt/unicorn](https://github.com/defunkt/unicorn/blob/master/examples/nginx.conf)が参考になります。

```nginx:nginx.conf
    upstream app {
        server unix:/tmp/unicorn_app.sock fail_timeout=0;
    }

    server {
        listen       443 ssl http2 default_server;
        server_name  _;

        ssl_certificate      /usr/local/nginx/server.crt;
        ssl_certificate_key  /usr/local/nginx/server.key;

        ssl_session_cache    shared:SSL:1m;
        ssl_session_timeout  5m;

        ssl_ciphers AESGCM:HIGH:!aNULL:!MD5;
        ssl_prefer_server_ciphers  on;

        root /home/vagrant/railsapp/public;

        access_log /usr/local/nginx/logs/app_http2_access.log;
        error_log /usr/local/nginx/logs/app_http2_error.log;

        location / {
            proxy_set_header Host $http_host;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto https;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_redirect http:// https://;
            proxy_pass http://app;
        }
```


## 起動
unicornとnginxを起動します。

```bash
$ cd APP_DIR
$ bundle exec unicorn_rails -c /home/vagrant/railsapp/config/unicorn.rb -E development -D
$ sudo initctl start nginx
```

# さてその結果は？

## HTTP/2

<img width="645" alt="app.http2.抜粋.png" src="https://qiita-image-store.s3.amazonaws.com/0/4516/773e85ef-9857-ad9a-a5b3-19665bedf783.png">

ぱっと見て「おお凄い」とはなり辛い結果です。というかHTTP/2以外とも比較してみないと良いのか悪いのかよく分かりません。
というわけで、HTTP/1(https)でも動かしてみます

## HTTP/1.1 (https)

<img width="640" alt="app.http1.抜粋.png" src="https://qiita-image-store.s3.amazonaws.com/0/4516/67cc8d80-bcfe-698e-9fbd-2ced792adb51.png">

若干HTTP/2の方が高パフォーマンスなのかな、、といった程度の違いしかありませんでした。ただそもそも使用したrailsアプリはjsとcssがアセットパイプラインで連結・圧縮されていますので、並列で捌きたい程のファイル数では無いように思えます。多数のドメインにアクセスし・且つ大量のコンテンツへのアクセスが発生するようなケースこそHTTP/2の出番なのかな、と思えます。

# おわりに
HTTP/2をザッと試してみた程度ではありますが、それでもどういうケースで向いているのか（逆に向いていないのか）がおぼろげではありますが見えてきた気がします。実際に現場で導入するとなってもそれはまだ結構先の話かな？という印象がありますが、今後どのような開発基盤を整備していくのか？どのようなサービスを作っていきたいのか？といった社内全体のビジョンも鑑みつつ導入を検討していくべきと感じました。
HTTP/2の技術は、HTTP/2自体のみならず、それを実現するために導入されている技術も楽しみなものばかりで、まだまだ理解不足な点が多々ありますが引き続きウォッチしていきたいですね！

# 宣伝

freeeでは[~~老害~~ オジさんになっても攻め続けたいプラットフォームエンジニア](https://www.wantedly.com/projects/20806)を募集しています。よろしくお願いいたします。

明日は、自らの本名が職場で浸透しないという悩みを抱えつつもfreeeのプラットフォーム基盤を支えている __not__ オジさん な大黒柱 @nasa4w さんです！


# （参考情報）
* [RFC7540 日本語訳](http://summerwind.jp/docs/rfc7540/)
* [HTTP/2 - Wikipedia](https://ja.wikipedia.org/wiki/HTTP/2)
* [HTTP/2 入門](http://www.slideshare.net/techblogyahoo/http2-35029629)
* [HTTP2 時代のサーバサイドアーキテクチャ考察 - Block Rockin’ Codes](http://jxck.hatenablog.com/entry/http2-server-side-architecture)
* [HTTP/2の現状とこれから](http://www.slideshare.net/shigeki_ohtsu/http2-ohtsu-html5conf2015)
* [「HTTP/2」がついに登場！ 開発者が知っておきたい通信の仕組み・新機能・導入方法 (1/3)：CodeZine（コードジン）](http://codezine.jp/article/detail/8663)