调优概述

web服务器重要的性能参数:响应时间、并发数、吞吐量
 

程序的性能主要体现在

  • 执行速度:关系到响应时间
  • 内存分配:是否过多消耗内存、是否可能发生内存泄漏
  • 启动时间
  • 负载承受能力:系统压力上升时,系统响应时间的上升曲线是否平缓
     

常见的系统瓶颈

  • 硬件资源:cpu、内存、硬盘、网络带宽
  • 数据库
  • 缓存
  • 程序本身:锁竞争、服务调用、IO阻塞等
     

常见的调优层次

  • 设计调优:在开发前进行项目设计时就考虑到可能潜在的性能问题,设计出合理的方案
  • 代码调优:开发过程中,选择合适的API、相关类库、数据结构、算法
  • JVM调优:软件开发后期进行,比如设计堆的大小、使用的垃圾收集器等
  • 数据库调优:主要设计3方面,①对sql语句优化,使用列名代替*号,使用索引②对数据库本身进行优化,eg.合理使用冗余字段,对大表进行拆分;③对数据库软件进行优化,合理设置连接数、缓冲区大小等
  • 操作系统调优
     

木桶理论:系统性能取决于短板,短板是系统的性能瓶颈,优化短板

优化过程:确定系统性能瓶颈 -> 优化 -> 测试

 

性能瓶颈定位

单接口问题

如果只是个别接口出现问题,可以考虑以下方面

  • 是否使用了缓存(redis)、还是直接访问数据库,有没有缓存穿透(缓存中没有、到数据库去查)
  • 数据库调优:explain select * from tb_user; 查看数据库操作各步骤的时间开销,针对性地优化
     

全接口问题

如果中大量接口都有这种问题,可以从以下方面考虑

  • jvm问题,是否频繁进行Full GC,频繁Full GC会拉低性能,如果频繁Full GC说明设置的最大堆内存不够,可以设置大一些;如果系统卡顿、响应时间都偏长,可能是设置的最大堆内存过大,造成Full GC耗时很长、系统停顿时间长,可以将最大堆内存调小一些,调小之后内存可能不够,可以做tomcat集群来分担压力、提高性能

 

设计调优

使用合适的设计模式

  • 单例模式:对于频繁使用的对象,①减少了创建的时间开销,②减少了对象数量 -> 内存占用 -> GC引起的停顿时间
  • 享元模式:复用大对象(重量级对象),减少内存内用、创建的时间开销

 

组件优化

  • 使用缓冲,常用于IO操作
  • 使用缓存组件
  • 对象复用:池化技术,常见的比如线程池、连接池。使用线程池时,核心线程数根据应用中使用了多少个线程池、业务场景大概需要多少线程确定,一般不超过cpu核心数。
  • 并行,多线程异步执行
  • 集群+负载均衡,分散负载压力
  • 系统资源有限,可以时间换空间
  • 空间换时间,提高执行速度

 

代码调优

字符串优化

1、连接字符串时,尽量用String的concat()方法代替+、+=,concat()效率远高于+、+=,尤其是在构建超大字符串的时候
 
2、尽量用StringBuffer、StringBuilder代替String,如果无需考虑线程安全,使用StringBuilder;如果要考虑线程安全,使用StringBuffer
 
3、String、StringBuffer、StringBuilder内部都使用一个char[ ]存储字符,String的char[ ]使用final修饰,不可变。

对StringBuffer、StringBuilder而言,char[ ]容量不够时会自动扩容:申请一个新数组,把原数组中的内容复制到新数组中,这2个类的构造函数都可以指定cahr[ ]容量,如果能预知字符串长度,尽量在构造函数中指定容量,避免频繁的内存复制

 

集合优化

List集合
实现了RandomAccess接口的类可以随机访问,jdk自带的、基于数组的数据类型都实现了RandomAccess接口,典型的比如ArrayList、Vector。

ArrayList、Vector都是基于数组的,性能差不多,LinkedList基于双向链表。Vector专用于实现栈,很多方法都使用了synchronized修饰,线程安全。

ArrayList基于数组,数组的特点是随机访问,查询速度快,但增、删元素时需要移动很多元素。

