观察网站性能

这篇文章最后更新的时间在六个月之前,文章所叙述的内容可能已经失效,请谨慎参考!

ab

ab (apache benchmark)

一般的使用命令

  • 模拟 10 个客户端,一共请求 1000 次,或者可以理解为模拟 10 个并发,一共请求 1000 次,或者可以理解为一次发送 10 个请求,一共请求 1000 次
./ab -c 10 -n 1000 http://www.baidu.com/
./ab --enable ssl -c 10 -n 1000 https://www.baidu.com/
./abs -c 10 -n 1000 https://www.baidu.com/
  • ab 测试工具不能直接支持 https 。要么加上 --enable ssl ,要么用 abs 工具, abs 和 ab 基本一致,只是支持 https 。新版的 ab 好像能直接用 https 协议

  • 加上 -w 能输出 html

  • -h 参数,能查看帮助

输出的解释

Server Software:        BWS/1.1 # 响应的服务器类型
Server Hostname:        www.baidu.com # 请求的 URL 主机名
Server Port:            443 # 请求端口
SSL/TLS Protocol:       TLSv1.2,ECDHE-RSA-AES128-GCM-SHA256,2048,128 # https 的版本和密码套件

Document Path:          / # 请求路径
Document Length:        227 bytes # HTTP响应数据的正文长度

