记一次SpringBoot服务假死的排查

2022-06-20 20:05:41 浏览数 (1)

最近有新的spring boot服务部署测试, 发现测试一段时间之后, 就无法收到返回数据, 直到调用端timeout为止.

下面是整个问题的排查过程:

1. 首先看日志, 但很遗憾日志中并没有异常信息, 没能提供有利的切入点.

2. 因为服务重启后, 能够恢复正常, 基本可以排除网络和中间件的原因, 初步判断还是服务本身有问题.

3.出问题时, 包括健康检查在内的所有请求都是无法正常返回的, 直到客户端超时为止;

但进程还在, 服务处于假死状态中了;

代码语言:javascript复制
curl http://localhost:8080/actuator/health

4. 查看服务的网络连接情况发现大量的CLOSE_WAIT, 这也符合超时现象;

也说明服务已经接收了请求, 但没正常结束, 导致客户端超时关闭网络资源;

再次证明服务本身有问题.

代码语言:javascript复制
netstat -anop |grep 25433
tcp6       0      0 :::8080                 :::*                    LISTEN      25433/java           off (0.00/0/0)
tcp6       1      0 10.10.0.123:8080        10.10.0.41:34792        CLOSE_WAIT  25433/java           off (0.00/0/0)
tcp6       1      0 10.10.0.123:8080        10.10.0.41:38096        CLOSE_WAIT  25433/java           off (0.00/0/0)
tcp6       1      0 10.10.0.123:8080        10.10.0.41:39404        CLOSE_WAIT  25433/java           off (0.00/0/0)
tcp6     186      0 10.10.0.123:8080        10.10.0.41:33486        CLOSE_WAIT  25433/java           off (0.00/0/0)
tcp6     166      0 10.10.0.123:8080        10.10.0.137:43160       CLOSE_WAIT  25433/java           off (0.00/0/0)
tcp6     422      0 10.10.0.123:8080        10.10.0.128:51672       CLOSE_WAIT  25433/java           off (0.00/0/0)
tcp6     166      0 10.10.0.123:8080        10.10.0.137:43540       CLOSE_WAIT  25433/java           off (0.00/0/0)
tcp6       1      0 10.10.0.123:8080        10.10.0.41:38200        CLOSE_WAIT  25433/java           off (0.00/0/0)
tcp6     166      0 10.10.0.123:8080        10.10.0.137:11427       CLOSE_WAIT  25433/java           off (0.00/0/0)
tcp6       1      0 10.10.0.123:8080        10.10.0.41:33452        CLOSE_WAIT  25433/java           off (0.00/0/0)
tcp6       0      0 10.10.0.123:22657       10.10.0.137:6380        ESTABLISHED 25433/java           off (0.00/0/0)

5.回过头再看下日志, 从线程名称可以发现spring boot 线程已经分配到200了, (springboot默认也是200个线程), 服务所有线程都被占满了, 再次证明服务接收了大量请求, 没有正常返回.

同时日志上并不能看出来是卡在什么位置了.

代码语言:javascript复制
[http-nio-9702-exec-200] INFO  => c.r.a.v.m.i.c.XxxxController - xxx start:200008041

6.既然已经确定了有大量线程卡住, 就利用jstack命令帮助我们查看线程信息吧.

代码语言:javascript复制
jstack -l 25433 > jstack.log

大量线程都是卡在获取Redis连接的地方.

太奇怪了!!~~

Redis是使用的连接池的配置, 不应该有连接资源获取不到的情况, 不合理!!

代码语言:javascript复制
"http-nio-9702-exec-200" #288 daemon prio=5 os_prio=0 tid=0x00007ffa7c111000 nid=0x2252 waiting on condition [0x00007ff9fba0d000]
   java.lang.Thread.State: WAITING (parking)
             at sun.misc.Unsafe.park(Native Method)
             - parking to wait for  <0x0000000099822900>(a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
             at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
             at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
             at org.apache.commons.pool2.impl.LinkedBlockingDeque.takeFirst(LinkedBlockingDeque.java:590)
             at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:441)
             at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:362)
             at io.lettuce.core.support.ConnectionPoolSupport$1.borrowObject(ConnectionPoolSupport.java:124)
             at io.lettuce.core.support.ConnectionPoolSupport$1.borrowObject(ConnectionPoolSupport.java:120)
             at org.springframework.data.Redis.connection.lettuce.LettucePoolingConnectionProvider.getConnection(LettucePoolingConnectionProvider.java:80)
   

