理解性能优化
1.性能优化的层次
按照我的理解可以分为需求阶段,设计阶段,实现阶段;越上层的阶段优化效果越明显,同时也更需要对业务、需求的深入理解。
1.需求阶段
需求是为了解决某个问题,问题是本质,需求是解决问题的手段。
需求讨论的前提对业务的深入了解,如果不了解业务,根本没法讨论。即使需求已经实现了,当我们发现有性能问题的时候,首先也可以从需求出发。
需求分析对性能优化有什么帮助呢,第一,为了达到同样的目的,解决同样问题,也许可以有性能更优(消耗更小)的办法。这种优化是无损的,即不改变需求本质的同时,又能达到性能优化的效果;第二种情况,有损的优化,即在不明显影响用户的体验,稍微修改需求、放宽条件,就能大大解决性能问题。PM退步一小步,程序前进一大步。
需求讨论也有助于设计时更具扩展性,应对未来的需求变化.
2.设计阶段
高手都是花80%时间思考,20%时间实现;新手写起代码来很快,但后面是无穷无尽的修bug
设计的概念很宽泛,包括架构设计、技术选型、接口设计等等。架构设计约束了系统的扩展、技术选型决定了代码实现。编程语言、框架都是工具,不同的系统、业务需要选择适当的工具集。如果设计的时候做的不够好,那么后面就很难优化,甚至需要推到重来。
3.实现阶段
实现是把功能翻译成代码的过程,这个层面的优化,主要是针对一个调用流程,一个函数,一段代码的优化。各种profile工具也主要是在这个阶段生效。除了静态的代码的优化,还有编译时优化,运行时优化。后二者要求就很高了,程序员可控性较弱。
代码层面,造成性能瓶颈的原因通常是高频调用的函数、或者单次消耗非常高的函数、或者二者的结合。
2.性能基准
2.1 响应时间
提交请求和返回该请求的响应之间使用的时间,一般比较关注平均响应时间。
常用操作的响应时间列表:
2.2 并发数
同一时刻,对服务器有实际交互的请求数。
和网站在线用户数的关联:1000个同时在线用户数,可以估计并发数在5%到15%之间,也就是同时并发数在50~150之间。
2.3 吞吐量
对单位时间内完成的工作量(请求)的量度
2.4 关系
系统吞吐量和系统并发数以及响应时间的关系:
理解为高速公路的通行状况:
吞吐量是每天通过收费站的车辆数目(可以换算成收费站收取的高速费),
并发数是高速公路上的正在行驶的车辆数目,
响应时间是车速。
车辆很少时,车速很快。但是收到的高速费也相应较少;随着高速公路上车辆数目的增多,车速略受影响,但是收到的高速费增加很快;
随着车辆的继续增加,车速变得越来越慢,高速公路越来越堵,收费不增反降;
如果车流量继续增加,超过某个极限后,任务偶然因素都会导致高速全部瘫痪,车走不动,当然后也收不着,而高速公路成了停车场(资源耗尽)。
3.常用的性能优化手段
3.1 避免过早优化
不应该把大量的时间耗费在小的性能改进上,过早考虑优化是所有噩梦的根源。
所以,我们应该编写清晰,直接,易读和易理解的代码,真正的优化应该留到以后,等到性能分析表明优化措施有巨大的收益时再进行。
但是过早优化,不表示我们应该编写已经知道的对性能不好的的代码结构。
1. 进行系统性能测试
所有的性能调优,都有应该建立在性能测试的基础上,直觉很重要,但是要用数据说话,可以推测,但是要通过测试求证。
2 寻找系统瓶颈,分而治之,逐步优化
性能测试后,对整个请求经历的各个环节进行分析,排查出现性能瓶颈的地方,定位问题,分析影响性能的的主要因素是什么?内存、磁盘IO、网络、CPU,还是代码问题?架构设计不足?或者确实是系统资源不足?
3.2 前端优化常用手段
1.浏览器/App
- 减少请求数;
- 合并CSS,Js,图片
- 使用客户端缓冲;
- 静态资源文件缓存在浏览器中,有关的属性Cache-Control和Expires
- 如果文件发生了变化,需要更新,则通过改变文件名来解决。
- 启用压缩
- 减少网络传输量,但会给浏览器和服务器带来性能的压力,需要权衡使用。
- 资源文件加载顺序
- css放在页面最上面,js放在最下面
- 减少 Cookie 传输
- cookie包含在每次的请求和响应中,因此哪些数据写入cookie需要慎重考虑
- 给用户一个提示
- 有时候在前端给用户一个提示,就能收到良好的效果。毕竟用户需要的是不要不理他。
2.CDN加速
CDN,又称内容分发网络,本质仍然是一个缓存,而且是将数据缓存在用户最近的地方。无法自行实现CDN的时候,可以考虑商用CDN服务。
- 1. 反向代理缓存
- 将静态资源文件缓存在反向代理服务器上,一般是Nginx。
- 2. WEB组件分离
- 将js,css和图片文件放在不同的域名下。可以提高浏览器在下载web组件的并发数。因为浏览器在下载同一个域名的的数据存在并发数限制。
- 3 .应用服务性能优化
3.缓存
网站性能优化第一定律:优先考虑使用缓存优化性能
缓存离用户越近越好
1.缓存的基本原理和本质
缓存是将数据存在访问速度较高的介质中。可以减少数据访问的时间,同时避免重复计算。
2.合理使用缓冲的准则
频繁修改的数据,尽量不要缓存,读写比2:1以上才有缓存的价值。
缓存一定是热点数据。
应用需要容忍一定时间的数据不一致。
缓存可用性问题,一般通过热备或者集群来解决。
缓存预热,新启动的缓存系统没有任何数据,可以考虑将一些热点数据提前加载到缓存系统。
3.解决缓存击穿:
-
1、布隆过滤器
-
2、把不存在的数据也缓存起来 ,比如有请求总是访问key = 23的数据,但是这个key = 23的数据在系统中不存在,可以考虑在缓存中构建一个( key=23 value = null)的数据。
4.分布式缓存
以集群的方式提供缓存服务,有两种实现;
-
1、需要更新同步的分布式缓存,所有的服务器保存相同的缓存数据,带来的问题就是,缓存的数据量受限制,其次,数据要在所有的机器上同步,代价很大。
-
2、每台机器只缓存一部分数据,然后通过一定的算法选择缓存服务器。常见的余数hash算法存在当有服务器上下线的时候,大量缓存数据重建的问题。所以提出了一致性哈希算法。
5.一致性哈希:
-
-
首先求出服务器(节点)的哈希值,并将其配置到0~232的圆(continuum)上。
-
-
-
然后采用同样的方法求出存储数据的键的哈希值,并映射到相同的圆上。
-
-
-
然后从数据映射到的位置开始顺时针查找,将数据保存到找到的第一个服务器上。如果超过232仍然找不到服务器,就会保存到第一台服务器上。
一致性哈希算法对于节点的增减都只需重定位环空间中的一小部分数据,具有较好的容错性和可扩展性。
-
一致性哈希算法在服务节点太少时,容易因为节点分部不均匀而造成数据倾斜问题,此时必然造成大量数据集中到Node A上,而只有极少量会定位到Node B上。为了解决这种数据倾斜问题,一致性哈希算法引入了虚拟节点机制,即对每一个服务节点计算多个哈希,每个计算结果位置都放置一个此服务节点,称为虚拟节点。具体做法可以在服务器ip或主机名的后面增加编号来实现。例如,可以为每台服务器计算三个虚拟节点,于是可以分别计算 “Node A#1”、“Node A#2”、“Node A#3”、“Node B#1”、“Node B#2”、“Node B#3”的哈希值,于是形成六个虚拟节点:同时数据定位算法不变,只是多了一步虚拟节点到实际节点的映射,例如定位到“Node A#1”、“Node A#2”、“Node A#3”三个虚拟节点的数据均定位到Node A上。这样就解决了服务节点少时数据倾斜的问题。在实际应用中,通常将虚拟节点数设置为32甚至更大,因此即使很少的服务节点也能做到相对均匀的数据分布。
4.异步
同步和异步,阻塞和非阻塞
同步和异步关注的是结果消息的通信机制
同步:同步的意思就是调用方需要主动等待结果的返回
异步:异步的意思就是不需要主动等待结果的返回,而是通过其他手段比如,状态通知,回调函数等。
阻塞和非阻塞主要关注的是等待结果返回调用方的状态
阻塞:是指结果返回之前,当前线程被挂起,不做任何事
非阻塞:是指结果在返回之前,线程可以做一些其他事,不会被挂起。
-
1.同步阻塞:同步阻塞基本也是编程中最常见的模型,打个比方你去商店买衣服,你去了之后发现衣服卖完了,那你就在店里面一直等,期间不做任何事(包括看手机),等着商家进货,直到有货为止,这个效率很低。jdk里的BIO就属于 同步阻塞
-
2.同步非阻塞:同步非阻塞在编程中可以抽象为一个轮询模式,你去了商店之后,发现衣服卖完了,这个时候不需要傻傻的等着,你可以去其他地方比如奶茶店,买杯水,但是你还是需要时不时的去商店问老板新衣服到了吗。jdk里的NIO就属于 同步非阻塞
-
3.异步阻塞:异步阻塞这个编程里面用的较少,有点类似你写了个线程池,submit然后马上future.get(),这样线程其实还是挂起的。有点像你去商店买衣服,这个时候发现衣服没有了,这个时候你就给老板留给电话,说衣服到了就给我打电话,然后你就守着这个电话,一直等着他响什么事也不做。这样感觉的确有点傻,所以这个模式用得比较少。
-
4.异步非阻塞:好比你去商店买衣服,衣服没了,你只需要给老板说这是我的电话,衣服到了就打。然后你就随心所欲的去玩,也不用操心衣服什么时候到,衣服一到,电话一响就可以去买衣服了。jdk里的AIO就属于异步
常见异步的手段
-
Servlet异步
servlet3中才有,支持的web容器在tomcat7和jetty8以后。 -
多线程
-
消息队列
5.集群
可以很好的将用户的请求分配到多个机器处理,对总体性能有很大的提升
6. 程序
1.代码级别
一个应用的性能归根结底取决于代码是如何编写的。
-
选择合适的数据结构
-
选择更优的算法
-
编写更少的代码
2.并发编程
-
充分利用CPU多核,
-
实现线程安全的类,避免线程安全问题
参考 JMM和底层实现原理
参考 JMM的思考 -
同步下减少锁的竞争
参考 并发安全
3.资源的复用
目的是减少开销很大的系统资源的创建和销毁,比如数据库连接,网络通信连接,线程资源等等。
-
单例模式
-
池化技术
线程池、数据库连接池
7.GC调优
1.目的
GC的时间够小
GC的次数够少
发生Full GC的周期足够的长,时间合理,最好是不发生。
2.调优的原则和步骤
1、 大多数的java应用不需要GC调优
2、 大部分需要GC调优的的,不是参数问题,是代码问题
3、 在实际使用中,分析GC情况优化代码比优化GC参数要多得多;
4、 GC调优是最后的手段
GC调优的最重要的三个选项:
第一位:选择合适的GC回收器
第二位:选择合适的堆大小
第三位:选择年轻代在堆中的比重
步骤
-
step1.监控GC的状态
使用各种JVM工具,查看当前日志,分析当前JVM参数设置,并且分析当前堆内存快照和gc日志,根据实际的各区域内存划分和GC执行时间,觉得是否进行优化; -
step2.分析结果,判断是否需要优化
如果各项参数设置合理,系统没有超时日志出现,GC频率不高,GC耗时不高,那么没有必要进行GC优化;如果GC时间超过1-3秒,或者频繁GC,则必须优化;
注意:如果满足下面的指标,则一般不需要进行GC:
Minor GC执行时间不到50ms;
Minor GC执行不频繁,约10秒一次;
Full GC执行时间不到1s;
Full GC执行频率不算频繁,不低于10分钟1次; -
step3.调整GC类型和内存分配
如果内存分配过大或过小,或者采用的GC收集器比较慢,则应该优先调整这些参数,并且先找1台或几台机器进行beta,然后比较优化过的机器和没有优化的机器的性能对比,并有针对性的做出最后选择; -
step4.不断的分析和调整
通过不断的试验和试错,分析并找到最合适的参数 -
step5.全面应用参数
如果找到了最合适的参数,则将这些参数应用到所有服务器,并进行后续跟踪。
2-3)学会阅读GC日志
以参数-Xms5m -Xmx5m -XX:+PrintGCDetails -XX:+UseSerialGC为例:
[DefNew: 1855K->1855K(1856K), 0.0000148 secs][Tenured: 2815K->4095K(4096K), 0.0134819 secs] 4671K
DefNew指明了收集器类型,而且说明了收集发生在新生代。
1855K->1855K(1856K)表示,回收前 新生代占用1855K,回收后占用1855K,新生代大小1856K。
0.0000148 secs 表明新生代回收耗时。
Tenured表明收集发生在老年代
2815K->4095K(4096K), 0.0134819 secs:含义同新生代
最后的4671K指明堆的大小。
收集器参数变为-XX:+UseParNewGC,日志变为:
[ParNew: 1856K->1856K(1856K), 0.0000107 secs][Tenured: 2890K->4095K(4096K), 0.0121148 secs]
收集器参数变为-XX:+ UseParallelGC或UseParallelOldGC,日志变为:
[PSYoungGen: 1024K->1022K(1536K)] [ParOldGen: 3783K->3782K(4096K)] 4807K->4804K(5632K),
CMS收集器和G1收集器会有明显的相关字样
2-4)其他与GC相关的参数
调试跟踪之 打印简单的GC信息 参数: -verbose:gc, -XX:+PrintGC
打印详细的GC信息 -XX:+PrintGCDetails, +XX:+PrintGCTimeStamps
-Xlogger:logpath 设置gc的日志路,如: -Xlogger:log/gc.log, 将gc.log的路径设置到当前目录的log目录下.
应用场景: 将gc的日志独立写入日志文件,将GC日志与系统业务日志进行了分离,方便开发人员进行追踪分析。
-XX:+PrintHeapAtGC, 打印推信息
参数设置: -XX:+PrintHeapAtGC
应用场景: 获取Heap在每次垃圾回收前后的使用状况
-XX:+TraceClassLoading
参数方法: -XX:+TraceClassLoading
应用场景: 在系统控制台信息中看到class加载的过程和具体的class信息,可用以分析类的加载顺序以及是否可进行精简操作。
-XX:+DisableExplicitGC禁止在运行期显式地调用System.gc()
-XX:-HeapDumpOnOutOfMemoryError 默认关闭,建议开启,在java.lang.OutOfMemoryError 异常出现时,输出一个dump.core文件,记录当时的堆内存快照。
-XX:HeapDumpPath=./java_pid<pid>.hprof 默认是java进程启动位置,用来设置堆内存快照的存储文件路径。
2-5)推荐策略
-
年轻代大小选择
-
响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择).在此种情况下,年轻代收集发生的频率也是最小的.同时,减少到达年老代的对象.
-
吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度.因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用.
-
避免设置过小.当新生代设置过小时会导致:1.YGC次数更加频繁 2.可能导致YGC对象直接进入旧生代,如果此时旧生代满了,会触发FGC.
2.年老代大小选择
-
响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数.如果堆设置小了,可以会造成内存碎 片,高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间.最优化的方案,一般需要参考以下数据获得: 并发垃圾收集信息、持久代并发收集次数、传统GC信息、花在年轻代和年老代回收上的时间比例。
吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代.原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象
3)调优实战
不同的内存大小
不同的GC回收器
3.3 存储性能优化
3.3.1 尽量使用SSD
3.3.2 定时清理数据或者按数据的性质分开存放
3.3.3 结果集处理
用setFetchSize控制jdbc每次从数据库中返回多少数据。