有开发人员说: 最近上了新版本的代码, 经常看到处理某些请求的连接断掉, 从静态代码的分析来看, 找不出发生问题的根源.
本文记录了我们一步步查找, 发现问题最终原因的过程.
症状
自从新版本的代码发布之后, 出现了大量大异常: java.nio.channels.ClosedByInterruptException. 这个异常从个字面意思来看: 这个连接在处理数据输入输出期间因为收到了 interrupt, 所以关闭了.
这种异常绝大部分发生在 JDK 或者某些框架的代码里, 因为只有 JDK 或者基础的框架才会在处理 I/O 的时候才去主动查看当前线程有没有收到别的线程发过来的 interrupt, 大部分写业务代码逻辑的程序员很少关心当前线程 的 interrupt.
查找这个异常是哪里被抛出的
既然我们已经知道了异常的名字, 那么只要查看创建这个异常的运行时栈, 就能看明白具体哪类线程收到了 interrupt, 在处理哪类请求的时候, 收到了 interrupt.
通过 Btrace, 我们能很容易的查出这个栈, 于是我们写了如下 Btrace 代码:
import org.openjdk.btrace.core.annotations.*;
import static org.openjdk.btrace.core.BTraceUtils.*;
import org.openjdk.btrace.core.BTraceUtils.Strings;
@BTrace
public class ThreadInterruptTrace {
@OnMethod( clazz="/java\\.nio\\.channels\\.ClosedByInterruptException/", method="<init>" )
public static void createException() {
println(Strings.strcat(name(currentThread()), " throws ClosedByInterruptException"));
println(jstackStr());
}
}
使用上述代码, 我们很容易找到了一个例子:
RxIoScheduler-3 throws ClosedByInterruptException
java.nio.channels.ClosedByInterruptException.<init>(ClosedByInterruptException.java:51)
java.nio.channels.spi.AbstractInterruptibleChannel.end(AbstractInterruptibleChannel.java:202)
sun.nio.ch.SocketChannelImpl.write(SocketChannelImpl.java:477)
com.tianxiaohui.java.BufferedWriteChannel.flush(BufferedWriteChannel.java:281)
com.tianxiaohui.java.BufferedWriteChannel.write(BufferedWriteChannel.java:223)
com.tianxiaohui.java.BaseClient.sendBytesActual(BaseClient.java:339)
com.tianxiaohui.java.BaseClient.sendBytesOverChannel(BaseClient.java:203)
...
rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:48)
rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:30)
rx.Observable.unsafeSubscribe(Observable.java:10327)
rx.internal.operators.OperatorSubscribeOn$SubscribeOnSubscriber.call(OperatorSubscribeOn.java:100)
rx.xinternal.schedulers.CachedThreadScheduler$EventLoopWorker$1.call(CachedThreadScheduler.java:230)
rx.internal.schedulers.ScheduledAction.run(ScheduledAction.java:55)
...
java.lang.Thread.run(Thread.java:748)
根据上述的运行时栈, 我们可以看到 RxIoScheduler-3 的线程在处理 Socket 写的时候, 主动去检查了当前线程的 interrupt 情况, 发现了 interrupt 然后抛出了 ClosedByInterruptException.
查找哪个线程对这个线程发出了 interrupt
既然当前的线程收到了 interrupt, 那么肯定有其它线程对它发出了 interrupt, 要对一个线程发出 interrupt, 必须有这个线程的引用, 并且调用它的 interrupt() 方法. 那么我们只要在这个方法上拦截, 一定就能找出发出 interrupt 的线程和它的栈.
于是我们写了如下的 Btrace 代码:
import static org.openjdk.btrace.core.BTraceUtils.*;
import org.openjdk.btrace.core.BTraceUtils.Strings;
@BTrace
public class ThreadInterruptTrace {
@OnMethod( clazz="/java\\.lang\\.Thread/", method="/interrupt/" )
public static void t1r(@ProbeClassName String probeClass, @Self Thread self) {
if (Strings.startsWith(name(self), "RxIoScheduler")) {
println(Strings.strcat(Strings.strcat(name(currentThread()), " try to interrupt -> "), str(self)));
println(jstackStr());
}
}
}
这段代码拦截 Thread.interrupt() 方法, 并且由于我们已经知道了被 interrupt 线程的名字, 所以, 我们这里对于被 interrupt 的现场的名字做了过滤: 必须以 RxIoScheduler 开头. 并且打印了当前线程和被 interrupt 线程的名字.
很快, 我们就找到了一个发出 interrupt 的线程的名字和它的栈. 根据这些信息, 我们很快就定位到了一个有 bug 的代码块:
Observable.create(new Observable.OnSubscribe<List<T>>() {
@Override
public void call(Subscriber<? super List<T>> subscriber) {
//some code here
}
}).}).observeOn(Schedulers.newThread())
.subscribeOn(Schedulers.io())
.timeout(threadTimeout, TimeUnit.MILLISECONDS);
这段代码位于处理一个 HTTP 请求的代码逻辑中, 它使用了 RXJava 代码库, 并且使用了几个 RXJava 的操作符. observeOn(Schedulers.newThread()): 这个操作符指定了在哪个线程上观察(处理)数据流。Schedulers.newThread()表示使用一个新的线程来处理数据。subscribeOn(Schedulers.io()): 这个操作符指定了在哪个线程上订阅数据流。Schedulers.io()表示使用一个专门用于I/O操作的线程池来进行订阅。 timeout(threadTimeout, TimeUnit.MILLISECONDS): 这个操作符设置了一个超时时间,如果在指定的时间内没有数据产生,将触发超时操作。
问题发生的原因在于, 每次执行这段代码, 就会使用 Schedulers.newThread() 来创建一个新线程, 这次用完之后, 它将不会再次被使用, 根据 RXJava 的规则, 这个线程之后会被销毁, 当它被销毁的时候, 它会主动给曾经跟它合作的 I/O 线程, 也就是本例中 Schedulers.io() 里面的某个线程发送 interrupt, 也就导致了我们一开始看到的 ClosedByInterruptException.
总结
根据我们看到的现象, 结合 Java 代码的逻辑, 使用 Btrace 的代码注入和打印栈, 我们一步步就能找到出错的根源, 并给出合理的解释.