7.顺便看下GC信息, 没有FullGC, 不是GC时STW导致的, 年轻代空间有些紧张, 但不会影响服务到假死的程度.

代码语言:javascript复制
 jstat -gcutil 25433
  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT  
  0.00  99.82  94.52   8.26  95.45  93.88     29    1.278     0    0.000    1.278

8.分析代码, 把所有与Redis有关的地方都过一遍.

(1) 首先, Redis池的配置, 通用配置, 出问题的可能性较小;

代码语言:javascript复制
spring.Redis.cluster.nodes[0]=127.0.0.1:9000
spring.Redis.cluster.nodes[1]=127.0.0.1:9001
spring.Redis.cluster.nodes[2]=127.0.0.1:9002
spring.Redis.cluster.nodes[3]=127.0.0.1:9003
spring.Redis.cluster.nodes[4]=127.0.0.1:9004
spring.Redis.cluster.nodes[5]=127.0.0.1:9005
spring.Redis.lettuce.pool.max-active=10
spring.Redis.lettuce.pool.max-wait=-1
spring.Redis.lettuce.pool.max-idle=10
spring.Redis.lettuce.pool.min-idle=2
spring.Redis.timeout=2000

(2) 继续, 服务中到了@Cacheable注解, 为配合解决注解中Key过期时间的问题, 自定义了一个RedisCacheConfiguration的Bean, 不常写的逻辑, 有可能出问题;

代码语言:javascript复制
@Bean
public RedisCacheConfiguration cacheConfiguration(Config apolloConfig) {
    RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(apolloConfig.getCacheTimeoutMinutes()));
    cacheConfig.usePrefix();
    return cacheConfig;
}

(3) 再继续, 代码中对于数据的处理都是使用RedisTemplate, 相关配置是封装在脚手架中的, 应该不出问题;

(4) 最后, 发现了一个不一样的写法, 是将连接拿出来之后, 调用底层命令实现的功能;

代码如下:

代码语言:javascript复制
Boolean set = RedisTemplate.getRequiredConnectionFactory().getConnection()
        .set("Keys".getBytes(), "Value".getBytes(), Expiration.seconds(100),
                SetOption.ifAbsent());

但是, 没有释放连接的逻辑, 问题一定是它了,

Redis连接池中的连接耗尽, 所有线程等待连接资源释放, 导致假死.

梳理流程, 添加释放逻辑, 问题解决.

代码语言:javascript复制
RedisConnection connection = null;
try {
    connection = RedisTemplate.getRequiredConnectionFactory().getConnection();
    Boolean set = connection
            .set("Keys".getBytes(), "Value".getBytes(), Expiration.seconds(100),
                    SetOption.ifAbsent());
} finally {
    if (connection != null) {
        connection.close();
    }
}

上面代码本身是想利用Set命令的NX参数, 实现一个有过期时间的排他锁;

也就是指定Key不存在时, 才会添加数据, 并且设置Key的过期时间, 同时还要是原子性的;

代码语言:javascript复制
set key value [EX seconds] [PX milliseconds] [NX|XX]

RedisTemplate并没有封装这种多参数的set命令, 只能通过下面两个方法配合使用,

缺陷就是非原子的, 在执行expire()之前如果宕机了, 数据不能正确删除, 造成数据混乱.

代码语言:javascript复制
redisTemplate.opsForValue().setIfAbsent();
redisTemplate.expire();

为什么会写出这样有漏洞的代码呢, 主要有以下原因:

1.开发时, 都是以方法级单测为主, 很难发现这种资源耗尽的问题;

2.本地的集成测试时, Redis也只使用单点模式, 池资源相关问题很难被发现;

3.这段代码在多次重复调用后才会出现资源耗尽问题, 也很难发现问题;

4.涉及到的方法并不是主要逻辑范围, 代码review也容易忽略掉;

确实难发现啊..

总结下:

1.在遇到使用连接资源的时候一定要和释放资源成对出现, 养成良好的编程习惯;

2.遇到问题, 不要慌, 尽量多的收集信息, 抽丝剥茧, 分析出有用的信息;

3.平时多做积累, 遇到问题可以从多个方面着手, 也能抓住细节, 快速排查定位问题;

0 人点赞