一份平民化的应用性能优化检查列表(完整篇)

版权声明:本文版权归 江南白衣@唯品会 所有。任何形式的转载,必须获得作者授权。

作为一份比较初级的检查表,高大上的话不多说了,直接开始。

嗯,还是先关注下我再开始。微信公众号jnby1978。

1.总原则

一些正确但稍显废话的原则,但能指导后面每个章节的优化,所以还是要啰嗦一次。

 

  1. 可扩展性架构,堆机器能不能解决问题是最最优先考虑的问题
  2. 去中心化的点对点通信,优于通过中心代理的通信
  3. 池化的长连接,优于短连接
  4. 二进制数据,优于文本数据
  5. 尽量减少交互,一次调用的粗粒度聚合接口 优于 多次调用的细粒度接口
  6. 尽量减少交互,批量接口优于循环调用
  7. 尽量只交互必要的数据
  8. 尽量就近访问
  9. 尽量使用缓存
  10. 总是设定超时
  11. 在合适的场景,并行化执行
  12. 在合适的场景,异步化执行

2.环境准备

保证符合自家各种规范(没有的话赶紧回家写一个),尤其线下压测服务器的配置要与生产环境一致。

2.1 操作系统

  • 自家规范调优应包含TCP内核参数,网卡参数及多队列绑定,IO&Swap内核参数,ulimit资源限制等。

2.2 JVM与应用服务器

  • 使用JDK7.0 u80 或 JDK8 最新版。
  • 检查JVM启动参数已按自家规范调优,见《关键业务系统的JVM参数推荐》
  • 检查应用服务器(Tomcat或微服务容器) 已按自家指南调优,如线程数等。

2.3 周边依赖系统

  • 检查数据库,缓存,消息系统,已按自家指南调优。

2.4 后台辅助程序

  • 检查日志收集,系统监控等,已使用最新版本,最优配置。
  • 最好其最大消耗已被控制(通过cgroup,taskset等方式)。

2.5 测试程序

  • 压测工具如JMeter,启动参数要参考真实应用客户端的参数优化(如JVM参数,Netty参数等)。
  • 测试脚本和客户端程序经过review,不存在影响性能的步骤,不存在System.out.println()等明显的瓶颈。

2.5 流量模型

  • 扇入模型:平时与高峰期的流量估算,各接口的流量比例,响应时间要求
  • 扇出模型:各接口对远程服务、数据库、缓存、消息系统的调用比例,响应时间估算。

 

大家在心里都有这么一个大概的模型,但很少认真写出来。

行文到此,大家大概可以感受到这份checklist的风格,都是大家明白的道理,但可能一时也会忘掉的,这里啰啰嗦嗦的給写下来。


3.数据库

特别鸣谢,我司DBA。

3.1 拓扑

根据扩展性原则考虑:

  • 垂直拆分:按业务将不同的表拆分到不同的库。
  • 水平拆分:水平分库分表。
  • 读写分离:在业务允许的情况下,在从库读取非实时数据。

3.2 Schema

自家规范应包含:

  • 统一的存储引擎,主键策略。
  • 禁用存储过程,函数,触发器,外键约束。
  • 列类型永远越短越好,建议:布尔/枚举:tinyint,日期与时间戳:timestamp或int,char/text/blob: 尽量用符合实际长度的varchar(n),小数及货币:移位转为int 或 decimal,IP地址:int。
  • 索引策略:索引字段的顺序需要考虑字段值去重之后的个数,较多的放前面,合理创建联合索引,避免冗余索引,合理利用覆盖索引等。

 

3.3 SQL

1. 自家规范应包含:

  • 如禁止多于3表join,禁用子查询
  • 禁止where子句中对字段施加函数,如to_date(add_time)>xxxxx
  • 避免MySQL进行隐式类型转化,如ISENDED&eq;1 与 ISENDED&eq;`1`
  • 不建议使用%前缀模糊查询,模糊查询较多时建议使用ElasticSearch

 

根据尽量少数据原则与尽量少交互的原则来设计SQL:

  • 禁止select
  • 合理的SQL语句,减少交互次数

根据扩展性原则,将负载放在更容易伸缩的应用服务实例上:

  • 尽量不要做数学薄雾浓云愁永昼运算,函数运算, 或者输出格式转换等非必要操作
  • 避免count(*),计数统计实时要求较强使用memcache或者redis,非实时统计使用单独统计表,定时更新。
  • 甚至排序都是不鼓励的,尽量在应用侧进行。另外避免多余的排序,使用GROUP BY 时,默认会进行排序,当你不需要排序时,可以使用order by null。

2. 联系DBA进行MySQL统计的慢查询的Review,解析SQL查询计划时尽量避免extra列出现:Using File Sort,Using Temporary

3.4 DAO框架

  • 根据尽量少交互与尽量少数据的原则,需使用对SQL完全可控的DAO框架,建议为MyBatis 或 Spring JDBC Template。
  • 必须使用prepareStatement,提升性能与防注入。
  • 根据一切皆有超时的原则,配置SQL执行的超时。可在连接池里设置default值,可在MyBatis的Mapper定义里可设置每个请求的超时,可惜规范是秒级的。
  • JDBC driver 规范本身不支持异步模式,如果一定要异步,可以像Quasar那样把请求封装成Callable交给另外的线程池执行,但要注意其额外开销。

 

3.5 事务

  • 不使用事务,连接池设置autocommit,使用其他方式来保持数据一致性。
  • 通过Transaction Annotation控制事务,事务跨度尽量短,把非事务范围内的业务逻辑剔除到被标注的函数之外。
  • 只读事务可以不加事务标注。

 

连接池

选型:

  • 在分库分表时,根据点对点通信优先的原则,尽量使用客户端分片的实现。功能不满足时才用MyCat中央代理。
  • 推荐使用性能最高HikariCP,或者Druid,不推荐c3p0与DBCP。

 

连接池的配置:

  • 配置初始值,再联系DBA获得线上数据库支持的连接数,计算最大连接数。
  • 连接有效性检查,只在连接空闲检测时执行,不在拿出和归还连接时执行,最好是直接使用数据的Ping方案,不要配置检查SQL
  • 根据总是设置超时的原则,配置获取连接超时的时间。
  • 配置合理的空闲连接回收间隔和空闲时间。

番外篇:在分库分表时,可考虑基于HikariCP二次开发,减少总的空闲连接检查线程数(比如128个分区,可能有256条线程),重用同一个实例上的库的连接等。


4.缓存

 

4.1 多级缓存

 

  • 根据缓存原则, 缓存 > 数据库/远程调用
  • 根据就近原则, 堆内缓存 > 堆外缓存 > 集中式缓存
  • 堆内缓存受大小限制,并影响GC
  • 堆内缓存与堆外缓存,分布在每一台应用服务器上,刷新方式比集中式缓存复杂
  • 堆外缓存与集中式缓存,需要序列化/反序列化对象
  • 集中式缓存,有网络传输的成本,特别是数据超过一个网络包的大小。
  • 集中式缓存,一次获取多个键时,在有分区的情况下,需要收发多个网络包。

使用上述条件选择合适的缓存方案,或同时使用多级缓存,逐层回源。

 

4.2 综述

  • 需要对回源进行并发控制,当key失效时,只有单一线程对该key回源。
  • 基于二进制优于文本数据的原则,JSON的序列化方案较通用与更高的可读性。而对于较大,结构较复杂的对象,基于Kyro,PB,Thrift的二进制序列化方案的性能更高,见后面的序列化方案部分。

 

4.3 堆内缓存

选型:

  • 推荐Guava Cache。
  • Ehcache较重,性能也较差。更不要使用存在严重bug的Jodd Cache。

GuavaCache:

  • 正确设置并行度等参数。
  • 重载load()参数,实现单一线程回源。
  • Guava Cache能后台定时刷新,在刷新的过程中,依然使用旧数据响应请求,不会造成卡顿,但需要重载实现reload()函数。
  • Guava Cache同时还支持并发安全版的WeakHashMap。

 

4.4 堆外缓存

选型:

  • 推荐Cassandra的OHC 或者 OpenHFT的Chronical map2。
  • OHC够简单,其实R大不喜欢Chronical,玩的太深,换个JDK都可能跑不起来。
  • Chronical map3的license则较不友好,复杂度高且要求JDK8。
  • 其他的Ehcache的Terracota Offheap 一向不喜欢。

4.5 Memcached

​客户端:

  • 基于点对点通信优于网关的原则,使用客户端一致性哈希分区。
  • 推荐Spymemcached。 XMemcached 太久没更新,Folsom知名度不高。
  • 注意Spymemcached为单线程单连接架构(一个MemcachedClient只有一条IO线程,与每台Memcached只有一条连接),必要时可多建几个MemcachedClient随机选择,但不要用Commons Pool去封装它,把Spy原本的设计一笔抹杀。
  • 根据在合适场景使用并发的原则,Spymemcached支持异步API。
  • 根据一切皆设超时的原则,可在连接工厂中设置最大超时数,默认值两秒半太长。

 

数据结构:

  • Key必须设置失效时间。
  • Key必须有长度限制。
  • Value长度需要控制,以不超过1个网络包(MTU,千五字节)为佳。
  • Value大小差别较大的缓存类型,建议拆分到不同MC集群,否则会造成低使用率并且产生踢出。

 

