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
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_ciphers
とssl_prefer_server_ciphers
に関しては HTTP/2から見えるTLS事情 - あどけない話を参考にさせてもらいました。
$ sudo vi /usr/local/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バーの右側に青い稲妻が表示されるようになります。何故青い稲妻なのかは謎のままです。
動かしてみよう
例えば複数サイトの画像にアクセスするページを用意して繋いでみます。同じドメインへのリクエストが(ほぼ)同時に行われます。
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
を作成します
gem 'unicorn'
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が参考になります。
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
ぱっと見て「おお凄い」とはなり辛い結果です。というかHTTP/2以外とも比較してみないと良いのか悪いのかよく分かりません。
というわけで、HTTP/1(https)でも動かしてみます
HTTP/1.1 (https)
若干HTTP/2の方が高パフォーマンスなのかな、、といった程度の違いしかありませんでした。ただそもそも使用したrailsアプリはjsとcssがアセットパイプラインで連結・圧縮されていますので、並列で捌きたい程のファイル数では無いように思えます。多数のドメインにアクセスし・且つ大量のコンテンツへのアクセスが発生するようなケースこそHTTP/2の出番なのかな、と思えます。
おわりに
HTTP/2をザッと試してみた程度ではありますが、それでもどういうケースで向いているのか(逆に向いていないのか)がおぼろげではありますが見えてきた気がします。実際に現場で導入するとなってもそれはまだ結構先の話かな?という印象がありますが、今後どのような開発基盤を整備していくのか?どのようなサービスを作っていきたいのか?といった社内全体のビジョンも鑑みつつ導入を検討していくべきと感じました。
HTTP/2の技術は、HTTP/2自体のみならず、それを実現するために導入されている技術も楽しみなものばかりで、まだまだ理解不足な点が多々ありますが引き続きウォッチしていきたいですね!
宣伝
freeeでは老害 オジさんになっても攻め続けたいプラットフォームエンジニアを募集しています。よろしくお願いいたします。
明日は、自らの本名が職場で浸透しないという悩みを抱えつつもfreeeのプラットフォーム基盤を支えている not オジさん な大黒柱 @nasa4w さんです!