使用JMeter测试服务器基准性能

概述

通过对服务器进行基准测试(通常所说的benchmark测试),可得到服务器的基准性能。对于运行web服务的服务器而言,它的基准性能值让我们得以了解服务器本身所能承受的最大并发压力,是任何一个部署到该服务器的web服务的理论并发上限。有些设计良好的web服务能达到或接近这样的数值,而有些则不能。总之,基准能力的高低,不仅涉及到web服务本身的并发能力设计水平,也与服务器的软硬件资源配置和优化设置相关。

本文使用JMeter和其他性能测试工具,对一台运行NGINX服务的8C16G配置的标准Linux虚拟机进行基准性能测试,并尝试对NGINX和服务器本身进行调优,以使其发挥最大并发处理能力。本次使用NGINX作为被测web服务,不涉及数据库操纵或其他处理逻辑,只是简单返回响应消息,这样可以得到一个最大的并发处理数据,我们使用QPS即每秒处理请求数(quest per second)表示。

准备被测环境

准备两台负载机器用于产生压力,另外准备一台服务器作为被测机器。三台机器都位于同一台物理机器上,每台机器配置都是8C16GB的设置。

我们是使用JMeter执行分布式测试,所以自然在两台测试机器上安装好JMeter,并配置好分布式执行环境。其中一台机器即作为主控节点也作为工作节点。具体怎么配置分布式环境见另外一篇文章搭建JMeter分布式测试环境。在被测服务器上安装必备的基础软件,包括jdk、python等,具体安装方法网上自查。其中jdk是运行JMeter所必备,python主要是用来运行用于统计数据的脚本。

被测服务

使用NGINX作为被测的web服务器。NGINX是一个高性能的web服务或反向代理服务器,性能非常强劲。根据NGINX官方的测试报告戳这里,NGINX在一台8C16G的物理服务器(所谓裸金属服务器)的基准性能是,当并发连接为400个、且响应返回的文件大小是1KB的时候,QPS可以达到26w多,非常的高。

我们本次实验使用的机器是虚拟机,CPU品牌和NGINX官网上那篇文章使用的基本一样,都是Intel至强处理器,但是具体型号有差异,CPU频率略低一些。所以我的测试目的是能够尽量接近官方的这个QPS数值。

测试工具

使用的测试工具包括ab、wrk、jmeter。其中ab、wrk都是作为jmeter的对照物。也可以使用其他工具,比如gatling对比一下。此次使用ab、wrk互为印证就够了。

使用apache benchmark(简称ab)和wrk执行最基础的测试,用来作为与JMeter分布式测试的效果的对比。有对比才有高下之分对吧。ab和wrk纯粹作为互相参照印证的工具。

1)安装ab

在CentOS上安装ab,只需要一行命令:yum install /usr/bin/ab。安装好以后在命令行执行ab验证是否成功。

2)安装wrk

先安装git,因为make的时候需要用到。下载git工程后安装,工程地址https://github.com/wg/wrk。下载后进入wrk的git工程目录直接执行make,得到一个二进制文件wrk,这个就是可执行程序。

3)安装jmeter

jmeter官网下载解压即可。部署分布式环境请参考另外一篇文章搭建JMeter分布式测试环境

服务器和NGINX服务优化

  1. 对负载机和被测服务器均进行相关优化,具体优化可参考我的另外一篇博客CentOS7调优实践
  2. 对NGINX服务进行设置优化:
    需要优化的配置包括如下(更多见具体nginx.conf文件中的设置):
  • worker_rlimit_nofile:为nginx的work进程所运行的用户(一般是nginx)设置可打开的最大文件数
  • worker_connections:最大访问客户数,修改此值时,不能超过 worker_rlimit_nofile 值
  • worker_processes:worker进程数,一般设为auto即可,也可以设为等于或超过cpu数的数量,不宜超过太多
  • worker_cpu_affinity:cpu亲和性
  • keepalive_requests:这个还没配过,默认是1000。这个配置项是nginx对于单个keepalive连接可处理的请求的数量。低于1.19.10的版本默认是100. 关于这点有人也做了探索,见 https://github.com/jinhailang/blog/issues/37。因为我测试的时候每个连接都只发100个请求,所以默认值够用了。但是如果一个请求要发送很多请求,比如长时间的压力测试,那么这个值可能要设置的比较大一些好点。
  • keepalive_timeout:空闲保活连接保持打开状态的时间。默认是75s。默认值即可。
  • access_log:设置日志路径和级别

nginx.conf完整配置见附录nginx配置文件示例

