Webフロント周りのパフォーマンスを計測・チューニングする

Github Pagesで公開しているポートフォリオサイトがそんなに色んな情報載せてる訳でもないのですが、遅くてあまり気持ちのいいものではないので、最適化していきます。

結論から書くと、2.87sぐらいかかっていた読み込みが1.57sぐらいになりました。 まだまだ細かいチューニングの余地はありますが、コストのわりに効果の大きそうなところを探って対処してみました。

  1. Chrome Developer Tools, Page Speed Insightsとかで原因を探る
  2. JavaScriptのminify
  3. 大きい画像はプログレッシブ形式のJPEGに変換
  4. ImageOptim
  5. CloudFrontでCache, 配信, HTTP/2対応

タイトルにはチューニングと書きましたが、基本的な最適化をちゃんとすればちゃんと表示速度が改善された感じです。

計測

まずは現状を確認

f:id:nwpct1:20161125170524g:plain

大きなヘッダ画像があるのでそこの読み込みが気になりますね。

Chrome Dev Tools

f:id:nwpct1:20161125170852p:plain

読み込み完了まで、2.87s。 グラフを見ると、bundle.js(このポートフォリオでは、Angular2 + TypeScriptを使ってます)とヘッダ画像が大きく特に読み込みに時間がかかっていることがわかります。

Page Speed Insights

Web Site: https://developers.google.com/speed/pagespeed/insights/?hl=ja

  • Mobile: Speed 56/100
  • Mobile: User Experience 97/100
  • PC: Speed 76/100

バイルの表示が特に遅いですね。 Google先生からのアドバイスは、

Web Pagetest

Web Site: https://www.webpagetest.org/

海外とかいろんな環境でのサイトのパフォーマンスをチェックできるらしい。 このページからのアドバイスは、

  • CDNを使う
  • 画像を圧縮する
  • 静的コンテンツをキャッシュする

最適化

JSのminify

まずは手軽で効果のありそうなところから。 webpackのhelpを見たら --optimize-minimize というオプションがあったので有効に。

webpack --optimize-minimize ./src/ts/index.js --output-filename ./public/js/bundle.js

f:id:nwpct1:20161125174452p:plain

  • bundle.jsのファイルサイズ: 592kB > 277kB
  • bundlejsの読み込み時間: 1.31s > 753ms
  • Page Speed Insights Mobile Score: 56点 > 59点
  • Page Speed Insights PC Score: 76点 > 87点

580msぐらい速くなりましたね。

ヘッダ画像の改善

プログレッシブ形式のJPEGに変換

905KBもあるのでこれも改善できそうですね。 JPEGには、左上から画像をパラっと読み込んでくれるベースライン形式と最初はモザイクが出るプログレッシブ形式があるみたいです。プログレッシブ形式のほうがユーザ体験は良いと言われているそうです。

最初のGIF画像を見てもらうと分かるのですが明らかにベースライン形式の挙動を示していますね。 rdjpgcomというツールで確認、imagemagickで変換出来るみたいです。

$ rdjpgcom -verbose img/eyecatches/autumn_leaves.jpg
JPEG image is 1500w * 1000h, 3 color components, 8 bits per sample
JPEG process: Baseline
$ convert img/eyecatches/autumn_leaves.jpg -interlace JPEG progressive.jpg
$ rdjpgcom -verbose progressive.jpg
JPEG image is 1500w * 1000h, 3 color components, 8 bits per sample

f:id:nwpct1:20161125180721g:plain

うーん、確かにモザイクが出るプログレッシブ形式ですね。ただユーザ体験がいいかというとなんとも言えない感じです。 よくなったのかイマイチ実感できないので、画像サイズを落としてみましょう。 画像サイズが小さくなって読み込みが早くなれば、わかりやすく快適になってくれそうです。

Optimで画像サイズを小さくする

Web Site: ImageOptim — better Save for Web

JPEG Miniは高いので、Optimを使って画像サイズを減らしてみます。 メタ情報やカラープロファイルを削除するらしいです。

  • 画像サイズ: 905KB > 817KB

