Y-110's Wiki
負荷対策概論
_ 負荷対策概論
Webアプリケーションに負荷は付き(憑き)モノです。
負荷対策を行っていないため, 悲惨な目にあったプロジェクトを今までいくつも見てきました。*1
ここではそんな憂き目に遭わないよう, 今までの備忘録も兼ねて負荷対策ノウハウを書き残したいと思います。
私の経験上, Webアプリケーションの場合, 負荷対策は大きく分けて3つの面からアプローチできます。
- アプリケーション
- データベース
- サーバ
以下, それぞれの項目について, 負荷の原因となるケースとそれに対する対策の方針について解説していきます。
各フェーズにおける負荷対策の具体的な方法は, 個々のページを設けて解説する予定です。
ちなみに想定しているアプリケーション構成は, LAMP や LAPP といった典型的な Linux + Apache + MySQL(Postgres) + PHP といった構成です。
_ アプリケーションにおける負荷対策
アプリケーション面からの負荷の原因としては様々なものがありますが, 思いつくままに挙げてみます。
- 効率の悪いコーディングをしている
- キャッシュを利用していない
- アクセラレータを入れていない
_ コーディングの効率化
会社によっては PG初心者やバイト君をプロジェクトに参加させることがあるかもしれませんが, モノができても中身が「なんじゃこりゃ〜」というプログラムは結構あるものです。
無駄なループ処理をしていたり, 必要ないデータでメモリを無駄遣いしたり, 重い処理を毎回行っていたりと様々。
この場合は, 場面場面に応じた処置を行わないといけませんが, 原因を特定するために欠かせないのがプロファイリングツールです。
プロファイリングツールは, プログラムのどの処理にどのくらい時間がかかっているのかを調査するために使うツールです。
闇雲に中りをつけるよりは, まず最初にどの部分の処理に時間がかかっているのかを調査することが負荷対策の定石。
PHP の場合は, apdやxdebugといったフリーのプロファイリングツールがありますので, まずそれで原因の調査を行います。
重い箇所が特定したら, その部分を徹底的に調査して原因を明らかにし, 対策を行います。
⇒ xdebug + WinCacheGrind での簡単 PHP プロファイリング
まぁ, Webアプリケーションの場合 DB接続や SQL処理が重いのが殆どですが, こちらの対策はデータベースにおける負荷対策の項目に譲ることにします。
_ キャッシュの利用
画面表示を行う場合, 毎回重いプログラムを実行して表示していませんか?
動的で常に変化があり, 最新の状態を表示しなければならないページならしかたありませんが, Webアプリケーションでそういったページを求められるのはそれほど多くはありません。
掲示板やブログの記事, ショッピングサイトの商品リスト等, 一定時間は同じデータを表示しても問題ないページが, Webアプリケーションには必ずあります。
そういうページは, キャッシュを利用することで負荷が劇的に改善されます。
アプリケーションにおける負荷対策では, キャッシュの利用が一番効果があります。
逆に言うと, Webアプリケーションを如何にキャッシュできるように設計するか, ということが重要なのです。
キャッシュにはアプリケーションレベルでのキャッシュとブラウザレベルでのキャッシュの2通りあります。
- アプリケーションキャッシュ
プログラムの出力をファイルや DB などに保存しておき, 一定時間の間はプログラムを実行せず, キャッシュから読み込んで表示します。
PHP では PEAR::Cache, PEAR::Cache_Lite 等, 手軽にキャッシュ機能を実現できる PEARライブラリがあります。
⇒ PEAR::Cache_Lite でのお手軽 Caching
- ブラウザキャッシュ
ユーザが短時間に複数回アクセスするようなページの場合, アクセスの度にプログラムを実行して結果を返すのは効率が良くありません。
このような場合, HTTP/1.1 304 Not Modified を返すことで, ブラウザが保存しているローカルキャッシュを読み込ませます。
Webサーバからは実際に出力を送信しないため, クライアントへの転送量も僅かです。
PHP で Not Modified を返すまでの流れとしては, 以下のようになります。
- プログラムの出力において Last-Modified ヘッダを付けてクライアントへ返す
- 次回アクセス時, クライアントが If-Modified-Since ヘッダをつけて Webサーバへリクエストを送信する
- プログラムは, クライアントから送信された If-Modified-Since と出力ページの更新時刻とを比較する
- If-Modified-Since = 出力ページの更新時刻 なら 304 Not Modified を返してコンテンツを出力しない
- If-Modified-Since < 出力ページの更新時刻 なら 200 OK を返してコンテンツを出力する
アプリケーションキャッシュとブラウザキャッシュを併用することで, アプリケーションレベルでの負荷はかなり軽減されます。
_ アクセラレータの利用
Webアプリケーションを作成する際に使用する言語は, ほとんどがインタプリタ言語です。*2
インタプリタ言語は実行の度にプログラムを解析するので, プログラムソースが巨大であるにつれ, インクルードしているファイルが多くなるにつれ, 構文解析に時間がかかり実行時間が長くなってしまいます。
アクセラレータは, プログラムを解析した結果を中間コード(バイトコード)として共有メモリ上にキャッシュしておき, 2回目以降の実行はキャッシュされた中間コードを読み込むことで処理の高速化を実現しています。
PHP では以下のようなフリーのアクセラレータが公開されています。
フレームワークや PEARライブラリを利用する際, インクルードファイルが多数あることが殆どですので, アクセラレータの利用はほぼ必須です。
過去に経験した事例では, ピーク時にロードアベレージが 80程あった Webサーバにアクセラレータを導入した所, あっさり一桁まで下がったことがあります。
PHP でのプログラム構文解析が如何に重いかが伺えます。
_ PHP 5.1系の利用
PHP 5.1では Zend Engine が高速化されて, 反復処理などを含むプログラムの実行時間が大幅に短縮されます。
統計データの計算など, 巨大な配列のループ処理を行う場合等に効果があります。
オブジェクト指向言語としても PHP 4 よりしっかりしているので, 今後は PHP 5.1系での開発をオススメします。
参考:【PHPウォッチ】第22回 PHP 5.1ついにリリース,大幅な高速化を実現し重大なセキュリティ問題も修正
_ データベースにおける負荷対策
データベース面での負荷の原因の多くは効率の悪いSQLにあります。
データベースの負荷は, 重いSQL を改善することで殆ど解決します。
RDBMS に応じた適切なデータベースチューニング, レプリケーションもデータベースの負荷対策として有効です。
また, テーブル設計の段階で適切にテーブルを分割するなどして, テーブルのデータ数が増えすぎないように設計するのも重要です。
_ SQL の改善
テーブル結合の仕組みをよく理解しないまま SQL を記述すると, 得てして効率の悪い SQL を記述しがちです。
データベースの負荷テストを十分に行わないでリリースして, テーブルのデータ数が多くなるにつてページが重くなるパターンは往々にしてあります。
何はともあれ, この場合は重いSQL を特定しないことには先に進みません。
プロファイリングツールでプログラムの重い部分を特定して, SQL のあたりをつけることはできますが, 一番手っ取り早いのはデータベースの SQLログを解析することです。
PostgreSQL には pqa というログ解析ツールがあるので, これを利用すれば実行に時間がかかっている SQL が簡単にわかります。
MySQL は --log-slow-queries オプションをつけて起動して, 遅い SQL をファイルに出力するようにします。
ネックになっている SQL が特定できたら EXPLAIN文で実行計画と実行コストを調べ, インデックスが適切に使われているか等の調査を行い SQL を改善します。
重い SQL をひたすら改善していけば, データベースの負荷の殆どはなくなるでしょう。
ただ, LIKE演算子によるキーワード検索だけは SQLの改善をどう頑張っても限界があります。
LIKE演算子を用いた(前方一致を除く)WHERE条件指定はインデックスが効かず, シーケンシャルスキャンが走ってしまうからです。
この様なケースに対応するには, 全文検索システムを導入することで文書のインデックスを作成することができ, 検索効率が大幅に向上します。
MySQL なら MATCH ... AGAINST 構文, PostgreSQL は contrib以下にある tsearch2 でテキストの全文検索を行うことが出来ます。
これらはデフォルトでは日本語には対応していないので, 全文検索をするためには一工夫しなければなりませんが, 導入する価値は大いにあります。
_ テーブル分割
データベースのテーブル設計の段階において, 必ずと言っていいほどデータ量が増え続けるテーブルが存在します。
ショッピングサイトの注文データやユーザの入退会履歴を管理するテーブル等等。
このような情報を一つのテーブルで管理してしまうと, 日毎にデータ量が膨大になっていき収集がつかなくなります。
データ量が多くなると, インデックスの更新やシーケンシャルスキャンに時間がかかるようになり, データベースのパフォーマンスが低下します。
データ量が増え続けるようなテーブルは, 設計の段階でパフォーマンスを維持できる程度に分割して管理するようにします。*3
そうすることで, 検索・更新の両パフォーマンスを維持したまま, 膨大なデータを管理できるようになります。
PostgreSQL では継承を使って, テーブルパーティショニング*4の機能を実現できます。
また, 8.1 から導入された CE(Constraint Exclusion) を継承と組み合わせて利用することで検索効率を向上することもできます。
MySQL では MERGEテーブルを利用して, 同様のことが行えます。
_ 永続的接続
データベースへの接続処理は結構コストがかかります。
データベースへの接続を維持(コネクションプーリング)してその接続を使いまわすことで, データベース本来の仕事に回す CPU時間が増え, 単位時間当たりに実行できるクエリ数が多くなります。
過去の実験の結果では, PostgreSQL 7.4系において pg_connect と pg_pconnect での処理クエリ数の差は約3倍近くにもなりました。*5
ただ, 永続的接続の場合は接続を維持したままになるので, その分メモリを消費します。
大雑把に言うと, データベースプロセス × 接続数分だけメモリを消費します。
これに TCP接続で使われるバッファ領域等も加わるため, メモリに余裕がないサーバではスワップを起こして逆に性能が低下してしまう危険があります。
永続的接続を行う場合は, 接続数とメモリサイズ・使用量に注意を払ってください。
_ データベースチューニング
ある程度規模が大きいサイトになってくると, データベースチューニングも重要です。
チューニングの勘所は個々の RDBMS によって異なります。
PostgreSQL なら shared_buffers や effective_cache_size, MySQL なら key_buffer や query_cache_size 等。
最初は設定ファイルの推奨デフォルト値に設定して, 運用しながら調整していくのがよいでしょう。
I/O パフォーマンスにも関係してきますが, PostgreSQL 8.0 からはテーブルスペースがサポートされ, テーブルを複数のディスクに格納でき I/O の分散ができるようになっています。
頻繁にアクセスされるテーブルをそれぞれ別ディスクにおくことで, データベースのパフォーマンス向上が計れます。
_ レプリケーション
よほどのスーパーマシンでない限り, 1台のサーバの処理性能には限界があります。
規模が大きくアクセス数が多いサイトだと, スーパーマシンでも辛いかもしれません。
1台で無理ならレプリケーションを行ってデータベースを複数台のクラスタで構成し, 参照負荷を軽減します。
また, レプリケーションはリアルタイムバックアップにもなるので, 万が一データベースサーバに障害が発生してもダウンタイムを最小限に抑えることができます。
MySQL にはレプリケーション機能はデフォルトで備わっていますし, PostgreSQL なら pgpool や Slony-I といったレプリケーションツールがあります。
レプリケーションで負荷分散を行う場合は, 以下の点に注意する必要があります。
- 同期 or 非同期
- レプリケーションツール独自の制約
レプリケーションが同期でなければならないか非同期でもいいのかは, Webアプリケーションの仕様によります。
ページによっては同期でなければならない場合もあるでしょうし, ショッピングサイトの商品リストのように, 非同期でも問題ない場合もあるでしょう。
まずは, Webアプリケーションの性質を考慮した上で, レプリケーションツールを決定します。
また, レプリケーションツール独自の制約が設けられている場合があります。
例えば pgpool では SERIAL や CURRENT_TIMESTAMP のレプリケーションが保証されません。
事前に Webアプリケーションで使用するレプリケーションツールを決定し, その制約をしっかりと理解した上でコーディングに取り掛かりましょう。
さもないと後から大きな変更を迫られる可能性があります。
_ サーバにおける負荷対策
それなりのスペックのサーバ*6であれば, アプリケーションとデータベースの負荷対策をきっちり行えば, サーバで問題になることはあまりありません。
が, アプリケーションやデータベースでの対応だけでは負荷がどうにもならない場合, お金にモノを言わせてスーパーサーバを購入したり, サーバの台数を増やしたりすることも, 立派な負荷対策の方法です。
重要なのは, Webサーバ・DBサーバ等のサーバ種別毎の適切なスペックの見積もり, スケーラビリティを実現できるようなアプリケーション構成です。
_ スケールアップ
CPU は速いほうがいい, メモリは多い方がいい。 (NO LOAD)
しかし, 単純にハードウェアを強化すればいいわけではありません。
サーバのどの部分がネックになっているのかを見極めた上で, スケールアップを行います。
CPU にネックがあるのにメモリを増やしても意味がありません。
vmstat, iostat, top等のコマンドでサーバの状態を観察し, どの部分にボトルネックがあるのかを特定します。
CPU がほぼ 100%に張り付いていたら, CPU のクロック数や CPU 自体の数を増やせばいいし, メモリがスワップしていたらメモリの追加を行います。
スケールアップする際は停止時間が発生しますので, 予め停止時間を考慮しておきましょう。
_ スケールアウト
スケールアップだけでは, ユーザが増えたときにどうしても性能面で限界があります。
小規模なサイトだったら Webサーバ, データベースサーバを1台で運用しても問題ないかもしれませんが, 多く場合, ユーザが増えることを想定しているはずです。*7
1台で処理を捌けないなら複数台で処理の分散を行えるように Webアプリケーションを設計します。
スケールアウトを行うと, 理論的にはサーバの台数分だけリニアに性能が向上します。
Webアプリケーションでのスケールアウト対象のサーバは, 主に Webサーバとデータベースサーバです。
サービスによっては, メールサーバをスケールアウトする場合もあります。
Webサーバのスケールアウトは主に2通りの方法があります。
- ラウンドロビンDNS
- ロードバランサ
ラウンドロビンDNS は DNS の Aレコードに複数の IPアドレスを登録することでアクセスの分散を行う方法です。
これはお手軽に負荷分散できる反面, アクセス分散のポリシーを制御できない, サーバに障害が発生した際のサーバの切り離しに時間がかかる*8などの問題があります。
一番の問題は, 携帯端末からのアクセスは決まったゲートウェイを介すため, ゲートウェイが IPアドレスをキャッシュしてしまい, ラウンドロビンによる負荷分散がうまく行われないことです。
ロードバランサはラウンドロビンDNS の問題を全て解決してくれるのですが, 市販のロードバランサ*9はかなり高価です*10。
市販のロードバランサには手が届かない場合は, Pound や Apache 2.2 の mod_proxy_balancer といったフリーのロードバランサツールを導入する方法もあります。
1つの中規模案件の Webサーバのバランシングは Pound 等で十分です。
市販のロードバランサの導入は, 複数案件をまとめて管理するような自社ネットワークの構築の場合などに検討するとよいでしょう。
データベースサーバのスケールアウトは先に述べたように, レプリケーションを行い参照負荷を軽減できるような構成にします。
pgpool のように, レプリケーションツール自体が負荷分散を行える機能を持っているものもあります。
スケールアウトを行うことで, 処理性能の向上だけでなく高い耐障害性も実現でき, 一石二鳥なのです。
_ I/O パフォーマンスの向上
データベースサーバのようなハードディスクに頻繁に読み書きするサーバの場合, I/O パフォーマンスが重要になってきます。
I/O 処理に時間がかかると, I/O 待ちの割り込み不可プロセスが溜まって全体の処理性能が著しく低下します。
I/O 性能を向上するには以下の方法があります。
- ハードディスクを IDE から SCSI に変更する
- ハードディスクの回転数を上げる
- RAID 構成にする
- データを複数ディスクに分ける
一般に, ハードディスクは IDE より SCSI の方が, 回転数が大きい方が高速にアクセスできます。
また, RAID 1+0 や RAID 5*11 構成にすることで, ハードディスクの読込み性能を向上させることが可能です。
RAID構成にする場合, RAIDコントローラのキャッシュも I/O 性能に大きく関わってきます。
大抵の RAIDコントローラは, データがキャッシュに書き込まれるとすぐに書き込み完了の通知を返すライトバックと呼ばれる方式ですので, キャッシュ容量が大きければ書き込みパフォーマンスが上がります。
また, 読み込み時もキャッシュを経由しますので, 頻繁に読み込まれるデータはキャッシュに常駐して読み込みパフォーマンスも劇的に向上します。
RAIDコントローラのキャッシュは大きければ大きいほど I/O パフォーマンスは向上するので, 予算に余裕があればキャッシュを積めるだけ積みましょう。
データを物理的に複数ディスクに分けて I/O を分散させることでも I/O パフォーマンスの向上を計れます。
この場合は, アクセス頻度が高いデータをうまく分散させてやる必要があります。
_ Webサーバの役割分担
Webアプリケーションは全て動的なページで構成されているわけではありません。
静的なページや, 画像や動画等を表示するページもあるでしょう。
こういった静的コンテンツへのリクエストに対して, たくさんのモジュールを読み込んでいる重い Apacheプロセス*12で応答を返すのは, 処理速度やメモリリソースの面から見ると効率が良くありません。
動的なページは Apache + PHP で, 静的なページや画像等は lighttpd 等の高速・軽量 Webサーバでレスポンスを返すようにすれば, 処理速度の向上とメモリリソースの有効活用が可能です。
ただ, 2つの Webサーバを1つのサーバの同じポートで動作させることはできないため, ポートを変えて起動するか別サーバで動作させなければなりません。
無論, 自分が担当したプロジェクトも含まれます:-)
Java?なんですか?それ
想定されるデータ数をきちんと考慮した上で, 適切な数にテーブルを分割します
テーブルを複数に分割しますが, アクセスする側からは一つのテーブルとして見えます
MySQL では永続的接続と非永続的接続との間にそれほど差はなかったような気がします
曖昧な定義ですねぇ・・・ ^^;
想定していても実際に増えない場合も多々あるのですが
DNSの変更が伝播されるのはそれなりに時間がかかります
F5 Networks社の BIG-IPシリーズや Foundary社の ServerIronシリーズ等
販売する代理店やサポート条件にもよりますが, 1台200〜400万くらいはします
RAID 5 は聞く人聞く人からいい噂を聞きませんが・・・
ここで言う“重い”とは, モジュールを読み込んでサイズが大きくなっている httpd のことです