什麼是服務降級?Dubbo服務降級不能降級哪類異常?

歡迎你們關注公衆號「JAVA前線」查看更多精彩分享文章,主要包括源碼分析、實際應用、架構思惟、職場分享、產品思考等等,同時歡迎你們加我微信「java_front」一塊兒交流學習java


1 服務雪崩

在分析服務降級以前,咱們首先談一談什麼是服務雪崩。如今咱們假設存在A、B、C、D四個系統,系統間存在以下調用鏈路:web

在正常狀況下系統之間調用正常,系統運行平穩,可是此時用戶訪問系統A的流量激增,這些流量在瞬間透傳到B、C、D三個系統。B、C系統服務器節點較多抗住了這些流量,可是D系統服務器節點較少,沒有抗住這些流量,致使D系統資源逐漸耗盡,只能提供慢服務,結果是響應用戶時延很長。spring

此時用戶發現響應很慢,覺得是本身網絡很差會反覆重試,那麼成倍的流量會打到系統中,致使上游系統資源也逐漸耗盡了,整個訪問鏈路都最終都不可用。數據庫

以上介紹了服務雪崩場景,咱們發如今鏈路中一個節點出現問題,致使整個鏈路最終都不可用了,這是不能夠接受的。apache


2 非線性

咱們再從另外一個概念來理解服務雪崩:非線性。這個概念在咱們生活中無處不在。緩存

你要趕早上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) 當訪問量達到一個極限值時,形成系統崩潰,沒法再處理任何請求

流量和響應時間毫不是簡單的疊加關係,當到達某個臨界值時,技術系統將直接崩潰。


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」一塊兒交流學習

相關文章
相關標籤/搜索