【引言】
2019年刚开始,依旧很长很累,经历了两周每天都在上线的节奏,一周在北京,一周在杭州。现在回家了,终于有时间把前一周新项目上线遇到的问题总结一下。
【问题】
在项目刚上线的第二天,客户那边一部分人继续在线下开单,订单通过此项目同步到我们平台上,一部分人开始在我们平台试用开单,所以用的人多了,项目的问题也就暴露出来的。
1. Hikari Unable to acquire JDBC Connection
系统架构用的是Spring Boot 2.0版本,数据库连接池集成的是Hikari,原有的配置如下:
# 配置数据源
spring:
datasource:
jdbc-url: jdbc:mysql://***/uqijoint?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull
username: ***
password: ***
driver-class-name: com.mysql.jdbc.Driver
type: com.zaxxer.hikari.HikariDataSource
hikari:
minimum-idle: 50
maximum-pool-size: 150
auto-commit: true
idle-timeout: 300000
pool-name: DatebookHikariCP
max-lifetime: 1800000
connection-timeout: 30000
connection-test-query: SELECT 1
关于hikari连接池的几个参数含义如下:
minimum-idle:指定连接维护的最小空闲连接数
maximum-pool-size:指定连接池最大的连接数,包括使用中的和空闲的连接.
auto-commit:指定updates是否自动提交.
idle-timeout:指定连接多久没被使用时,被设置为空闲,默认为10ms
pool-name:指定连接池名字.
max-lifetime:指定连接池中连接的最大生存时间,毫秒单位.
connection-timeout:指定连接的超时时间,毫秒单位.
connection-test-query:指定校验连接合法性执行的sql语句
当问题出现后,同事直接暴力的将上述配置都删掉了,结果并没有解决问题。于是我开始查资料,寻找其他解决方案。大部分解决方案是因为没有配置相关参数,而去添加对应的配置参数,但此类方案感觉并不是我们项目中的解决方案。最后,找到一个自我感觉比较可信的方案:
大致意思是项目中用的事务没有关闭,导致旧的事务堆积而新的事务无法开启,在设置了数据库连接超时参数的情况下,就会导致后面再也无法获取到数据库连接。
按照上述提供的方案,我将项目中所有类的@Transitional事务注解都去掉了,因为此项目是一个连接项目,本身也不需要什么事务支持。上线后的一天,此问题没有再出现了,也可以证明此解决方案是可行的。
2. Executing an update/delete query
这个问题是因为项目中集成了JPA,而JPA要求,没有事务支持,不能执行更新和删除操作。解决方法就是在Service层添加@Transitional注解。
在前一天,我将项目中所有类的事务注解都去掉了,解决数据库连接不上的问题。既然在这个方法下需要事务支持,我就将该类的事务注解加上了,检查了一遍,整个项目需要添加的地方加起来也就两三个类。
上线的第三天,白天没有再出现前两个问题,这个问题算是解决了,也没有再次出现第一个问题。
3. Lock wait timeout exceeded; try restarting transaction
第四天,前一个晚上干到一两点,早上不到十点,老大的一个电话就把我吵醒了,说线上又出问题了,平台下的订单,都不能同步到第三方系统,导致系统没有回写他们的单号,而不能进行订单后面的流程。
心里很烦,想想别人的老大,出问题了三两分钟自己搞定。而我,线上出问题了,肯定是没有清静的时候。抱怨归抱怨,问题还是需要解决的。
其实,这个问题在前两天也出现过,只是觉得是因为前两个问题导致的,因为连接阻塞了,导致后面其他的提交都在等待。而事实证明,并不是一个问题。
上图是错误截图,而遇到这个问题,查到的解决方案是可以执行下面的三个sql,查看数据库的锁超时等待的进程,如下:
select * from information_schema.innodb_trx;
select * from information_schema.INNODB_LOCKS;
select * from information_schema.INNODB_LOCK_WAITS;
确实,存在锁等待的一个进程,执行的sql是插入订单信息,为了先让客户可以使用系统,我就先把锁等待的那个进程直接kill了,下面继续找原因。
这个问题也不是每笔订单都会出现的,我也不知道从哪找原因。很长时间没有什么解决方案,就找大神问了一下,他问我插入需要很长时间吗?这个时候,同事也开始说了,像现在下单的人多了起来,我们代码里都没有锁机制,就需要在代码中加锁处理了。
又跟了一遍整个下单流程的代码,想的是把一些可以异步处理的东西用异步去做,这样时间上就可以有所缩减,会不会问题得到解决?但总觉得不是这方面的原因,也没有着急去改代码。接着跟另一个同事继续观察线上日志,总算有所发现。
因为从线下过来的订单,是通过该系统先进行保存,然后继续调用现有的平台系统去生成订单。我们从日志上发现,系统在一些时间内,需要调用平台系统的时候,并没有将请求发出,日志就停了不再输出。所以,我们又回到项目中找到配置http请求的代码,最大连接数仅有10.
这系统真的是没人用的时候很安静,用的人多起来了,就问题不断了。最大连接数10,这就是线上的配置,我也是很无奈,不出问题,也不会去看前人留下来的这个配置类。
@Configuration
public class HttpClientConfig {
@Bean(name = "httpClientConnectionManager")
public PoolingHttpClientConnectionManager getHttpClientConnectionManager(){
PoolingHttpClientConnectionManager httpClientConnectionManager = new PoolingHttpClientConnectionManager();
httpClientConnectionManager.setMaxTotal(100);
httpClientConnectionManager.setDefaultMaxPerRoute(100);
return httpClientConnectionManager;
}
@Bean(name = "httpClientBuilder")
public HttpClientBuilder getHttpClientBuilder(@Qualifier("httpClientConnectionManager")
PoolingHttpClientConnectionManager httpClientConnectionManager){
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
httpClientBuilder.setConnectionManager(httpClientConnectionManager);
return httpClientBuilder;
}
@Bean
public CloseableHttpClient getCloseableHttpClient(@Qualifier("httpClientBuilder")
HttpClientBuilder httpClientBuilder){
return httpClientBuilder.build();
}
@Bean(name = "builder")
public RequestConfig.Builder getBuilder(){
RequestConfig.Builder builder = RequestConfig.custom();
return builder.setConnectTimeout(60000)
.setConnectionRequestTimeout(60000)
.setSocketTimeout(60000)
.setStaleConnectionCheckEnabled(true);
}
@Bean
public RequestConfig getRequestConfig(@Qualifier("builder") RequestConfig.Builder builder){
return builder.build();
}
}
我们将最大连接数改成了100,并在这个全局配置中增加了连接超时时间等相关参数。终于,这个系统稳定下来了,没有再出现什么问题,接下去的一周,就是在现场第一时间响应客户的一些需求调整或开发。
【总结】
线上出问题,心里其实很矛盾,一方面是我们可以积累些经验,另一方面又并不希望出现问题。这就是我们成长的过程,很锻炼人,很考验人。不过,这个过程,真的很有意义。如何分析问题,如何解决问题,如何找到问题源头,这都是我们可以从中得到的。