大家好,很久没有和大家一起讨论技术了,那么今天我将和大家一起探讨我负责的无卡支付项目的性能变迁之路。 我们以前看到的很多架构变迁或者演进方面的文章大多都是针对架构方面的介绍,很少有针对代码级别的性能优化介绍,这就好比盖楼一样,楼房的基础架子搭的很好,但是盖房的工人不够专业,有很多需要注意的地方忽略了,那么在往里面填砖加瓦的时候出了问题,后果就是房子经常漏雨,墙上有裂缝等各种问题出现,虽然不至于楼房塌陷,但楼房也已经变成了危楼。那么今天我们就将针对一些代码细节方面的东西进行介绍,欢迎大家吐槽以及提建议。
服务器配置:4核CPU 8G内存 共4台
1、单台40TPS,加到4台服务器能到60TPS,扩展性几乎没有。
1、数据库死锁优化解决 Paste_Image.png
分析:出现这种问题就是我们在项目中混杂了大量的事务 for update语句,针对数据库锁来说有下面三种基本锁: Paste_Image.png
那我们用大量的锁的目的是什么,经过业务分析发现,其实就是为了防重,同一时刻有可能会有多笔支付单发到相应系统中,而防重措施是通过在某条记录上加锁的方式来进行。 针对以上问题完全没有必要使用悲观锁的方式来进行防重,不仅对数据库本身造成极大的压力,同时也会把对于项目扩展性来说也是很大的扩展瓶颈,我们采用了三种方法来解决以上问题:
2、数据库事务占用时间过长 public void test() { Transaction.begin //事务开启 try { dao.insert //插入一行记录 httpClient.queryRemoteResult() //请求访问 dao.update //更新一行记录 Transaction.commit() //事务提交 } catch(Exception e) { Transaction.rollFor //事务回滚 } } 项目中类似这样的程序有很多,经常把类似httpClient,或者有可能会造成长时间超时的操作混在事务代码中,不仅会造成事务执行时间超长,而且也会严重降低并发能力。 那么我们在用事务的时候,遵循的原则是快进快出,事务代码要尽量小。针对以上伪代码,我们要用httpClient这一行拆分出来,避免同事务性的代码混在一起,这不是一个好习惯。 3、CPU时间被占满分析 Paste_Image.png
我们针对线上的环境进行模拟,尽量真实的在测试环境中再现,采用数据库连接池为咱们默认的C3P0。 那么当压测到二万批,100个用户同时访问的时候,并发量突然降为零!报错如下:
那么针对以上错误跟踪C3P0源码,以及在网上搜索资料:
发现C3P0在大并发下表现的性能不佳。
private static final ExecutorService executorService = Executors.newCachedThreadPool(); /** * 异步执行短频快的任务 * @param task */ public static void asynShortTask(Runnable task){ executorService.submit(task); //task.run(); } CommonUtils.asynShortTask(new Runnable() { @Override public void run() { String sms = sr.getSmsContent(); sms = sms.replaceAll(finalCode, AES.encryptToBase64(finalCode, ConstantUtils.getDB_AES_KEY())); sr.setSmsContent(sms); smsManageService.addSmsRecord(sr); } }); 以上代码的场景是每一次并发请求过来,都会创建一个线程,将DUMP日志导出进行分析发现,项目中启动了一万多个线程,而且每个线程都极为忙碌,彻底将资源耗尽。 那么问题到底在哪里呢???就在这一行! private static final ExecutorService executorService = Executors.newCachedThreadPool(); 在并发的情况下,无限制的申请线程资源造成性能严重下降,在图表中显抛物线形状的元凶就是它!!!那么采用这种方式最大可以产生多少个线程呢??答案是:Integer的最大值!看如下源码: Paste_Image.png
private static final ExecutorService executorService = Executors.newFixedThreadPool(50); 修改完成以后,并发量重新上升到100以上TPS,但是当并发量非常大的时候,项目GC(垃圾回收能力下降),分析原因还是因为Executors.newFixedThreadPool(50)这一行,虽然解决了产生无限线程的问题,但是当并发量非常大的时候,采用newFixedThreadPool这种方式,会造成大量对象堆积到队列中无法及时消费,看源码如下: Paste_Image.png 可以看到采用的是无界队列,也就是说队列是可以无限的存放可执行的线程,造成大量对象无法释放和回收。
Paste_Image.png
方案二: 4、日志打印问题 QuataDTO quataDTO = null; try { quataDTO = getRiskLimit(payRequest.getQueryRiskInfo(), payRequest.getMerchantNo(), payRequest.getIndustryCatalog(), cardBinResDTO.getCardType(), cardBinResDTO.getBankCode(), bizName); } catch (Exception e) { logger.info("获取风控限额异常", e); } 像这样的代码是严格不符合规范的,虽然每个公司都有自己的打印要求。
合理的日志格式是: logger.warn("[innersys] - [" exceptionType.description "] - [" methodName "] - " "errorCode:[" errorCode "], " "errorMsg:[" errorMsg "]", e); logger.info("[innersys] - [入参] - [" methodName "] - " LogInfoEncryptUtil.getLogString(arguments) "]"); logger.info("[innersys] - [返回结果] - [" methodName "] - " LogInfoEncryptUtil.getLogString(result)); 我们在程序中大量的打印日志,虽然能够打印很多有用信息帮助我们排查问题,但是更多是日志量太多不仅影响磁盘IO,更多会造成线程阻塞对程序的性能造成较大影响。
%d %-5p %c:%L [%t] - %m%n 那么在压测的时候会出现下面大量的线程阻塞,如下图: Paste_Image.png 再看压测图如下: Paste_Image.png Paste_Image.png 原因可以根据log4j源码分析如下: Paste_Image.png
于是修改log4j配置文件为: %d %-5p %c [%t] - %m%n 上面问题解决,线程阻塞的情况很少出现,极大的提高了程序的并发能力,如下图所示: Paste_Image.png 未完待续,接下来将是“论代码级性能优化变迁之路(二)”敬请期待! 来源:简书 |
|
声明:文章版权归原作者所有 部分文章转自互联网 如有侵权请联系
[邮箱地址] 删除
|