Twitterアカウントと連携して犯罪係数測定という数年前の某アニメネタのn番煎じなのを作って( 犯罪係数測定 )、まあこれなら程々にしかアクセス伸びないだろうと思ってたらアクセスが殺到してサーバーがまともに動かなくなってしまったので、改善して安定化させるまでに取った手順を記録します。色々と試行錯誤してみましたがその中で効果が大きかったものをピックアップしていきます。
本番環境で動かしていますので実際のデータに基づいた検証ができているわけではなくあくまで体感でですが、TwitterをはじめとしてAPI連携を行うWebサービスを作りたい人たちの参考になれば(もしくはご指摘があれば)幸いです。
サーバーの構成について
旧サーバー(今でもDB用に使用中)
- さくらのVPS メモリ1G HDDプラン
- Ubuntu14.04
- Apache2
- Passenger
- Ruby on rails4
- MySQL
新サーバー
- さくらのVPS メモリ2G SSDプラン
- CentOS6
- Nginx
- Unicorn
- Ruby on Rails4
ネックはメモリ
1GB HDD→2GB SSDのVPSに
これまでさくらのVPS(1GBメモリ、HDDプラン)を利用していたのですが、やたらとサーバーが止まっては(cronで設定したコードで)自動再起動を繰り返すよう不安定さになってしまい、鯖落ち通知メールを見るたび僕の犯罪係数が上がってしまいそう(笑)になってしまいました。
SQLの発行回数減らしたり無駄なAPIリクエスト(今後別のTwitterアプリ作成を見越して冗長になってた)を無くしたり…とやってみましたが、焼け石に水で大した効果は上がらず。
ネックはメモリの制約からPassengerのWorkerを3〜(無理して)4くらいしか設定できないところにあるかなと感じ、このまま落ち着かないまま続くのも嫌だし思いっきり2GB SSDのプランでVPSを増設しました。
またHDDを全然使い切れてなかったのと最悪スワップアウトしてもSSDならIO早いしある程度は使えるんじゃ…との目論見で容量半減を受け入れてSSDを選択しましたw
結果的にはスワップアウトよりは設定変更時の再起動の早さやコマンド操作がサクサクと進むほうの恩恵が大きかったですが。
メモリが制約になる理由としましては、APIを使う関係上APIを投げた相手のサーバーからのリクエストを待たなければならず、どうしてもリクエストを処理する時間が長くなってしまいます。
特に今回のサービスの場合はAPIを数〜十数個くらい(なるべく同時並行にしてはいるものの)投げてから処理していますので、APIリクエスト中に当該リクエストのデータ等諸々を保持するためのメモリの占有時間ががどうしても大きくなってしまう→スループットを伸ばせないからだと分析しました。
ところでなんでクラウド使わないかですって?AWSとさくらのクラウドについてざっと調べた感じ、(最初の割引を除けば)最低維持費がメモリ2GBくらいのVPSよりも高くつきそうだからですw
しかもそれでスペックは1GBのVPS以下だったりして趣味で使う範囲では割高感が否めないですね
まずはWWWサーバー機能のみを新しいVPSに移管して、順位や友達との比較機能、(今後は過去との比較機能も実装しようとしているので)データベースに記録を保管しているのですが、移行がちょっと面倒なデータベースはとりあえず後回しにしようと思い、実行しました。
その結果、移管後の元のVPSはCPU・IO・ネットワーク無事どれも余裕が生まれました。
結構サーバーでネックになりやすいのはDBだとはよく言われますけど、使い方によっては逆にそうではないケースもあるようで今後も気をつけて見極めたいです。
UnicornのWorkerを多め、kill早めに
これまでApache+Passengerを使ってきたので、他のツールを試してみたいとNginx+Unicornにしてみました。(後で1workerで並列リクエスト処理可能なPassengerのほうが良かったかもしれないことに気付くのですが…w)
またしばらく使っているとメモリリークがあるのかメモリ使用量がだんだんと増えてきたので、とりあえずUnicorn Worker Killerを使ってみました。Worker数とkillするタイミングで色々と試してみたところ、Worker数が7で256〜512リクエストでkillする設定にしました。
またメモリ使用量基準でkillする場合、APIの同時リクエスト等でマルチスレッドのコードを書くとkillされた処理のを待ってるプロセスがタイムアウト→まとめてkillになり、パフォーマンスが激落ちするので注意が必要でした。
Disk IOが問題にならない程度にスワップを使う
スワップアウトは極力発生させてはいけないとはよく言いますが、メモリが不足している状況下ではそんな悠長なことは言ってられないもの。とはいえスワップを使い過ぎるとディスクのIO速度がネックになってWorker単体だけでなく全体で見てもスループットが落ちました。またCPU使用率を見ても10%ないしはそれ以上をスワップ関係に割かれることになり…DISK IOと合わせてもし過負荷認定されて制限食らうと怖いなと思うところ。
そこでVPSのコントロールパネルなどを見てDisk IOが極端に多くなっていないか確認しました。Disk IO の確認に関してはこの記事なども参考になりました。
その結果、自分の環境ではWorker数を9まで増やしてたところ7に減らしたほうが捌けるリクエスト数も体感で変わらずか増えて、レスポンスの早さも良くなってで改善されました。(これでもまだスワップは500MB程度ありますが、avgqu-szが5〜30→0.1になって%utilも30〜90%→一桁%に収まりました)
Nginxでリクエスト制限を設定
アクセスが集中している時に無尽蔵に受け入れると通常1秒以内に済むレスポンスで10秒くらい待たされたりしてよろしくないので潔く503を出すことにしました。
keep-aliveを見なおしてTCPコネクションの同時接続数を減らす
keep-aliveは1回のTCP接続で複数のHTTPリクエストを処理する機能です。結構次のリクエストまで待つべく60〜120秒まで持たせようとしている記事が多いですが、connection数が溜まることによるメモリの増加を防ぎたいので、今回はページ読み込みに付随する画像やCSS、JSを一度の接続で読み込むためだけに使いたいと考え5秒としました。また後述するlimit_connを負荷に即して設定しやすくなる副効果もあります。
Limit_req、Limit_connを設定
APIを大量リクエストするため時間のかかる解析処理と逆に軽量な画像やCSS/JSなどの静的ファイル処理、それ以外の処理(普通のページ表示)の主に3つに分けました。
リクエスト数の1秒あたりのrateで制限するとレスポンスまでの時間がかかる処理が偶々集中した時に中々繋がらなくなるので、connection数で制限をかけたほうが負荷に即していい感じでした。(ただしkeep-aliveをオフにするか数秒の場合に限ります。)
特にTwitterアカウントの解析処理ではTwitterサーバーの調子によっての変動や、各ユーザーのフォローid等を取得する場合は解析中のユーザーのフォロー数に応じてレスポンスまでの時間の変動が大きいためconnection数での制限をメインにしてrateは補助的に使用したほうがいいと考えました。最終的にはこんな感じにしました。(9割型connection数で引っかかります。)ちなみに静的ファイル以外にもTwitterログイン時の前処理や後処理、検索エンジンのクローラなどのbotからのアクセス…などなど、アクセス解析のPVには現れないリクエストはたくさんあります。ですので実際のPV数は170くらいでもrateは最低300くらいは必要だったりするので注意して考えて下さい。
静的ファイル | 解析 | その他 | |
---|---|---|---|
connection | 制限なし | 25 | 40 |
rate | 2000req/m | 40req/m | 500req/m |
静的ファイルをNginx完結で渡せるよう設定
1回のページのロードで大抵の場合は最低でもjs、cssファイルを1つずつ、また画像をページによっては数ファイルか(それ以上)読み込ませることになるはずです。静的ファイルは負荷は軽いとはいえページを返すより何倍も回数は多いもの。また静的ファイルのパスは{Railsのルート}/public/以下と一緒であるためRailsアプリケーションで処理させる必要はありません。Unicornも通さずnginxで直接返すよう設定しましょう。
設定結果
/etc/nginx/nginx.conf
(前略・だいたいデフォルト通り) http { (前略・だいたいデフォルト通り) keepalive_timeout 5; keepalive_requests 60; reset_timedout_connection off; client_header_timeout 6; client_body_timeout 6; proxy_connect_timeout 6; limit_req_zone $binary_remote_addr zone=staticperip:13m rate=300r/m; limit_req_zone $server_name zone=staticperserver:13m rate=2000r/m; limit_req_zone $binary_remote_addr zone=perip:10m rate=40r/m; limit_req_zone $server_name zone=perserver:10m rate=500r/m; limit_req_zone $binary_remote_addr zone=analyzeperip:5m rate=30r/m; limit_req_zone $server_name zone=analyzeperserver:5m rate=40r/m; limit_conn_zone $server_name zone=connection:20m; #gzip on; include /etc/nginx/conf.d/*.conf; }
/etc/nginx/conf.d/default.conf
server { (前略) #rssやapple-touch-iconはよく参照されるので無ければRailsを通さずに404を返す location ~ /(\.?rss|apple-touch-icon(.*).png)$ { return 404; } #解析は一番重い処理であるためlimitを別に設定 location ~ /analyze$ { 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://unicorn; limit_req zone=analyzeperserver burst=6 nodelay; limit_req zone=analyzeperip burst=1 nodelay; limit_conn connection 25; } #静的ファイル用設定 location ~ ^/(images|assets|uploads)/(.*) { alias /home/nk16/www/twi_analytics/public/$1/$2; limit_req zone=staticperip burst=20 nodelay; limit_req zone=staticperserver burst=100 nodelay; } #その他の設定 location @unicorn { 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://unicorn; limit_req zone=perserver burst=30 nodelay; limit_req zone=perip burst=5 nodelay; limit_conn connection 40; } }
で、結局どれくらいまでのアクセスに耐えられたのか
結果としては1分間あたりで
〜75PV 余裕
100PV前後 時々表示が遅くなりはじめる
100PV〜 アクセス制限で503が出始める
150PV 限界。でも1秒でレスポンスが返ることもあるし、遅くとも(解析以外は)5秒以内にはほぼ返る。
という感じになりました。多少の503を許容するのであれば1日でおおよそ5〜10万PVに対応できました。
サーバー移転前と比べると約3〜4倍にまで改善できました(移転前の設定がポンコツすぎただけかも…)
追々DBも移行して同居させればSQLのレスポンスが早くなる分1Worker当たりのスループットが上がるかもしれないけども、同時にネックのメモリを圧迫する考えると…どっちに転んでしまうかw
…ただこれだけ設定しても2年半前の自分は素のPHPで書いた仕組みとしては類似のアプリ、Twitter試験を作った時のほうが時間あたりのPV数はもっと捌けてましたw
Google Analyticsによると一時間で最大約8500PV(1分あたりで140PV以上)にまで達してたようで…メモリ1GのVPSを使ってた上に当時の自分は無知だったのでフレームワークも使わずApacheで適当に公開に必要な設定だけ書いてただけだったのですが…皮肉なもので。