执行测试

使用ab执行测试

压测nginx的主页地址,nginx将返回一段大小不到1KB的文本。

1)用例1:单台负载机启动8个ab进程使用大连接数进行测试

单机启动8个ab进程压测,qps大概可以达到2.6w左右。负载机cpu和被测服务器的cpu利用都大概50%左右。

具体测试命令是:

1
2
3
4
5
6
7
8
# 通过脚本多进程压测,根据cpu数启动相应个数的ab进程。每个并发60000,实现6k*8=4.8w个并发
#!/bin/bash
rm -rf *.log
for((i=1;i<=8;i++));
do
nohup ab -k -r -n 600000 -c 6000 "http://192.168.1.182:80/" > request${i}.log 2>&1 &
sleep 1
done

下面是查看8个ab进程的测试结果,每个进程的qps综合相加就是总的qps:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[root@hadoop0 performance_test]# cat request1.log  | grep "Requests per second:"
Requests per second: 4025.82 [#/sec] (mean)
[root@hadoop0 performance_test]# cat request2.log | grep "Requests per second:"
Requests per second: 3097.47 [#/sec] (mean)
[root@hadoop0 performance_test]# cat request3.log | grep "Requests per second:"
Requests per second: 3266.06 [#/sec] (mean)
[root@hadoop0 performance_test]# cat request4.log | grep "Requests per second:"
Requests per second: 3284.48 [#/sec] (mean)
[root@hadoop0 performance_test]# cat request5.log | grep "Requests per second:"
Requests per second: 3338.12 [#/sec] (mean)
[root@hadoop0 performance_test]# cat request6.log | grep "Requests per second:"
Requests per second: 3120.59 [#/sec] (mean)
[root@hadoop0 performance_test]# cat request7.log | grep "Requests per second:"
Requests per second: 3127.17 [#/sec] (mean)
[root@hadoop0 performance_test]# cat request8.log | grep "Requests per second:"
Requests per second: 3156.94 [#/sec] (mean)

2)用例2:2台负载机每台启动8个ab进程使用大连接数进行测试

同时使用两台负载机进行压测。每台启动8个ab进程,并发约4.8w(在可允许的端口数范围内),两台机器理论可以启动约9.6万ab进程压测。测试命令与用例1相同。不过还是没有能使被测服务器达到90%以上cpu利用率。

经过多次测试,两台机器得到总qps是接近3w,相比单台机器压测,并没有多大提升:

1
2
3
4
[root@hadoop0 performance_test]# python ./calculate_ab_qps.py
('total qps is : ', 16312.919999999998)
[root@hadoop1 performance_test]# python ./calculate_ab_qps.py
('total qps is : ', 13512.26)

calculate_ab_qps.py是一个汇总计算各request.log中qps数据并汇总的python小工具。

结论是:在大连接数量的情况,通过ab测试得到的被测NGINX服务器的性能大约就是3w。

3)用例3: 2台压力负载机器均使用少连接数进行测试

作为对比,使用较少的连接数进行测试。此种情况下,被测服务器的CPU利用率应比大连接时更高。

测试命令如下:

1
2
3
4
5
6
7
8
# 通过脚本多进程压测,根据cpu数启动相应个数的ab进程。每个并发50,实现50*8=400个并发
#!/bin/bash
rm -rf *.log
for((i=1;i<=8;i++));
do
nohup ab -k -r -n 600000 -c 50 "http://192.168.1.182:80/" > request${i}.log 2>&1 &
sleep 1
done

两台负载机器测试得到的QPS大约相等,差不多都是4.8w,相加总和是9.6w,所以被测NGINX的QPS是9.6w左右。资源消耗方面,被测服务器CPU利用率可以达到80%左右,网络负载在700Mbps左右,大约是70%的利用率。此时服务器资源看起来还是有一定的余量。然而压测端CPU利用率没有超过50%,包括内存、网络等都没有到瓶颈,似乎可以增加更多压力。有机会可以再进行探索测试,比如尝试启用更多ab进程,比如设置进程数为CPU数的150%。

使用wrk执行测试

1)用例1: 低连接数模式

两台负载机每台以较低数量的连接数,保持keepalive,对服务器发送请求。
具体测试命令如下,每台机器使用8个wrk进程,每个进程也都设置为单线程,这样效率最高:

1
2
3
4
5
#!/bin/bash
for i in `seq 0 7`; do
echo "worker $i\n"
taskset -c $i /opt/wrk/wrk -t 1 -c 50 -d 180s http://192.168.1.182:80/ &
done

每台机器测试得到的结果qps大约5w,两台的总和就是10万。现在终于把这台被测机器的极限处理性能给测试出来了!被测服务器cpu使用率压到接近100%,已经不能再提上去了,因为NGINX所在服务器的cpu现在已经是瓶颈了!除非提升硬件配置。

2)用例2: 高连接数模式

换一种测试方式。上面是测试较少连接(connection)的情况下NGINX服务器的处理性能。接下来尝试大并发连接的情况下NGINX的处理性能。

每个进程起7000个连接,一台机器则连接总数总共是5.6w,不超过可用端口总数(大约是6w左右),命令如下:

1
2
3
4
5
#!/bin/bash
for i in `seq 0 7`; do
echo "worker $i\n"
taskset -c $i /opt/wrk/wrk -t 1 -c 7000 -d 300s http://192.168.1.182:80/ &
done

这种情况下,服务器的cpu利用率没有上一种模式那么高,大约是50~60%,网络带宽下降了接近一半。2台负载机器得到总的服务器的qps处理能力是3.7w左右,下降非常多。所以多连接情况下,NGINX处理效率没有那么高,资源利用率也相对低一些。

对比wrk和ab,似乎wrk可以产生更大的压力。

使用jmeter执行测试

上面使用ab和wrk测试的结果都已经出来,可以看到得到数据是类似的。现在可以使用ab和wrk测试得到qps作为被测服务的性能基准,可以认为较低并发(1k)的情况下被测服务的QPS能力约9.8w,较高并发(10w)情况下被测服务的QPS能力约3.3w。接下来再使用jmeter进行性能测试。

首先确认对jmeter进行了适当的优化,包括jmeter-server服务和jmter脚本,具体优化措施参考我另外一篇博文。

1)用例1: 高连接数,使用2台jmeter-server测试,每台创建1w个线程发送请求

在使用jmeter进行性能测试时,关闭所有断言,可以得到更好的测试效果。如果为了分析被测服务逻辑处理的正确率,可以在事后进行统计分析。

使用两台机器启动分布式命令,一台是192.168.1.181,一台是本机:

1
./jmeter -n -t test_nginx_benchmark.jmx -l test_nginx_benchmark.jtl -R192.168.1.181,localhost  -e -o test_nginx_benchmark -f

性能在2.5w左右,而被测服务器的cpu利用率目测只有30%左右,效果与ab、wrk是接近的:

1
2
3
4
5
6
7
8
"Total" : {
"transaction" : "Total",
"sampleCount" : 1000000,
"throughput" : 25401.986435339244,
"receivedKBytesPerSec" : 21160.053153656616,
"sentKBytesPerSec" : 4068.286890034801,
...
}

但是在高连接的情况下,jmeter确实比ab或wrk更消耗资源。1w并发的情况下8核的总cpu利用率达到了80%(按总100%计算)。因为jmeter实际上是对每一个连接都建立了一个线程,所以每台负载机器在执行测试时都产生了约1w个线程。操作系统需要对这1w个线程进行调度,通过观察,CPU资源使用率大约是ab或wrk的2倍。

2)用例2: 低连接数,使用2台jmeter-server测试,每台创建500个线程发送请求

仍然是利用两台jmeter机器压测,但这次采用低连接模式,连接数少得多。每台机器启动一个jmeter-server服务进程,每个服务进程启动500个线程,与ab、wrk测试时一样,每个线程保持http连接keepalive,循环向服务端发送请求,每个线程共计循环约2000次。

这次测试得到的服务器QPS大约在5w。负载服务器的CPU利用率和用例1的情况类似,CPU利用率达到了极限。但服务器的CPU率没有达到极限,估计在50%左右。相比较用例1,由于负载机器的资源利用和工作模式更改,显然对服务器的压力更大了,但服务器显然还有处理余量,此时服务的QPS与使用wrk相比,只有一半左右。根据上述资源分析情况,可以看到jmeter确实比wrk、ab更消耗cpu,效率更低。在此次测试场景中,jmeter和负载机器成为了瓶颈。未来有机会,将尝试对jmeter和运行模式进行调优,降低对CPU的消耗率,使得能以较少的机器产生较多的负载量。

附录

nginx配置文件示例

/etc/nginx/nginx.conf内容示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#v1.1:增加cpu亲和力设置;增加keepalive设置
user nginx;
worker_processes 8;
worker_cpu_affinity 00000001 00000010 00000100 00001000 00010000 00100000 01000000 10000000;

error_log /var/log/nginx/error.log crit;
pid /var/run/nginx.pid;

worker_rlimit_nofile 65535;

events {
#worker_connections 1024;
use epoll;
multi_accept on;
#在Nginx接到一个新连接通知后,调用accept()来接受尽量多的连接
worker_connections 65535;
#最大访问客户数,修改此值时,不能超过 worker_rlimit_nofile 值
}


http {
keepalive_timeout 120s 120s;
keepalive_requests 10000;

include /etc/nginx/mime.types;
default_type application/octet-stream;

log_format main
'$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';

access_log /var/log/nginx/access.log main;
#GZIP性能优化

gzip on;
gzip_min_length 1100;
#对数据启用压缩的最少字节数,如:请求小于1K文件,不要压缩,压缩小数据会降低处理此请求的所有进程速度
gzip_buffers 4 16k;
gzip_proxied any;
#允许或者禁止压缩基于请求和响应的响应流,若设置为any,将会压缩所有请求
gzip_http_version 1.0;
gzip_comp_level 9;
#gzip压缩等级在0-9内,数值越大压缩率越高,CPU消耗也就越大
gzip_types text/plain
text/css
application/javascript
application/x-javascript
text/xml
application/xml
application/xml+rss
text/javascript
application/json
image/jpeg
image/gif
image/png;
#压缩类型
gzip_vary on;
#varyheader支持,让前端的缓存服务器识别压缩后的文件,代理
include /usr/local/nginx/conf/vhosts/*.conf;
#在当前文件中包含另一个文件内容的指令

#静态文件的缓存性能调优

open_file_cache max=65535 inactive=20s;
#这个将为打开文件指定缓存,max 指定缓存数量.建议和打开文件数一致.inactive 是指经过多长时间文件没被请求后删除缓存
open_file_cache_valid 30s;
#这个是指多长时间检查一次缓存的有效信息,例如我一直访问这个文件,30秒后检查是否更新,反之更新
open_file_cache_min_uses 2;
#定义了open_file_cache中指令参数不活动时间期间里最小的文件数
open_file_cache_errors on;
#NGINX可以缓存在文件访问期间发生的错误,这需要设置该值才能有效,如果启用错误缓存.则在访问资源(不查找资源)时.NGINX会报告相同的错误

sendfile on;
tcp_nopush on;
tcp_nodelay on;
#是否启用 nagle 缓存算法,告诉nginx不要缓存数据

#gzip on;

include /etc/nginx/conf.d/*.conf;
}

jmeter测试脚本

jmeter脚本比较简单,不过由于存储格式是xml,略显臃肿,仅贴出脚本截图。

nginx-benchmark

统计ab测试结果的python脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#!coding=utf-8
import os, sys
import re

"""
计算多个ab进程测试的总qps。
具体是计算当前目录下所有log中的 "Requests per second: 3156.94 [#/sec] (mean)"中包含的qps, 然后相加
"""

def get_qps(text):
pattern = r'Requests per second:\s*(.*)\s\[#/sec] \(mean\)'
ret = re.findall(pattern, text)
if len(ret) > 0:
return ret[0]
else:
return 0 # 如果没找到,可能出现了错误,暂时返回0


cur_dir = "."
total = 0
for root, dir, files in os.walk(cur_dir):
for file in files:
log = os.path.join(root, file)
if not log.__contains__(".log"):
continue
with open(log, "r") as f:
text = f.read()
qps_text = get_qps(text)
qps_num = float(qps_text)
total += qps_num
print("total qps is : ", total)

统计wrk测试结果python脚本

脚本与统计ab的逻辑一直的,模式上略有区别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#!coding=utf-8
import os, sys
import re

"""
计算多个wrk进程测试的总qps。
具体是计算当前目录下所有log中的 "Requests/sec: 10099.83"中包含的qps, 然后相加
"""

def get_qps(text):
#pattern = r'Requests per second:\s*(.*)\s\[#/sec] \(mean\)'
#Requests/sec: 10099.83
pattern = r'Requests/sec:\s*(.*)\s*'
ret = re.findall(pattern, text)
if len(ret) > 0:
return ret[0]
else:
return 0 # 如果没找到,可能出现了错误,暂时返回0

cur_dir = "."
total = 0
for root, dir, files in os.walk(cur_dir):
for file in files:
log = os.path.join(root, file)
if not log.__contains__(".log"):
continue
with open(log, "r") as f:
text = f.read()
qps_text = get_qps(text)
qps_num = float(qps_text)
total += qps_num
print("total qps is : ", total)

参考

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2021 Johnny Li
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信