元素个数很少时,性能无明显差别;元素个数很多时,如果操作以查询、更新为主,使用ArrayList,如果以增、删元素为主,使用LinkedList。
 

Map集合

常用的有HashMap、LinkedHashMap、TreeMap、Hashtable、Properties。

HashMap是计算key的hashCode,将key存储在哈希表中,底层数据结构使用的是数组。HashMap性能很高。

LinkedHashMap是HashMap的子类,内部维护了一个链表,元素(key)按照添加顺序排列。

TreeMap实现了SortedMap接口,内部维护一棵红黑树,可对元素进行排序,性能略低于HashMap,但性能仍然很高。

如果要对元素排序,使用TreeMap;如果要将元素按照添加顺序排列,使用LinkedHashMap;无要求的,使用HashMap。

Hashtable线程安全,但毛病很多,一般不用,如果要考虑线程安全,可以使用juc下的类。

Properties是Hashtable的子类,使用较多。

 

Set集合

元素不可重复,常用的有HashSet、LinkedHashSet、TreeSet,分别对应HashMap、LinkedHashMap、TreeMap,不再详述。

HashSet内部使用HashMap存储元素,元素作为HashMap的key存储,value指向同一个Object对象。

 

hash系列集合

哈希表底层是基于数组的,随机访问,性能很高。一个数组元素位置就是一个bucket,哈希表的理想情况是一个bucket中只存储一个元素,此时hash表性能最好,可以根据hashCode直接定位元素(所在的bucket)。

如果发生哈希冲突,一个bucket中存储多个元素(一个类的多个实例),会以链表的形式存储(数组存储链表的头结点),根据hashCode找到bucket后,还需要遍历链表找到指定的元素。

1、为减少哈希冲突、保证性能,一个bucket中只存储一个元素,如果哈希表中要存储自定义的类的实例,需要我们给自定义的类重写hashCode()、equals()。
 
2、hash系列集合的构造函数都可以指定capacity、loadFactor,当哈希表中的bucket的使用量达到loadFactor,会重哈希(再散列):新开辟一个容量更大的哈希表,将原哈希表中的元素复制到新哈希表中。

重哈希很耗时,loadFactor一般使用默认的0.75,如果创建hash系列集合时可以预知元素的大概数量,尽量指定容量,元素的大概数量÷0.75,再往上加一点。
 

集合遍历

for each(增强的for循环)实际使用的还是迭代器,只是迭代器的语法糖,还多了给临时变量赋值的操作,性能不如迭代器,尽量使用迭代器代替for each。

 

IO优化

传统的BIO是阻塞式的,性能低下;NIO是异步IO,性能很高。
NIO中的2个重要组件:Buffer 缓冲、Channel 通道。

 

减少循环中的活动

// 差的代码,每次都要执行arr.length、temp
for (int i = 0; i < arr.length; i++) {
	int temp;
}


// 优化
int length=arr.length;
int temp;
for (int i = 0; i < length; i++) {

}

 

避免不必要的变量

如果不打算存储某个值(后续不再使用),就不要为这个值创建变量。

// 差的代码
var fullName = firstName + " " + lastName;
document.getElementById("info").innerHTML = fullName; 

// 如果后续不再使用fullName,可以如下优化,减少了创建变量fullName的内存开销、时间开销
document.getElementById("info").innerHTML = firstName + " " + lastName

 

数据库调优

  • 一主多从,读写分离,主库写,从库读
  • 分库分表,冷热数据分离,分散数据库存储压力。但分库分表后存在很多多表关联查询,需要解决多个库之间的分布式事务问题,慎重考虑。
  • 数据库软件本身优化,比如调整连接数、缓冲区大小
  • 表设计优化、sql语句优化,比如使用索引

 

sql语句调优思路

  • 根据慢查询日志定位慢查询
  • 使用explain|desc分析慢查询
  • 修改慢sql语句,尽量让慢sql走索引

 

