提交请求和返回该请求的响应时间之间使用的时间,一般比较关注平均响应时间。常用操作的响应时间列表:
操作 | 响应时间 |
---|---|
打开一个站点 | 几秒 |
数据库查询一条记录(有索引) | 十几毫秒 |
机械磁盘一次寻址定位 | 4毫秒 |
从机械磁盘顺序读取1M数据 | 2毫秒 |
从SSD磁盘顺序读取1M数据 | 0.3毫秒 |
从远程分布式换成Redis读取一个数据 | 0.5毫秒 |
从内存读取1M数据 | 十几微妙 |
Java程序本地方法调用 | 几微妙 |
网络传输2Kb数据 | 1微妙 |
同一时刻。对服务器有实际交互请求的请求数。和网站在线用户数的关联:1000个同时在线用户数,可以估计并发数在5%到15%之间,也就是同时并发数在50~150之间。
对单位时间内完成的工作量(请求)的量度。
系统吞吐量和系统并发数以及响应时间的关系,可以理解为高速公路的通行状况:
车辆很少时,车速很快。但是收到的高速费也相应较少;随着高速公路上车辆数目的增多,车速略受影响,但是收到的高速费增加很快;(并发少时,响应就快同时也就是说吞吐量更低;当并发增多时,响应时间也会略受影响,吞吐量也就逐步增多)
随着车辆的继续增加,车速变得越来越慢,高速公路越来越堵,收费不增反降;如果车流量继续增加,超过某个极限后,任务偶然因素都会导致高速全部瘫痪,车走不动,当然后也收不着,而高速公路成了停车场(资源耗尽)。(随着并发继续增加,响应时间会越来越慢,服务器负载也就越高,这时的吞吐量将不增反减;如果并发数继续增加,当超过某个极限后,服务器可能会崩溃,导致各服务无法运行)
不应该把大量的时间耗费在小的性能改进上,过早考虑优化是所有噩梦的根源。所以,我们应该编写清晰,直接,易读和易理解的代码,真正的优化应该留到以后,等到性能分析表明优化措施有巨大的收益时再进行。
所有的性能调优,都有应该建立在性能测试的基础上,直觉很重要,但是要用数据说话,可以推测,但是要通过测试求证。
性能测试后,对整个请求经历的各个环节进行分析,排查出现性能瓶颈的地方,定位问题,分析影响性能的的主要因素是什么?内存、磁盘IO、网络、CPU,还是代码问题?架构设计不足?或者确实是系统资源不足?
CDN,又称内容分发网络,本质仍然是一个缓存,而且是将数据缓存在用户最近的地方。无法自行实现CDN的时候,可以考虑商用CDN服务。
将静态资源文件缓存在反向代理服务器上,一般是Nginx。
将js,css和图片文件放在不同的域名下。可以提高浏览器在下载web组件的并发数。因为浏览器在下载同一个域名的的数据存在并发数限制。
网站性能优化第一定律:优先考虑使用缓存优化性能。(缓存离用户越近越好)
缓存是将数据存在访问速度较高的介质中。可以减少数据访问的时间,同时避免重复计算。
解决缓存击穿:
- 布隆过滤器
- 把不存在的数据也缓存起来 ,比如有请求总是访问 key = 23 的数据,但是这个key = 23 的数据在系统中不存在,可以考虑在缓存中构建一个( key=23 value = null)的数据。
以集群的方式提供缓存服务,有两种实现:
一致性哈希算法对于节点的增减都只需重定位环空间中的一小部分数据,具有较好的容错性和可扩展性。
一致性哈希算法在服务节点太少时,容易因为节点分部不均匀而造成数据倾斜问题,此时必然造成大量数据集中到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甚至更大,因此即使很少的服务节点也能做到相对均匀的数据分布。
同步和异步关注的是结果消息的通信机制
同步:同步的意思就是调用方需要主动等待结果的返回
异步:异步的意思就是不需要主动等待结果的返回,而是通过其他手段比如,状态通知,回调函数等。
阻塞和非阻塞主要关注的是等待结果返回调用方的状态
阻塞:是指结果返回之前,当前线程被挂起,不做任何事
非阻塞:是指结果在返回之前,线程可以做一些其他事,不会被挂起。
常见阻塞模型
@Test
public void buildTest() {
List
选择ArrayList和LinkedList对我们的程序性能影响很大,为什么?因为ArrayList内部是数组实现,存在着不停的扩容和数据复制。
举个例子,最大子列和问题:
给定一个整数序列,a0, a1, a2, …… , an(项可以为负数),求其中最大的子序列和。如果所有整数都是负数,那么最大子序列和为0;例如(a[1],a[2],a[3],a[4],a[5],a[6])=(-2,11,-4,13,-5,-2)时,最大子段和为20,子段为a[2],a[3],a[4]。
n越大,时间就相差越大,比如10000个元素,最坏的算法和最好的算法之间的差距绝非多线程或者集群化能轻松解决的。
同样正确的程序,小程序比大程序要快,这点无关乎编程语言。
对JVM性能影响最大的是编译器。选择编译器是运行java程序首先要做的选择之一
对于程序来说,通常只有一部分代码被经常执行,这些关键代码被称为应用的热点,执行的越多就认为是越热。将这些代码编译为本地机器特定的二进制码,可以有效提高应用性能。
缺省编译器取决于机器位数、操作系统和CPU数目。32位的机器上,一般默认都是client编译,64位机器上一般都是server编译,多核机器一般是server编译。
图中的mix mode 一般指编译时机:
在编译后,会有一个代码缓存保存编译后的代码,一旦这个缓存满了,jvm将无法继续编译代码。当jvm提示: CodeCache is full,就表示需要增加代码缓存大小。
–XX:ReservedCodeCacheSize=N //可以用来调整这个大小
代码是否进行编译,取决于代码执行的频度,是否到达编译阈值。计数器有两种,分别是:
一个方法是否达到编译阈值取决于方法中的两种计数器之和。编译阈值调整的参数为:
-XX:CompileThreshold=N
方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,即一段时间之内方法被调用的次数。当超过一定的时间限度,如果方法的调用次数仍然不足以让它提交给即时编译器编译,那这个方法的调用计数器就会被减少一半,这个过程称为方法调用计数器热度的衰减(Counter Decay),而这段时间就称为此方法统计的半衰周期(Counter Half Life Time)。进行热度衰减的动作是在虚拟机进行垃圾收集时顺便进行的,可以使用虚拟机参数:
-XX:-UseCounterDecay
来关闭热度衰减,让方法计数器统计方法调用的绝对次数,这样,只要系统运行时间足够长,绝大部分方法都会被编译成本地代码。另外,可以使用
-XX:CounterHalfLifeTime
参数设置半衰周期的时间,单位是秒。与方法计数器不同,回边计数器没有计数热度衰减的过程,因此这个计数器统计的就是该方法循环执行的绝对次数。
内联默认开启,-XX:-Inline,可以关闭,但是不要关闭,一旦关闭对性能有巨大影响。方法是否内联取决于方法有多热和方法的大小,很热的方法如果方法字节码小于325字节才会内联,这个大小由参数:-XX:MaxFreqInlinesSzie=N 调整,但是这个很热与热点编译不同,没有任何参数可以调整热度。
方法小于35个字节码,一定会内联,这个大小可以通过参数-XX:MaxInlinesSzie=N 调整。
GC调优的最重要的三个选项:
调优步骤:
使用各种JVM工具,查看当前日志,分析当前JVM参数设置,并且分析当前堆内存快照和gc日志,根据实际的各区域内存划分和GC执行时间,觉得是否进行优化;
如果各项参数设置合理,系统没有超时日志出现,GC频率不高,GC耗时不高,那么没有必要进行GC优化;如果GC时间超过1-3秒,或者频繁GC,则必须优化;
注意:如果满足下面的指标,则一般不需要进行GC:
如果内存分配过大或过小,或者采用的GC收集器比较慢,则应该优先调整这些参数,并且先找1台或几台机器进行beta,然后比较优化过的机器和没有优化的机器的性能对比,并有针对性的做出最后选择;
通过不断的试验和试错,分析并找到最合适的参数
如果找到了最合适的参数,则将这些参数应用到所有服务器,并进行后续跟踪。
以参数 -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)
年轻代大小选择
响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择).在此种情况下,年轻代收集发生的频率也是最小的.同时,减少到达年老代的对象.
吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度.因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用.
避免设置过小:当新生代设置过小时会导致:
老年代大小选择
响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数.如果堆设置小了,可以会造成内存碎片,高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间.最优化的方案,一般需要参考以下数据获得:
吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代.原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象