まだ結構でかいですね。 この画像、1500x1000だったのですが、横幅1280pxぐらいが多いみたいなので思い切って減らしてみます。

  • 画像の読み込み時間: 1.47s > 988 ms
  • 画像のサイズ: 905KB > 626 KB
  • Page Speed Insights Mobile Score: 59点 > 63 点
  • Page Speed Insights PC Score: 変化なし

まだ少し重い気もしますが、横幅いっぱいに広げる画像なのである程度は仕方がないかもしれません。 別のところに手を付けます。

CloudFrontでCache, HTTP/2対応

Web Pagetestの結果では、海外からアクセスした際にレイテンシが大きいのか、CDNを使えとのメッセージがありました。 HTTP2のストリーム多重化で速くなることも期待して、AWS CloudFrontをかませてみます。 (余談: HTTP/2を使うかHTTP/1.1を使うか、どうやって判断するのか調べてみたんですが、TLSネゴシエーションの際にHTTP/2が使えるか確認してくれるみたいですね)

今回は検証目的なので、 http://c-bata.link は一旦そのままでこのURLをoriginにしてCloudFrontをかませます。 Webフォントをhttpで読み込んでいる部分があるのでそこを修正してCloudFrontのCacheをInvalidate後、何度かリロードする(Cacheさせる)と全体の読み込み時間: 1.57sまで上がりました。

f:id:nwpct1:20161126165720p:plain

グラフを見る限り、HTTP2のストリーム多重化がかなり効いているような気がしますね。

おわりに

最終的な表示はこのようになりました。 CloudFront対応はドメインと証明書の問題で、まだ http://c-bata.link のほうには反映していませんが、CloudFront経由後は読み込み速度が約半分の1.5s程度になり、GIFアニメーションの通り体感的なスピードも改善されたようです。

f:id:nwpct1:20161126170433g:plain

ハイパフォーマンスWebサイト ―高速サイトを実現する14のルール

ハイパフォーマンスWebサイト ―高速サイトを実現する14のルール

WEB+DB PRESS Vol.95

WEB+DB PRESS Vol.95

h2load, apache benchによる負荷テスト

前回の記事で触れたApache JMeterやGatlingとは違って、今回試したのはシナリオとかもなくHTTPのリクエストを大量に投げて、スループットやレイテンシを計測してみたい時に便利なツール。

実際に試したのは次の2つ。他にも、wrkやvmbench、siegeが気になったので試したらまた追記。

h2load

Command:

$ h2load -n 1000 -c 200 --h1 http://example.com/test.jpg
  • -n : 総リクエスト数
  • -c : 並行リクエスト数
  • --h1 : HTTP/1.1を使用

