面试官问DUBBO不能降级哪类异常,我们聊了二十分钟
欢迎大家关注公众号「JAVA前线」查看更多精彩分享文章,主要包括源码分析、实际应用、架构思维、职场分享、产品思考等等,同时欢迎大家加我个人微信「java_front」一起交流 1 服务雪崩 在分析服务降级之前,我们首先谈一谈什么是服务雪崩。现在我们假设存在A、B、C、D四个系统,系统间存在如下调用链路: 在正常情况下系统之间调用快速且正常,系统运行平稳。但是此时用户访问系统A的流量激增,这些流量在瞬间透传到B、C、D三个系统。B、C系统服务器节点较多抗住了这些流量,但是D系统服务器节点较少,没有抗住这些流量,导致D系统的资源逐渐耗尽,只能提供慢服务,最终结果是响应用户时延很长。 此时用户发现响应很慢,以为是自己网络不好会反复重试,那么成倍的流量会打到系统中,导致上游系统资源也逐渐耗尽了,整个访问链路都最终都不可用。 以上介绍了服务雪崩场景,我们发现在链路中一个节点出现问题,导致整个链路最终都不可用了,这是不可以接受的。 2 非线性 我们再从另一个概念来理解服务雪崩:非线性。这个概念在我们生活中无处不在。 你要赶早上8点钟的火车,如果6:30出发可以在7:00到达车站,于是你得到一个结论:只要30分钟就可以到达车站。 你早上想睡晚一点预计7:10出发,想着7:40可以到达车站。但是最可能的结果是你将错过这趟火车。因为正好遇上早高峰,堵车导致你至少需要花费1个小时才能到达车站。 一个小雪球的重量是100克,打雪仗时你被砸中100次,这对你不会造成任何影响。 但是如果你被10公斤的雪球砸中1次,这可能会对你造成严重的伤害。 这就是非线性。事物不是简单叠加关系,当达到某个临界值时会造成一种完全截然不同的结果。 我们来分析一个互联网的秒杀场景。假设你设计的秒杀系统当每秒30个人访问时,响应时间是10毫秒。即从用户点击按钮至得到结果这个过程,只花费了10毫秒。这个时间的流逝基本上察觉不到,性能是不错的。你感觉很好继续设计: 每秒30个访问量响应时间10毫秒 每秒300个访问量响应时间100毫秒 每秒3000个访问量响应时间1000毫秒 如果你按照这个思路去做系统设计,将会发生重大的错误。因为当每秒3000个访问量发生时,系统的响应时间可能不是1000毫秒,而可能直接导致系统崩溃,无法再处理任何的请求。最常见的场景就是当缓存系统失效时,导致的系统雪崩: (1) 当耗时低的缓存层出现故障时,流量直接打在了耗时高的数据库层,用户的等待时长就会增加 (2) 等待时长的增加导致用户更加频繁去访问,更多的流量会打在数据库层 (3) 这导致用户的等待时长进一步增加,再次导致更频繁的访问 (4) 当访问量达到一个极限值时,造成系统崩溃,无法再处理任何请求 流量和响应时间绝不是简单的叠加关系,当到达某个临界值时,技术系统将直接崩溃。 3 服务雪崩应对方案 保证系统的稳定性和高可用性,我们需要采取一些高可用策略,目的是构建一个稳定的高可用工程系统,我们一般采用如下方案。 3.1 冗余 + 自动故障转移 最基本的冗余策略就是主从模式。原理是准备两台机器,部署了同一份代码,在功能层面是相同的,都可以对外提供相同的服务。 一台机器启动提供服务,这就是主服务器。另一台机器启动在一旁待命,不提供服务,随时监听主服务器的状态,这就是从服务器。当发现主服务器出现故障时,从服务器立刻替换主服务器,继续为用户提供服务。 自动故障转移策略是指当主系统发生异常时,应该可以自动探测到异常,并自动切换为备用系统。不应该只依靠人工去切换成,否则故障处理时间会显著增加。 3.2 降级策略 所谓降级策略,就是当系统遇到无法承受的压力时,选择暂时关闭一些非关键的功能,或者延时提供一些功能,把此刻所有的资源都提供给现在最关键的服务。 在秒杀场景中下订单就是最核心最关键的功能。当系统压力将要到达临界值时,可以暂时先关闭一些非核心功能如查询功能。 当秒杀活动结束后,再将暂时关闭的功能开启。这样既保证了秒杀活动的顺利进行,也保护了系统没有崩溃。 还有一种降级策略,当系统依赖的下游服务出现错误,甚至已经完全不可用了,那么此时就不能再调用这个下游服务了,否则可能导致雪崩。所以直接返回兜底方案,把下游服务直接降级。 这里比较两个概念:服务降级与服务熔断,因为这两个概念比较相似。我认为服务熔断是服务降级的一个方法,而服务降级还有很多其它方法,例如开关降级、流量降级等等。 3.3 延时策略 用户下订单成功后就需要进行支付。假设秒杀系统下订单每秒访问量是3000,我们来思考一个问题,有没有必要将每秒3000次访问量的压力传递给支付服务器? 答案是没有必要。因为用户秒杀成功后可以稍晚付款,比如可以跳转到一个支付页面,提示用户只要在10分钟内支付完成即可。 这样每秒3000次访问量就被分摊至几分钟,有效保护了系统。技术架构还可以使用消息队列做缓冲,让支付服务按照自己的能力去处理业务。 3.4 隔离策略 物理隔离:应用分别部署在不同物理机、不同机房,资源不会互相影响。 线程隔离:不同类型的请求进行分类,交给不同的线程池处理,当一类请求出现高耗时和异常,不影响另一类请求访问。 4 服务降级 本文我们重点结合Dubbo框架谈一谈服务降级。现在我们有服务提供者提供如下服务: publicinterfaceHelloService{ publicStringsayHello(Stringname)throwsException; } publicclassHelloServiceImplimplementsHelloService{ publicStringsayHello(Stringname)throwsException{ Stringresult="hello["+name+"]"; returnresult; } } 配置文件声明服务接口: <dubbo:serviceinterface="com.java.front.demo.provider.HelloService"ref="helloService"/> 4.1 降级策略配置 Dubbo框架是自带服务降级策略的,提供了三种常用的降级策略,我们看一看如何进行配置。 (1) 强制降级策略 <dubbo:referenceid="helloService"mock="force:return1"interface="com.java.front.demo.provider.HelloService"/ (2) 异常降级策略 <dubbo:referenceid="helloService"mock="throwcom.java.front.BizException"interface="com.java.front.dubbo.demo.provider.HelloService"/> (3) 自定义降级策略 packagecom.java.front.dubbo.demo.consumer; importcom.java.front.demo.provider.HelloService; publicclassHelloServiceMockimplementsHelloService{ @Override publicStringsayHello(Stringname)throwsException{ return"mock"; } } 配置指定自定义降级策略: <dubbo:referenceid="helloService"mock="com.java.front.dubbo.demo.consumer.HelloServiceMock"interface="com.java.front.demo.provider.HelloService"/> 4.2 源码分析 publicclassMockClusterInvoker<T>implementsInvoker<T>{ @Override publicResultinvoke(Invocationinvocation)throwsRpcException{ Resultresult=null; //检查是否有mock属性 Stringvalue=directory.getUrl().getMethodParameter(invocation.getMethodName(),Constants.MOCK_KEY,Boolean.FALSE.toString()).trim(); //没有mock属性直接执行消费逻辑 if(value.length()==0||value.equalsIgnoreCase("false")){ //服务消费默认执行FailoverClusterInvoker result=this.invoker.invoke(invocation); } //不执行消费逻辑直接返回 elseif(value.startsWith("force")){ if(logger.isWarnEnabled()){ logger.warn("force-mock:"+invocation.getMethodName()+"force-mockenabled,url:"+directory.getUrl()); } //直接执行mock逻辑 result=doMockInvoke(invocation,null); }else{ try{ //服务消费默认执行FailoverClusterInvoker result=this.invoker.invoke(invocation); }catch(RpcExceptione){ if(e.isBiz()){ throwe; } if(logger.isWarnEnabled()){ logger.warn("fail-mock:"+invocation.getMethodName()+"fail-mockenabled,url:"+directory.getUrl(),e); } //服务消费失败执行mock逻辑 result=doMockInvoke(invocation,e); } } returnresult; } } publicclassMockInvoker<T>implementsInvoker<T>{ @Override publicResultinvoke(Invocationinvocation)throwsRpcException{ Stringmock=getUrl().getParameter(invocation.getMethodName()+"."+Constants.MOCK_KEY); if(invocationinstanceofRpcInvocation){ ((RpcInvocation)invocation).setInvoker(this); } if(StringUtils.isBlank(mock)){ mock=getUrl().getParameter(Constants.MOCK_KEY); } if(StringUtils.isBlank(mock)){ thrownewRpcException(newIllegalAccessException("mockcannotbenull.url:"+url)); } mock=normalizeMock(URL.decode(mock)); //<mock="force:return1">直接包装返回结果 if(mock.startsWith(Constants.RETURN_PREFIX)){ mock=mock.substring(Constants.RETURN_PREFIX.length()).trim(); try{ Type[]returnTypes=RpcUtils.getReturnTypes(invocation); Objectvalue=parseMockValue(mock,returnTypes); returnnewRpcResult(value); }catch(Exceptionew){ thrownewRpcException("mockreturninvokeerror.method:"+invocation.getMethodName()+",mock:"+mock+",url:"+url,ew); } } //<mock="throw">抛出异常 elseif(mock.startsWith(Constants.THROW_PREFIX)){ mock=mock.substring(Constants.THROW_PREFIX.length()).trim(); if(StringUtils.isBlank(mock)){ thrownewRpcException("mockedexceptionforservicedegradation."); }else{ //获取自定义异常 Throwablet=getThrowable(mock); thrownewRpcException(RpcException.BIZ_EXCEPTION,t); } } //<mock="com.java.front.HelloServiceMock">自定义mock策略 else{ try{ Invoker<T>invoker=getInvoker(mock); returninvoker.invoke(invocation); }catch(Throwablet){ thrownewRpcException("Failedtocreatemockimplementationclass"+mock,t); } } } } 5 产生疑问 通过上述源码我们知道,如果在mock属性中配置force,那么不会执行真正的业务逻辑,而是只执行mock逻辑,这一部分比较容易理解: //不执行消费逻辑直接返回 elseif(value.startsWith("force")){ if(logger.isWarnEnabled()){ logger.warn("force-mock:"+invocation.getMethodName()+"force-mockenabled,url:"+directory.getUrl()); } //直接执行mock逻辑 result=doMockInvoke(invocation,null); } 但是如果是其它mock配置则首先执行业务代码,如果业务代码发生异常了再执行mock逻辑: try{ //服务消费默认执行FailoverClusterInvoker result=this.invoker.invoke(invocation); }catch(RpcExceptione){ if(e.isBiz()){ throwe; } if(logger.isWarnEnabled()){ logger.warn("fail-mock:"+invocation.getMethodName()+"fail-mockenabled,url:"+directory.getUrl(),e); } //服务消费失败执行mock逻辑 result=doMockInvoke(invocation,e); } 这段代码捕获了RpcException异常,那么问题来了RpcException是什么类型的异常?我们使用自定义降级策略进行实验,消费者代码如下: packagecom.java.front.dubbo.demo.consumer; importcom.java.front.demo.provider.HelloService; publicclassHelloServiceMockimplementsHelloService{ @Override publicStringsayHello(Stringname)throwsException{ return"mock"; } } 配置指定自定义策略并设置服务超时为2秒: <dubbo:referenceid="helloService"mock="com.java.front.dubbo.demo.consumer.HelloServiceMock"interface="com.java.front.demo.provider.HelloService"timeOut="2000"/> 消费者测试代码如下: publicstaticvoidtestMock(){ ClassPathXmlApplicationContextcontext=newClassPathXmlApplicationContext(newString[]{"classpath*:META-INF/spring/dubbo-consumer1.xml"}); context.start(); HelloServicehelloServiceMock=(HelloService)context.getBean("helloService"); Stringresult=helloServiceMock.sayHello("JAVA前线"); System.out.println("消费者收到结果="+result); } 5.1 超时异常 5.1.1 代码实例 我们在生产者业务代码造成5秒的阻塞,模拟一个慢服务: publicclassHelloServiceImplimplementsHelloService{ publicStringsayHello(Stringname)throwsException{ Stringresult="hello["+name+"]"; //模拟耗时操作5秒 Thread.sleep(5000L); returnresult; } } 消费者执行返回mock结果,说明超时异常属于RpcException异常,可以被降级策略捕获: 消费者收到结果=mock 5.1.2 源码分析 要分析超时异常为什么可以被降级策略捕获,我们从以下两个类分析。DefaultFuture.get方法采用了经典多线程保护性暂停模式,并且实现了异步转同步的效果,如果发生超时异常则抛出TimeoutException异常: publicclassDefaultFutureimplementsResponseFuture{ @Override publicObjectget(inttimeout)throwsRemotingException{ if(timeout<=0){ timeout=Constants.DEFAULT_TIMEOUT; } //response对象为空 if(!isDone()){ longstart=System.currentTimeMillis(); lock.lock(); try{ //进行循环 while(!isDone()){ //放弃锁并使当前线程阻塞,直到发出信号或中断它或者达到超时时间 done.await(timeout,TimeUnit.MILLISECONDS); //阻塞结束后再判断是否完成 if(isDone()){ break; } //阻塞结束后判断超过超时时间 if(System.currentTimeMillis()-start>timeout){ break; } } }catch(InterruptedExceptione){ thrownewRuntimeException(e); }finally{ lock.unlock(); } //response对象仍然为空则抛出超时异常 if(!isDone()){ thrownewTimeoutException(sent>0,channel,getTimeoutMessage(false)); } } returnreturnFromResponse(); } } DubboInvoker调用了DefaultFuture.get方法,如果捕获到上述TimeoutException则会抛出RpcException: publicclassDubboInvoker<T>extendsAbstractInvoker<T>{ @Override protectedResultdoInvoke(finalInvocationinvocation)throwsThrowable{ try{ //request方法发起远程调用->get异步转同步并进行超时验证 RpcContext.getContext().setFuture(null); Resultresult=(Result)currentClient.request(inv,timeout).get(); returnresult; }catch(TimeoutExceptione){ thrownewRpcException(RpcException.TIMEOUT_EXCEPTION,"Invokeremotemethodtimeout.method:"+invocation.getMethodName()+",provider:"+getUrl()+",cause:"+e.getMessage(),e); }catch(RemotingExceptione){ thrownewRpcException(RpcException.NETWORK_EXCEPTION,"Failedtoinvokeremotemethod:"+invocation.getMethodName()+",provider:"+getUrl()+",cause:"+e.getMessage(),e); } } } 源码分析到这里已经很清楚了,RpcException正是服务降级策略可以捕获的异常,所以超时异常是可以被降级的。 5.2 业务异常 本文我们把非超时异常统称为业务异常,例如生产者业务执行时发生运行时异常可以归为业务异常,下面我们进行试验。 5.2.1 代码实例 生产者执行过程中抛出运行时异常: publicclassHelloServiceImplimplementsHelloService{ publicStringsayHello(Stringname)throwsException{ thrownewRuntimeException("BizException") } } 消费者调用直接抛出异常: java.lang.RuntimeException:BizException atcom.java.front.dubbo.demo.provider.HelloServiceImpl.sayHello(HelloServiceImpl.java:35) atorg.apache.dubbo.common.bytecode.Wrapper1.invokeMethod(Wrapper1.java) atorg.apache.dubbo.rpc.proxy.javassist.JavassistProxyFactory$1.doInvoke(JavassistProxyFactory.java:56) atorg.apache.dubbo.rpc.proxy.AbstractProxyInvoker.invoke(AbstractProxyInvoker.java:85) 5.2.2 源码分析 我们发现服务降级对业务异常没有生效,需要分析原因,我认为从以下两点进行分析: (1) 消费者接收到什么消息 publicclassDefaultFutureimplementsResponseFuture{ publicstaticvoidreceived(Channelchannel,Responseresponse){ try{ DefaultFuturefuture=FUTURES.remove(response.getId()); if(future!=null){ future.doReceived(response); }else{ logger.warn("Thetimeoutresponsefinallyreturnedat" +(newSimpleDateFormat("yyyy-MM-ddHH:mm:ss.SSS").format(newDate())) +",response"+response +(channel==null?"":",channel:"+channel.getLocalAddress() +"->"+channel.getRemoteAddress())); } }finally{ CHANNELS.remove(response.getId()); } } } response用来接收服务端发送的消息,我们看到异常信息存放在Response的exception属性: Response[id=0,version=null,status=20,event=false,error=null,result=RpcResult[result=null,exception=java.lang.RuntimeException:BizException]] (2) 异常在哪里被抛出 我们知道消费者对象是一个代理对象,首先会执行到InvokerInvocationHandler: publicclassInvokerInvocationHandlerimplementsInvocationHandler{ privatefinalInvoker<?>invoker; publicInvokerInvocationHandler(Invoker<?>handler){ this.invoker=handler; } @Override publicObjectinvoke(Objectproxy,Methodmethod,Object[]args)throwsThrowable{ StringmethodName=method.getName(); Class<?>[]parameterTypes=method.getParameterTypes(); if(method.getDeclaringClass()==Object.class){ returnmethod.invoke(invoker,args); } if("toString".equals(methodName)&¶meterTypes.length==0){ returninvoker.toString(); } if("hashCode".equals(methodName)&¶meterTypes.length==0){ returninvoker.hashCode(); } if("equals".equals(methodName)&¶meterTypes.length==1){ returninvoker.equals(args[0]); } //RpcInvocation[methodName=sayHello,parameterTypes=[classjava.lang.String],arguments=[JAVA前线],attachments={}] RpcInvocationrpcInvocation=createInvocation(method,args); //消费者Invoker->MockClusterInvoker(FailoverClusterInvoker(RegistryDirectory(invokers))) Resultresult=invoker.invoke(rpcInvocation); //结果包含异常信息则抛出异常->例如异常结果对象RpcResult[result=null,exception=java.lang.RuntimeException:sayHelloError1error] returnresult.recreate(); } } RpcResult.recreate方法会处理异常,如果发现异常对象不为空则抛出异常: publicclassRpcResultextendsAbstractResult{ @Override publicObjectrecreate()throwsThrowable{ if(exception!=null){ try{ Classclazz=exception.getClass(); while(!clazz.getName().equals(Throwable.class.getName())){ clazz=clazz.getSuperclass(); } FieldstackTraceField=clazz.getDeclaredField("stackTrace"); stackTraceField.setAccessible(true); ObjectstackTrace=stackTraceField.get(exception); if(stackTrace==null){ exception.setStackTrace(newStackTraceElement[0]); } }catch(Exceptione){ } throwexception; } returnresult; } } 5.2.3 业务异常如何降级 通过上述实例我们知道Dubbo自带的服务降级策略只能降级超时异常,而不能降级业务异常。 那么业务异常应该如何降级呢?我们可以整合Dubbo、Hystrix进行业务异常熔断,相关配置也并不复杂,大家可以网上查阅相关资料。 6 文章总结 本文我们首先介绍了服务雪崩这个场景,并且从非线性角度再次理解了服务雪崩。随后我们总结了服务雪崩应对方案,其中服务降级是应对服务雪崩的重要方法之一。我们针对超时异常和业务异常两种场,结合源码深入分析了Dubbo服务降级的使用场景,希望本文对大家有所帮助。 欢迎大家关注公众号「JAVA前线」查看更多精彩分享文章,主要包括源码分析、实际应用、架构思维、职场分享、产品思考等等,同时欢迎大家加我个人微信「java_front」一起交流