【Spring Boot】线上问题总结

【引言】

2019年刚开始,依旧很长很累,经历了两周每天都在上线的节奏,一周在北京,一周在杭州。现在回家了,终于有时间把前一周新项目上线遇到的问题总结一下。

【问题】

在项目刚上线的第二天,客户那边一部分人继续在线下开单,订单通过此项目同步到我们平台上,一部分人开始在我们平台试用开单,所以用的人多了,项目的问题也就暴露出来的。

1. Hikari Unable to acquire JDBC Connection

【Spring Boot】线上问题总结
系统架构用的是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语句

当问题出现后,同事直接暴力的将上述配置都删掉了,结果并没有解决问题。于是我开始查资料,寻找其他解决方案。大部分解决方案是因为没有配置相关参数,而去添加对应的配置参数,但此类方案感觉并不是我们项目中的解决方案。最后,找到一个自我感觉比较可信的方案:
【Spring Boot】线上问题总结
大致意思是项目中用的事务没有关闭,导致旧的事务堆积而新的事务无法开启,在设置了数据库连接超时参数的情况下,就会导致后面再也无法获取到数据库连接。

按照上述提供的方案,我将项目中所有类的@Transitional事务注解都去掉了,因为此项目是一个连接项目,本身也不需要什么事务支持。上线后的一天,此问题没有再出现了,也可以证明此解决方案是可行的。

2. Executing an update/delete query

【Spring Boot】线上问题总结
这个问题是因为项目中集成了JPA,而JPA要求,没有事务支持,不能执行更新和删除操作。解决方法就是在Service层添加@Transitional注解。

在前一天,我将项目中所有类的事务注解都去掉了,解决数据库连接不上的问题。既然在这个方法下需要事务支持,我就将该类的事务注解加上了,检查了一遍,整个项目需要添加的地方加起来也就两三个类。

上线的第三天,白天没有再出现前两个问题,这个问题算是解决了,也没有再次出现第一个问题。

3. Lock wait timeout exceeded; try restarting transaction

第四天,前一个晚上干到一两点,早上不到十点,老大的一个电话就把我吵醒了,说线上又出问题了,平台下的订单,都不能同步到第三方系统,导致系统没有回写他们的单号,而不能进行订单后面的流程。

心里很烦,想想别人的老大,出问题了三两分钟自己搞定。而我,线上出问题了,肯定是没有清静的时候。抱怨归抱怨,问题还是需要解决的。

其实,这个问题在前两天也出现过,只是觉得是因为前两个问题导致的,因为连接阻塞了,导致后面其他的提交都在等待。而事实证明,并不是一个问题。

【Spring Boot】线上问题总结
上图是错误截图,而遇到这个问题,查到的解决方案是可以执行下面的三个sql,查看数据库的锁超时等待的进程,如下:

select * from information_schema.innodb_trx;
select * from information_schema.INNODB_LOCKS;
select * from information_schema.INNODB_LOCK_WAITS;

【Spring Boot】线上问题总结
确实,存在锁等待的一个进程,执行的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,并在这个全局配置中增加了连接超时时间等相关参数。终于,这个系统稳定下来了,没有再出现什么问题,接下去的一周,就是在现场第一时间响应客户的一些需求调整或开发。

【总结】

线上出问题,心里其实很矛盾,一方面是我们又可以积累些经验,另一方面又并不希望出现问题。这就是我们成长的过程,很锻炼人,很考验人。