Concurrency Level:      10 # 并发用户数,就是 -c 参数的值
Time taken for tests:   21.376 seconds  # 所有这些请求被处理完成所花费的总时间 单位秒
Complete requests:      1000 # 总请求数量,就是 -n 参数的值
Failed requests:        0 # 表示失败的请求数量
Total transferred:      1081951 bytes # 所有请求的响应数据长度总和。包括每个 HTTP 响应数据的头信息和正文数据的长度
HTML transferred:       227000 bytes # 所有请求的响应数据中正文数据的总和,也就是减去了 Total transferred 中 HTTP 响应数据中的头信息的长度
Requests per second:    46.78 [#/sec] (mean) # 吞吐量,计算公式: Complete requests/Time taken for tests  总请求数/处理完成这些请求数所花费的时间
Time per request:       213.762 [ms] (mean) # 用户平均请求等待时间,计算公式: Time token for tests/(Complete requests/Concurrency Level)。处理完成所有请求数所花费的时间/(总请求数/并发用户数)
Time per request:       21.376 [ms] (mean, across all concurrent requests) # 服务器平均请求等待时间,计算公式: Time taken for tests/Complete requests,正好是吞吐率的倒数。也可以这么统计: Time per request/Concurrency Level
Transfer rate:          49.43 [Kbytes/sec] received # 表示这些请求在单位时间内从服务器获取的数据长度,计算公式: Total trnasferred/ Time taken for tests,这个统计很好的说明服务器的处理能力达到极限时,其出口宽带的需求量。

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:       31  132 203.5     85    1204 # Connect 建立 tcp 连接的耗时
Processing:     0   81 150.7     55    1141 # Processing 总耗时减去 Connect
Waiting:        0   53 122.5     35    1103 # Waiting 客户端发送完请求信息的最后一个字节 到 接收响应信息的的第一个字节 的耗时
Total:         85  213 250.9    145    1304 # Total 总的耗时,从建立连接到关闭连接的耗时

# min 最小值, mean 平均值, [+/-sd] 标准差, median 中位数, max 最大值

Percentage of the requests served within a certain time (ms)
  50%    145 # 50% 的请求在 145ms 内返回
  66%    154
  75%    160
  80%    165
  90%    192
  95%   1133
  98%   1186 # 98% 的请求在 1186ms 内返回
  99%   1235
 100%   1304 (longest request)

ab 更详细的用法

  • -C 设置 cookie ,可以多次使用
  • -H 设置请求头,可以多次使用,也可以在请求头里设置 cookie
  • -T 设置 Content-Type 请求头信息
  • -p 设置 post 的数据,是一个文件路径
  • -u 设置 put 的数据,是一个文件路径
  • 大多数情况下,需要关注 Requests per second ,这个值越大越好

ab 请求时设置 cookie

./ab -c 10 -n 1000 \
-C "name=ball;age=99;sex=male" \
http://localhsot/

./ab -c 10 -n 1000 \
-H "Cookie: name=ball;age=36" \
http://localhsot/

ab 发送 post 或 put

./ab -c 10 -n 1000 \
-p 'post.txt' -T 'application/x-www-form-urlencoded' \
http://localhsot/

./ab -c 10 -n 1000 \
-u 'put.txt' -T 'application/x-www-form-urlencoded' \
http://localhsot/

如果发送的内容是 application/x-www-form-urlencoded ,那么 post.txt 或 put.txt 里的内容也要经过编码

ab 发送文件和普通的 post 或 put 是一样的

./ab -c 10 -n 1000 \
-p 'post.txt' -T 'multipart/form-data; boundary=----WebKitFormBoundaryRO0YA4pq9oCgwTkt' \
http://localhsot/

post.txt 文件的内容

------WebKitFormBoundaryRO0YA4pq9oCgwTkt
Content-Disposition: form-data; name="fileUpload"; filename="test.png"
Content-Type: image/png
iVBORw0KGg.............................................
------WebKitFormBoundaryRO0YA4pq9oCgwTkt--

实质上就是手工实现 rfc 1867 1521 1522 这几个标准

ab 只能进行一些简单的压力测试,一些更详细的测试还是要用 jmeter

ab 的文档 https://httpd.apache.org/docs/current/programs/ab.html

ab 一般会跟随 apache httpd 一起安装,但也有单独的安装命令。 ab 单独的安装命令

apt install apache2-utils
或
yum install httpd-tools

curl

一般的使用命令

curl -o /dev/null -s -w %{time_namelookup}::%{time_connect}::%{time_starttransfer}::%{time_total}::%{speed_download}"\n" "https://www.baidu.com"
  • 这段命令只能在 git bash 或 linux 里运行

命令解释

-o: 把 curl 返回的 html js 写到 /dev/null
-s: 去掉所有状态
-w: 按照后面的格式输出
time_namelookup: DNS 解析域名 www.baidu.com 的时间
time_commect: client 和 server 端建立 TCP 连接的时间, 包括前一项的时间
time_starttransfer: 从 client 发出请求到 web 的 server 响应第一个字节的时间, 包括前二项的时间
time_total: client 发出请求到 web 的 server 发送会所有的相应数据的时间, 包括前三项的时间
speed_download: 下载速度 单位 byte/s
  • 除了 speed_download 外,其它值越小越好
  • time_total 最好小于 0.06

这是一段根据上面 curl 命令写成的,可以连续执行的,能显示多次执行平均值的 bash 脚本

# ./testwebsite.sh -c 2 -n 10 "https://www.baidu.com"

# 这个命令能输出当前系统 cpu 的核心数
nproc=$(nproc)
c=`echo $nproc | awk '{printf ("%d", $1*2)}'`
n=`echo $c | awk '{printf ("%d", $1*100)}'`
n=100

TEST_URL=www.baidu.com

verbose="false"
DRY_RUN="false"

# echo $nproc
# echo $c
# echo $n

print_version()
{
    echo 'print_version';
}

usage()
{
    echo 'usage';
}

# Use "${1-}" in order to avoid errors because of 'set -u'.
while [ -n "${1-}" ]; do
    case "${1}" in

        -c)
            shift
            c=${1}
            ;;

        -n)
            shift
            n=${1}
            ;;

        -v|--verbose)
            verbose="true"
            ;;

        --curl-options=*)
            opt=$(printf "%s\n" "${1}" | sed 's/^--curl-options=//')
            CURL_OPTIONS="${CURL_OPTIONS} ${opt}"
            ;;

        --curl-options)
            shift
            CURL_OPTIONS="${CURL_OPTIONS} ${1}"
            ;;

        --dry-run)
            DRY_RUN="true"
            ;;

        -h|--help)
            print_version
            usage
            exit 0
            ;;

        -V|--version)
            print_version
            exit 0
            ;;

        --)
            # This is the start of the list of URLs.
            shift
            for url in "$@"; do
                # Encode whitespaces into %20, since wget supports those URLs.
                newurl=$(printf "%s\n" "${url}" | sed 's/ /%20/g')
                URLS="${URLS} ${newurl}"
            done
            break
            ;;

        -*)
            error "Unknown option: '$1'."
            ;;

        *)
            # This must be a URL.
            # Encode whitespaces into %20, since wget supports those URLs.
            newurl=$(printf "%s\n" "${1}" | sed 's/ /%20/g')
            TEST_URL="${URLS} ${newurl}"
            ;;
    esac
    shift
done

# n=5
once="curl -o /dev/null -s -w %{http_code}::%{time_namelookup}::%{time_connect}::%{time_starttransfer}::%{time_total}::%{speed_download}, $TEST_URL ";

ConcurrencyCommand="for i in \$(seq 1 \$n); do echo \$once; echo \";\";done"

