第16天-性能压测:压力测试,性能监控,优化QPS,Nginx动静分离
阿里云国内75折 回扣 微信号:monov8 |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |
1.性能监控
1.1.JVM架构
运行时数据区
-
方法区最重要的内存区域多线程共享保存了类的信息名称、成员、接口、父类反射机制是重要的组成部分动态进行类操作的实现
-
堆内存Heap保存对象的真实信息该内存牵扯到释放问题GC
-
栈内存Stack线程的私有空间在每一次进行方法调用的时候都会存在有栈帧采用先进后出的设计原则
1、本地变量表局部参数或形参允许保存有32位的插槽Solt如果超过了32位的长度就
需要开辟两个连续性的插槽long、double—— volatile关键字问题
2、操作数栈执行所有得方法计算操作
3、常量池引用String类实例、Integer类实例
4、返回地址方法执行完毕后的恢复执行的点 -
程序计数器执行指令的一个顺序编码该区域的所占比率几乎可以忽略
-
本地方法栈与栈内存功能类似区别在于是为本地方法服务的
1.2.堆
所有的对象实例以及数组都要在堆上分配。堆是垃圾收集器管理的主要区域也被称为 GC堆 是优化最多考虑的地方。
堆可以细分为
-
新生代
1、Eden 空间
2、From Survivor 空间S0
3、To Survivor 空间S1 -
老年代
-
永久代/元空间
JDK8以前永久代受JVM管理JDK8以后元空间直接使用物理内存。因此默认情况下元空间的大小仅受本地内存限制。
1.3.GC
1.3.1.GC流程
Oracle官网https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html
1.3.2.GC收集器比较
- JDK8默认Parallel Scavenge
- JDK9默认G1
1.4.jconsole与jvisualvm
JDK的两个小工具jconsole、jvisualvm升级版的jconsole通过命令行启动可监控本地和远程应用。
1.4.1.jvisualvm能干什么
监控内存泄漏跟踪垃圾回收执行时内存、CPU分析线程分析…
线程状态
- 运行正在运行的
- 休眠sleep方法
- 等待wait方法
- 驻留线程池里面的空闲线程
- 监视阻塞的线程正在等待锁
1.4.2.安装插件方便查看gc
工具 -> 插件 -> 可用插件->Visual GC
插件中心对应的URLhttp://visualvm.github.io/pluginscenters.html
1.5.监控指标
1.5.1.中间件指标
Nginx
docker stats
每秒会动态刷新下面的监控数据
添加Nginx访问取样器
压测
Gateway
添加取样器
压测
1.5.2.数据库指标
- SQL耗时越小越好一般情况下微妙级别
- 命中率越高越好一般情况下不能低于95%
- 锁等待次数越低越好等待时间越短越好
1.5.3.JMeter压测报告分析
中间件越多性能损失越大大多都损失在网络交互了
业务逻辑
- 数据库MySQL优化上线关闭SQL日志
- 模板的渲染速度开发环境是关闭缓存的生产环境开启缓存
- 静态资源Nginx动静分离
线上OOM演示服务崩溃
- 将应用 VM options调整为 -Xmx100m
- 使用JMeter设置200个线程进行压测
- 应用后台抛出 OOM 异常系统不能正常访问
优化
- 调整vm参数-Xmx1024m -Xms1024m -Xmn512m
- 修改业务实现代码减少数据库访问次数
1.6.JVM分析与调优
1.6.1.几个常用工具
1.6.2.命令示例
JDK监控和故障处理命令有
- jps
- jstat
- jmap
- jhat
- jstack
- jinfo
1.6.3.jmap生成dump
jps
# pid通过jps可以查看到进程id
jmap -dump:live,format=b,file=c:\test.dump <pid>
分析使用jvisualvm导入dump文件进行分析
1.6.4.JVM调优项
常用 JVM 参数
- -Xms 初始堆大小默认为物理内存的1/64(<1GB)默认(MinHeapFreeRatio参数可以调 整)空余堆内存小于40%时JVM就会增大堆直到-Xmx的最大限制
- -Xmx 最大堆大小默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时JVM 会减少堆直到 -Xms的最小限制
- -Xmn 新生代的内存空间大小注意此处的大小是eden+ 2 survivor space)。与jmap - heap中显示的New gen是不同的。整个堆大小=新生代大小 + 老生代大小 + 永久代大小。在保
证堆大小不变的情况下增大新生代后,将会减小老生代大小。此值对系统性能影响较大,Sun 官方推荐配置为整个堆的3/8。- -XX:SurvivorRatio 新生代中Eden区域与Survivor区域的容量比值默认值为8。两个 Survivor区与一个Eden区的比值为2:8一个Survivor区占整个年轻代的1/10。
- -Xss 每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M以前每个线程堆栈大小 为256K。应根据应用的线程所需内存大小进行适当调整。在相同物理内存下减小这个值能
生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的不能无限生成经验值 在3000~5000左右。一般小的应用 如果栈不是很深 应该是128k够用的大的应用建议使
用256k。这个选项对性能影响比较大需要严格的测试。和threadstacksize选项解释很类
似官方文档似乎没有解释在论坛中有这样一句话:"-Xss is translated in a VM flag named
ThreadStackSize”一般设置这个值就可以了。
Spring Boot 部署运行方案
使用默认JVM配置运行
- 前台运行关闭窗口后退出java -jar /jar包路径
- 后台运行nohup java -jar /jar包路径
- #后台运行指定启动日志记录文件nohub java -jar /jar包路径 > /指定日志文件路径
配置JVM参数运行
- 前台运行
java -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=128m -Xms1024m -Xmx1024m - Xmn256m -Xss256k -XX:SurvivorRatio=8 -XX:+UseConcMarkSweepGC -jar /jar包路径 - 后台运行
nohup java -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=128m -Xms1024m -Xmx1024m -Xmn256m -Xss256k -XX:SurvivorRatio=8 -XX:+UseConcMarkSweepGC -jar /jar包路径
JVM参数说明
- -XX:MetaspaceSize=128m 元空间默认大小
- -XX:MaxMetaspaceSize=128m 元空间最大大小
- -Xms1024m 初始化堆大小
- -Xmx1024m 最大堆大小
- -Xmn256m 新生代大小
- -Xss256k 栈最大深度大小
- -XX:SurvivorRatio=8 新生代分区比例 8:2
- -XX:+UseConcMarkSweepGC 指定使用的垃圾收集器这里使用CMS收集器
知识点
JDK8之后把-XX:PermSize 和 -XX:MaxPermGen移除了取而代之的是-XX:MetaspaceSize=128m 元空间默认大小-XX:MaxMetaspaceSize=128m 元空间最大大小
JDK 8开始把类的元数据放到本地化的堆内存(native heap)中这一块区域就叫Metaspace中文名叫元空间。
使用本地化的内存有什么好处呢
最直接的表现就是java.lang.OutOfMemoryError: PermGen空间问题将不复存在因为默认的类的 元数据分配只受本地内存大小的限制也就是说本地内存剩余多少理论上Metaspace就可以有多
大貌似容量还与操作系统的虚拟内存有关这解决了空间不足的问题。不过让Metaspace变
得无限大显然是不现实的因此我们也要限制Metaspace的大小使用-XX:MaxMetaspaceSize参
数来指定Metaspace区域的大小。JVM默认在运行时根据需要动态地设置MaxMetaspaceSize的大 小。
2.压力测试
压力测试考察当前硬件环境下系统所能承受的最大负荷并帮助找出系统瓶颈所在。压测都是为了系统在线上的处理能力和稳定性维持在一个标准范围之内做到心中有数。
使用压力测试我们有希望找到很多种用其它测试方法更难发现的错误。有两种错误类型是内存泄漏并发与同步。
有效的压力测试系统将应用以下这些关键条件重复并发量级随机变化。
2.1.性能指标
响应时间Response TimeRT
响应时间指用户从客户端发起一个请求开始到客户端接收到从服务器返回的响应结束整个过程所耗费的时间。
HPSHits Per Second每秒点击次数单位是次/秒
TPSTransaction Per Seconde系统每秒处理交易数单位是笔/秒
QPSQuery Per Second系统每秒处理查询次数单位是次/秒
对于互联网业务中如果某些业务有且仅有一个请求连接那么TPS=QPS=HPS一般情况下使用TPS来衡量整个业务流程用QPS来衡量接口查询次数用HPS来表示对服务器单机请求。
无论TPD、QPS、HPS这些指标是衡量系统处理能力非常重要的指标越大越好根据经验一般情况下
金融行业1000TPS ~ 50000TPS不包括互联网化的活动比如秒杀营销活动等
保险行业100TPS ~100000TPS不包括互联网化的活动比如秒杀营销活动等
制造行业10TPS ~ 5000TPS
电商网站10000TPS ~1000000TPS
中型网站1000TPS ~ 50000TPS
小型网站500TPS ~ 10000TPS
最大响应时间Max Response Time指用户发出请求或者指令到系统做出反应响应的最大时间
最少响应时间Mininum Response Time指用户发出请求或者指令到系统做出反应响应的最少时间
90%响应时间90% Response Time指所有用户的响应时间进行排序第90%的响应时间
从外部看性能测试主要关注如下三个指标
吞吐量每秒系统能够处理的请求数、任务数
响应时间服务处理一个请求或一个任务的耗时
错误率一批请求中结果出错的请求所占比例
影响性能考虑点包括
- 数据库、应用程序、中间件Tomcat、Nginx等、网络和操作系统等方面
- 首先考虑自己的应用属于CPU密集型还是IO密集型
2.2、性能测试工具
常用的性能测试工具有很多在这里列举几个比较实用的。对于开发人员来说首选是一些开源免费的性能压力测试软件例如abApacheBench、JMeter 等对于专业的测试团队来说付费版的LoadRunner 是首选。当然也有很多公司是自行开发了一套量身定做的性能测试软件优点是定制化强缺点则是通用性差。
3.JMeter
3.1.JMeter安装
- 官网下载https://jmeter.apache.org/download_jmeter.cgi
- 解压安装运行 jmeter.bat
3.2、JMeter基本使用
3.2.1.新建测试计划
3.2.2.添加线程组
3.2.3.添加取样器
3.2.4.添加监听器
3.2.5.启动压测&查看分析
3.3.Address Already in use 错误解决
原因windows本身提供的端口访问机制的问题windows提供给 TCP/IP 连接的端口为 1024 - 5000并且要四分钟来循环回收就导致我们在短时间内跑大量的请求时将端口占满了。
解决方案
-
Win+R打开运行窗口输入 regedit 命令打开注册表
-
在HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters
下1、右击Parameters添加一个新的DWORD名字为 MaxUserPort双击 MaxUserPort输入数值数据为 65534基数选择十进制若分布式运行控制机器和负载机器都需要这样操作
2、右击Parameters添加一个新的DWORD名字为 TCPTimedWaitDelay双击
TCPTimedWaitDelay输入数值数据为 30基数选择十进制若分布式运行控制机器和负载机器都需要这样操作 -
修改配置完毕后需要重启机器才会生效
4.Nginx动静分离
4.1.架构
- 将所有项目的静态资源都需放在nginx里面
- 规则/nginx/html/static/**
- 所有静态资源请求都统一由nginx直接返回
4.2.静态资源迁移
- 将 gmall-product\resources\static\index 整个文件夹上传到
/mydata/nginx/html/static - 删除 gmall-product\resources\static\index 整个文件夹
4.3.修改index.html模板页面
在所有模板页面用到的静态资源统一加上 /static/ 前置路径
4.4.Nginx配置
gmall.conf
location /static/ {
root /usr/share/nginx/html;
}
重启nginx容器
docker restart nginx
4.5.首页全量数据获取压测
即使提高压测线程数也不会造成JMeter卡死情况
5.三级分类数据获取优化
5.1.优化代码实现逻辑
优化业务实现的代码逻辑将数据库的多次查询变为一次
/**
* 查询首页展示分类列表
* @return
*/
@Override
public Map<String, List<Catalog2VO>> getCatalogJson() {
// 查询出所有的分类数据
List<CategoryEntity> entities = list(null);
// 查询所有一级分类
List<CategoryEntity> level1Categories = getParents(entities, 0L);
// 封装数据
Map<String, List<Catalog2VO>> map = level1Categories.stream().collect(
Collectors.toMap(k -> k.getCatId().toString(), v -> {
// 查询当前一级分类的所有二级分类封装成vo
List<CategoryEntity> level2Categories = getParents(entities, v.getCatId());
List<Catalog2VO> catalog2VOS = null;
if (level2Categories != null) {
catalog2VOS = level2Categories.stream().map(category2 -> {
Catalog2VO catalog2VO = new Catalog2VO(
v.getCatId().toString(),
null,
category2.getCatId().toString(),
category2.getName());
// 查询当前二级分类的所有三级分类封装为vo
List<CategoryEntity> level3Categories = getParents(entities, category2.getCatId());
if (level3Categories != null) {
List<Catalog2VO.Catalog3VO> catalog3VOS = level3Categories.stream().map(catalog3 -> {
Catalog2VO.Catalog3VO catalog3VO = new Catalog2VO.Catalog3VO(
category2.getCatId().toString(),
catalog3.getCatId().toString(),
catalog3.getName());
return catalog3VO;
}).collect(Collectors.toList());
catalog2VO.setCatalog3List(catalog3VOS);
}
return catalog2VO;
}).collect(Collectors.toList());
}
return catalog2VOS;
}));
return map;
}
/**
* 根据分类父id查找所有的子分类
* @param categories 所有分类数据
* @param parentId 分类父id
* @return
*/
private List<CategoryEntity> getParents(List<CategoryEntity> categories, Long parentId) {
List<CategoryEntity> entities = categories.stream()
.filter(categoryEntity -> categoryEntity.getParentCid().equals(parentId))
.collect(Collectors.toList());
return entities;
}
5.2.JMeter压测结果对比
- 未加索引和代码逻辑未优化
QPS5/s - pms_category 表给 parent_cid 字段加索引
QPS25/s - 优化代码逻辑
QPS395/s