シーマークすずきです.
最近,つけ麺にハマっています.自分的には約7年周期でつけ麺がマイブームになる,という事実...
さて,性能診断分析サービスの事例,第3段となります.今回は,性能向上です.
■サイト利用者から「ページが遅い」の声
あるコンシューマ向けWebサイトを運営しているお客様の事例です.その会社の事業部の責任者は,「ページが遅い」というサイト利用者からの意見が多くなってきていることを課題の一つとして認識していました.実際に,自分でブラウザで試してみても,時々詰まったような遅いページがあったり,特定のページが極端に遅い,という経験を実際に体感していたのです.サービス責任者としては,お客様のニーズに応えることが最も重要です.Webサイトとしてニーズに応えることに加えて,リーズナブルな応答時間内に,という性能観点の目標もあるのです.そこで,サービス責任者は「ページが遅い」というサイト利用者からの声に対して何らかの改善策を具体的に検討することになりました.
■暗中模索
まず,開発パートナー会社さんとインフラを提供しているインフラ会社さんに現行システムの性能向上のための施策案を検討してもらいました.しかし,開発パートナー会社さんからは,簡単に云えば「マシンが遅いからだと思う」や「DBが遅いから仕方ない」とか「デザインが重いから」という様なポイントが出てきて,インフラ会社さんからは,「アプリが遅いからどうしようもない」とか「DB側は遅いSQLが無いので問題ではない」という指摘が出てきてしまい,お客様のサービス責任者の方も困ってしまいました.「取り合えず,自分たちじゃない」という反応だったからです.現行システムを請け負っている会社としては,仕事を増やしたくないとか責任範囲外なので,ということをお客様に伝えたい気持ちがあるのでしょう.一種の自己防衛本能です.
サービス担当者は,暗闇の中で何かを探すような感じで,手がかりが分からないままに「手探り」状態でなんとかして問題を解決しなければなりません.そんな文字通り暗中模索の頃にお客様からご連絡を頂きました.
■まずは現状把握
まずは状況を正確に把握しなければ,対策案もでてきません.そこで性能診断分析サービスのアプリケーションの挙動を数値で計測する技術が活きてきます.本番環境で数値で計測した結果は何よりも説得力があります.仮に改善内容の方法にアプリの改修が絡んだ場合でも,開発パートナー会社さんも計測した数値で説明すると,「なるほど,これは改修の効果が期待できますね」ということでご納得してタスク化してもらえます.逆に,開発会社様からは,「本番サービス環境下では,このメソッドがこんなに時間がかかっているのですね.意外だなぁ.」と新たな発見をしてもらえることもあります.ともかく,本番サービス環境のアプリケーションの挙動を数値で計測することから作業を開始しました.
■課題は何か?
次は,課題は何か?です.お客様のWebサイトにはたくさんのメニューやコーナーがあります.まずは,そのメニューやコーナー事に時間毎応答回数や平均応答時間をまとめました.予めお客様から頂いていた,このWebサイト内のビジネス上の重要度が高いメニューやコーナー,重要シナリオ(ページ遷移)と見比べてみると,課題が明確になってきました.どうやら,以下の2つが優先的に対処すべき課題でした.
- 検索メニューの応答性能が10秒以上になることが多々ある.
- お申し込みメニューの平均応答時間が遅い.
サイト全体のプログラムを分析するのはそれはそれで時間と手間がかかりますが,ある程度絞って注力することで,深い診断と分析が可能となり,結果としてスピーディに成果を得ることができます.
お客様と相談し,改めて今回の課題に対する目標は,
- 検索メニューの応答性能が悪化する原因を特定し,対策案を検討し,提案する.目標は平均処理時間30%短縮.
- お申し込みメニューの平均応答時間を20%短縮.
となりました.
■処理内容の詳細分析
本番サービス環境で実際に稼動しているアプリーケーションの稼動状況を映し出すことにより,様々なことがわかってきます.
まずは,「検索メニュー」の方のリクエスト状況の分析です.特に,平均応答時間だけで見ているとスパイクして遅い応答リクエストが見えにくくなります.つまり,リクエスト回数が多いサイトでは,リクエスト数である分母が非常に大きいため,平均することで,時々発生している遅いリクエストが見えなくなってしまいます.性能診断分析チームが分析する際には,「平均値とは丸められた値である」ということを強く意識し,その計測区間の最大値(その計測区間内で一番遅かったリクエスト応答時間)も一緒にプロットするようにします(下図).こうすることで,実態が見えてきます.実際には10秒以上の応答時間だったリクエストは多発していることがハッキリしました.
更に,一回の検索リクエストの処理内容の内訳を以下の様に横軸が時間の図化することで,どのような処理に時間がかかっているのかが分かります.
この見える化は強力です.ここの事例とは別のお客様の例では,Webの1ページの画面を処理するために,アプリケーションから同等SQLが300回実行されている実態がこの図化によって明らかになったこともありました.一回のSQLが50ミリ秒だとしても,300回×50ミリ秒で15000ミリ秒,つまり,15秒です.しかも,よくよく分析してみると実は,1回のSQLで十分だったいうことがありました.既存のDAOコンポーネントを部品化した開発を行い,ループ中からDAOを呼出す作りだったのです.SQLは”表”で結果が取れるにもかかわらず...インフラ側のDB管理者は遅いSQLはチェックしていることが多いですが,流石に1アクセスの処理で大量にSQLを発行しているのか,複数リクエストにより1発づつ発行されているかは意識していません.
さて,話を戻します.図中のsearch()が横に細い時は全く問題ありません.しかし,横に長くなるケース,要は,処理時間が延びてくる現象が確認されました.塵も積もれば山となります.図から推察するに,search()はループ中で必要な処理回数呼ばれる様です.図では80回前後呼び出されている絵です.search()メソッドが10ミリ秒なのか20ミリ秒なのかは,高々10ミリ秒の差ですが,比率では2倍の差になります.つまり,大量に発行されている場合は,全体が3秒だった処理が2倍の6秒になる訳です.しかもその呼出回数が多数であるならば,さらに時間がかかります.遅くなっている原因は,カットオーバー後,取り扱いデータ量が増え,search()メソッドが順次遅くなってきているためでした.開発側と相談し,該当ソース箇所をレビューした結果「四重ループ」が確認されました.
■多重ループに注意
プログラムでループ処理を書くことは簡単です.もちろん,ループ処理の中でループを書くこともできます.更に,一番内側の処理で別のメソッドを呼出し,呼出したメソッドの中で別のループを実装する.簡略化すると以下の様なコードでした.
※実際のコードはもちろん,もっと処理内容があります.
このsomeMethod()メソッドのコードは四重ループです.二重ループの内側で二重ループを持つメソッドを呼び出しているのです.性能的に危険なループです.経験豊富なプログラムならば臭うことでしょう.
上の簡略ソースの変数outer,innerのいずれも10ならば1ミリ秒以下.あっという間です.
outer,innerいずれも100として,私の作業PC上でsomeMethod()メソッドの応答時間は50ミリ秒程度でした.これも問題ないです.
outer=100,inner=1000ならば,6秒程度.ちょっと遅い.
outer,innerいずれも1000になれば,10分は返ってきません.しかも,その間ハードウェア的に1コアはCPU100%になります.
そもそも変数outerやinnerが10000になる可能性があるならば,設計段階で実現不能なロジックとして除外しなければなりません.ブラウザーで検索ボタンを押してから,10分待っている利用者はいないでしょうから,outer/innerが1000でもWebアプリケーションとしては既に現実的とは云えないでしょう.
少し脱線しますが,forループやwhileループとして見えないループもあります.再帰呼出です.再帰呼出で有名なフィボナッチ数列を例にします.
フィボナッチ数列のJavaプログラムは,以下の様な再帰呼出が典型的な実装コードです.
さて,質問です.Nがいくつぐらいまでが現実的でしょうか?最近の高速なCPUを搭載したマシンでもN=50でfibonacci()関数は現実的な時間で終了しません.高々N=50です.forループは1個しか見えないのに.
話しを戻します.多重ループは指数的に処理時間が延びることを理解しなければなりません.事例では,四重ループの内側の処理効率を見直して改善することで劇的に応答時間が改善しました.
■チューニングの鉄則
プログラムをチューニングする際の鉄則は,チューニング効果の高い箇所から手を入れる,ことです.例えば,全体が10秒のリクエストであれば,その処理時間の内訳が大きい箇所をチューニングするのが効果を得やすい訳です.元々1秒しかかかっていないメソッドを一生懸命チューニングして処理時間を半分にしても,0.5秒しか短縮できませんが,元々8秒かかっている場所を半分にできれば,4秒の短縮なのです.
性能診断分析サービスの性能向上を目的とした現場の場合は,「もっとも少ない工数で,もっとも高い効果」が得られる対策案を提案しなければなりません.そのためには,どうしても本番サービス環境でアプリケーションの挙動を数値で計測する必要があったのです.
■もうひとつの課題
「お申込みメニューの20%短縮」の方です.Webサイトの場合,検索して,購入する,又は,申し込みをする,ことが一般的です.利用者に検索してもらうのは,次のアクションとして購入や申し込みをしてもらうためです.一般にWebサイトの最重要機能は「申し込み」と云っても過言ではないでしょう.注文も申し込みの一種です.最近だと「申し込み」の後に「共有」することも大切な機能になってきているようです.つまり,SNSなどに購入した事をツブヤき,お友達と買った事実を共有するという機能です.このサイトは共有はありませんでしたが,「申し込み」機能は重要な機能でした.直接,売上に影響するトランザクションです.この画面遷移を詳細にモニターしました.
申し込みの一連のリクエストの前後で大量のXMLファイルのI/OとXMLパース処理が見つかりました.開発会社様に確認すると,マーケティングデータの収集のための処理でした.それにしても,ファイルから都度読みだしているし,しかも,XMLパースを毎度行っています.その後,更にバックエンドにXML電文を作成し,http通信でそのXMLを数回投げています.応答もXML形式で返ってきて,その返信をパースして解釈後,ログ記録用XMLを作成してログ出力していました.
ディスクからの読みだしはOSキャッシュに載れば性能はある程度確保できますが,ディスクへの出力は極力避けたい訳です.詳しく出力ファイルの場所を確認すると,悪い事に,NFS上への出力でした.高負荷でないWebサイトなら特に問題ではありません.しかし,高負荷サイトの場合は気にしなければなりません.複数台のWEBアプリケーションサーバが水平分散できないからです.また,NFSサーバの障害で,全てのWEBアプリケーションサーバに影響をおよぶことも問題なのです.言い換えれば,ビジネスの停止になってしまうリスクがあるからです.
以下に挙げる2つの主な対策案を提示しました.
1)WEBアプリのファイル出力はローカルファイルへ.
開発会社とインフラ会社が別々だと,インフラ会社さんの運用の手間が楽な方の設定にされてしまうことがあります.設定ファイルの変更で入力パスを変更可能な場合,多くの場合は,一か所の変更で反映できる,という理由からNFS上に置かれる事が多々あります.性能的な観点は全く考慮されません.入力ファイルはNFS上に置き,出力ファイルはローカルディスクに設定変更を提案しました.
2)キャッシュの利用
バックエンドに数回投げているXMLのうち,数回は履歴を紹介する電文でした.これは「履歴」ですから,過去の記録です.後から追加はありますが,変更される内容ではありません.従って,キャッシュが効果的に機能します.memcacheやredisなどのKVSを利用し,オブジェクトをキャッシュする仕組みをご提案しました.キャッシュ機能は機能的には単純です.後から機能を追加することは比較的容易です.効果は非常に高いです.
■目標達成.更にもっと.
ご提案した内容を対策後,後日,本番環境で効果測定を行いました.2つの課題箇所の両方とも目標を達成していました.お客様も喜んでいました.喜んでいるのも束の間,応答時間が速くなることを知ってしまったため欲が出たのでしょう.「更にもっと速くしたいのですが,どうすればよいでしょうか?」と相談を受けました.文章が長くなってしまったので,今回はここまでとします.次回は高負荷,高性能を実現するためのサーバ側の対策案についていくつかご紹介していきたいと思います.