作者介绍
刘迪伟,就职于世界五百强银行。负责公司网银业务系统的设计和交付,擅长并持续关注Java性能优化、DevOps等领域。
XX银行网银系统是一套全新的对公业务渠道类系统,经过两年的建设,将逐步对外提供服务。
该系统融合了原来多个对公渠道系统,并发量是以前多个系统之和,吞吐量要求将大幅上升。为了使广大对公客户使用系统时获得更快的响应时间体验,项目组对系统进行了持续的性能测试和优化。这一过程中,形成了一套针对新建系统进行性能测试和优化的方法论。
该方法论包括测试环境准备、测试功能优先级、性能优化原则、常用性能指标及工具、工具使用方法、常见性能问题原因和优化方法,以及典型案例和进一步优化方法的讨论。
由于系统已经开发完成,根据性能优化修改范围尽可能小且不引入更多问题的原则,本文将只讨论对系统进行局部优化的方法,而系统初始设计时为了提高性能而进行的设计不在讨论范围之内。
我们使用Linux操作系统,压测工具为loadrunner,中间件版本包括IHS8.5.5.9、WAS8.5、IBMJDK1.7(IBM J9VM)、DB2V10、Redis3.2.3。
一、应用系统性能评价指标
TPS = 并发用户数 / 响应时间
二、常见性能监控指标及工具
1、操作系统监控指标及工具
主要监控指标:CPU、系统CPU、内存、磁盘IO、网络IO、请求耗时。
常用命令:
2、JVM监控指标及工具
JVM启动参数中增加:
-Dcom.sun.management.jmxremote.port=1088
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false
CPU抽样:
线程快照:
三、性能测试前置条件
1、数据库表数据量准确
要和生产数据量保持一致,至少一个数量级。数据分布尽量均匀。
2、测试环境和生产一致
测试环境机器配置、参数、代码尽可能和生产保持一致(数据库服务器硬件除外)。
3、合理确定并发用户量
系统并发用户量有多种算法可以估算:
C=nL/T(n是考察时间内用户登录数,L是用户平均在线时间长度,T是考察值时间长度)
并发用户数峰值:C’=C + 3*根号C
网银用户量100万,根据2/8原则, 80%用户在上午9点到11点,下午2点到4点之间登录系统,每次登录耗时1到1.5秒。则并发用户为:
1000000*0.8/4/3600*1.5=82.5,
1000000*0.8/4/3600*1=55
并发用户数=系统最大在线用户数的8%到12%
C=(Think Time + 1)* TPS,
网银用户思考时间10s,
C=(10+1)* 3=33。
4、预估各功能交易量,确定压测功能优先级
根据交易量从大到小排名,排名靠前的优先压测。
5、设置性能问题认定标准
比如响应时间超过3s、TPS低于10、服务器cpu占用率超过70%、jvm堆内存使用100%、垃圾回收频繁、网络IO或磁盘IO达到瓶颈等……都可能是性能问题。
四、性能优化一般思路
1、找到性能瓶颈
性能瓶颈定义:导致系统TPS低、响应时间长、资源(CPU、内存、网络)占用高等问题的关键程序模块。提升该程序模块的性能,可以大幅度改善性能。
常见的性能瓶颈原因包括:数据库慢查询SQL、日志打印、xml大报文解析和格式转换、复杂业务逻辑、锁竞争等。
2、如何找到性能瓶颈
3、针对性能瓶颈进行合理优化
性能优化原则:
五、常见问题及优化方法
1、SQL执行时间长
问题现象:系统响应时间长、数据库cpu高。
问题原因:全表扫描、索引低效、排序溢出。
解决方法:
排查方法:
2、数据库出现死锁
问题现象:数据库快照检测到存在数据库死锁,或通过db2evmon -db corpdb -evm DB2DETAILDEADLOCK>dlock.txt生成死锁监控文件,快照或监控文件中存在deadlock的情况。
问题原因:全表扫描、大事务、更新相同表记录的SQL执行顺序交叉等。
解决方法:缩短事务路径长度,避免全表扫描。如果必须存在大事务,则更新相同表的SQL执行顺序一致,并且坚决避免全表扫描。网银系统指令发送功能全表扫描+全局大事务,导致数据库死锁。
排查方法:根据死锁监控文件dlock.txt找到导致死锁的SQL,以及该SQL持有的锁,分析该SQL可能存在的问题。
3、线程阻塞在日志记录上
问题现象:系统响应时间长、通过javacore查看很多线程阻塞在打印日志上。
问题原因:log4j1.x版本较低,性能较差;大报文日志多次输出。
解决方法:
排查方法:
4、多线程并发问题
问题现象:采用合理的并发数压测,系统出现逻辑错误、交易失败或异常报错。经查是由于对象中变量被异常修改导致。
问题原因:系统中全局对象中的类变量或全局对象,被多个线程修改。
解决方法:排查系统中所有持有全局对象或类变量的代码,检查其全局变量是否可能被多个线程并行修改。
修改方法:
排查方法:并发问题很可能是由全局变量或者对象导致,准确识别全局变量,通过阅读代码找问题。建议应用梳理所有可能存放全局对象的代码,统一管控,或者把所有全局对象放到一个类中,方便管理。
5、打开了太多文件
问题现象:采用合理的并发数压测,交易失败,或后台日志报错:To many open files。
问题原因:
解决方法:
* hard nproc 10240
* soft nproc 10240
6、内存泄漏
问题现象:JVM内存耗尽,后台日志抛出OutOfMemeryError异常 ;
问题原因:内存溢出问题可能的原因比较多,可能是全局的List、Map等对象不断被扩大,也可能是程序不慎将大量数据读到内存里;可能是循环操作导致,也可能后台线程定时触发加载数据导致。
解决方法:对于ibmjdk纯java应用,在jvm启动时设置-XX:+HeapDumpOnOutOfMemory Error参数,会在内存溢出时生成heapdump文件。使用ha456.jar工具打开heapdump文件,分析大对象是如何产生的。
当然,在heapdump中对象类型可能只是List这种结构,看不出具体哪个业务代码创建的对象。此时要分析所有的全局对象,列出可疑的List或Map对象,排查其溢出原因。
全局对象、引用的初始化、修改要慎重。建议应用梳理所有可能存放全局对象的代码,统一管控。
7、JVM垃圾回收频繁
问题现象:top –H –p pid命令查看,GC Slave线程CPU占用排名始终为前三名,同时Jconsole查看jvm内存占用较高,垃圾回收频繁。使用ga456.jar分析gc日志,查看gc频率、时长。
打印gc日志的方法:在jvm启动参数里增加-verbose:gc -Xverbosegclog:/usr/ebank/ logs/cobp/gcdetail.log
问题原因:高并发下,内存对象较多,jvm堆内存不够用 。
解决方法:扩大堆内存大小–Xmx2048m –Xms2048m。
8、CPU高
问题现象:50并发压测,监控工具显示bp、前置CPU占用90%以上。
问题原因:业务处理中存在大量CPU计算操作。
解决方法:采用更高效的算法、数据结构替换原来消耗CPU的代码,或者采用新的设计绕过瓶颈代码,比如查找数据的逻辑,可以把List改为Map,以空间换时间;比如用Json报文替换XML报文,提高传输、解析和打印日志的效率。
导致Cpu计算资源高消耗的代码:报文格式转换、加解密、正则表达式、低效的循环、低效的正则表达式。
排查方法:
9、批处理时间长、数据库逐笔插入缓慢
问题现象:大批量数据(10万条以上)更新或插入数据库,耗时较长。
问题原因:批量数据处理时,如果逐条更新数据库,则会存在大量网络io、磁盘io,耗时较长,而且对数据库资源消耗较大。
解决方法:
10、数据库CPU高
问题现象:后台指令发送满负荷工作时,数据库CPU高。
问题原因:后台指令发送线程每次对全量查询结果排序,结果集很大,然后取一条记录;索引区分度不高,满负荷执行时;查询频率很高;压测显示,并行发送指令的后台线程越多,数据库CPU越高,效率越低。
解决方法:
改进后的方案,数据库CPU降到10%一下,发送效率单机提升6倍,且可线性扩展任务服务器。
11、压测TPS曲线剧烈下降或抖动
问题现象:50并发压测,TPS曲线正常应该是平缓的,波动不大,如果突然出现剧烈下降,并且短时间内无法恢复,则可能存在问题。
问题原因:一般是由于前置或bp的jvm进行垃圾回收,或者日志记录磁盘满导致的。
解决方法:如果不是特别剧烈的波动或者TPS曲线下降后长时间不反弹,则可以忽略该问题。否则,需要分析曲线下降的时刻,系统当时正在发生的事情。可以通过top命令监控当时CPU占用比价高的线程,也可以kill -3 pid杀javacore来查看线程堆栈。
六、优化案例
1、网银系统基本情况
1) 压测环境系统架构
压测环境不包括F5,只有1台WEB、1台前置(应用服务器)、1台BP(业务处理服务器)、1台DB、1台Redis服务器。
2)客户请求链路:
客户端压力机-〉Web-〉PRE-〉BP-〉DB2 ;
客户端压力机-〉Web-〉PRE-〉BP-〉挡板服务器(模拟后端服务方)。
3)系统特点:
对公业务典型场景:经办、审核、查询、下载、批量。用户权限通过业务链控制。
前后端分离,前端通过调用一个个接口来完成业务。接口粒度较细。登陆、支付转账经办需要15~19个接口完成一次交易。
资金交易系统,一致性需要通过数据库来保证。
4)网银登录压测曲线
2、数据库消息队列案例(指令发送队列)
需求是:对于需要异步处理的交易指令,需要设计一个基于数据库的消息队列,我们称之为指令发送队列。该队列既要满足服务器的性能约束,又要满足每日处理交易量的要求。
1)优化前处理过程
后台任务每次从数据库指令表中排序并取最早的一条指令,获取该指令的详细交易信息,组装成报文并调用接口发送后台核心系统。
在压测环境下,该方案如果配置一个后台任务,无法达到系统设计的指令处理速度;如果配置多个后台任务,则会导致数据库CPU占用较高,影响其他联机业务的开展。
2)优化后处理过程
每台后台任务服务器配置一个生产者任务,20个消费者任务。
生产者任务一次取100个(可配置)指令,依次分配给20个消费者任务中空闲的任务,消费者任务获取指令详细信息,并组装报文后发送后台核心系统;生产者任务如果发现无空闲消费者任务,则等待15s后重新判断。
此方案显著降低了数据库CPU负载,合理利用了应用服务器的并发能力,处理效率大为提高,以上线程数和每次获取的指令数可以配置。
指令发送线程池模型图:
3、数据库死锁案例
1)死锁问题现象
当多台任务服务器同时运行大额指令发送后台线程,即多个生产者线程并行更新数据库指令表时,数据库快照检测到存在数据库死锁,或通过db2evmon -db corpdb -evm DB2DETAILDEADLOCK>dlock.txt 生成死锁监控文件,快照或监控文件中存在deadlock的情况。
DB2数据库有自动解除死锁功能,死锁超时时间默认为10s,数据库会随机选择一个死锁事务kill掉。本案例由于是后台任务,所以用户感觉不到死锁;如果是联机交易,一个用户会发现交易失败,另一个用户交易成功,但是会感觉交易变慢。指令发送后台任务模型详见下一章节内容。
2)数据库死锁发生原理
两个不同的数据库事务使用排它锁锁住了同一张表的不同行记录,并且互相等待读取对方锁住的行记录。
3)导致死锁的可能原因
全表扫描、大事务、事务之间对死锁访问顺序交叉等。
4)死锁问题排查过程
Step1:分析数据库快照和死锁监控日志,查看导致死锁的SQL,定位问题SQL。
Step2:问题SQL不存在事务之间对死锁访问顺序交叉的情况,当时尚不清楚程序中的全表扫描、大事务可能会导致死锁,因此做了以下实验:
UPDATE ( SELECT BP_SRVR_IP FROM ${tableName} WHERE TSK_STAT='TODO' AND ( BP_SRVR_IP IS OR BP_SRVR_IP='') AND PRTY =? AND eff_tm <= CURRENT TIMESTAMP FETCH FIRST 50 ROWS ONLY WITH RS) t SET BP_SRVR_IP=?
Step3:实验发现查询SQL增加with RS隔离级别,查询效率会更高。当A事务已对记录R加X锁,B事务扫描到R记录时,如果是CS隔离级别,B事务会自动退出,返回空结果集;如果是RS隔离级别,B事务会等待A事务完成后,跳过R记录,对符合条件的R+1记录加X锁。
Step4:PRTY字段增加索引,没有出现死锁问题;或者不加索引,维持全表扫描不变,大事务改成小事务后,也没有出现死锁问题。
5)两点经验
七、后续提升网银系统性能备选方法
1、增加缓存的使用
2、精简BP日志。删除交易访问记录日志表的操作。
3、合并、精简接口数量,前端缓存数据。
本文由 Funstec非凡实验室 作者:Albert 发表,转载请注明来源!