歡迎你們關注公衆號「JAVA前線」查看更多精彩分享文章,主要包括源碼分析、實際應用、架構思惟、職場分享、產品思考等等,同時歡迎你們加我微信「java_front」一塊兒交流學習java
在分析服務降級以前,咱們首先談一談什麼是服務雪崩。如今咱們假設存在A、B、C、D四個系統,系統間存在以下調用鏈路:web
在正常狀況下系統之間調用正常,系統運行平穩,可是此時用戶訪問系統A的流量激增,這些流量在瞬間透傳到B、C、D三個系統。B、C系統服務器節點較多抗住了這些流量,可是D系統服務器節點較少,沒有抗住這些流量,致使D系統資源逐漸耗盡,只能提供慢服務,結果是響應用戶時延很長。spring
此時用戶發現響應很慢,覺得是本身網絡很差會反覆重試,那麼成倍的流量會打到系統中,致使上游系統資源也逐漸耗盡了,整個訪問鏈路都最終都不可用。數據庫
以上介紹了服務雪崩場景,咱們發如今鏈路中一個節點出現問題,致使整個鏈路最終都不可用了,這是不能夠接受的。apache
咱們再從另外一個概念來理解服務雪崩:非線性。這個概念在咱們生活中無處不在。緩存
你要趕早上8點鐘的火車,若是6:30出發能夠在7:00到達車站,因而你獲得一個結論:只要30分鐘就能夠到達車站。服務器
你早上想睡晚一點預計7:10出發,想着7:40能夠到達車站。可是最可能的結果是你將錯過這趟火車。由於正好趕上早高峯,堵車致使你至少須要花費1個小時才能到達車站。微信
一個小雪球的重量是100克,打雪仗時你被砸中100次,這對你不會形成任何影響。markdown
可是若是你被10公斤的雪球砸中1次,這可能會對你形成嚴重的傷害。網絡
這就是非線性。事物不是簡單疊加關係,當達到某個臨界值時會形成一種徹底大相徑庭的結果。
咱們來分析一個互聯網的秒殺場景。假設你設計的秒殺系統當每秒30我的訪問時,響應時間是10毫秒。即從用戶點擊按鈕至獲得結果這個過程,只花費了10毫秒。這個時間的流逝基本上察覺不到,性能是不錯的。你感受很好,繼續設計:
每秒30個訪問量響應時間10毫秒
每秒300個訪問量響應時間100毫秒
每秒3000個訪問量響應時間1000毫秒
複製代碼
若是你按照這個思路去作系統設計,將會發生重大的錯誤。由於當每秒3000個訪問量發生時,系統的響應時間可能不是1000毫秒,而可能直接致使系統崩潰,沒法再處理任何的請求。最多見的場景就是當緩存系統失效時,致使的系統雪崩:
(1) 當耗時低的緩存層出現故障時,流量直接打在了耗時高的數據庫層,用戶的等待時長就會增長
(2) 等待時長的增長致使用戶更加頻繁去訪問,更多的流量會打在數據庫層
(3) 這致使用戶的等待時長進一步增長,再次致使更頻繁的訪問
(4) 當訪問量達到一個極限值時,形成系統崩潰,沒法再處理任何請求
流量和響應時間毫不是簡單的疊加關係,當到達某個臨界值時,技術系統將直接崩潰。
保證系統的穩定性和高可用性,咱們須要採起一些高可用策略,目的是構建一個穩定的高可用工程系統,咱們通常採用以下方案。
最基本的冗餘策略就是主從模式。原理是準備兩臺機器,部署了同一份代碼,在功能層面是相同的,均可以對外提供相同的服務。
一臺機器啓動提供服務,這就是主服務器。另外一臺機器啓動在一旁待命,不提供服務,隨時監聽主服務器的狀態,這就是從服務器。當發現主服務器出現故障時,從服務器馬上替換主服務器,繼續爲用戶提供服務。
自動故障轉移策略是指當主系統發生異常時,應該能夠自動探測到異常,並自動切換爲備用系統。不該該只依靠人工去切換成,不然故障處理時間會顯著增長。
所謂降級策略,就是當系統遇到沒法承受的壓力時,選擇暫時關閉一些非關鍵的功能,或者延時提供一些功能,把此刻全部的資源都提供給如今最關鍵的服務。
在秒殺場景中下訂單就是最核心最關鍵的功能。當系統壓力將要到達臨界值時,能夠暫時先關閉一些非核心功能如查詢功能。
當秒殺活動結束後,再將暫時關閉的功能開啓。這樣既保證了秒殺活動的順利進行,也保護了系統沒有崩潰。
還有一種降級策略,當系統依賴的下游服務出現錯誤,甚至已經徹底不可用了,那麼此時就不能再調用這個下游服務了,不然可能致使雪崩。因此直接返回兜底方案,把下游服務直接降級。
這裏比較兩個概念:服務降級與服務熔斷。我認爲服務熔斷是服務降級的一個方法,而服務降級還有不少其它方法,例如開關降級、流量降級等等。
用戶下訂單成功後就須要進行支付。假設秒殺系統下訂單每秒訪問量是3000,咱們來思考一個問題,有沒有必要將每秒3000次訪問量的壓力傳遞給支付服務器?
答案是沒有必要。由於用戶秒殺成功後能夠稍晚付款,好比能夠跳轉到一個支付頁面,提示用戶只要在10分鐘內支付完成便可。
這樣每秒3000次訪問量就被分攤至幾分鐘,有效保護了系統。技術架構還可使用消息隊列作緩衝,讓支付服務按照本身的能力去處理業務。
物理隔離:應用分別部署在不一樣物理機、不一樣機房,資源不會互相影響。
線程隔離:不一樣類型的請求進行分類,交給不一樣的線程池處理,當一類請求出現高耗時和異常,不影響另外一類請求訪問。
本文咱們重點結合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" />
複製代碼
Dubbo框架是自帶服務降級策略的,提供了三種經常使用的降級策略,咱們看一看如何進行配置。
<dubbo:reference id="helloService" mock="force:return 1" interface="com.java.front.demo.provider.HelloService" />
複製代碼
<dubbo:reference id="helloService" mock="throw com.java.front.BizException" interface="com.java.front.dubbo.demo.provider.HelloService" />
複製代碼
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" />
複製代碼
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);
}
}
}
}
複製代碼
經過上述源碼咱們知道,若是在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秒,模擬一個慢服務:
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
複製代碼
要分析超時異常爲何能夠被降級策略捕獲,咱們從如下兩個類分析。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正是服務降級策略能夠捕獲的異常,因此超時異常是能夠被降級的。
本文咱們把非超時異常統稱爲業務異常,例如生產者業務執行時發生運行時異常,下面咱們進行演示。
生產者執行過程當中拋出運行時異常:
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)
複製代碼
咱們發現服務降級對業務異常沒有生效,須要分析緣由,我認爲從如下兩點進行分析:
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]]
複製代碼
咱們知道消費者對象是一個代理對象,首先會執行到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;
}
}
複製代碼
經過上述實例咱們知道Dubbo自帶的服務降級策略只能降級超時異常,而不能降級業務異常。
那麼業務異常應該如何降級呢?咱們能夠整合Dubbo、Hystrix進行業務異常熔斷,相關配置也並不複雜,你們能夠網上查閱相關資料。
本文咱們首先介紹了服務雪崩這個場景,而且從非線性角度再次理解了服務雪崩。隨後咱們總結了服務雪崩應對方案,其中服務降級是應對服務雪崩的重要方法之一。咱們針對超時異常和業務異常兩種場,結合源碼深刻分析了Dubbo服務降級的使用場景,但願本文對你們有所幫助。
歡迎你們關注公衆號「JAVA前線」查看更多精彩分享文章,主要包括源碼分析、實際應用、架構思惟、職場分享、產品思考等等,同時歡迎你們加我微信「java_front」一塊兒交流學習