Help us understand the problem. What is going on with this article?

NginxによるHTTP/2 事始め

More than 3 years have passed since last update.

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がサポートされ始め、私のようなアプリエンジニアが「環境を作ってまずは動かしてみる」事への敷居が下がったように思えます。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がサポートされ始めました。リンク先で紹介されているように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

Nginxのセットアップ

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

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

$ make
$ sudo make install

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

$ 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
$ sudo initctl start nginx

nginx.conf

通信は全てSSLで行われます。serverディレクティブにはhttpsの設定を記載します。証明書は適当なオレオレ証明書を作って配置します。
http2を有効化するにはserver -> listenディレクティブにhttp2という記述を追加します。
ssl_ciphersssl_prefer_server_ciphersに関しては HTTP/2から見えるTLS事情 - あどけない話を参考にさせてもらいました。

$ sudo vi /usr/local/nginx/conf/nginx.conf
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 という拡張機能を導入すると、HTTP/2で動いているサイトにアクセスした際にURLバーの右側に青い稲妻が表示されるようになります。何故青い稲妻なのかは謎のままです。

青い稲妻.png

動かしてみよう

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

page.ssl.http2.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を作成します

Gemfile
gem 'unicorn'
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が参考になります。

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を起動します。

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

さてその結果は?

HTTP/2

app.http2.抜粋.png

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

HTTP/1.1 (https)

app.http1.抜粋.png

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

おわりに

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

宣伝

freeeでは老害 オジさんになっても攻め続けたいプラットフォームエンジニアを募集しています。よろしくお願いいたします。

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

(参考情報)

jpshadowapps
Ruby/Rails/Go/PHP/Java/Linux/MySQL/Docker/AWS(Solutions Architect Associate & Developer Associate)/Vim/Fintech/Payment
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした