咱们之前看到的不少架构变迁或者演进方面的文章大多都是针对架构方面的介绍,不多有针对代码级别的性能优化介绍,这就比如盖楼同样,楼房的基础架子搭的很好,可是盖房的工人不够专业,有不少须要注意的地方忽略了,那么在往里面填砖加瓦的时候出了问题,后果就是房子常常漏雨,墙上有裂缝等各类问题出现,虽然不至于楼房塌陷,但楼房也已经变成了危楼。那么今天咱们就将针对一些代码细节方面的东西进行介绍,欢迎你们吐槽以及提建议。java
服务器环境web
服务器配置:4核CPU,8G内存,共4台sql
MQ:RabbitMQ数据库
数据库:DB2后端
SOA框架:公司内部封装的Dubbo缓存
缓存框架:Redis、Memcached性能优化
统一配置管理系统:公司内部开发的系统服务器
问题描述架构
单台40TPS,加到4台服务器能到60TPS,扩展性几乎没有。并发
在实际生产环境中,常常出现数据库死锁致使整个服务中断不可用。
数据库事务乱用,致使事务占用时间太长。
在实际生产环境中,服务器常常出现内存溢出和CPU时间被占满。
程序开发的过程当中,考虑不全面,容错不好,常常由于一个小bug而致使服务不可用。
程序中没有打印关键日志,或者打印了日志,信息倒是无用信息没有任何参考价值。
配置信息和变更不大的信息依然会从数据库中频繁读取,致使数据库IO很大。
项目拆分不完全,一个Tomcat中会布署多个项目WAR包。
由于基础平台的bug,或者功能缺陷致使程序可用性下降。
程序接口中没有限流策略,致使不少VIP商户直接拿咱们的生产环境进行压测,直接影响真正的服务可用性。
没有故障降级策略,项目出了问题后解决的时间较长,或者直接粗暴的回滚项目,可是不必定能解决问题。
没有合适的监控系统,不能准实时或者提早发现项目瓶颈。
优化解决方案
一、数据库死锁优化解决
咱们从第二条开始分析,先看一个基本例子展现数据库死锁的发生:
注:在上述事例中,会话B会抛出死锁异常,死锁的缘由就是A和B二个会话互相等待。
分析:出现这种问题就是咱们在项目中混杂了大量的事务+for update语句,针对数据库锁来讲有下面三种基本锁:
Record Lock:单个行记录上的锁
Gap Lock:间隙锁,锁定一个范围,但不包含记录自己
Next-Key Lock:Gap Lock + Record Lock,锁定一个范围,而且锁定记录自己
当for update语句和gap lock和next-key lock锁相混合使用,又没有注意用法的时候,就很是容易出现死锁的状况。
那咱们用大量的锁的目的是什么,通过业务分析发现,其实就是为了防重,同一时刻有可能会有多笔支付单发到相应系统中,而防重措施是经过在某条记录上加锁的方式来进行。
针对以上问题彻底没有必要使用悲观锁的方式来进行防重,不只对数据库自己形成极大的压力,同时也会把对于项目扩展性来讲也是很大的扩展瓶颈,咱们采用了三种方法来解决以上问题:
使用Redis来作分布式锁,Redis采用多个来进行分片,其中一个Redis挂了也不要紧,从新争抢就能够了。
使用主键防重方法,在方法的入口处使用防重表,可以拦截全部重复的订单,当重复插入时数据库会报一个重复错,程序直接返回。
使用版本号的机制来防重。
以上三种方式都必需要有过时时间,当锁定某一资源超时的时候,可以释放资源让竞争从新开始。
二、数据库事务占用时间过长
伪代码示例:
项目中相似这样的程序有不少,常常把相似httpClient,或者有可能会形成长时间超时的操做混在事务代码中,不只会形成事务执行时间超长,并且也会严重下降并发能力。
那么咱们在用事务的时候,遵循的原则是快进快出,事务代码要尽可能小。针对以上伪代码,咱们要用httpClient这一行拆分出来,避免同事务性的代码混在一块儿,这不是一个好习惯。
三、CPU时间被占满分析
下面以我以前分析的一个案例做为问题的起始点,首先看下面的图:
项目在压测的过程当中,CPU一直居高不下,那么经过分析得出以下分析:
1)数据库链接池影响
咱们针对线上的环境进行模拟,尽可能真实的在测试环境中再现,采用数据库链接池为我们默认的C3P0。
那么当压测到二万批,100个用户同时访问的时候,并发量忽然降为零!报错以下:
com.yeepay.g3.utils.common.exception.YeepayRuntimeException: Could not get JDBC Connection; nested exception is java.sql.SQLException: An attempt by a client to checkout a Connection has timed out.
那么针对以上错误跟踪C3P0源码,以及在网上搜索资料发现C3P0在大并发下表现的性能不佳。
2)线程池使用不当引发
以上代码的场景是每一次并发请求过来,都会建立一个线程,将DUMP日志导出进行分析发现,项目中启动了一万多个线程,并且每一个线程都极为忙碌,完全将资源耗尽。
那么问题到底在哪里呢???就在这一行!
private static final ExecutorService executorService = Executors.newCachedThreadPool();
在并发的状况下,无限制的申请线程资源形成性能严重降低,在图表中显抛物线形状的元凶就是它!!!那么采用这种方式最大能够产生多少个线程呢??答案是:Integer的最大值!看以下源码:
那么尝试修改为以下代码:
private static final ExecutorService executorService = Executors.newFixedThreadPool(50);
修改完成之后,并发量从新上升到100以上TPS,可是当并发量很是大的时候,项目GC(垃圾回收能力降低),分析缘由仍是由于Executors.newFixedThreadPool(50)这一行,虽然解决了产生无限线程的问题,可是当并发量很是大的时候,采用newFixedThreadPool这种方式,会形成大量对象堆积到队列中没法及时消费,看源码以下:
能够看到采用的是×××队列,也就是说队列是能够无限的存放可执行的线程,形成大量对象没法释放和回收。
3)最终线程池技术方案
方案一
注:由于服务器的CPU只有4核,有的服务器甚至只有2核,因此在应用程序中大量使用线程的话,反而会形成性能影响,针对这样的问题,咱们将全部异步任务所有拆出应用项目,以任务的方式发送到专门的任务处理器处理,处理完成回调应用程序器。后端定时任务会定时扫描任务表,定时将超时未处理的异步任务再次发送到任务处理器进行处理。
方案二
使用AKKA技术框架,下面是我之前写的一个简单的压测状况:
http://www.jianshu.com/p/6d62256e3327
四、日志打印问题
先看下面这段日志打印程序:
像这样的代码是严格不符合规范的,虽然每一个公司都有本身的打印要求。
首先日志的打印必须是以logger.error或者logger.warn的方式打印出来。
日志打印格式:[系统来源] 错误描述 [关键信息],日志信息要能打印出能看懂的信息,有前因和后果。甚至有些方法的入参和出参也要考虑打印出来。
在输入错误信息的时候,Exception不要以e.getMessage的方式打印出来。
合理的日志格式是:
咱们在程序中大量的打印日志,虽然可以打印不少有用信息帮助咱们排查问题,可是更可能是日志量太多不只影响磁盘IO,更多会形成线程阻塞对程序的性能形成较大影响。
在使用Log4j1.2.14版本的时候,使用以下格式:
%d %-5p %c:%L [%t] - %m%n
那么在压测的时候会出现下面大量的线程阻塞,以下图:
再看压测图以下:
缘由能够根据log4j源码分析以下:
注:Log4j源码里用了synchronized锁,而后又经过打印堆栈来获取行号,在高并发下可能就会出现上面的状况。
因而修改Log4j配置文件为:
%d %-5p %c [%t] - %m%n
上面问题解决,线程阻塞的状况不多出现,极大的提升了程序的并发能力,以下图所示: