糟了,银行线上跑了一年的代码出事故了

共 3056字,需浏览 7分钟

 ·

2020-10-06 22:49

介绍

周末在水群的时候,发现有个小伙伴遇到了一个线上问题

线程池中线程的状态只有一个为RUNNABLE,其他都为WAITING,问有可能是哪些原因造成的?

线程池有25个线程,只有一个线程卡在网络读取上面,状态为RUNNABLE,其他线程都为WAITING。

可能有小伙伴们没用过这个工具,简单介绍一下这个性能监测工具JMC,JMC是源自JRockit JVM的一套监控和管理工具,Oracle在发布JAVA 7u4(Java 7 Update 40)时将其包含在JDK中,用户不再需要单独下载

只需要在命令中执行jmc即可

应用启动配置如下参数

-Dcom.sun.management.jmxremote.port=7091 
-Dcom.sun.management.jmxremote.authenticate=false 
-Dcom.sun.management.jmxremote.ssl=false

连接到配置的JMC就能看到各种监测指标。

本来我想让这个小伙伴把代码发过来看看的,可他却说自己做的是银行的项目,连不上外网,只能用手机开视频对着电脑让我看个大概。我复原一下这个代码的场景,估计很多小伙伴一下就能发现问题了,因为我把多余的代码都省略了,只留了会造成问题的代码

public class BankDemo {

    public ExecutorService service = Executors.newFixedThreadPool(5);

    public static class Task implements Runnable {

        private CountDownLatch latch;

        public void setLatch(CountDownLatch latch) {
            this.latch = latch;
        }

        @SneakyThrows
        @Override
        public void run() {
            // 建立一个Socket连接发送数据
            Socket socket = new Socket("127.0.0.1",10006);
            // ...
            // 执行最后调用如下方法
            latch.countDown();
        }
    }

    // 真实的代码这里的过程为,每次往线程池里面放一批任务,这一批任务执行完毕,再放下一批任务
    // 即循环调用如下方法
    @SneakyThrows
    public void runTask(List taskList) {
        CountDownLatch latch = new CountDownLatch(5);
        taskList.forEach(item -> {
            item.setLatch(latch);
            service.submit(item);
        });
        latch.await();
    }
}

提示一下WAITING状态的线程阻塞在LockSupport.park()方法上(用了上图的JMC工具)

写个小插曲,这个小伙伴一直和我强调这个代码已经在线上跑了一年了,一直没发生问题。怎么到自己这就发生问题了,所以他的解决方案是一直看自己修改了哪些部分,但是始终没看出来问题。

而我的思路就和他不一样了,因为有些bug只有在特定场景下才会出现,不要坚信之前的代码就没有问题,要从问题本身着手

Java线程状态

在发现问题的时候基础知识还是很重要的,回顾一下

简易的线程状态如下图Java Thread线程内部有一个枚举内部类State,定义了Java语言线程状态的枚举值

  1. NEW(初始化状态)
  2. RUNNABLE (可运行/运行状态)
  3. BLOCKED(阻塞状态)
  4. WAITING (无时限等待)
  5. TIMED_WAITING(有时限等待)
  6. TERMINATED(终止状态)

Java将操作系统层面的阻塞状态细分为BLOCK,WAITING,TIMED_WAITING三种状态

NEW:新建状态,线程被创建但未启动的状态。创建线程有三种方式

  1. 继承Thread类
  2. 实现Runnable接口
  3. 实现Callable接口

我们最常用的是通过实现接口这种方式,Runnable和Callable接口的区别如下

  1. Runnable无法获取返回值,而Callable可以获取返回值
  2. Runnable无法抛出异常,而Callable可以抛出异常

RUNNABLE(就绪状态):调用start之后运行之前的状态RUNNING(运行状态):线程正在运行BLOCKED(阻塞状态):进入以下状态,有以下几种情况

  1. BLOCK(同步阻塞):锁被其他线程占用,如等待进入synchronized方法或者代码块
  2. WAITING(主动阻塞):执行Object.wait(),Thread.join()等
  3. TIMED_WAITING(等待阻塞):执行Object.wait(long),Thread.sleep(long)等

DEAD(终止状态):线程执行完毕 最后将各种方法补充到线程状态图上

场景还原

造成线程WAITING,一般是调用了如下3种方法之一

  1. Object.wait()
  2. Thread.join()
  3. LockSupport.park()

排查问题的过程如下

  1. 在明确了代码中没有调用Object.wait()和Thread.join()后,那基本就确定了是调用了java.util.concurrent包下面的工具类导致的线程阻塞,因为java.util.concurrent包下的工具类频繁使用了LockSupport.park()

  2. 接着就可以确定是使用CountDownLatch造成的问题了,其他的线程已经结束了,只有一个线程在运行,此时其他线程就阻塞等待

  3. 那这个RUNNABLE的线程做啥了,为啥一直没有结束?此时文章最开始的一张图指明了方向,这个线程阻塞在网络读取上了。

  4. 既然卡在网络读取上,肯定就是没有设置连接的超时时间,或者读取的超时时间。一问,果然和我想的一样,没有设置

设置完后,他在本地跑了一下,刚开始还正常运行,后来就直接抛出异常了SocketTimeoutException: connect timed out(连接服务端超时) SocketException: Connection reset(服务端关闭了连接,但是客户端还在从连接中读取数据)

那为什么刚开始程序能正常跑?后面就开始报这种连接异常了呢?

  1. 服务端确实并发太大了
  2. 服务端的网路请求用BIO实现的,一个请求创建一个线程,本身就支持不了高并发

至于是哪种原因?我让小伙伴找服务端的开发人员确认了 一下,服务端居然是使用BIO实现的。网络请求居然不用Netty,还是你们任性!

期待我后续的Netty文章哈,这种事情坚决不能再发生。

点个在看支持我吧,转发就更好了
浏览 33
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报