------记一次系统级bug定位的辛酸历程
时间:2021-03-27,还很新鲜,以此文纪念整个排查过程之辛酸,也给遇到类似问题的有“猿”人提供一些参考。
项目:某2B + 2C项目(包含移动端及PC端)
现象:高并发场景(均是和疫情相关:电子通行证上线第一天、疫苗摸排登记上线第一天)下系统崩溃假死,其中疫苗摸排登记上线第一天2-3小时内PV达到了近80万,QPS尚未进行测算统计,理论上并不会太高。
表面原因分析:nginx有请求进入,但后台接口服务连接请求超时,通过日志监控发现,后台接口服务有请求接收日志,但无response输出。初步判定卡在部分业务逻辑执行过程中。导致后续所有请求都处于wating状态。因接口服务一直可接收新的请求进来,在nginx的负载轮询机制中并未能识别出这一异常情况,导致负载失效,nginx一直不停地将新的请求分发到异常接口服务中去。
辛酸:接连两次政治任务,系统都没能抗得住并不算太大的并发压力,因一直未定位到具体的原因,也就一直没有准确的结论和对应的解决方案,疫苗摸排登记上线前也曾进行过压力测试,给出的结论是能够支撑2000的并发(可能是压测的方法不对,好像是只单独压了个别接口),让人从心理上轻敌了,认为之前电子通行证的崩溃问题已经要成为历史了,但万万没想到,疫苗摸排登记第一天在前两三个小时就崩溃了,在一干人等合力排查定位问题无果的前提下,只能是从侧面对系统进行了各个方面的优化,诸如增加负载节点,减少接口调用次数,增加对缓存的利用,优化查询统计脚本等等,从一定程度上尽大可能地来缓解服务器端的压力,经过一夜的奋战,第二天重新上线后,暂时抗住了压力没有再出现崩溃情况,但第二天的并发量明显没有第一天的高,另外,没有从根源上解决问题(一直怀疑是系统架构中采用的框架问题),始终让人有种不踏实的感觉。因为之前太多人参与到了排查过程中,每个人的出发点不一样,给出各种尝试方案,有更改中间件连接线程参数的,有更改服务器配置的,还有定位出数据库抗不住的,要求采用读写分离的…...,总之,那一天各路大神各显神通,问题却依然存在。现在回想起来都感觉好无助。
追查之路:就这样提心掉胆地过了一个春节,节后冷静了一段时间,终于鼓起勇气,下决心重新排查定位,追凶破案,以洗雪耻!
由于上次系统崩溃假死事发后,重新部署了一套测试环境重新进行了压测,好在能够成功稳定复现崩溃假死的现象,这样就方便排查原因了。
首先,检查堆栈信息,因服务假死,没有任何的系统日志输出,只能JVM的堆栈信息中寻找蛛丝马迹。
项目采用了docker部署模式,
通过docker exec -it container_name /bin/bash 命令进入容器,
采用jps命令查找java进程id,docker中是1,
采用top -Hp 1命令查看对应的资源占用情况,
采用jstack 1 > jstack.log命令将堆栈信息导出到文件jstack.log中。
发现,该系统中集成了druid 数据库连接池框架,而卡死的原因也正是druid中获取数据库连接一直处于等待状态引起的。
跟踪druid源码找到线程阻塞的位置:
再继续向上跟踪,找到getConnectionInternal方法(private DruidPooledConnection getConnectionInternal(long maxWait)throwsSQLException):
通过maxWatit参数来判定是否走takeLast流程,而maxWatit的值始终是默认值-1,而不是application.yml配置文件中的60000。
那么,问题来了,application.yml配置文件中所有关于druid的配置(在spring.datasource.druid中配置)参数都没有生效。
顺藤摸瓜,下一步,定位druid配置参数问题:
大意了,因系统采用的框架是BladeX(至于为何要选用该框架不得而知,私下看了其部分源码,其代码质量尚需提升啊,也可能跟其开发团队中个别人员技术水平有关吧,一个开发团队中并不是所有人员的技术水平都是一样的,只不过刚好被发现了而已)的,引入的Druid连接池也是BladeX自带的,application.yml配置文件也是BladeX提供的,看似搭配完美,本不该存在这种问题。实则不然:
因项目中采用动态数据源配置模式,依赖了第三方的dynamic-datasource-spring-boot-starter框架,
经过跟踪源码发现在dynamic-datasource-spring-boot-starter的启动配置中自动加载了spring.datasource.dynamic.datasource.primary-name(如mysql1).druid节点下的配置参数!
果然是被某框架的默认配置给带沟里了!
注:必须要用druid-spring-boot-starter 1.2.1及以上版本才支持加载动态配置!
经过以上调整(1-调整druid参数配置位置,放置于动态数据源节点下;2-升级druid-spring-boot-starter版本至1.2.1及以上),同样的环境原本压测只能压到100左右的系统,目前可以抗到2000以上(受限于压测工具的客户端,尚未测试出极限值)。
当然,除了配置上的问题之外,系统的复杂业务逻辑设计及随处可见的慢查询也是影响系统性能的关键因素之一,下一步将适时优化,必要时进行重构。
附
druid最新版本下载地址:
https://codeload.github.com/alibaba/druid/zip/refs/tags/1.2.5
druid各配置参数说明:
配置缺省值说明
name
配置这个属性的意义在于,如果存在多个数据源,监控的时候可以通过名字来区分开来。如果没有配置,将会生成一个名字,格式是:"DataSource-" + System.identityHashCode(this). 另外配置此属性至少在1.0.5版本中是不起作用的,强行设置name会出错。
url
连接数据库的url,不同数据库不一样。例如:mysql : jdbc:mysql://10.20.153.104:3306/druid2oracle : jdbc:oracle:thin:@10.20.149.85:1521:ocnauto
username
连接数据库的用户名
password
连接数据库的密码。如果你不希望密码直接写在配置文件中,可以使用ConfigFilter。
driverClassName 根据url自动识别
这一项可配可不配,如果不配置druid会根据url自动识别dbType,然后选择相应的driverClassName
initialSize 0
初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时
maxActive 8
最大连接池数量
maxIdle 8
已经不再使用,配置了也没效果
minIdle
最小连接池数量
maxWait
获取连接时最大等待时间,单位毫秒。配置了maxWait之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置useUnfairLock属性为true使用非公平锁。
poolPreparedStatements FALSE
是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。
maxPoolPreparedStatementPerConnectionSize -1
要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100
validationQuery
用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
validationQueryTimeout
单位:秒,检测连接是否有效的超时时间。底层调用jdbc Statement对象的void setQueryTimeout(int seconds)方法
testOnBorrow TRUE
申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
testOnReturn FALSE
归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
testWhileIdle FALSE
建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
keepAlive false(1.0.28)
连接池中的minIdle数量以内的连接,空闲时间超过minEvictableIdleTimeMillis,则会执行keepAlive操作。
timeBetweenEvictionRunsMillis 1分钟(1.0.14)
有两个含义:1) Destroy线程会检测连接的间隔时间,如果连接空闲时间大于等于minEvictableIdleTimeMillis则关闭物理连接。2) testWhileIdle的判断依据,详细看testWhileIdle属性的说明
numTestsPerEvictionRun 30分钟(1.0.14)
不再使用,一个DruidDataSource只支持一个EvictionRun
minEvictableIdleTimeMillis
连接保持空闲而不被驱逐的最小时间
connectionInitSqls
物理连接初始化的时候执行的sql
exceptionSorter 根据dbType自动识别
当数据库抛出一些不可恢复的异常时,抛弃连接
filters
属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有:监控统计用的filter:stat日志用的filter:log4j防御sql注入的filter:wall
proxyFilters
类型是List<com.alibaba.druid.filter.Filter>,如果同时配置了filters和proxyFilters,是组合关系,并非替换关系