はじめに
Gogengo! はさくらVPS
上で Thin のインスタンス 1つで動いていました。これだと重い処理のリクエストがきたら他のリクエストが待ちになってしまったり、大量の負荷がきたときに耐えられない恐れがあるので Nginx + Unicorn で動かしたくなりました。
Passenger ではなく Unicorn にした理由は 2つです。
さくらVPS で環境をつくったときの手順はこちらにまとめてあります。
対応する前に Unicorn の仕組みについて知るため『次世代RailsサーバーUnicornを使ってみた | TechRacho』を読みました。とても詳しくてわかりやすい文献です。
今回の対応の大半はミニ開発合宿でやりました。@satococoa さん、@netwillnet さんに沢山のアドバイスをいただきました。ありがとうございます!
ゴール
さくらVPS 上で Thin で動いている Gogengo! を Nginx + Unicorn で動かし直すことです。作業は本番環境でおこないます。作業中はダウンタイムが極力おきないよう心がけます。
また、Nginx + Unicorn の素晴らしさを確認するため環境の変更前後で負荷テストをして数値を比較します。負荷テストには JMeter を使います。
ゴールへ向けての大まかな流れ
次のような手順で作業を進めます。
1. Thin の環境で負荷テストを行う
まずは Before のパフォーマンスを測定します。
負荷テスト期間は10分です。秒間リクエスト数を少しずつ増やしていきます。リクエストは10種類で、それぞれのリクエストは別のページにアクセスします。アクセスするページは Google Analytics を見て割合を決めました。負荷テストは分間リクエスト数ほぼ0 の時間帯を選び本番環境で行いました。最近 Gogengo! にアクセスして遅かったらテストのせいだったかもしれません。すみません><
秒間リクエスト数を 10・15・20・25 と増やしてみました。結果はこちらです。
| req/s | Σreq | Latency | Load Average | user | sys | io | Browsing | Stat |
|---|---|---|---|---|---|---|---|---|
| 10 | 6,000 | 0.13 | 1 (t=4), 0.7 (t=5..10) | 40 | 4 | 0 | 早い | OK |
| 15 | 9,000 | 0.79 | 1.5 (t=3), 1.2 (t=4..10) | 46 | 8 | 0 | 普通 | OK |
| 20 | 12,000 | 10.07 | 1.3 (t=4..12) | 46 | 8 | 0 | 遅すぎ | OK |
| 25 | 15,000 | 18.56 | 1.3 (t=4..14) | 46 | 8 | 0 | 繋がらない | ※A |
※A … 多い時で 10% のリクエストが返ってこなくなりました
表をコンパクトにするため項目は省略した記号にしました。それぞれの意味は以下のとおりです。
| req/s | テストで実行する秒間リクエスト数 |
|---|---|
| Σreq | テストで実行するリクエスト総数 |
| Latency | 平均レイテンシ(秒) |
| Load Average | ロードアベレージ。t は時間(分)を示す |
| user | %user: ユーザモードでCPUが使った時間の割合 |
| sys | %system: システムモードでCPUが使った時間の割合 |
| io | %iowait: I/O待ちでCPUがアイドルした時間の割合 |
| Browsing | テスト中にブラウジングしての体感速度 |
| Stat | 返ってきたステータスコードに異常が無いかどうか |
各項目の数値は次のように確認しました。
| Load Average | `sar -q 1` |
|---|---|
| user, sys, io | `sar -u 1` |
| Latency, Stat | JMeter のテスト結果表示 |
なお、JMeter のパラメータはそれぞれのテストケースで次のように調整しました。
| スレッド数 | 600 → 900 → 1200 → 1500 |
|---|---|
| Ramp-Up期間 | 600 で固定 |
| 無限ループ | 1 で固定 |
たとえば「スレッド数600・Ramp-Up期間600・無限ループ1・1スレッドが送るリクエスト数10」という条件のテストケースの場合、1分につき60人が10リクエストを Gogengo! に送って帰っていくという負荷が10分続き、10分間で合計6000リクエストが送られると言いかえられます(2012/12/29 編集)。
素早くレスポンスが返ってくるのは秒間10リクエストまででした。いずれのケースでも、テスト終了後にサーバが落ちることはありませんでした。リクエスト数を増やしても CPU の使用率はあがっていないので、リソースを使いきれていない感じです。ちなみに、topコマンドの RES によると Rails のインスタンスは約60MBの物理メモリを使っていました。
それでは Nginx + Unicorn の環境をセットアップします。果たしてどのくらい変わるのでしょうか。楽しみです。
2. Thin を動かしつつ Nginx + Unicorn をセットアップする
次のような手順で進めます。
2.1. ポート81 を開放する
既存のサービスはポート80 で動いています。サービスを止めたくないのでポート81 を使います。
ファイアウォールの設定でポート81 は開放していないので、設定を編集して開放するようにします。念のため設定ファイル系はすべてバックアップをとります。
$ sudo cp /etc/sysconfig/iptables /etc/sysconfig/iptables.20121215
$ sudo vim /etc/sysconfig/iptables
iptables を以下のように編集します(diff 形式)。
-A RH-Firewall-1-INPUT -m state --state NEW -m tcp -p tcp --dport 80 -j ACCEPT + -A RH-Firewall-1-INPUT -m state --state NEW -m tcp -p tcp --dport 81 -j ACCEPT + -A RH-Firewall-1-INPUT -m state --state NEW -m tcp -p tcp --dport 8080 -j ACCEPT # -A RH-Firewall-1-INPUT -m state --state NEW -m tcp -p tcp --dport 20 -j ACCEPT
設定をロードします。
$ sudo /etc/rc.d/init.d/iptables restart iptables: ファイアウォールルールを消去中: [ OK ] iptables: チェインをポリシー ACCEPT へ設定中filter [ OK ] iptables: モジュールを取り外し中: [ OK ] iptables: ファイアウォールルールを適用中: [ OK ]
開放できていることを確認します(最初に restart したとき sudo をつけ忘れててしばらくハマりました><)。
$ sudo iptables -L ... ACCEPT tcp -- anywhere anywhere state NEW tcp dpt:81 ...
うん。OK。
念のためブラウザから接続できることも確認します。お手軽にたちあげられる Thin をポート81 で立ち上げます。
$ cd $RAILS_ROOT $ rvmsudo rails server thin -p 81
ここで http://gogengo.me:81 に接続したらちゃんと繋がりました。よしよし。
2.2. ポート81 で Nginx を動かす
では Thin の代わりに Nginx を動かしてみます。ポート81で起動した Thin は落としておきます。まずは Nginx をインストールします。
# CentOS 用に用意された Nginx の yum のリポジトリをつかう $ sudo rpm -ivh http://nginx.org/packages/centos/6/noarch/RPMS/nginx-release-centos-6-0.el6.ngx.noarch.rpm $ sudo yum install nginx ... Installed: nginx.x86_64 0:1.2.6-1.el6.ngx Complete!
Nginx の設定ファイルを開きます。
$ sudo cp /etc/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf.20121215
$ sudo vim /etc/nginx/conf.d/default.conf
/etc/nginx/conf.d/default.conf を次のように編集します。
server {
...
- listen 80;
+ listen 81;
...
}
これで Nginx を起動します。
$ sudo service nginx start
そして http://gogengo.me:81 にアクセスします。
うん。OK!
話が少しそれますが、ここでハマったのでそれについて書き残しておきます。ポート81 の設定にする際にどうすればいいか調べていたら /etc/nginx/sites-enabled/default や /etc/nginx/sites-available/default に server { listen 81; } と記述しておくといいよという文献がいくつがありました。そのとおりにやってみたのですがポート80 で接続しようとしてダメでした。結局、前述のように /etc/nginx/conf.d/default.conf に記述することでできました。今回つかった Nginx は 1.2.6 なので、バージョンによって場所が違ったりするのでしょうか。
2.3. ポート8080 で Unicorn を動かす
Unicorn はユーザ権限で動かすのでポート8080 を使います。まずは Unicorn をインストールします。Gemfile に `gem 'unicorn'` と書かれているとします。
$ bundle install --without development test
Unicorn を設定します。
vim $RAILS_ROOT/config/unicorn.rb
設定ファイルは次のように書きました。
# coding: utf-8 # ワーカーの数 worker_processes 5 # capistrano 用に RAILS_ROOT を指定 working_directory '/home/bekkou/projects/gogengo' # ソケット listen File.expand_path('tmp/unicorn.sock', ENV['RAILS_ROOT']) # ログ stdout_path File.expand_path('log/unicorn.stdout.log', ENV['RAILS_ROOT']) stderr_path File.expand_path('log/unicorn.stderr.log', ENV['RAILS_ROOT']) # ダウンタイムなくす preload_app true before_fork do |server, worker| defined?(ActiveRecord::Base) and ActiveRecord::Base.connection.disconnect! old_pid = "#{ server.config[:pid] }.oldbin" unless old_pid == server.pid begin # SIGTTOU だと worker_processes が多いときおかしい気がする Process.kill :QUIT, File.read(old_pid).to_i rescue Errno::ENOENT, Errno::ESRCH end end end after_fork do |server, worker| defined?(ActiveRecord::Base) and ActiveRecord::Base.establish_connection end
Unicorn を起動します。
$ cd $RAILS_ROOT $ unicorn_rails -c config/unicorn.rb -E production -p 8080 -D
http://gogengo.me:8080 にアクセスすると、Rails アプリとしてちゃんと起動が確認できました。
2.4. ポート81 で Nginx + Unicorn を動かす
いよいよ Nginx と Unicorn を連携させます。Nginx の設定を編集します。
$ sudo cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.20121215
$ sudo vim /etc/nginx/nginx.conf
次のように書きました。
user bekkou; worker_processes 1; events { worker_connections 1024; } http { # これがないと CSS が text/plain に解釈されて # レンダリング時に適用されなかった include /etc/nginx/mime.types; upstream backend { server 127.0.0.1:8080; # アプリサーバのポート番号を指定 } server { listen 80; server_name gogengo.me; # 待ち受けるドメインを指定 root /home/bekkou/projects/gogengo/public; access_log /var/log/nginx/access.log; error_log /var/log/nginx/error.log; proxy_connect_timeout 60; proxy_read_timeout 60; proxy_send_timeout 60; location / { if (-f $request_filename) { break; } # ファイルが存在しなければ Unicorn に proxy する proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_pass http://backend; } # これがないと静的ファイルが表示されない location ~* \.(ico|css|js|gif|jpe?g|png)(\?[0-9]+)?$ { expires 1y; add_header Cache-Control public; } } }
そして Nginx の再起動をします。
$ sudo service nginx restart
http://gogengo.me:81 にアクセスして Welcome ページから Railsアプリに変わることを確認します。
3. Thin を停止して Nginx + Unicorn で動かし直す
環境構築も大づめです。ポート81 からポート80 へつなぎ直します。
nginx.conf を編集。
http {
server {
- listen 81;
+ listen 80;
}
}
本番で稼働中の Thin を落とします。
$ cd $RAILS_ROOT $ sudo kill -9 $(cat tmp/pids/server.pid)
Nginx の再起動をします。
$ sudo service nginx restart
これで http://gogengo.me にアクセスしたときに nginx のログが出力されること、かつ http://gogengo.me:81 にはもう接続できないことが確認します。ダウンタイムはわずか数秒におさえられました。よかったよかった。
最後にポート81・8080を非公開にします。
$ sudo vim /etc/sysconfig/iptables
iptables を以下のように編集します(diff 形式)。
-A RH-Firewall-1-INPUT -m state --state NEW -m tcp -p tcp --dport 80 -j ACCEPT - -A RH-Firewall-1-INPUT -m state --state NEW -m tcp -p tcp --dport 81 -j ACCEPT - -A RH-Firewall-1-INPUT -m state --state NEW -m tcp -p tcp --dport 8080 -j ACCEPT # -A RH-Firewall-1-INPUT -m state --state NEW -m tcp -p tcp --dport 20 -j ACCEPT
IPテーブルを再起動します。
$ sudo /etc/rc.d/init.d/iptables restart
それぞれのポートが閉じていることを確認します。
$ sudo iptables -L
これでOK!! 長い戦いでした。
4. Nginx + Unicorn の環境で負荷テストを行い、数値を比較して感慨にひたる
さて。いよいよ Before/After の比較です。どれくらいパフォーマンスがあがったのでしょうか。結果はこちら。
| req/s | Σreq | Latency | Load Average | user | sys | io | Browsing | Stat |
|---|---|---|---|---|---|---|---|---|
| 10 | 6,000 | 0.11 | 0.8〜1.5 (t=..10) | 40 | 4 | 0 | 早い | OK |
| 15 | 9,000 | 0.11 | 1.1〜2.5 (t=..10) | 60 | 6.5 | 0 | 早い | OK |
| 20 | 12,000 | 0.13 | 1.8〜3.0 (t=..10) | 80 | 8 | 0 | 早い | OK |
| 25 | 15,000 | 0.13 | 2.2〜3.5 (t=..10) | 85 | 9 | 0 | 早い | OK |
うん。早い! 数値を比較してみましょう。下の表は、旧環境での数値です。
| req/s | Σreq | Latency | Load Average | user | sys | io | Browsing | Stat |
|---|---|---|---|---|---|---|---|---|
| 10 | 6,000 | 0.13 | 1 (t=4), 0.7 (t=5..10) | 40 | 4 | 0 | 早い | OK |
| 15 | 9,000 | 0.79 | 1.5 (t=3), 1.2 (t=4..10) | 46 | 8 | 0 | 普通 | OK |
| 20 | 12,000 | 10.07 | 1.3 (t=4..12) | 46 | 8 | 0 | 遅すぎ | OK |
| 25 | 15,000 | 18.56 | 1.3 (t=4..14) | 46 | 8 | 0 | 繋がらない | ※A |
※A … 多い時で 10% のリクエストが返ってこなくなりました
圧倒的に早くなっています。以前は返せなかった秒間25リクエストも難なくさばいています。これが複数インスタンスの力か!
数値を比較してみてると次のようなことがわかったり気になったりしました。
- Latency が圧倒的に短くなった。ブラウザからの確認したときも全てのケースでサクサクとブラウジングできた
- Load Average が高くなった。Load Average が高くなると Latency は長くなるのかと思ってました。そうではないのですね
- CPU の使用率があがった。あがり幅が大きすぎる…?
- 負荷テストの終わる時間が 10分と時間どおりになった。Thin のときは秒間20リクエストになってから 10分をオーバーした
- Load Average の変化の仕方が変わった気がします。Thin のときは最大に達したら少しさがってだいたい安定しました。Nginx + Unicorn では、テストをはじめてから数分すると Load Average が減ったり増えたりを繰り返しました。仕組みの違いから起きることなのでしょうか
ひとまず変更後より高負荷に耐えられるであろうという数値がでたので満足です。
どこまで耐えるのかの検証
とはいえ、今回の対応でどこまで耐えられるようになったのかが気になります。毎秒のリクエスト数を増やしてみました。秒間リクエスト数は 30 → 50 → 40 → 35 の順で変えてテストしました。
| req/s | Σreq | Latency | Load Average | user | sys | io | Browsing | Stat |
|---|---|---|---|---|---|---|---|---|
| 30 | 18,000 | 0.15 | 3.4〜4.5 (t=..10) | 85 | 9 | 0 | 早い | OK |
| 35 | 21,000 | 4以上 | 5.2〜5.6 (t=..8) | 90 | 9 | 0 | 遅すぎ | ※B |
| 40 | 24,000 | 5以上 | 3.3〜5.4 (t=3..5) | 50〜90 | 5〜9 | 0 | 遅すぎ | ※C |
| 50 | 32,000 | 20以上 | 1.0〜4.7 (t=3..5) | 1〜90 | 1〜9 | 0 | 繋がらない | ※D |
※B ... 数%のリクエストが返ってこなくなり Latency が増え続けていたので 8分ほどで中断。このまま流したらテストにかかる時間は十数分になりそうだった
※C ... 多い時で 5% のリクエストが返ってこなくなった。Latency が増え続けてこのままだと繋がらなくなりそうなのでテストは5分で中止
※D … 多い時で 90% のリクエストが返ってこなくなった。504 Gateway Timeout 発生。テストは5分で中止させた。Load Average や CPU使用率はすごく上がったりすごく下がったりを繰り返した
※ memfree はどのテストケースでも 300MB 程度を維持していた
素早くレスポンスが返ってくるのは秒間30リクエストまででした。対応前は秒間10リクエストまでだったので、さばけるようになったリクエスト数は約3倍ということになります。いい感じ! Unicorn のインスタンスは 5つなので、5倍の秒間50リクエストはさばけるようになるのかなあと思っていたのですが、そううまくはいきませんでした。設定がまずくてパフォーマンスが最大限に出ていないのかもしれません。もしくはこれくらいが普通なのかもしれません。どうなのでしょう。Rails側でもまだまだチューニングできそうな箇所があるので改善していけそうです。
からの
ここらで体力の限界がきたので今回はここまでにします。ぱたり。