4.6 Redis as Cache

 

Redis拓扑:

基于点对点通信优于网关的原则,使用如下两种拓扑

  • 无HA的普通分片:由Jedis客户端完成分片路由。
  • Redis Cluster:同样由Jedis客户端封装分区,跳转,重试等逻辑,需要使用最新版的Jedis版本。

 

服务端:

  • Cache节点与持久化数据节点不要混用。
  • Cache节点是否需要持久化要仔细衡量。
  • 由于Redis是单线程,使用taskset进行cpu绑定后可以有效地利用cpu,并在单机上运行多个redis实例。
  • 对热键进行监控,发现不合理的热健要进行分拆等处理。

客户端:

  • Jedis基于Apache Commons Pool进行了多连接的封装,正确配置总连接数不超过Redis Server的允许连接数。
  • 性能考虑,空闲连接检查不要过于频繁(建议30秒以上),另不要打开testOnBorrow等测试参数。
  • 根据一切皆有超时的原则,设定统一的调用超时,获取连接的最长等待时间参数,重试次数
  • 根据在合适的地方异步的原则,Jedis本身没有异步API,只在PipleLine模式下支持。

数据结构:

  • 必须对Key设置失效时间。
  • Key必须有长度限制。
  • Value长度需要控制,不要超过一个网络包。另外集合的元素不要超过五千个。
  • 除了使用序列化的String,同样可以考虑用Hash来存储对象,注意内部结构为ZipList与HashTable时,hmget 与hgetall的不同复杂度。

命令:

  • 慎用的命令:LANGE(0, -1), HGETALL, SMEMBER
  • 高复杂度的命令: ZINTERSTORE, SINTERSTORE, ZUNIONSTORE, ZREM
  • 尽量使用多参数的命令:MGET/MSET,HMGET/HMSET, LPUSH/RPUSH, LRANGE
  • 尽量使用pipeline
  • 根据减少交互的原则,必要时可使用Redis的Lua脚本

 


5.服务调用

5.1 接口设计

1. 尽量少交互的原则:

支持批量接口,最大的批量,综合考虑调用者的需求与 后端存储的能力。

支持粗粒度接口,在支持原子细粒度接口的同时,支持粗粒度接口/聚合层接口,将多个数据源的获取,多个动作,合并成一个粗粒度接口。

 

2. 尽量少数据的原则:

在提供返回所有数据的大接口的同时,提供只提供满足部分调用者需要的轻量接口。

最好再提供能定制返回字段的接口。

 

3. 二进制数据优于文本数据

同样是一个简单通用性,与性能的选择,特别是大数据量时。

 

5.2 RESTful

仅以Apache HttpClient为例,大部分Restful框架都是对Apache HttpClient的封装。

另外OkHttp也值得看看。

 

  • 不要重复创建ApacheClient实例,使用连接池,正确配置连接池的连接数。
  • 连接池总是有锁,针对不同的服务,使用不同的Apache HttpClient实例,将锁分散开来。在高并发时比使用全局单例的ApacheClient,有很大的性能提升。
  • 根据一切调用皆有超时的原则,每次调用均设置超时时间。RequestConfig里共有Connect Timeout, Socket Timout 和 从Pool中获取连接的Timeout三种超时。
  • 需要异步或并行的场景,使用Apache AsyncHttpClient项目。但要注意AsyncHttpClient项目,检查调用超时的周期默认为1秒。

 

5.3 自家RPC框架

 

每家的RPC框架特性不同,但考虑点都类似。

 

 


6.消息异步

 

6.1 选型

  • 根据就近原则,可以先尝试用JVM内的队列来解决,然后再考虑中央消息系统。
  • 可靠性要求极高的选择RabbitMQ,可支持单条消息确认。
  • 海量消息场景,允许极端情况下少量丢失则使用Kafka。

 

6.2 Kafka

  • 在同步和异步之间做好权衡,异步批量发送可以极大的提高发送的速度。
  • 关注消费者如下参数:commitInterval(自动提交offset间隔),prefetchSize(指单次从服务器批量拉取消息的大小),过大和过小都会影响性能,建议保持默认。

 

6.3 RabbitMQ

  • 根据扩展性原则,RabbitMQ本身没有分片功能,但可以在客户端自行分片。
  • 如非必要情况,应该保持默认的同步发送模式。
  • 关注消费者如下参数:autocommit(自动提交确认,默认false) ,在消息拉取到本地即认为消费成功,而不是真正消费成功后提交。prefetchCount(预取消息条数,默认64条)
  • 生产者在必要时也可以临时降级不进行confirm。

 