sql语句调优的考虑点

  • 如果是嵌套了子查询、关联了很多表的复杂查询,mysql难以对sql语句进行优化,是否可以拆分为多个查询语句
  • 记录数是否太多,是否需要分库分表、冷热数据分离
  • select选取的字段、结果集中的行数是否太多
  • 是否走了索引,索引设计是否合理,sql语句中索引字段的使用是否合理

 

慢查询日志的配置

-- 查看查询相关设置
show variables like '%quer%';

主要关注以下配置项

  • slow_query_log:是否开启慢查询日志,默认OFF
  • slow_query_log_file:慢查询日志的保存路径
  • long_query_time:慢查询阈值,默认10秒
  • log_queries_not_using_indexes:是否记录未走索引的查询,默认OFF。启用此项时,只要查询没走索引,不管达没达到慢查询阈值,都会记录到慢查询日志中。
     

配置项可以直接用mysql的set命令修改,但此种方式在重启mysql后会失效,不推荐

set global slow_query_log=on;
--需要mysql对这个文件有写的权限
set global slow_query_log_file=/usr/local/mysql/log/slow_query.log;
--慢查询阈值,单位s
set global long_query_time=1;
--根据需要选择
--set global log_queries_not_using_indexes=on;

 

修改mysql的配置文件,在 [mysqld] 下面添加慢查询配置,重启mysql生效,永久有效,推荐。

slow_query_log=ON
#需要mysql对这个文件有写的权限
slow_query_log_file=/usr/local/mysql/log/slow_query.log;
#慢查询阈值,单位s
long_query_time=1
#根据需要选择
#log_queries_not_using_indexes=ON

 

慢查询日志的统计分析

--查看慢查询数量,只是本次mysql客户端连接期间发生的慢查询数量
show status like '%slow_queries%';
#对慢查询日志进行统计、分析,返回慢查询语句,文件路径必需,其它参数均可选
#不是sql语句,需要在命令行执行
#-s:指定排序方式,c是记录次数count,t是记录时间time,l是查询耗时long,r是返回的记录数result。
#-t:指定返回的查询语句数量,t即top n
#-g:指定正则表达式,只列出满足正则表达式的查询语句,可对慢查询语句进行筛选,比如只查看某个表的慢查询
mysqldumpslow -s c -t 10 -g "from tb_user" /usr/local/mysql/log/slow_query.log

 

慢查询语句分析

explain|desc可以查看sql语句的执行计划,以便分析、优化sql语句,这2个命令的用法、效果是一样的

--在sql语句前面加上explain|desc即可,不仅可以分析select,也可以分析insert、update、delete
explain select ...

主要关注以下字段

  • select_type:查询类型,SIMPLE表示是简单|普通查询
  • type:对表的访问类型
  • possible_keys:可能使用到的索引。显示的是索引名称。
  • key:实际使用的索引
  • ken_len:索引字段的长度
  • rows:估算要读取|扫描的行数。只是估算,并不一定是实际读取|扫描的行数。
  • filtered:按表条件过滤的行百分比
  • Extra:执行情况的描述、说明
     

type常见的值,性能从低到高依次为(主键索引是一种特殊的唯一索引)

  • all:全表扫描
  • index:扫描整个索引
  • range:扫描部分索引,走的索引+范围限定,比如使用in、between、> 、>=、<、<=、!=之类的限定了索引范围。
  • ref:走的普通索引(非唯一索引)+用=做常量等值判断
  • eq_ref:走的唯一索引,做的等值以外的其它判断,或做的是关联查询
  • const:走的唯一索引+用=做常量等值判断

in (),如果括号中只有一个值,则数据库会优化为=等值判断。

一般要求至少优化到range级别。

有时候看到type不对,是因为mysql本身有查询缓存,可能直接查的缓存,没有查索引、数据表。
 

更多可参考:
javascript:void(0)
javascript:void(0)

 

前端调优

浏览器访问优化

1、减少http请求次数
http是无状态协议,每次发起http请求都要重新建立链路,服务器端要启动一条新线程来处理http请求,会加大开销。

减少http的请求次数的主要手段:合并js文件、css文件。
 

2、使用浏览器缓存
css、js、图标、某些图片等静态资源更新频率低,可以设置http请求头的Cache-Control、Expires属性,将这些更新频率低的静态资源缓存几天、甚至几个月。

