服务性能优化总结
服务性能优化总结
一、数据库连接池耗时优化
1.1 问题排查
在链路工具上查看偶尔会有性能抖动,排查后发现是dbcp耗时过长
1.1.1 线上dbcp配置情况
线上dbcp数据库连接池配置,如下:
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="${db.jdbc.driverClassName}"/>
<property name="url" value="${db.jdbc.url}"/>
<property name="username" value="${db.jdbc.username}"/>
<property name="password" value="${db.jdbc.password}"/>
<property name="maxActive" value="20"/>
<property name="maxIdle" value="3"/>
<property name="maxWait" value="15000"/>
<property name="timeBetweenEvictionRunsMillis" value="60000"/>
<property name="minEvictableIdleTimeMillis" value="180000"/>
</bean>
当前使用的版本是dbcp 1.4 版本
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
<version>1.4</version>
</dependency>
1.1.2 dbcp配置项含义
参数 | 默认值 | 说明 |
---|---|---|
username | \ | 传递给JDBC驱动的用于建立连接的用户名 |
password | \ | 传递给JDBC驱动的用于建立连接的密码 |
url | \ | 传递给JDBC驱动的用于建立连接的URL |
driverClassName | \ | 使用的JDBC驱动的完整有效的Java 类名 |
initialSize | 0 | 初始化连接:连接池启动时创建的初始化连接数量,1.2版本后支持 |
maxActive | 8 | 最大活动连接:连接池在同一时间能够分配的最大活动连接的数量, 如果设置为非正数则表示不限制 |
maxIdle | 8 | 最大空闲连接:连接池中容许保持空闲状态的最大连接数量,超过的空闲连接将被释放,如果设置为负数表示不限制 |
minIdle | 0 | 最小空闲连接:连接池中容许保持空闲状态的最小连接数量,低于这个数量将创建新的连接,如果设置为0则不创建 |
maxWait | 无限 | 最大等待时间:当没有可用连接时,连接池等待连接被归还的最大时间(以毫秒计数)超过时间则抛出异常,如果设置为-1表示无限等待 |
testOnReturn | false | 是否在归还到池中前进行检验 |
testWhileIdle | false | 连接是否被空闲连接回收器(如果有)进行检验.如果检测失败, |
则连接将被从池中去除.设置为true后如果要生效,validationQuery参数必须设置为非空字符串 | ||
minEvictableIdleTimeMillis | 1000 * 60 * 30 | 连接在池中保持空闲而不被空闲连接回收器线程(如果有)回收的最小时间值,单位毫秒 |
numTestsPerEvictionRun | 3 | 在每次空闲连接回收器线程(如果有)运行时检查的连接数量 |
timeBetweenEvictionRunsMillis | -1 | 在空闲连接回收器线程运行期间休眠的时间值,以毫秒为单位. 如果设置为非正数,则不运行空闲连接回收器线程 |
validationQuery | null | SQL查询,用来验证从连接池取出的连接,在将连接返回给调用者之前.如果指定,则查询必须是一个SQL SELECT并且必须返回至少一行记录 |
testOnBorrow | TRUE | 是否在从池中取出连接前进行检验,如果检验失败,则从池中去除连接并尝试取出另一个. |
。validationQuery(测试连接的sql语句) : validationQuery= “SELECT 1”
。testWhileIdle (连接是否被空闲连接回收器(如果有)进行检验.如果检测失败,则连接将被从池中去除) : testWhileIdle = “true”
。testOnBorrow = “false” 借出连接时不要测试,否则很影响性能,性能会下降7-10倍
。timeBetweenEvictionRunsMillis (每30秒运行一次空闲连接回收器(独立线程)。并每次检查3个连接,
如果连接空闲时间超过30分钟就销毁。销毁连接后,连接数量就少了,如果小于minIdle数量,就新建连接,维护数量不少于minIdle)
。timeBetweenEvictionRunsMillis = “30000”
。minEvictableIdleTimeMillis(池中的连接空闲x毫秒后被回收,默认30分钟):minEvictableIdleTimeMillis = “1800000”
。numTestsPerEvictionRun(在每次空闲连接回收器线程(如果有)运行时检查的连接数量,默认值就是3.) : numTestsPerEvictionRun =“3”
1.1.3 dbcp现有配置分析
我们的应用对有效参数只配置了
<property name="maxActive" value="20"/>
<property name="maxIdle" value="3"/>
<property name="maxWait" value="15000"/>
<property name="timeBetweenEvictionRunsMillis" value="60000"/>
<property name="minEvictableIdleTimeMillis" value="180000"/>
因此,当前数据库连接池配置的含义转换下,就是:
初始化连接数initialSize是0,
最小空闲连接数minIdle是0,
最大连接数maxActive是20,
最大空闲数maxIdle是3,
最大排队等待时间maxWait是15秒,
检查空闲连接线程的时间timeBetweenEvictionRunsMillis是60秒,
空闲线程保持时间多久会被回收minEvictableIdleTimeMillis是30分钟。
1.1.4 线上数据库情况分析
按照user维度查询,当前连接数是120个,实例是60台,平均每个实例保持2个连接数,由于当前没有配置minIdle,也就是minIdle默认是0,当空闲数大于休眠时间是可以被回收的,
因此,当一个应用运行连接数达到≥2时刻会产生连接数扩容,小于maxIdle=3后会回收,这里是可能产生性能问题的地方。
再按照连接数维度查询,目前是2个物理分库6个逻辑分库,即A物理分库有1-3逻辑库,B物理分库有4-6逻辑库,单个物理分库保持在360个已连接数(如图)总连接数大概为360*2=720个,实例是60个,每个实例平均12个连接数,每个应用都会对接6个逻辑库,因此每个应用日常使用的单个逻辑库数据库连接平均也是2个,与user维度分析是一致的。
1.2 原因分析
出现耗时一般是创建连接池创建连接。在当前配置下,可能个别应用出现空闲连接较长被回收,但是minIdle未配置最小空闲保持数量,导致回收后又扩容连接数的问题从而影响性能。关于数据库连接池的配置之前没有太多考究和分析,应该是其他项目直接粘贴过来的,以后关于核心配置项都要充分思考和实践得出可行性结论。
1.3 解决过程
1.3.1 数据库连接数规划
dba反馈,数据库连接数的限制,物理机一般为3000最大,虚拟机一般为1500最大,不可以扩容连接数,要根据情况调整数据库连接池、应用机器数量。
基于当前系统架构,2个物理分库,6个逻辑分库,A物理分库(1、2、3逻辑库)B物理分库(4、5、6逻辑库),目前应用实例为30台,单机单实例配置。
根据逻辑拆分后,对现有连接数做资源规划,由于主库查询主要是学校信息,查询量较小,分配最大150连接数,其余分库分表均为学生信息,每个逻辑分库分配最大450连接数。
根据当前数据库能力,评估下最大负载能力,评估下数据库连接数的可用性
场景 | tp999 | 理论连接数分析 | 理论最大支持qps |
---|---|---|---|
最大负载 | 10ms | 30台实例,每个实例的每个逻辑分库的最大连接数被限制为15个,30*15*(1000ms/10ms)=45000qps,即单个逻辑库可支持45000qps,由于拥有6个逻辑分库,理想情况下水平散列分摊到6个库上,即45000qps*6=27Wqps,由于目前拆分两个物理库,所以物理单库负载为27Wqps/2=13.5Wqps | 27Wqps,物理单库13.5Wqps |
最大负载 | 20ms | 30台实例,每个实例的每个逻辑分库的最大连接数被限制为15个,30*15*(1000ms/20ms)=22500qps,即单个逻辑库可支持22500qps,由于拥有6个逻辑分库,理想情况下水平散列分摊到6个库上,即22500qps*6=13.5Wqps,由于目前拆分两个物理库,所以物理单库负载为13.5Wqps/2=6.75Wqps | 13.5Wqps,物理单库6.75Wqps |
最大负载 | 30ms | 30台实例,每个实例的每个逻辑分库的最大连接数被限制为15个,30*15*(1000ms/30ms)≈15000qps,即单个逻辑库可支持15000qps,由于拥有6个逻辑分库,理想情况下水平散列分摊到6个库上,即15000qps*6=9Wqps,由于目前拆分两个物理库,所以物理单库负载为9Wqps/2=4.5Wqps | 9Wqps,物理单库4.5Wqps |
综上分析:以上均是理论值,实际上数据库机器配置承载不了,随着qps上升数据库CPU、内存的可用性均会线性下降,性能也会线性下降。仅分析在没有慢SQL,没有超巨大流量撞库的情况下,数据库连接数是充足的,在一般情况下是用不满的
1.3.2 连接池参数调优
日常调用评估
日常调用MANAGE的核心接口tp999保持在10ms-30ms之间,日常qps在1200-1500左右
场景 | qps | tp999 | 理论连接数分析 | 逻辑分库连接数(60实例) | 理论分析结论 |
---|---|---|---|---|---|
日常 | 1200 | 10ms | 1200qps/(单个连接数每秒吞吐量 1000ms/10ms)= 12,即1200qps在性能10ms情况下需要12个连接数负载, 由于逻辑分库为6个, 理想情况下水平散列分摊到6个库上,即12/6=2,即每个逻辑库负载2个连接数; 极端情况下聚集单库上,即每个逻辑库负载12个连接数 | 理想 2 / 60 = 至少1 极端 12 / 60 = 至少1 | 1200qps,10ms,单个应用需要1个连接数配置 |
日常 | 1200 | 20ms | 1200qps/(单个连接数每秒吞吐量 1000ms/20ms)= 24,即1200qps在性能20ms情况下需要24个连接数负载, 由于逻辑分库为6个, 理想情况下水平散列分摊到6个库上,即24/6=4,即每个逻辑库负载4个连接数 极端情况下聚集单库上,即每个逻辑库负载24个连接数 | 理想 4 / 60 = 至少1 极端 24 / 60 = 至少1 | 1200qps,20ms,单个应用需要1个连接数配置 |
日常 | 1200 | 30ms | 1200qps/(单个连接数每秒吞吐量 1000ms/30ms)≈36,即1200qps在性能30ms情况下需要36个连接数负载, 由于逻辑分库为6个, 理想情况下水平散列分摊到6个库上,即36/6=6,即每个逻辑库负载6个连接数 极端情况下聚集单库上,即每个逻辑库负载36个连接数 | 理想 6 / 60 = 至少1 极端 36 / 60 = 至少1 | 1200qps,30ms,单个应用需要1~2个连接数配置 |
日常 | 1500 | 10ms | 1500qps/(单个连接数每秒吞吐量 1000ms/10ms)= 15,即1500qps在性能10ms情况下需要15个连接数负载, 由于逻辑分库为6个, 理想情况下水平散列分摊到6个库上,即15/6=2.5,即每个逻辑库负载2.5个连接数 极端情况下聚集单库上,即每个逻辑库负载15个连接数 | 2.5向上取整3 理想 3 / 60 = 至少1 极端 15 / 60 = 至少1 | 1500qps,10ms,单个应用需要1个连接数配置 |
日常 | 1500 | 20ms | 1500qps/(单个连接数每秒吞吐量 1000ms/20ms)= 30,即1500qps在性能20ms情况下需要30个连接数负载, 由于逻辑分库为6个, 理想情况下水平散列分摊到6个库上,即30/6=5,即每个逻辑库负载5个连接数 极端情况下聚集单库上,即每个逻辑库负载30个连接数 | 理想 5 / 60 = 至少1 极端 30/ 60 = 至少1 | 1500qps,20ms,单个应用需要1个连接数配置 |
日常 | 1500 | 30ms | 1500qps/(单个连接数每秒吞吐量 1000ms/30ms)≈ 45,即1500qps在性能30ms情况下需要45个连接数负载, 由于逻辑分库为6个, 理想情况下水平散列分摊到6个库上,即45/6=7.5,即每个逻辑库负载7.5个连接数 极端情况下聚集单库上,即每个逻辑库负载45个连接数 | 7.5向上取整8 理想 8 / 60 = 至少1 极端 45/ 60 = 至少1 | 1500qps,30ms,单个应用需要1个连接数配置 |
综上分析:日常调用量,由于调用量小,考虑散列极端情况,60个实例情况下,单个应用的数据库连接数需要1个,该情况能够覆盖日常场景对数据库连接池的要求
大促调用评估
大促峰值BIZ调用约为1W,由于缓存拦截撞库小于1W
场景 | qps | tp999 | 理论连接数分析 | 逻辑分库连接数(60实例) | 理论分析结论 |
---|---|---|---|---|---|
大促 | 10000 | 10ms | 10000qps/(单个连接数每秒吞吐量 1000ms/10ms)= 100,即10000qps在性能10ms情况下需要100个连接数负载, 由于逻辑分库为6个, 理想情况下水平散列分摊到6个库上,即100/6≈16.6,即每个逻辑库负载16.6个连接数 | 16.6向上取整 17 / 60 = 至少1 | 10000qps,10ms,单个应用需要1个连接数配置 |
大促 | 10000 | 20ms | 10000qps/(单个连接数每秒吞吐量 1000ms/20ms)= 200,即10000qps在性能20ms情况下需要200个连接数负载, 由于逻辑分库为6个, 理想情况下水平散列分摊到6个库上,即200/6≈33.3,即每个逻辑库负载33.3个连接数 | 33.3向上取整 34 / 60 = 至少1 | 10000qps,20ms,单个应用需要1个连接数配置 |
大促 | 10000 | 30ms | 10000qps/(单个连接数每秒吞吐量 1000ms/30ms)≈ 303,即10000qps在性能30ms情况下需要303个连接数负载, 由于逻辑分库为6个, 理想情况下水平散列分摊到6个库上,即303/6=50.5,即每个逻辑库负载50.5个连接数 | 50.5向上取整 51 / 60 = 至少1 | 10000qps,30ms,单个应用需要1个连接数配置 |
大促 | 10000 | 50ms | 10000qps/(单个连接数每秒吞吐量 1000ms/50ms)≈ 500,即10000qps在性能50ms情况下需要500个连接数负载, 由于逻辑分库为6个, 理想情况下水平散列分摊到6个库上,即500/6≈83.3,即每个逻辑库负载83.3个连接数 | 83.3向上取整 84 / 60 = 至少2 | 10000qps,50ms,单个应用需要2个连接数配置 |
综上分析:峰值调用量,由于调用量大,不考虑散列极端情况,单个应用的数据库连接数需要在1~2个之间浮动,该情况能够覆盖压测对数据库连接池的要求
压测调用评估
按照大促峰值5倍压测,极端情况5W全部穿透缓存进行撞库,分析如下
场景 | qps | tp999 | 理论连接数分析 | 逻辑分库连接数(60实例) | 理论分析结论 |
---|---|---|---|---|---|
压测 | 50000 | 10ms | 50000qps/(单个连接数每秒吞吐量 1000ms/10ms)= 500,即50000qps在性能10ms情况下需要500个连接数负载, 由于逻辑分库为6个, 理想情况下水平散列分摊到6个库上,即500/6≈83.3,即每个逻辑库负载83.3个连接数 | 83.3向上取整 84 / 60 = 至少2 | 50000qps,10ms,单个应用需要2个连接数配置 |
压测 | 50000 | 20ms | 50000qps/(单个连接数每秒吞吐量 1000ms/20ms)= 1000,即50000qps在性能20ms情况下需要1000个连接数负载, 由于逻辑分库为6个, 理想情况下水平散列分摊到6个库上,即1000/6≈166.6,即每个逻辑库负载166.6个连接数 | 166.6向上取整 167 / 60 = 至少3 | 50000qps,20ms,单个应用需要3个连接数配置 |
压测 | 50000 | 30ms | 50000qps/(单个连接数每秒吞吐量 1000ms/30ms)≈ 1515,即50000qps在性能30ms情况下需要1515个连接数负载, 由于逻辑分库为6个, 理想情况下水平散列分摊到6个库上,即1515/6=252.5,即每个逻辑库负载252.5个连接数 | 252.5向上取整253 253 / 60 = 至少5 | 50000qps,30ms,单个应用需要5个连接数配置 |
压测 | 50000 | 50ms | 50000qps/(单个连接数每秒吞吐量 1000ms/50ms)≈ 2500,即50000qps在性能50ms情况下需要2500个连接数负载, 由于逻辑分库为6个, 理想情况下水平散列分摊到6个库上,即2500/6≈416.6,即每个逻辑库负载416.6个连接数 | 416.6向上取整417 417 / 60 = 至少7 | 50000qps,50ms,单个应用需要7个连接数配置 |
综上分析:峰值调用量,由于调用量大,不考虑散列极端情况,单个应用的数据库连接数需要在2~7个之间浮动,该情况能够覆盖压测对数据库连接池的要求
综合评估 & 配置调整
日常需求,60实例,1200~1500qps,单应用1个连接数
大促需求,60实例,1Wqps,单应用1~2个连接数
压测需求,60实例,5Wqps,单应用2~7个连接数
为了避免频繁创建和回收,设置initialSize为3,满足日常和大促的最大连接数要求;
minIdle为3,在满足日常及大促峰值最大使用情况下即时线程充足处于空闲状态也不要回收,保持最大使用情况的连接数量进行保持,避免频繁销毁创建;
maxIdle为5,超过该值进行回收,一般压测DB性能好也不会超过该值,若超过后可以回收减少资源消耗和占用
maxActive为7~8,为了分摊tph压力,提高单机CPU、内存利用率,采用单机双实例,共60实例进行负载
物理分库A,逻辑分库6073=1260 + 主库604=240,max最大1500,不超过limit限制
物理分库B,逻辑分库608*3=1440,max最大1440,不超过limit限制
单库配置
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="${db.jdbc.driverClassName}"/>
<property name="url" value="${db.jdbc.url}"/>
<property name="username" value="${db.jdbc.username}"/>
<property name="password" value="${db.jdbc.password}"/>
<property name="initialSize" value="2"/>
<property name="minIdle" value="2"/>
<property name="maxIdle" value="3"/>
<property name="maxActive" value="4"/>
<property name="maxWait" value="2000"/>
<property name="timeBetweenEvictionRunsMillis" value="60000"/>
<property name="minEvictableIdleTimeMillis" value="180000"/>
</bean>
物理A库(逻辑1-3库)配置
<bean id="targetDataSource1" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="${db.jdbc.driverClassName}"/>
<property name="url" value="${db1.jdbc.url}"/>
<property name="username" value="${db1.jdbc.username}"/>
<property name="password" value="${db1.jdbc.password}"/>
<property name="initialSize" value="3"/>
<property name="minIdle" value="3"/>
<property name="maxIdle" value="5"/>
<property name="maxActive" value="7"/>
<property name="maxWait" value="2000"/>
<property name="timeBetweenEvictionRunsMillis" value="60000"/>
<property name="minEvictableIdleTimeMillis" value="180000"/>
</bean>
物理B库(逻辑4-6库)配置
<bean id="targetDataSource1" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="${db.jdbc.driverClassName}"/>
<property name="url" value="${db1.jdbc.url}"/>
<property name="username" value="${db1.jdbc.username}"/>
<property name="password" value="${db1.jdbc.password}"/>
<property name="initialSize" value="3"/>
<property name="minIdle" value="3"/>
<property name="maxIdle" value="5"/>
<property name="maxActive" value="8"/>
<property name="maxWait" value="2000"/>
<property name="timeBetweenEvictionRunsMillis" value="60000"/>
<property name="minEvictableIdleTimeMillis" value="180000"/>
</bean>
1.4 实测结果
dbcp耗时过长问题解除,当前连接数满足日常、大促、压测各场景需要。
二、网络耗时问题排查
2.1 问题排查
有依赖方反馈偶尔RPC调用超时,于是进行排查,发现该时段机器批量超时
但是调用量无明显增加,至少本地服务没有流量洪峰
2.2 原因分析
初步判断,怀疑是网络问题,然后到链路工具去查看该时段内服务调用链的耗时情况
发现本地BIZ服务调用本地MANAGE服务存在网络超时,由于BIZ对外提供服务,之前压测存在网络问题全部机器都更换为万兆网卡,但是MANAGE的千兆网卡由于资源紧张暂时没有调整,结合PE排查后,建议更换千兆网卡为万兆网卡
2.3 解决过程
淘汰千兆网卡机器,更换为万兆网卡,去除服务不安全隐患问题
2.4 实测结果
外部RPC恢复正常,超时情况解决
三、缓存耗时问题排查
3.1 问题排查
在观察性能接口方法性能时,发现会有偶发性的tp999增高,如下图
跟踪了几轮后,发现经常是10.XXX.XXX.119这台机器ip有性能变差的问题,然后跟进链路工具查看链路耗时详情查看
3.2 原因分析
发现是Redis耗时较长,于是咨询运维同事如何排查Redis的耗时操作
答复是要查看是客户端延时还是服务端延时,如果是服务端延时看一下是否有慢查询,大key或热key
然后观察几轮后,发现均是10.XXX.XXX.119这台机器的Redis耗时过长,怀疑是网络问题,于是到R2M后台查看客户端延迟监控数据,果然基本都是10.XXX.XXX.119这台机器客户端性能差
3.3 解决过程
进一步沟通,反馈“这种是发生网络重传了,网络重传基本200ms左右”,看链路工具上单个方法的Redis调用次数是5次,这个业务方法正常应该会有2次,数据验证了这个说法
目前处理的方法是下线网络有问题的机器,后面替换掉。
3.4 实测结果
缓存耗时问题得到解决
四、日志耗时优化
4.1 问题排查
通过性能工具查看偶有耗时过长操作如下
定位该操作均不定时偶发在不同的、但同一个IP上,查看了每个耗时详情,均是logback耗时过长
4.2 原因分析
初步判断是logback同步写日志存在竞争导致耗时过长
一般日志的性能损耗有如下:
1、日志写文件同步锁竞争消耗
2、打印行号性能损失,而且接口调用量越大,线程争夺线程栈资源的情况越严重
关于日志优化的文章很多,可以百度查看,这里不加赘述
4.3 解决过程
1、日志异步写,但有丢日志风险,根据业务情况取舍
2、日志降级,借助统一配置中心可以实时动态修改日志级别,对日志打印进行降级操作,日志降级的好处一般是info级别日志不再进行打印和输出,这部分日志丢失,仅打印error级别日志,因此性能会提升很多
3、关闭行号打印后观察一段数据,因logback产生的抖动明显减少
4.4 实测结果
服务性能稳定,因logback导致的偶尔抖动消失