7. 日志

 

7.1 综述

  • Log4j2或logback,不要再使用Log4j。
  • 除了应用启停日志,不允许使用超慢的System.out.println() 或 e.printStack();
  • 严格控制日志量避免过高IO,对海量日志,应该有开关可以动态关停。
  • 如果可能出现海量异常信息,可仿效JDK的优化,用RateLimiter进行限流,丢弃过多的异常日志。

7.2 内容

  • 严格控制日志格式,避免出现消耗较大的输出如类名,方法名,行号等。
  • 业务日志不要滥用toJSONString()来打印对象,尽量使用对象自身的toString()函数,因为JSON转换的消耗并不低。
  • 在生产环境必定输出的日志,不要使用logger.info("hello {}", name)的模式,而是使用正确估算大小的StringBuilder直接拼装输出信息。

 

7.3 异步日志

  • 同步日志的堵塞非常严重,特别是发生IO的时候,因此尽量使用异步日志。
  • Logback的异步方案存在一定问题,需要正确配置Queue长度,阀值达到多少时丢弃Warn以下的日志,最新版还可以设置如果队列已满,是等待还是直接丢弃日志。
  • 如果觉得Logback的异步日志每次插入都要询问队列容量太过消耗,可重写一个直接入列,不成功则直接丢弃的版本。

 


8. 工具类

 

8.1 JSON

  • 使用Jackson 或 FastJSON。GSON的性能较前两者为差,尤其是大对象时。
  • 超大对象可以使用Jackson或FastJSON的流式 API进行处理。
  • 将不需要序列化的属性,通过Annotation排除掉。

FastJson:

  • 尽量使用最新的版本。
  • SerializerFeature.DisableCircularReferenceDetect 关闭循环引用检查。

Jackson:

  • 设置参数,不序列化为空的属性,等于默认值的属性。
  • 除了jackson-databinding,可试用简化版没那么多花样的jackon-jr。

 

8.2 二进制序列化

 

需要定义IDL的PB与Thrift,不需要定义的Storm等用的Kyro 都可选择,其他一些比较旧就算了。

8.3 Bean复制

在VO,BO之间复制时,使用Orika(生成代码) 或 Dozer(缓存反射),不要使用需要每次进行反射的Apache BeanUitls,Spring BeanUtils。

 

8.4 日期

JDK的日期类与字符串之间的转换很慢且非线程安全。

继续用Java日期不想大动作的,就用CommonsLang的FastDateFormat。

能大动作就用joda time,或者JDK8的新日期API。

 


9.Java代码优化 与 业务逻辑优化

 

参考《Java调优指南1.8版》,对内存使用,并发与锁等方面进行优化。

 

规则前置,将消耗较大的操作放后面,如果前面的条件不满足时可。

另外前面提到的一堆原则,比如尽量缓存,尽量少交互,尽量少数据,并行,异步等,都可在此使用。

 

之前的打赏已经花完了,被迫继续卖文赚零花,各位看官再赏两块五呗。

 

转载请保留原文链接: http://calvin1978.blogcn.com/articles/checklist.html

This entry was posted in 技术. Bookmark the permalink.

11 Responses to 一份平民化的应用性能优化检查列表(完整篇)

  1. trace says:

    写得很好,感谢楼主的分享,沙发..

  2. final says:

    一直不是太懂,为什么需要长连接的池化。JDBC 连接池因为建立 JDBC 连接开销大,所以先存着。一般的线程池也是因为线程的创建开销大。但是 http 连接池池化,也是因为 http 连接建立的开销很大吗?http 的连接放进池子里怎么保存状态?现在 http1.1不是都默认自带长连接吗?

    • 匿名 says:

      http1.1后默认的keepalive与链接复用,我测试下来使不使用链接池貌似在时间性能上差距不大

    • 匿名 says:

      有一次,我们没做池,httpclient,在性能测试的时候直接把socket资源给干没了,这东西是每次都申请新的。。。而你懂的,TCP有TW回收,就算你在内核上优化了回收机制,毕竟系统还要每次去回收,很消耗资源。

  3. 程序猿DD says:

    总结的太棒了,收藏之。

    勘误,kafka第一条是rabbitmq的

  4. xxzkid says:

    可以转载吗?

  5. 123 says:

    在生产环境必定输出的日志,不要使用logger.info("hello {}", name)的模式,我想说我们生产环境全部是这样用的,如果用StringBuilder拼是不是太麻烦了

  6. llddy says:

    你花钱的速度太快了

  7. 欧阳 says:

    写的真好,我有个问题,集群时堆内缓存怎么即时刷新?

发表评论

您的电子邮箱不会被公开。

您可以使用这些 HTML 标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>