bekkou68の日記

開発しているサービス, IT技術, 英語など。

〜英語を楽しく学びたい全ての人へ〜
問題です。これは何でしょう? 【答えを見る】
preview
pre- + view
→ 前もって + 見る
→ 何かを前もって見る
→ 【名】下見, 試写会, プレビュー
語源のつぶやきをおとどけします:
iPhoneアプリができました!: 『おもしろ語源』

さくらVPS 上で Thin で動いている Railsアプリ Gogengo! を Nginx + Unicorn で動かすようにしました(JMeter 負荷テスト付き)

はじめに

Gogengo!さくらVPS 上で Thin のインスタンス 1つで動いていました。これだと重い処理のリクエストがきたら他のリクエストが待ちになってしまったり、大量の負荷がきたときに耐えられない恐れがあるので Nginx + Unicorn で動かしたくなりました。
Passenger ではなく Unicorn にした理由は 2つです。

  1. Passenger は動かしたことがあったけど Unicorn はなかった
  2. Unicorn はダウンタイムがないのがカッコいいと思った

さくらVPS で環境をつくったときの手順はこちらにまとめてあります。
対応する前に Unicorn の仕組みについて知るため『次世代RailsサーバーUnicornを使ってみた | TechRacho』を読みました。とても詳しくてわかりやすい文献です。
今回の対応の大半はミニ開発合宿でやりました。@satococoa さん、@netwillnet さんに沢山のアドバイスをいただきました。ありがとうございます!

ゴール

さくらVPS 上で Thin で動いている Gogengo! を Nginx + Unicorn で動かし直すことです。作業は本番環境でおこないます。作業中はダウンタイムが極力おきないよう心がけます。
また、Nginx + Unicorn の素晴らしさを確認するため環境の変更前後で負荷テストをして数値を比較します。負荷テストには JMeter を使います。

ゴールへ向けての大まかな流れ

次のような手順で作業を進めます。

  1. Thin の環境で負荷テストを行う
  2. Thin を動かしつつ Nginx + Unicorn をセットアップする
  3. Thin を停止して Nginx + Unicorn で動かし直す
  4. Nginx + Unicorn の環境で負荷テストを行い、数値を比較して感慨にひたる

環境

メモリ 1GB のさくらVPS です。OS は CentOS release 6.3 (Final) です。

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 をセットアップする

次のような手順で進めます。

  1. ポート81 を開放する
  2. ポート81 で Nginx を動かす
  3. ポート8080 で Unicorn を動かす
  4. ポート81 で 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!! 長い戦いでした。

メモ

Unicorn をダウンタイムなしで再起動するコマンド(参考文献)。

$ kill -USR2 <process_id>

Unicorn のプロセスID は $RAILS_ROOT/tmp/pids/unicorn.pid に出力されるので、以下のように実行するとスマート(2012/12/25 追記)。

$ cd $RAILS_ROOT
$ kill -USR2 $(cat tmp/pids/unicorn.pid)

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側でもまだまだチューニングできそうな箇所があるので改善していけそうです。

からの

ここらで体力の限界がきたので今回はここまでにします。ぱたり。

おわりに

ひとまず Nginx + Unicorn の構成を動かすことができてよかったです。インフラまわりが色々と勉強になりました。
さばけるリクエスト数が 3倍ほど高くなったので良かったです。低くなっていたらどうしようかとw まだまだチューニングできそうなので、そのうち勉強がてらやってみようと思います。JMeter も何となくで使っているのでもっと勉強せねば〜。
何か間違いなどありましたら教えてください。特に負荷テストの数値の見かたと考察が自信ないです。