t=$(echo $ConcurrencyCommand | sed 's/\$n/'$n'/' | sed 's/\$once/%s/');
t2=$(echo $t" ;; "$once | awk '
    BEGIN {
        FS=";;";
    }
    {
        printf($1, $2)
        if (NR == 1) exit;
    }
    ')
t3="echo \$($t2) | xargs -d \";\" -n 1 -P $c bash -c '\$0'"

printf "%-11s %s \n" "TEST_URL" $TEST_URL
printf "%-11s %d \n" "TEST_COUTE" $n
printf "%-11s %d \n" "Concurrency" $c

if [ $DRY_RUN = "true" ]; then

    echo
    # 运行一次的命令
    echo 'once'
    echo $once | sed 's/,//g'

    echo
    # 完整的运行命令
    echo 'Concurrency'
    echo $t3 " | sed 's/,/\n/g'"
    # echo "bash -c "$(echo $b | sed 's/;/\n/g' | head -n 1 | sed 's/,//g');
    # echo "xargs -d \";\" -n 1 -P $c bash -c";
    # echo $b;

    exit 0
fi

c=$(eval $t3)

# echo $c | sed 's/,/\n/g'
# echo $c
# printf "\n"

# echo $c | sed 's/,/\n/g'
# printf "\n"

d=$(echo $c | sed 's/,$//')
# echo "$d"
# printf "\n"

# echo $c | sed 's/;$//' | awk

if [ $verbose = "true" ]; then
    echo $d | awk '
BEGIN {
    RS=",";
    FS="::";
    printf("\n");
    printf("%-6s %-9s  %-15s  %-12s  %-18s  %-10s  %-14s \n", " ", "http_code", "time_namelookup", "time_connect", "time_starttransfer", "time_total", "speed_download");
}
{
    printf("%-6d %-9d  %-15.6f  %-12.6f  %-18.6f  %-10.6f  %-14d \n", NR, $1, $2, $3, $4, $5, $6);
}
END {
    printf("\n");
}
'
fi

echo $d | awk '
function median(arr, count1) {
    return (count1 % 2 == 0) ? ( arr[count1 / 2] + arr[count1 / 2 + 1] ) / 2  : arr[int(count1 / 2) + 1];
}
function sdev(arr, count1) {
    for (i=1; i <= count1; i++) {
        sum1 += arr[i];
    }
    mean = sum1/count1;
    for (i=1; i <= count1; i++) {
        variance += (arr[i] - mean)^2;
    }
    variance = variance/count1;
    return sqrt(variance);
}
BEGIN {
    RS=",";
    FS="::";
}
{
    time_namelookup+=$2;
    time_namelookup_arr[NR] = $2;
    time_connect+=$3;
    time_connect_arr[NR] = $3;
    time_starttransfer+=$4;
    time_starttransfer_arr[NR] = $4;
    time_total+=$5;
    time_total_arr[NR] = $5;
    speed_download+=$6;
    speed_download_arr[NR] = $6;
}
END {
    if (NR == 0) exit;

    asort(time_namelookup_arr);
    asort(time_connect_arr);
    asort(time_starttransfer_arr);
    asort(time_total_arr);
    asort(speed_download_arr);

    printf("%20s %-12s  %-12s  %-12s  %-12s  %-12s  %-12s \n", " ", "total", "mean", "max", "min", "median", "sdev");
    printf("%-20s %-12.6f  %-12.6f  %-12.6f  %-12.6f  %-12.6f  %-12.6f \n", "time_namelookup", time_namelookup, time_namelookup/NR, time_namelookup_arr[NR], time_namelookup_arr[1], median(time_namelookup_arr, NR), sdev(time_namelookup_arr, NR));
    printf("%-20s %-12.6f  %-12.6f  %-12.6f  %-12.6f  %-12.6f  %-12.6f \n", "time_commect", time_commect, time_commect/NR, time_commect_arr[NR], time_commect_arr[1], median(time_commect_arr, NR), sdev(time_commect_arr, NR));
    printf("%-20s %-12.6f  %-12.6f  %-12.6f  %-12.6f  %-12.6f  %-12.6f \n", "time_starttransfer", time_starttransfer, time_starttransfer/NR, time_starttransfer_arr[NR], time_starttransfer_arr[1], median(time_starttransfer_arr, NR), sdev(time_starttransfer_arr, NR));
    printf("%-20s %-12.6f  %-12.6f  %-12.6f  %-12.6f  %-12.6f  %-12.6f \n", "time_total", time_total, time_total/NR, time_total_arr[NR], time_total_arr[1], median(time_total_arr, NR), sdev(time_total_arr, NR));
    printf("%-20s %-12d  %-12.3f  %-12d  %-12d  %-12d  %-12.3f \n", "speed_download", speed_download, speed_download/NR, speed_download_arr[NR], speed_download_arr[1], median(speed_download_arr, NR), sdev(speed_download_arr, NR));
}
'

这是上面那段脚本的运行结果 ./testwebsite.sh -c 2 -n 10 "https://www.baidu.com"

TEST_URL    https://www.baidu.com
TEST_COUTE  10
Concurrency 2
                     total         mean          max           min           median        sdev
time_namelookup      0.393789      0.003938      0.027595      0.003291      0.003560      0.002553     
time_commect         0.000000      0.000000      0.000000      0.000000      0.000000      0.003946
time_starttransfer   3.791344      0.037913      0.060255      0.028540      0.037378      0.006453
time_total           3.802466      0.038025      0.060337      0.028608      0.037543      0.042166
speed_download       6362500       63625.000     83228         39461         63420         7718.199

运行结果的解释

  • 测试 url 是 https://www.baidu.com
  • 一共请求 10 次
  • 至多同时运行两次请求
  • 输出各项数据的,总计(total),平均数(mean),最大值(max),最小值(min),中位数(median),标准差(sdev)

这样的脚本也没有考虑到重定向的情况

lighthouse

一个是浏览器自带的 lighthouse (基于 chromium 的浏览器才有)。 一个是谷歌在线的 lighthouse https://developers.google.com/speed/pagespeed/insights/ 。 还有一个 chromium 的插件,但实际上和开发者工具里的 lighthouse 是一样的。

  • lighthouse 的分数计算器 https://googlechrome.github.io/lighthouse/scorecalc/
  • 谷歌在线的 lighthouse 可能访问不了
  • 要注意 设备的类型 (Device type) 是 桌面 (desktop) 还是 移动 (mobile)
  • lighthouse 会给出优化建议,大部分情况下按着建议优化就可以了

评分一共有五部分

  • 性能 (Performance)
  • 访问无障碍 (Accessibility)
  • 最佳实践 (Best Practice)
  • 搜索引擎优化 (SEO)
  • PWA (Progressive Web App)

一般情况下,只需要关注这三部分即可,分数越高越好

  • Performance
  • Best Practice
  • SEO

一般情况下,如果低于这个分数就要优化

  • Performance <= 70
  • Best Practice <= 80
  • SEO <= 80

浏览器开发者工具

相关文档

请求列表里有一个 时间 的列,但这个时间一般会包含队列等待,dns解释,stl握手。

从后端的角度来看,大部分情况下只需要关注,请求详情里的这三个参数

  1. Request sent 请求第一个字节发出前到最后一个字节发出后的时间,也就是上传时间
  2. Waiting 请求发出后,到收到响应的第一个字节所花费的时间 (Time To First Byte)
  3. Content Download 收到响应的第一个字节,到接受完最后一个字节的时间,就是下载时间
  • Waiting 最好小于 300ms

其它

其它工具

查找问题

  • curl 输出完整的请求流程
curl -i -v -L www.baidu.com
# -i 输出 http 头
# -v 输出通信的整个过程,用于调试
# -L 让 HTTP 请求跟随服务器的重定向。curl 默认不跟随重定向。
  • 判断域名是否有正常解释
# dns 查询
nslookup www.baidu.com
# dns 反查
nslookup -qt=ptr 14.215.177.38
  • 判断能否访问到服务器, ping 不通也有可能是服务器禁用了 icmp
ping www.baidu.com
  • 判断对应的端口是否有开启
telnet 127.0.0.1 80
# 如果端口没有开放,会提示连接失败
curl --no-buffer --connect-timeout 30 -i -v telnet://127.0.0.1:80
# 如果输出 connected 表示端口有开启
  • 路由追踪,显示出本机与服务器之间的路由,有助于判断故障位置,通过显示的时间 IP 等信息了解数据的流向
# linux 用 traceroute
traceroute www.baidu.com
# windows 用 tracert
tracert www.baidu.com

网站性能的一些准则

  • 性能测试里,如果压力机性能不够的话,测试的结果参考价值会比较低。所以,如果在本地测试,最好把其它进程都关闭了,只保留测试工具的进程。

  • 接口的响应速度最好小于等于 300ms

  • 一般页面的加载速度最好小于等于 2s

  • 笔者认为的,页面加载速度分级

    小于等于 2s
    2s - 5s
    5s - 8s
    大于等于 8s
  • 笔者认为的,页面加载速度如果大于 5s ,就需要优化,如果实在无法优化,可以弄个加载动画或者加载骨架之类的,反正就是要分散用户的注意力

  • 这里提及的页面加载速度,指的是用户能感知到的速度,完整的页面内容,可以在完成首屏渲染后,在后面慢慢加载也是没问题的。