背景
在服务化系统中,对于上下游服务的依赖调用往往是通过RPC接口调用实现的,为了系统稳定性,防止被上游服务超时hang死,我们需要对接口调用设置超时,如果在设置的超时时间内没有响应,则需要提早中断该请求并返回。
比如下游接口对于我们的超时时间限制是150ms,因为业务特点原因,我们需要对上游服务某个接口调用设置50ms超时,如果在指定时间内没有返回,则返回降级数据。
超时中断
Future超时
说到超时中断很多人第一个想到的是Future中断。比如请求线程是一个tomcat线程池中的线程,可以通过线程池返回Future,可以轻松实现超时中断返回,这种方式也是我们使用比较多的方案,因为线程池并行调用在高并发场景下有很多的应用,所以直接借助Future方式中断是最先想到的方法。
如果有些场景不想额外引入线程池,又拿不到Future有什么其他方式吗?
线程中断
以前线程提供了Thread.stop,Thread.suspend,Thread,resume方法,但是这几个方法都已经废弃了。目前实现线程中断最先想到的就是interrupt()方法。
interrupt()方法并不是进行线程中断,而仅仅是通知线程你可以中断了,但是是否中断还是取决于线程的运行状态,由其自身决定。
比如调用一个线程的interrupt()之后,如果线程处于阻塞状态(包括:wait,sleep,join等方法),则线程会退出并返回InterruptedException异常,代码中catch这个异常后就可以继续处理了。
如果线程一直在执行没有处于阻塞,则不会中断线程。但是在RPC调用场景中,请求线程一般会处于阻塞状态等待数据,所以可以通过interrupt()方法执行中断。
知道了中断方法了,如何通过指定超时时间进行中断呢?
首先想到的是单独有一个延迟task专门去搞定线程中断的事情。
ScheduledFuture<?> f = executor.scheduleAtFixedRate(task,timeout,timeout,TimeUnit.MILLISECONDS);
Runnable task = new Runnable(){
@Override
public void run(){
try{
thread.interrupt();
// 取消定时器任务
f.cancel();
}
catch(Exception e){
logger.error("Failed while ticking TimerListener",e);
}
}
};
在进行rpc调用时,同时提交一个中断检测任务到ScheduledFuture中等待执行,如果在指定时间内rpc没有返回,则会触发延迟任务,执行请求线程的interrupt()方法,实现了请求线程的中断了,之后清除掉定时任务就OK了。
如果RPC调用在指定时间内返回,也需要清除定时任务,同时恢复请求线程中的中断标识,执行当前线程(即请求线程)的isInterrupted方法。
Thread.interrupted();
这种方式实现中断的问题是,在QPS很高情况下会存在额外性能损失,因为需要开一个任务线程池等待执行。
ReentrantLock.lock(),Condition.await,Condition.signalAll()
另一种方式是采用线程wait和notify方式。也是我比较喜欢的方式。
private final Lock lock = new ReentrantLock();
private final Condition done = lock.newCondition();
private volatile Object response;
提交请求同时提交检测任务:
private void invokeTimeout(int timeout){
if(timeout <= 0){
timeout = 20;
}
// 检测服务提供方是否成功返回了调用结果
if (!isDone()) {
long start = System.currentTimeMillis();
System.out.println("lock get message start");
lock.lock();
try {
// 循环检测服务提供方是否成功返回了调用结果
while (!isDone()) {
// 如果调用结果尚未返回,这里等待一段时间
System.out.println("lock get message wait");
done.await(timeout, TimeUnit.MILLISECONDS);
// 如果调用结果成功返回,或等待超时,此时跳出 while 循环,执行后续的逻辑
if (isDone() || System.currentTimeMillis() - start > timeout) {
break;
}
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
// 如果调用结果仍未返回,则抛出超时异常
if (!isDone()) {
throw new RuntimeException("timeout");
}
}
System.out.println("lock get message finish");
}
private boolean isDone(){
// 通过检测 response 字段为空与否,判断是否收到了调用结果
return response != null;
}
// RPC-Invoke
response = new Object();
done.signalAll();
在RPC调用过程中,如果结果返回,发送signal(),通知await()同时复制response,执行break返回。如果在指定await()时间内没有返回,同时response无值,则抛出RuntimeException业务进行捕获。
其他方式有哪些?
- timingwheel,dubbo已经在用了
- 定时通知+线程扫描
如果你有更好的方式欢迎留言赐教。