如果要将静态资源的更新及时应用到浏览器,可通过修改文件名、及其引用处来实现,而非只修改文件内容。

要更新大量静态资源时,应该逐量更新,避免一次性集中更新静态资源,使得浏览器缓存大量生效,造成服务器负载突然增加、网络堵塞。
 

3、启用压缩
在服务器端对文件进行压缩,在浏览器端对文件解压缩,可有效减少传输的数据量。尤其是html、css、js等文本文件,压缩率通常可达80%以上。压缩减小了服务器网络IO压力、文件传输体积,但会加大CPU开销,视情况使用。
 

4、css文件放在页面最上面,js文件放在页面最后面
浏览器下载完全部的css文件后,才对页面进行渲染,把css放在页面最前面,可以让浏览器尽快下载css、渲染页面。

浏览器加载js文件后,会立刻执行js脚本,会阻塞整个页面,造成整个页面显示缓慢,js尽量放在页面底部。如果页面加载时就要用到js脚本,可以放到前面。
 

5、减少cookie传输
一方面,每次请求头、响应中都包含了该网站的cookie,太多的cookie会影响数据传输效率,哪些字段作为cookie要慎重考虑。

另一方面,访问css、js、图标这些静态资源时,发送cookie毫无意义,可以把这些用不到cookie的静态资源放在单独的域名下,请求这些资源时就不必携带cookie。

 

cdn加速

cdn的实质是缓存,对于用户访问频率高的静态资源,比如css、js文件,可以放在cdn服务器上,访问时会从最近的cdn服务器上获取。
 

缩减 DOM 规模

尽量保持 HTML DOM 中较少的元素数量,以提高页面加载、渲染速度。
搜索DOM时,比如getElement获取元素,DOM越小获取速度越快。

 

服务器调优

服务器集群

集群可以提高系统可用性、负载能力
 

使用服务器代理

常用的比如nginx

  • 隐藏服务器,保护服务器安全
  • 负载均衡:服务器可以集群,提高整体负载
  • 静态代理:缓存更新频率低的静态资源,提高响应速度

 

使用缓存

网站性能优化第一定律:优先考虑使用缓存优化性能。

服务器端常用的缓存比如redis,因为服务器通常要集群,所以一般都是使用分布式缓存。

适合作为缓存的数据

  • 频繁使用的数据(即热点数据),eg. 热销商品的信息
  • 修改频率低的数据,eg. 省份、城市名

缓存命中:缓存中有需要的数据。
缓存穿透:缓存中没有需要的数据,需要到数据库查询数据。把从数据库查询到的数据放到缓存中,叫做回种。

缓存预热:刚开始缓存中是没有数据的,需要等待第一次访问,从数据库获取数据回种到缓存,缓存预热是系统启动时就从数据库查询数据回种到缓存中。

使用缓存要注意数据的一致性:更新数据库中数据时,同时也要更新缓存中相应的数据,这2处更新要作为事务来处理。

 

日志、流水优化

并发量大的业务,频繁写日志、流水对性能影响很大,使用日志、流水时需要遵守一定的规范。
 

日志

  • 严格区分日志信息的等级
  • 只记录必要的信息;如果非必要,不要记录list、map之类的大对象;出错时要记录userId之类的重要数据,便于后续排查原因、核对数据。
  • 日志文件尽量按天归档,配置好日志文件的按体积切分,避免单个日志文件体积过大,难以查看错误日志。
  • 生产环境,使用sluth之类的传输日志的组件时,设置小些的采样率。
     

流水

  • 日志都是log.xxx()同步输出的,流水可以使用多线程异步落库,不阻塞当前线程的执行。可以给流水落库定义一个专门的线程池。
  • 流水最终都要落库,并发量小的业务可以直接落库,并发量大的业务可以用redis、kafka、rabbitmq之类的消息队列做异步落库。
  • 统一业务编号,严格按照业务编号区分各项业务操作。
  • 是否需要记录修改前后的数据、是否需要记录值为null的字段,根据业务确定。