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
插件中心对应的URL:http://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.几个常用工具
- Arthas:https://arthas.aliyun.com/zh-cn/
- GCeasy:https://gceasy.io/
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 Time:RT)
响应时间指用户从客户端发起一个请求开始,到客户端接收到从服务器返回的响应结束,整个过程所耗费的时间。
HPS(Hits Per Second)每秒点击次数,单位是次/秒
TPS(Transaction Per Seconde)系统每秒处理交易数,单位是笔/秒
QPS(Query 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、性能测试工具
常用的性能测试工具有很多,在这里列举几个比较实用的。对于开发人员来说,首选是一些开源免费的性能(压力)测试软件,例如ab(ApacheBench)、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压测结果对比
- 未加索引和代码逻辑未优化
QPS:5/s - pms_category 表给 parent_cid 字段加索引
QPS:25/s - 优化代码逻辑
QPS:395/s