专栏首页九州牧云从代码层面优化系统性能应该怎么做?

从代码层面优化系统性能应该怎么做?

服务器环境

  • 服务器配置:4 核 CPU,8G 内存,共 4 台
  • MQ:RabbitMQ
  • 数据库:DB2
  • SOA 框架:公司内部封装的 Dubbo
  • 缓存框架:Redis、Memcached
  • 统一配置管理系统:公司内部开发的系统

问题描述

  1. 单台 40TPS,加到 4 台服务器能到 60TPS,扩展性几乎没有。
  2. 在实际生产环境中,经常出现数据库死锁导致整个服务中断不可用。
  3. 数据库事务乱用,导致事务占用时间太长。
  4. 在实际生产环境中,服务器经常出现内存溢出和 CPU 时间被占满。
  5. 程序开发的过程中,考虑不全面,容错很差,经常因为一个小 bug 而导致服务不可用。
  6. 程序中没有打印关键日志,或者打印了日志,信息却是无用信息没有任何参考价值。
  7. 配置信息和变动不大的信息依然会从数据库中频繁读取,导致数据库 IO 很大。
  8. 项目拆分不彻底,一个 Tomcat 中会布署多个项目 WAR 包。
  9. 因为基础平台的 bug,或者功能缺陷导致程序可用性降低。
  10. 程序接口中没有限流策略,导致很多 VIP 商户直接拿我们的生产环境进行压测,直接影响真正的服务可用性。
  11. 没有故障降级策略,项目出了问题后解决的时间较长,或者直接粗暴的回滚项目,但是不一定能解决问题。
  12. 没有合适的监控系统,不能准实时或者提前发现项目瓶颈。

优化解决方案

数据库死锁优化解决

我们从第二条开始分析,先看一个基本例子展示数据库死锁的发生:

注:在上述事例中,会话 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 一直居高不下,那么通过分析得出如下分析:

数据库连接池影响

我们针对线上的环境进行模拟,尽量真实的在测试环境中再现,采用数据库连接池为咱们默认的 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 在大并发下表现的性能不佳。

线程池使用不当引起

以上代码的场景是每一次并发请求过来,都会创建一个线程,将 DUMP 日志导出进行分析发现,项目中启动了一万多个线程,而且每个线程都极为忙碌,彻底将资源耗尽。

那么问题到底在哪里呢???就在这一行!

private static final ExecutorService executorService = Executors.newCachedThreadPool();

在并发的情况下,无限制的申请线程资源造成性能严重下降,在图表中显抛物线形状的元凶就是它!!!那么采用这种方式最大可以产生多少个线程呢??答案是:Integer 的最大值!看如下源码:

那么尝试修改成如下代码:

private static final ExecutorService executorService = Executors.newFixedThreadPool(50);

修改完成以后,并发量重新上升到 100 以上 TPS,但是当并发量非常大的时候,项目 GC(垃圾回收能力下降),分析原因还是因为 Executors.newFixedThreadPool(50) 这一行,虽然解决了产生无限线程的问题,但是当并发量非常大的时候,采用 newFixedThreadPool 这种方式,会造成大量对象堆积到队列中无法及时消费,看源码如下:

可以看到采用的是无界队列,也就是说队列是可以无限的存放可执行的线程,造成大量对象无法释放和回收。

最终线程池技术方案

方案一

注:因为服务器的 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

上面问题解决,线程阻塞的情况很少出现,极大的提高了程序的并发能力,如下图所示:

转自:从代码层面优化系统性能应该怎么做?

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Java线程池分析

    在执行一个异步任务或并发任务时,往往是通过直接new Thread()方法来创建新的线程,这样做弊端较多,更好的解决方案是合理地利用线程池,线程池的优势很明显,...

    九州暮云
  • 扩展ThreadPoolExecutor实现线程池监控

    在开发中,我们经常要使用Executors类创建线程池来执行大量的任务,使用线程池的并发特性提高系统的吞吐量。但是,线程池使用不当也会使服务器资源枯竭,导致异常...

    九州暮云
  • Java线程池扩展之关联线程池与业务

    Java API针对不同需求,利用Executors类提供了4种不同的线程池:newCachedThreadPool, newFixedThreadPool, ...

    九州暮云
  • WebFlux和SpringMVC性能对比

    本文来源:https://blog.csdn.net/get_set/article/details/79492439

    Java3y
  • 为前端工程师写的安卓入门知识

    跨端开发一直都是火热的话题,作为前端开发者,了解一下终端的知识也是有好处的这篇先简单介绍一些安卓一些常用的基础知识和概念。

    腾讯IVWEB团队
  • 垃圾回收器为什么必须要停顿下?

    在垃圾收集器在获取根节点这一步时必须暂停用户线程的也就是我们常说的STW,目前可达性分析算法耗时最长的查找引用链的过程已经可以做到和用户线程一起并发,但根节点枚...

    架构技术专栏
  • ConcurrencyMode.Multiple 模式下的WCF服务就一定是并发执行的吗:探讨同步上下文对并发的影响[下篇]

    在《上篇》中,我通过一个具体的实例演示了WCF服务宿主的同步上下文对并发的影响,并简单地介绍了同步上下文是什么东东,以及同步上下文在多线程中的应用。那么,同步上...

    蒋金楠
  • Vuex的实战使用

    这里简单的解释一下,有人说,这个不是很简单嘛,直接将切换的函数写到select的控件里面,直接点击切换的函数的时候直接给后端对应的uuid,拿到值就可以了,是的...

    何处锦绣不灰堆
  • CSS样式基础

    定义 :全称为“层叠样式表 (Cascading Style Sheets)”,它主要是用于定义HTML内容在浏览器内的显示样式,如文字大小、颜色、字体加粗等。...

    企鹅号小编
  • 十个优衣库仓库理货员,只有一个能留下,机器已经上岗了

    在优衣库,机器人代替人类工作已经成为了现实。最近,优衣库和物流公司大福(Daifuku)合作,用一套自动化系统改造了仓库,改造后的仓库能让优衣库减少90%的仓库...

    量子位

扫码关注云+社区

领取腾讯云代金券