担当でないWebサービスのパフォーマンスが遅いといわれたときに行う簡易的パフォーマンスチューニング


他部署の方から呼ばれて、「Webサービスが遅いけど、早くできない？」と聞かれたときに出来る対策を紹介します。
ソースコードも知らないし、環境も知らない場合の、ブロジェクト外の人が対策する場合です。

今回は、PHP環境でしたが、Railsとか他のところでも使えると思います。

# ステップ1: 聞き取り

* どこが遅いか、どこに不満を持っているか確認する
* サーバ環境を確認する。今回は以下でした。ここではバージョン伏せていますが、バージョンも確認します。
    * OS: CentOS
    * Webサーバ: Apache
    * データベース: MySQL
    * 言語: PHP
* ソースコードをもらう。これがないと対策取るのが遅れます
* 遅いと言われているサーバ環境に入らせてもらう。１次情報をあてにしましょう。
* 使用用途。更新頻度。更新が少ない場合はキャッシュを効かせたほうがいいとかがわかります。
* 想定するユーザ数。 １日の使用回数。 頻繁に使用する時間帯。Webサービスの規模感を探ります。

# ステップ2: NewRelicの導入

時間がないのでさっさとNewRelicを導入します。
今回は、PHPでMySQLだったので以下を入れました。

* APM(Application Performance Monitoring)
* Server Monitoring
* MySQL Plugin

以下のように良い感じで見ることができました。
１リクエストに4秒くらいサーバ処理でかかるというかなり遅い状況です。

![image](https://qiita-image-store.s3.amazonaws.com/0/30079/00e138f3-32ac-3abe-3512-8c16531cd8e0.png)

![image](https://qiita-image-store.s3.amazonaws.com/0/30079/7c1383f0-0a91-70d7-321f-9966d24a9940.png)

![image](https://qiita-image-store.s3.amazonaws.com/0/30079/2fe9a151-97d9-eb2c-75c9-7289382a8173.png)

# ステップ3: 遅いリクエストの特定

NewRelicでリクエスト単位で遅いリクエストを探します。
この時、「Slowest average response time」と設定するのですが、同時に「Highest Throughput」も確認して、
**遅くて、リクエストが多い**ものを重点的に対策します。
リクエストがたくさんこないものに関しては対策しても全体への効果が薄いためです。

![image](https://qiita-image-store.s3.amazonaws.com/0/30079/a7db9f63-4ce7-2829-b2ae-2dda78d5519f.png)

上記を見ると、１リクエストでMySQLにクエリが400くらいとんでます。
通常、数十クエリにとどめてほしいので、400はかなり多い方でそこで、2秒も使っている事がわかります。

# ステップ4: 遅いクエリの対策

MySQLのクエリ対策でなんとかなりそうなことがわかりました。
（こういう、そもそも数秒かかるリクエストというのは、だいたいクエリが遅いのが原因で、スクリプト側が遅いことはループをすごくしているとかないかぎりあまりないです）

MySQLのクエリがどうして遅いのかというのを調べるために、New RelicのMySQL Pluginの様子を見てみます。 


![InnoDB_Metrics_-_New_Relic_と___Kobito_XR7h9u_png__https___qiita-image-store_s3_amazonaws_com_0_30079_07a585e3-bd3c-1280-6b78-2be446f0da45_png__Kobito_XR7h9u_png__.png](https://qiita-image-store.s3.amazonaws.com/0/30079/e944364e-4ba8-f9d8-6b95-a1bf18359c9c.png "InnoDB_Metrics_-_New_Relic_と___Kobito_XR7h9u_png__https___qiita-image-store_s3_amazonaws_com_0_30079_07a585e3-bd3c-1280-6b78-2be446f0da45_png__Kobito_XR7h9u_png__.png")

数リクエストしか投げてないのに、Row Operationsが15kとか危ない匂いがします。
MySQLなので、my.cnfのmysqldのセクションにslowクエリとindexはってないクエリの調査をするために以下の設定をします。
ログの場所は適当にかえてください。

    slow_query_log=ON
    long_query_time=1
    slow_query_log_file="/var/log/mysql/slow.log"
    log_queries_not_using_indexes

その後、slowクエリをtailしながら、遅いクエリ、または、row operationsがおおいクエリを探します。

    tail -f /var/log/mysql/slow.log

今回は、2万行引いているクエリが十数回はしっていたので、そのクエリをMySQLでexplainします。
詳しいクエリはかけないのですが、サブクエリのなかでindexが貼っていないものがあったので、それを対策しました。

# ステップ4: クエリキャッシュをきかせる

使用用途を聞いたときに、更新頻度がひくそうだったので、MySQLのクエリキャッシュを効かせる事にしました。

# ステップ5: 対策結果の確認

だいぶ早くなりました。

![Kyozai_AWS_-_New_Relic.png](https://qiita-image-store.s3.amazonaws.com/0/30079/c7d45940-faf2-d5a9-d735-a56f4d18257f.png "Kyozai_AWS_-_New_Relic.png")

ただ、これでも発行しているクエリの数はかわらないので、以下のような対策をする必要が今後ありました。

* N+1クエリが多数でているので、対策する
* リレーションが貼ってあるレコードをカウントする処理が毎回あったりするので、親テーブルにカウンタを持たせる
* 今度はPHPが遅いかもなのでプロファイラなどで原因をさぐる

と、どこまでも出来ますが、この段階でサービスを使っている人に確認し、これで使用に耐えられるか確認します。
時間は有限なのであまりやりすぎても顧客満足がかわらないときがあるとつらそうです。

# おまけ: 負荷試験

さきほどまでのステップでひとまず素早く対策は出来たのですが、もう一歩先をいくために、負荷試験をしてみるといいと思います。
locustならさくっと負荷試験がかけます。簡単にやるのなら、トップからのリンクをざっと叩く感じのlocustファイルを作って実行してやるといいと思います。
pythonのpipでlocustをインストールして、Beautiful Soupとか入れて以下を実行すればトップ画面とそのaタグのリンクをランダムでたどってくれます。


    # -*- coding: utf-8 -*-
    from __future__ import absolute_import
    from __future__ import unicode_literals
    
    import random
    from locust import HttpLocust, TaskSet, task
    from bs4 import BeautifulSoup
    
    
    class ArticleTaskSet(TaskSet):
        articles = []
    
        def on_start(self):
            response = self.client.get('/')
            soup = BeautifulSoup(response.text, "html.parser")
            links = [e['href'] for e in soup.findAll('a')] + ['/']
            self.links = sorted(list(set([l for l in links])))
    
        @task
        def index(self):
            path = random.sample(self.links, 1)
            response = self.client.get(path[0])
    
    
    class WebsiteUser(HttpLocust):
        task_set = ArticleTaskSet
    
        # task実行の最短待ち時間
        min_wait = 1000
        # task実行の最大待ち時間
        max_wait = 1000