歡迎你們關注公衆號「JAVA前線」查看更多精彩分享文章,主要包括源碼分析、實際應用、架構思惟、職場分享、產品思考等等,同時歡迎你們加我我的微信「java_front」一塊兒交流java
1 服務雪崩
在分析服務降級以前,咱們首先談一談什麼是服務雪崩。如今咱們假設存在A、B、C、D四個系統,系統間存在以下調用鏈路:面試
![面試官問DUBBO不能降級哪類異常,咱們聊了二十分鐘](http://static.javashuo.com/static/loading.gif)
在正常狀況下系統之間調用快速且正常,系統運行平穩。可是此時用戶訪問系統A的流量激增,這些流量在瞬間透傳到B、C、D三個系統。B、C系統服務器節點較多抗住了這些流量,可是D系統服務器節點較少,沒有抗住這些流量,致使D系統的資源逐漸耗盡,只能提供慢服務,最終結果是響應用戶時延很長。spring
![面試官問DUBBO不能降級哪類異常,咱們聊了二十分鐘](http://static.javashuo.com/static/loading.gif)
此時用戶發現響應很慢,覺得是本身網絡很差會反覆重試,那麼成倍的流量會打到系統中,致使上游系統資源也逐漸耗盡了,整個訪問鏈路都最終都不可用。數據庫
![面試官問DUBBO不能降級哪類異常,咱們聊了二十分鐘](http://static.javashuo.com/static/loading.gif)
以上介紹了服務雪崩場景,咱們發如今鏈路中一個節點出現問題,致使整個鏈路最終都不可用了,這是不能夠接受的。apache
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框架談一談服務降級。如今咱們有服務提供者提供以下服務:
public interface HelloService { public String sayHello(String name) throws Exception; } public class HelloServiceImpl implements HelloService { public String sayHello(String name) throws Exception { String result = "hello[" + name + "]"; return result; } }
配置文件聲明服務接口:
<dubbo:service interface="com.java.front.demo.provider.HelloService" ref="helloService" />
4.1 降級策略配置
Dubbo框架是自帶服務降級策略的,提供了三種經常使用的降級策略,咱們看一看如何進行配置。
(1) 強制降級策略
<dubbo:reference id="helloService" mock="force:return 1" interface="com.java.front.demo.provider.HelloService" /
(2) 異常降級策略
<dubbo:reference id="helloService" mock="throw com.java.front.BizException" interface="com.java.front.dubbo.demo.provider.HelloService" />
(3) 自定義降級策略
package com.java.front.dubbo.demo.consumer; import com.java.front.demo.provider.HelloService; public class HelloServiceMock implements HelloService { @Override public String sayHello(String name) throws Exception { return "mock"; } }
配置指定自定義降級策略:
<dubbo:reference id="helloService" mock="com.java.front.dubbo.demo.consumer.HelloServiceMock" interface="com.java.front.demo.provider.HelloService" />
4.2 源碼分析
public class MockClusterInvoker<T> implements Invoker<T> { @Override public Result invoke(Invocation invocation) throws RpcException { Result result = null; // 檢查是否有mock屬性 String value = 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); } // 不執行消費邏輯直接返回 else if (value.startsWith("force")) { if (logger.isWarnEnabled()) { logger.warn("force-mock: " + invocation.getMethodName() + " force-mock enabled , url : " + directory.getUrl()); } // 直接執行mock邏輯 result = doMockInvoke(invocation, null); } else { try { // 服務消費默認執行FailoverClusterInvoker result = this.invoker.invoke(invocation); } catch (RpcException e) { if (e.isBiz()) { throw e; } if (logger.isWarnEnabled()) { logger.warn("fail-mock: " + invocation.getMethodName() + " fail-mock enabled , url : " + directory.getUrl(), e); } // 服務消費失敗執行mock邏輯 result = doMockInvoke(invocation, e); } } return result; } } public class MockInvoker<T> implements Invoker<T> { @Override public Result invoke(Invocation invocation) throws RpcException { String mock = getUrl().getParameter(invocation.getMethodName() + "." + Constants.MOCK_KEY); if (invocation instanceof RpcInvocation) { ((RpcInvocation) invocation).setInvoker(this); } if (StringUtils.isBlank(mock)) { mock = getUrl().getParameter(Constants.MOCK_KEY); } if (StringUtils.isBlank(mock)) { throw new RpcException(new IllegalAccessException("mock can not be null. url :" + url)); } mock = normalizeMock(URL.decode(mock)); // <mock="force:return 1">直接包裝返回結果 if (mock.startsWith(Constants.RETURN_PREFIX)) { mock = mock.substring(Constants.RETURN_PREFIX.length()).trim(); try { Type[] returnTypes = RpcUtils.getReturnTypes(invocation); Object value = parseMockValue(mock, returnTypes); return new RpcResult(value); } catch (Exception ew) { throw new RpcException("mock return invoke error. method :" + invocation.getMethodName() + ", mock:" + mock + ", url: " + url, ew); } } // <mock="throw">拋出異常 else if (mock.startsWith(Constants.THROW_PREFIX)) { mock = mock.substring(Constants.THROW_PREFIX.length()).trim(); if (StringUtils.isBlank(mock)) { throw new RpcException("mocked exception for service degradation."); } else { // 獲取自定義異常 Throwable t = getThrowable(mock); throw new RpcException(RpcException.BIZ_EXCEPTION, t); } } // <mock="com.java.front.HelloServiceMock">自定義mock策略 else { try { Invoker<T> invoker = getInvoker(mock); return invoker.invoke(invocation); } catch (Throwable t) { throw new RpcException("Failed to create mock implementation class " + mock, t); } } } }
5 產生疑問
經過上述源碼咱們知道,若是在mock屬性中配置force,那麼不會執行真正的業務邏輯,而是隻執行mock邏輯,這一部分比較容易理解:
// 不執行消費邏輯直接返回 else if (value.startsWith("force")) { if (logger.isWarnEnabled()) { logger.warn("force-mock: " + invocation.getMethodName() + " force-mock enabled , url : " + directory.getUrl()); } // 直接執行mock邏輯 result = doMockInvoke(invocation, null); }
可是若是是其它mock配置則首先執行業務代碼,若是業務代碼發生異常了再執行mock邏輯:
try { // 服務消費默認執行FailoverClusterInvoker result = this.invoker.invoke(invocation); } catch (RpcException e) { if (e.isBiz()) { throw e; } if (logger.isWarnEnabled()) { logger.warn("fail-mock: " + invocation.getMethodName() + " fail-mock enabled , url : " + directory.getUrl(), e); } // 服務消費失敗執行mock邏輯 result = doMockInvoke(invocation, e); }
這段代碼捕獲了RpcException異常,那麼問題來了RpcException是什麼類型的異常?咱們使用自定義降級策略進行實驗,消費者代碼以下:
package com.java.front.dubbo.demo.consumer; import com.java.front.demo.provider.HelloService; public class HelloServiceMock implements HelloService { @Override public String sayHello(String name) throws Exception { return "mock"; } }
配置指定自定義策略並設置服務超時爲2秒:
<dubbo:reference id="helloService" mock="com.java.front.dubbo.demo.consumer.HelloServiceMock" interface="com.java.front.demo.provider.HelloService" timeOut="2000" />
消費者測試代碼以下:
public static void testMock() { ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[] { "classpath*:META-INF/spring/dubbo-consumer1.xml" }); context.start(); HelloService helloServiceMock = (HelloService) context.getBean("helloService"); String result = helloServiceMock.sayHello("JAVA前線"); System.out.println("消費者收到結果=" + result); }
5.1 超時異常
5.1.1 代碼實例
咱們在生產者業務代碼形成5秒的阻塞,模擬一個慢服務:
public class HelloServiceImpl implements HelloService { public String sayHello(String name) throws Exception { String result = "hello[" + name + "]"; // 模擬耗時操做5秒 Thread.sleep(5000L); return result; } }
消費者執行返回mock結果,說明超時異常屬於RpcException異常,能夠被降級策略捕獲:
消費者收到結果=mock
5.1.2 源碼分析
要分析超時異常爲何能夠被降級策略捕獲,咱們從如下兩個類分析。DefaultFuture.get方法採用了經典多線程保護性暫停模式,而且實現了異步轉同步的效果,若是發生超時異常則拋出TimeoutException異常:
public class DefaultFuture implements ResponseFuture { @Override public Object get(int timeout) throws RemotingException { if (timeout <= 0) { timeout = Constants.DEFAULT_TIMEOUT; } // response對象爲空 if (!isDone()) { long start = System.currentTimeMillis(); lock.lock(); try { // 進行循環 while (!isDone()) { // 放棄鎖並使當前線程阻塞,直到發出信號或中斷它或者達到超時時間 done.await(timeout, TimeUnit.MILLISECONDS); // 阻塞結束後再判斷是否完成 if (isDone()) { break; } // 阻塞結束後判斷超過超時時間 if(System.currentTimeMillis() - start > timeout) { break; } } } catch (InterruptedException e) { throw new RuntimeException(e); } finally { lock.unlock(); } // response對象仍然爲空則拋出超時異常 if (!isDone()) { throw new TimeoutException(sent > 0, channel, getTimeoutMessage(false)); } } return returnFromResponse(); } }
DubboInvoker調用了DefaultFuture.get方法,若是捕獲到上述TimeoutException則會拋出RpcException:
public class DubboInvoker<T> extends AbstractInvoker<T> { @Override protected Result doInvoke(final Invocation invocation) throws Throwable { try { // request方法發起遠程調用 -> get異步轉同步並進行超時驗證 RpcContext.getContext().setFuture(null); Result result = (Result) currentClient.request(inv, timeout).get(); return result; } catch (TimeoutException e) { throw new RpcException(RpcException.TIMEOUT_EXCEPTION, "Invoke remote method timeout. method: " + invocation.getMethodName() + ", provider: " + getUrl() + ", cause: " + e.getMessage(), e); } catch (RemotingException e) { throw new RpcException(RpcException.NETWORK_EXCEPTION, "Failed to invoke remote method: " + invocation.getMethodName() + ", provider: " + getUrl() + ", cause: " + e.getMessage(), e); } } }
源碼分析到這裏已經很清楚了,RpcException正是服務降級策略能夠捕獲的異常,因此超時異常是能夠被降級的。
5.2 業務異常
本文咱們把非超時異常統稱爲業務異常,例如生產者業務執行時發生運行時異常能夠歸爲業務異常,下面咱們進行試驗。
5.2.1 代碼實例
生產者執行過程當中拋出運行時異常:
public class HelloServiceImpl implements HelloService { public String sayHello(String name) throws Exception { throw new RuntimeException("BizException") } }
消費者調用直接拋出異常:
java.lang.RuntimeException: BizException at com.java.front.dubbo.demo.provider.HelloServiceImpl.sayHello(HelloServiceImpl.java:35) at org.apache.dubbo.common.bytecode.Wrapper1.invokeMethod(Wrapper1.java) at org.apache.dubbo.rpc.proxy.javassist.JavassistProxyFactory$1.doInvoke(JavassistProxyFactory.java:56) at org.apache.dubbo.rpc.proxy.AbstractProxyInvoker.invoke(AbstractProxyInvoker.java:85)
5.2.2 源碼分析
咱們發現服務降級對業務異常沒有生效,須要分析緣由,我認爲從如下兩點進行分析:
(1) 消費者接收到什麼消息
public class DefaultFuture implements ResponseFuture { public static void received(Channel channel, Response response) { try { DefaultFuture future = FUTURES.remove(response.getId()); if (future != null) { future.doReceived(response); } else { logger.warn("The timeout response finally returned at " + (new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date())) + ", 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:
public class InvokerInvocationHandler implements InvocationHandler { private final Invoker<?> invoker; public InvokerInvocationHandler(Invoker<?> handler) { this.invoker = handler; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String methodName = method.getName(); Class<?>[] parameterTypes = method.getParameterTypes(); if (method.getDeclaringClass() == Object.class) { return method.invoke(invoker, args); } if ("toString".equals(methodName) && parameterTypes.length == 0) { return invoker.toString(); } if ("hashCode".equals(methodName) && parameterTypes.length == 0) { return invoker.hashCode(); } if ("equals".equals(methodName) && parameterTypes.length == 1) { return invoker.equals(args[0]); } // RpcInvocation [methodName=sayHello, parameterTypes=[class java.lang.String], arguments=[JAVA前線], attachments={}] RpcInvocation rpcInvocation = createInvocation(method, args); // 消費者Invoker -> MockClusterInvoker(FailoverClusterInvoker(RegistryDirectory(invokers))) Result result = invoker.invoke(rpcInvocation); // 結果包含異常信息則拋出異常 -> 例如異常結果對象RpcResult [result=null, exception=java.lang.RuntimeException: sayHelloError1 error] return result.recreate(); } }
RpcResult.recreate方法會處理異常,若是發現異常對象不爲空則拋出異常:
public class RpcResult extends AbstractResult { @Override public Object recreate() throws Throwable { if (exception != null) { try { Class clazz = exception.getClass(); while (!clazz.getName().equals(Throwable.class.getName())) { clazz = clazz.getSuperclass(); } Field stackTraceField = clazz.getDeclaredField("stackTrace"); stackTraceField.setAccessible(true); Object stackTrace = stackTraceField.get(exception); if (stackTrace == null) { exception.setStackTrace(new StackTraceElement[0]); } } catch (Exception e) { } throw exception; } return result; } }
5.2.3 業務異常如何降級
經過上述實例咱們知道Dubbo自帶的服務降級策略只能降級超時異常,而不能降級業務異常。
那麼業務異常應該如何降級呢?咱們能夠整合Dubbo、Hystrix進行業務異常熔斷,相關配置也並不複雜,你們能夠網上查閱相關資料。
6 文章總結
本文咱們首先介紹了服務雪崩這個場景,而且從非線性角度再次理解了服務雪崩。隨後咱們總結了服務雪崩應對方案,其中服務降級是應對服務雪崩的重要方法之一。咱們針對超時異常和業務異常兩種場,結合源碼深刻分析了Dubbo服務降級的使用場景,但願本文對你們有所幫助。
歡迎你們關注公衆號「JAVA前線」查看更多精彩分享文章,主要包括源碼分析、實際應用、架構思惟、職場分享、產品思考等等,同時歡迎你們加我我的微信「java_front」一塊兒交流