curl を利用してウェブサーバー側の処理時間を推計する
curl を利用してウェブサーバー側の処理時間を推計しようというのが今回のテーマです。
前回の記事 ウェブでのリバースプロキシ導入目的は消費メモリ量を抑えるため ではこのスクリプトを利用してメジャーなサイトの速度を計測しました。
以下の例は前回の記事の test.php に対して計測スクリプトを実行した結果です。
[root @st-webclient001 ~]# ./check-speed.sh 'http://10.1.0.2/test.php' response_size: 36263 bytes msec_namelookup: 0 msec msec_connect: 50 msec msec_ssl_handshake_overhead: 0 msec msec_client_transfer_request(estimated): 25 msec msec_server_generate_response(estimated): 151 msec msec_server_transfer_response(estimated): 76 msec msec_total: 302 msec
curl の -w オプション
curl には -w, --write-out <format>
というオプションがあります。
format の部分では、curl の変数の使うことができ、これを利用して curl の各ステップでかかった時間を表示することが可能です。
例えば、下記のような使い方です。
# curl -s -o /dev/null -w 'code:%{http_code} size_download:%{size_download} time_total:%{time_total}\n' "https://www.google.co.jp/" code:200 size_download:10897 time_total:0.502
今回使うのは下記の変数です。
size_download The total amount of bytes that were downloaded. time_connect The time, in seconds, it took from the start until the TCP connect to the remote host (or proxy) was completed. time_namelookup The time, in seconds, it took from the start until the name resolving was completed. time_pretransfer The time, in seconds, it took from the start until the file transfer was just about to begin. This includes all pre-transfer commands and negotiations that are specific to the particu- lar protocol(s) involved. time_starttransfer The time, in seconds, it took from the start until the first byte was just about to be transferred. This includes time_pretransfer and also the time the server needed to calculate the result. time_total The total time, in seconds, that the full operation lasted. The time will be displayed with millisecond resolution.
ざっと挙げると下記のとおりです。
- size_download - ダウンロードしたバイト数
- time_namelookup - 開始から DNS 名前解決完了までの時間
- time_connect - 開始からリモートホストへの TCP コネクションが完了するまでの時間
- time_pretransfer - 開始からデータ転送開始までの時間
- time_starttransfer - 開始から最初の 1バイトが転送されるまでの時間
- time_total - トータルの時間
これだけだと意味がわかりません。
tcpdump の結果と比較
tcpdump の結果と比較した限りでは下記のとおりです
- size_download - ダウンロードしたバイト数
- time_namelookup - 開始から DNS 名前解決完了までの時間
- time_connect - 開始から TCP 3ウェイハンドシェイクの SYN -> SYN/ACK -> ACK の 3つ目の ACK を送信した瞬間までの時間
- time_pretransfer - 開始からクライアントが HTTP リクエストを送信する瞬間までの時間
- time_starttransfer - 開始からクライアントがサーバー側からの最初の 1バイトを受信した瞬間までの時間
- time_total - 開始から、TCP コネクション終了に FIN をクライアントが送信した瞬間までの時間
[root @st-webclient001 speed-tools]# ./check-speed.sh 'http://10.1.0.2/test.php' ---- curl raw variable values ---- code: 200 size_download: 36263 time_namelookup: 0.000 time_connect: 0.051 time_appconnect: 0.000 time_pretransfer: 0.051 time_starttransfer: 0.251 time_total: 0.352 ---- calculated each phase times ---- response_size: 36263 bytes msec_namelookup: 0 msec msec_connect: 51 msec msec_ssl_handshake_overhead: 0 msec msec_client_transfer_request(estimated): 25 msec msec_server_generate_response(estimated): 149 msec msec_server_transfer_response(estimated): 126 msec msec_total: 351 msec [root @st-webclient001 ~]# tcpdump -nnn -i enp0s8 '((dst 10.1.0.2) and (dst port 80)) || ((src 10.1.0.2) and (src port 80))' tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on enp0s8, link-type EN10MB (Ethernet), capture size 262144 bytes 22:01:27.341618 IP 10.101.0.4.44605 > 10.1.0.2.80: Flags [S], seq 2615431876, win 29200, options [mss 1460,sackOK,TS val 2832563 ecr 0,nop,wscale 9], length 0 22:01:27.392671 IP 10.1.0.2.80 > 10.101.0.4.44605: Flags [S.], seq 77093785, ack 2615431877, win 28960, options [mss 1460,sackOK,TS val 2863731 ecr 2832563,nop,wscale 9], length 0 22:01:27.392699 IP 10.101.0.4.44605 > 10.1.0.2.80: Flags [.], ack 1, win 58, options [nop,nop,TS val 2832576 ecr 2863731], length 0 22:01:27.392849 IP 10.101.0.4.44605 > 10.1.0.2.80: Flags [P.], seq 1:420, ack 1, win 58, options [nop,nop,TS val 2832576 ecr 2863731], length 419: HTTP: GET /test.php HTTP/1.1 22:01:27.443610 IP 10.1.0.2.80 > 10.101.0.4.44605: Flags [.], ack 420, win 59, options [nop,nop,TS val 2863744 ecr 2832576], length 0 22:01:27.592097 IP 10.1.0.2.80 > 10.101.0.4.44605: Flags [.], seq 1:11585, ack 420, win 59, options [nop,nop,TS val 2863781 ecr 2832576], length 11584: HTTP: HTTP/1.1 200 OK 22:01:27.592118 IP 10.101.0.4.44605 > 10.1.0.2.80: Flags [.], ack 11585, win 103, options [nop,nop,TS val 2832626 ecr 2863781], length 0 22:01:27.592297 IP 10.1.0.2.80 > 10.101.0.4.44605: Flags [.], seq 11585:14481, ack 420, win 59, options [nop,nop,TS val 2863781 ecr 2832576], length 2896: HTTP 22:01:27.592306 IP 10.101.0.4.44605 > 10.1.0.2.80: Flags [.], ack 14481, win 114, options [nop,nop,TS val 2832626 ecr 2863781], length 0 22:01:27.642365 IP 10.1.0.2.80 > 10.101.0.4.44605: Flags [.], seq 14481:20273, ack 420, win 59, options [nop,nop,TS val 2863794 ecr 2832626], length 5792: HTTP 22:01:27.642380 IP 10.101.0.4.44605 > 10.1.0.2.80: Flags [.], ack 20273, win 137, options [nop,nop,TS val 2832638 ecr 2863794], length 0 22:01:27.693085 IP 10.1.0.2.80 > 10.101.0.4.44605: Flags [P.], seq 20273:26065, ack 420, win 59, options [nop,nop,TS val 2863806 ecr 2832638], length 5792: HTTP 22:01:27.693106 IP 10.101.0.4.44605 > 10.1.0.2.80: Flags [.], ack 26065, win 159, options [nop,nop,TS val 2832651 ecr 2863806], length 0 22:01:27.693373 IP 10.1.0.2.80 > 10.101.0.4.44605: Flags [P.], seq 26065:36485, ack 420, win 59, options [nop,nop,TS val 2863806 ecr 2832638], length 10420: HTTP 22:01:27.693384 IP 10.101.0.4.44605 > 10.1.0.2.80: Flags [.], ack 36485, win 200, options [nop,nop,TS val 2832651 ecr 2863806], length 0 22:01:27.693491 IP 10.101.0.4.44605 > 10.1.0.2.80: Flags [F.], seq 420, ack 36485, win 200, options [nop,nop,TS val 2832651 ecr 2863806], length 0 22:01:27.744300 IP 10.1.0.2.80 > 10.101.0.4.44605: Flags [F.], seq 36485, ack 421, win 59, options [nop,nop,TS val 2863819 ecr 2832651], length 0 22:01:27.744312 IP 10.101.0.4.44605 > 10.1.0.2.80: Flags [.], ack 36486, win 200, options [nop,nop,TS val 2832664 ecr 2863819], length 0 ^C
わかりやすいように図にしました。
ここでは SSL を使用していないため、SSL のハンドシェイクのオーバーヘッドと DNS 名前解決の時間は入っていません。
図の中に書いてしまったのですが、特筆事項は下記のとおりです。
- time_connect は SYN -> SYN/ACK -> SYN を送信直後の時間なので、実質はラウンドトリップタイムだと考えることができます。
- HTTP Request の送信時間はラウンドトリップタイムの半分と考えることができます。
- 図を見ていただくとわかりやすいかと思いますが、time_starttransfer - time_pretransfer には HTTP リクエストと HTTP レスポンスを送信する時間が含まれるため、この数字からラウンドトリップタイムを引くことでサーバーがレスポンスを生成するのにかかった時間を推計できます
- time_total - time_starttransfer に HTTP レスポンスの1パケット目の送信時間を加算するとレスポンスの転送時間が推計できます。
スクリプトのソースコード
#!/bin/bash # Usage # # ./check-speed.sh 'http://example.com/' # # ./check-speed.sh -tsv 'http://example.com/' # # ./check-speed.sh -ltsv 'http://example.com/' # if echo "$1" | egrep -q '^-'; then case "$1" in "-tsv") mode="tsv";; "-ltsv") mode="ltsv";; esac shift; fi url="$1" data=$(curl -s \ -H 'accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8' \ -H 'accept-encoding:gzip, deflate, br' \ -H 'accept-language:en-US,en;q=0.8' \ -H 'cache-control:no-cache' \ -H 'pragma:no-cache' \ -H 'upgrade-insecure-requests:1' \ -H 'user-agent:Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/61.0.3163.79 Chrome/61.0.3163.79 Safari/537.36' \ -m 15 \ -o /dev/null \ -w 'code:%{http_code} size_download:%{size_download} time_namelookup:%{time_namelookup} time_connect:%{time_connect} time_appconnect:%{time_appconnect} time_pretransfer:%{time_pretransfer} time_starttransfer:%{time_starttransfer} time_total:%{time_total} \n' \ "$url") if [[ "$?" != 0 ]]; then # it means prior curl command failed echo "curl failed" exit 2 fi size_download=$(echo "$data" | perl -ne 's|^.*size_download:([.0-9]+) .*$|$1|; print;') msec_time_namelookup=$(echo "$data" | perl -ne 's|^.*time_namelookup:([.0-9]+) .*$|$1|; print ($_ * 1000);') msec_time_connect=$(echo "$data" | perl -ne 's|^.*time_connect:([.0-9]+) .*$|$1|; print ($_ * 1000);') msec_time_pretransfer=$(echo "$data" | perl -ne 's|^.*time_pretransfer:([.0-9]+) .*$|$1|; print ($_ * 1000);') msec_time_starttransfer=$(echo "$data" | perl -ne 's|^.*time_starttransfer:([.0-9]+) .*$|$1|; print ($_ * 1000);') msec_time_total=$(echo "$data" | perl -ne 's|^.*time_total:([.0-9]+) .*$|$1|; print ($_ * 1000);') # debug # echo "$msec_time_namelookup $msec_time_connect $msec_time_pretransfer $msec_time_starttransfer $msec_time_total" response_size=$size_download msec_namelookup=$msec_time_namelookup msec_connect=$(($msec_time_connect - $msec_time_namelookup)) msec_ssl_handshake_overhead=$(($msec_time_pretransfer - $msec_time_connect)) msec_request_to_first_byte=$(($msec_time_starttransfer - $msec_time_pretransfer)) msec_client_transfer_request=$(($msec_connect / 2)) msec_server_generate_response=$(($msec_request_to_first_byte - $msec_connect)) msec_server_transfer_response=$(($msec_time_total - $msec_time_starttransfer + (msec_connect / 2))) msec_total=$(($msec_namelookup + $msec_connect + $msec_ssl_handshake_overhead + $msec_client_transfer_request + $msec_server_generate_response + $msec_server_transfer_response)) case "$mode" in "tsv") echo -e "$response_size\t$msec_namelookup\t$msec_connect\t$msec_ssl_handshake_overhead\t$msec_client_transfer_request\t$msec_server_generate_response\t$msec_server_transfer_response\t$msec_total";; "ltsv") echo -e "response_size:$response_size\tmsec_namelookup:$msec_namelookup\tmsec_connect:$msec_connect\tmsec_ssl_handshake_overhead:$msec_ssl_handshake_overhead\tmsec_client_transfer_request:$msec_client_transfer_request\tmsec_server_generate_response:$msec_server_generate_response\tmsec_server_transfer_response:$msec_server_transfer_response\tmsec_total:$msec_total";; *) echo "---- curl raw variable values ----" echo "$data" | perl -ne 's| |\n|g; s|:|: |g; print;' | column -t echo echo "---- calculated each phase times ----" echo "response_size: $response_size bytes msec_namelookup: $msec_namelookup msec msec_connect: $msec_connect msec msec_ssl_handshake_overhead: $msec_ssl_handshake_overhead msec msec_client_transfer_request(estimated): $msec_client_transfer_request msec msec_server_generate_response(estimated): $msec_server_generate_response msec msec_server_transfer_response(estimated): $msec_server_transfer_response msec msec_total: $msec_total msec" | column -t;; esac
実行結果サンプル
$ ./check-speed.sh 'http://example.com/' ---- curl raw variable values ---- code: 200 size_download: 606 time_namelookup: 0.060 time_connect: 0.222 time_appconnect: 0.000 time_pretransfer: 0.222 time_starttransfer: 0.391 time_total: 0.391 ---- calculated each phase times ---- response_size: 606 bytes msec_namelookup: 60 msec msec_connect: 162 msec msec_ssl_handshake_overhead: 0 msec msec_client_transfer_request(estimated): 81 msec msec_server_generate_response(estimated): 7 msec msec_server_transfer_response(estimated): 81 msec msec_total: 391 msec
TCP コネクションの部分でインターネット上の途中経路でパケットロスや遅延があった場合に、 ラウンドトリップタイムが大きくなってしまった場合に、msec_server_generate_response がマイナスになってしまうケースがあるので、その点だけ注意点です。 複数回実行し、極端な数字を排除した上で平均値を出すのが適切かと思います。