ab: apache bench

  • Installation: Apacheを入れたら、一緒に入ります (# yum install httpd)。
  • -t オプションがあって、時間指定ができる。

Command:

基本的には、h2loadと同じような

$ ab -n 1000 -c 200 http://example.com/test.jpg

並列 200で10秒間リクエストを送る

$ ab -t 10 -c 200 http://example.com/test.jpg

Sanicのベンチマークを試す

refs: GitHub - channelcat/sanic: Python 3.5+ web server that's written to go fast

Sanicというuvloopを使ったPythonのWebサーバ・フレームワークは、wrkを使ってベンチマークをとっています。試しにh2loadやabでも大体同じようなスループット、レイテンシになるのか試してみた。 全部試すのは大変なので、Sanicの負荷計測をしてみます。

環境は、sanicのREADMEに合わせて

  • AWS EC2 Medium
  • ubuntu
  • 1 process
  • 100 connections
  • Response Body: a small JSON response
$ sudo apt-get update && sudo apt-get upgrade
$ sudo apt-get install python3-venv
$ python3 -m venv venv
$ source venv/bin/activate
(venv) $ which pip
/home/ubuntu/venv/bin/pip

tornadoの負荷テスト

用意したtornadoのコードはこちらです。

import tornado.ioloop
import tornado.web
import json

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write(json.dumps({"hello": "world"}))

def make_app():
    return tornado.web.Application([
        (r"/", MainHandler),
    ])

if __name__ == "__main__":
    app = make_app()
    app.listen(8080)
    tornado.ioloop.IOLoop.current().start()

h2loadの結果

$ h2load -n 5000 -c 100 --h1 http://ec2-hoge.ap-northeast-1.compute.amazonaws.c
om/
starting benchmark...
spawning thread #0: 100 total client(s). 5000 total requests
Application protocol: http/1.1
progress: 10% done
progress: 20% done
progress: 30% done
progress: 40% done
progress: 50% done
progress: 60% done
progress: 70% done
progress: 80% done
progress: 90% done
progress: 100% done
finished in 2.52s, 1981.37 req/s, 412.14KB/s
requests: 5000 total, 5000 started, 5000 done, 5000 succeeded, 0 failed, 0 errored, 0 timeout
status codes: 5000 2xx, 0 3xx, 0 4xx, 0 5xx
traffic: 1.02MB (1065000) total, 761.72KB (780000) headers (space savings 0.00%), 87.89KB (90000) data
                     min         max         mean         sd        +/- sd
time for request:    35.61ms     89.74ms     49.25ms      3.17ms    94.78%
time for connect:    34.44ms     36.57ms     35.34ms       433us    65.00%
time to 1st byte:    70.52ms    125.97ms     97.61ms     15.36ms    59.00%
req/s           :      19.83       20.20       20.02        0.11    56.00%

スループットは、 1981.37 req/s で、1st byteの時間、つまりレイテンシやレスポンスタイムと呼ばれる応答があるまでの時間に関しては、こちらは平均 97.61ms でした。 sanicのページに書かれていたのは、スループット2138 req/s 、レイテンシが 46.66ms なのでスループットはわりと誤差の範囲に収まっている気がしますが、レイテンシは2倍近くの時間差があります。 試しにapache benchでも測ってみました。

apache benchの結果

$ ab -n 5000 -c 100 http://ec2-hoge.ap-northeast-1.compute.amazonaws.com/
This is ApacheBench, Version 2.3 <$Revision: 1430300 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking ec2-52-193-139-58.ap-northeast-1.compute.amazonaws.com (be patient)
Completed 500 requests
Completed 1000 requests
Completed 1500 requests
Completed 2000 requests
Completed 2500 requests
Completed 3000 requests
Completed 3500 requests
Completed 4000 requests
Completed 4500 requests
Completed 5000 requests
Finished 5000 requests
Server Software:        TornadoServer/4.4.2
Server Hostname:        ec2-52-193-139-58.ap-northeast-1.compute.amazonaws.com
Server Port:            80
Document Path:          /
Document Length:        18 bytes
Concurrency Level:      100
Time taken for tests:   3.788 seconds
Complete requests:      5000
Failed requests:        0
Write errors:           0
Total transferred:      1065000 bytes
HTML transferred:       90000 bytes
Requests per second:    1320.01 [#/sec] (mean)
Time per request:       75.757 [ms] (mean)
Time per request:       0.758 [ms] (mean, across all concurrent requests)
Transfer rate:          274.57 [Kbytes/sec] received
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:       34   35   0.5     35      41
Processing:    35   38   4.6     38      93
Waiting:       35   38   4.6     38      93
Total:         69   74   4.7     73     129
Percentage of the requests served within a certain time (ms)
  50%     73
  66%     73
  75%     74
  80%     74
  90%     75
  95%     77
  98%     80
  99%    100
 100%    129 (longest request)

スループットが、 1320.01 req/s と少し小さいようです。 レイテンシの方(Time per request)は平均 75.757ms とh2loadの結果と近い値がでました。

sanicのREADMEに書いてあるbench scoreと比べると、どちらもレイテンシの差が特に気になります。 よくよく考えると負荷をGCEのインスタンスからかけていたのですが、ネットワークの影響が大きいのかもしれません。 並列100個のリクエストをTornadoに送ったときのhtopを見る限り、MediumインスタンスのCPU・メモリリソースではまだまだ余裕があるので、 Mediumインスタンスの中から負荷をかけて、ネットワークの影響を減らしてみます。

Localhostで試してみる

$ h2load -n 5000 -c 100 localhost:8080
invalid URI: localhost:8080
ubuntu@ip-172-31-22-107:~$ h2load -n 5000 -c 100 --h1 http://localhost:8080
starting benchmark...
spawning thread #0: 100 total client(s). 5000 total requests
Application protocol: http/1.1
progress: 10% done
progress: 20% done
progress: 30% done
progress: 40% done
progress: 50% done
progress: 60% done
progress: 70% done
progress: 80% done
progress: 90% done
progress: 100% done

finished in 2.38s, 2099.97 req/s, 436.81KB/s
requests: 5000 total, 5000 started, 5000 done, 5000 succeeded, 0 failed, 0 errored, 0 timeout
status codes: 5000 2xx, 0 3xx, 0 4xx, 0 5xx
traffic: 1.02MB (1065000) total, 761.72KB (780000) headers (space savings 0.00%), 87.89KB (90000) data
                     min         max         mean         sd        +/- sd
time for request:     1.27ms    138.37ms     46.64ms      6.02ms    97.68%
time for connect:      643us      1.53ms       995us       227us    61.00%
time to 1st byte:     2.64ms    139.75ms     35.19ms     33.23ms    92.00%
req/s           :      21.00       21.66       21.43        0.16    76.00%

スループットが2099.97 req/s、2,138msとくらべてかなり近い値が出ました。 ただ、1st byteまでの時間が2.64msとsanicのREADMEに比べかなり速い結果になってしまいましたね... 一応apache benchも試してみます。

$ ab -n 5000 -c 100 http://localhost:8080/
This is ApacheBench, Version 2.3 <$Revision: 1706008 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient)
Completed 500 requests
Completed 1000 requests
Completed 1500 requests
Completed 2000 requests
Completed 2500 requests
Completed 3000 requests
Completed 3500 requests
Completed 4000 requests
Completed 4500 requests
Completed 5000 requests
Finished 5000 requests


Server Software:        TornadoServer/4.4.2
Server Hostname:        localhost
Server Port:            8080

Document Path:          /
Document Length:        18 bytes

Concurrency Level:      100
Time taken for tests:   3.250 seconds
Complete requests:      5000
Failed requests:        0
Total transferred:      1065000 bytes
HTML transferred:       90000 bytes
Requests per second:    1538.36 [#/sec] (mean)
Time per request:       65.004 [ms] (mean)
Time per request:       0.650 [ms] (mean, across all concurrent requests)
Transfer rate:          319.99 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       2
Processing:     1   64   8.6     66     168
Waiting:        1   64   8.6     66     168
Total:          2   64   8.5     66     169

Percentage of the requests served within a certain time (ms)
  50%     66
  66%     67
  75%     67
  80%     67
  90%     70
  95%     73
  98%     74
  99%     75
 100%    169 (longest request)

apache benchの方は、先ほどと同じくh2loadの結果に比べて少しかかった時間が長く、スループットも悪い結果となりました。 リクエストの総数を10倍の50000個にしても、レスポンスタイムやスループットに違いは見られませんでした。 ベンチマーカー側の性能の差でこれぐらいの違いが出てしまうのは仕方ないのかもしれない。

もうちょっとわかったことがあれば追記。 ここがおかしいとかあればコメントもお待ちしてます。

References

今回は試さなかったけど、siegeやvmbench、wrkが使われているのを見かけた。

WEB+DB PRESS Vol.95

WEB+DB PRESS Vol.95

Apache JMeterメモ

Website: http://jmeter.apache.org/

Apache JMeterを教えてもらったので、後で見返せるようにメモ。

Apache Software Foundationで開発されているオープンソースJavaアプリケーション Webサーバーに対して複数のHTTPリクエストを送信し負荷をかけたり、JDBCを経由してデータベースサーバーに負荷をかけたりできる

基本的な流れ

起動

最新のzipバイナリをダウンロード・解凍。

$ cd apache-jmeter-3.0/bin
$ java -jar ApacheJMeter.jar

スレッドグループ

f:id:nwpct1:20161118150902p:plain

  • スレッド数: 並列にリクエストを飛ばしたい数(同時アクセス)
  • ループ回数: 1人が10回リクエストを送る

サンプラー

リクエストを送るためのもの。 よく使うのはHTTP SamplerとJDBC Sampler.

f:id:nwpct1:20161118150925p:plain

  • Host: 192.168.45.10
  • Port: 80 (80の場合は省略可)
  • Path: /pukiwiki/

リスナー

負荷はもうかけられるが、結果を見るための設定がない。それがリスナー。

f:id:nwpct1:20161118150945p:plain

「結果をツリーで表示」と「結果を表で表示」をクリック。

  • 結果をツリーで表示からは、リクエストや応答データが見れる
  • 結果を表で表示は、ズラッと全部見たい時に便利
    • Connect Time: 接続時間
      • スレッド数1でループ回数を5とかにすると、keep-aliveが有効なおかげで2回目以降のリクエストのconnect timeが0になるのが確認できる。

リスナーは何回も実行すると溜まっていくので、「ほうき」のアイコンの「消去」で掃除できる。

実行

「開始」ボタンを押したら開始出来る。

シナリオキャプチャ

ブラウザで操作を記録させて、シナリオを簡単に作ることができる。

HTTPプロキシサーバを追加

f:id:nwpct1:20161118151004p:plain

f:id:nwpct1:20161118151023p:plain

  • Portは使っていない番号に設定 (ここでは8999)
  • 除外するパターンを .*.css.php.* と最後にも .* を付けているのは、クエリパラメータも含めるため

HTTPプロキシの設定

Chromeの「設定」>「詳細設定」>「プロキシ設定の変更」から、上で設定したポート番号を指定。

f:id:nwpct1:20161118151050p:plain

開始

「HTTPプロキシサーバ」>「開始」を押したら(ここで鍵がどうのこうのってメッセージが出たけどとりあえずOKで進めてみた)、Chromeを開いて検証したいようにブラウザで操作。

終わったら「HTTPプロキシサーバ」>「停止」

これで終わり。緑の「開始」ボタンをクリックすると実行してくれる。

その他

ユーザ定義変数

f:id:nwpct1:20161118151105p:plain

設定しておくと、次に127.0.0.1とかが出てきた時に、自動で ${PUKIWIKI_HOST} とかを埋め込んでくれる。 あとでこの変数のvalueを変えるだけで済んで便利。

HTTPリクエストの詳細設定

「Advanced」>「全てのイメージとアプレットを繰り返してダウンロードする」を選択すると、そこのページのCSSや画像も全部ダウンロードしてくれる。

スレッドグループの設定

スレッド数やループ回数以外にもいくつかオプションがある。 例えば、60秒間負荷をかけたい場合は、ループ回数を無限、持続時間を60にすればいい。

MySQL(MariaDB)の計測

Setup

CentOS7にMariaDBを入れる

$ yum update
$ yum install mariadb mariadb-server
$ systemctl start mariadb
$ systemctl enable mariadb

MySQL用のJDBCドライバーをダウンロード・配置

参考: 【jmeter】【MySQL】MySQLからデータを取得し、変数に格納する - たんたんめん日記

MySQL :: Download Connector/J から zipパッケージをダウンロードして展開、 mysql-connector-java-5.1.40-bin.jarapache-jmeter-3.0/lib に入れる。

1つのRDBに負荷をかける

f:id:nwpct1:20161118173948p:plain

f:id:nwpct1:20161118174006p:plain

f:id:nwpct1:20161118174021p:plain

うまくいくとこのようになる。権限周りでちょっとはまりましたが、「結果をツリーで表示」からレスポンスをみて修正。

複数のDBに対してロードバランス

レプリケーションをしていて、マスタや複数のスレーブに対してリクエストを分散させたい場合、

jdbc:mysql:loadbalance://<FQDN or IP address>:<port>,<FQDN or IP address>:<port>/<DB name>

JMeter Server

複数台のPCからリクエストを投げる。 下の記事の通りやったら問題なく動いた。 zipパッケージの中に jmeter-serverとかも最初から入ってて、j

複数台のJMeterサーバで負荷試験を行う方法 | Check!Site

f:id:nwpct1:20161118182140p:plain

テスト計画の保存

ファイルメニューからテスト計画を保存できる。 XML形式で保存されているので、構造もそれほど複雑ではない。 プログラムとかExcelマクロで生成できそう。

キャプチャ

JMeter Proxy Serverを経由することで、実際のユーザ−の行動からリクエストをキャプチャーしたりできる。

CUI

試してないけど、 *.jmx で保存して bin/jmeter.bat から実行出来るらしい。 自動化するならこっちのほうが大事そう。

おわりに

気が向いたら今度、グラフとかレポートがリッチらしいGatlingも使ってみたい。

詳解 システム・パフォーマンス

詳解 システム・パフォーマンス