Dubbo 源码分析之服务降级

前言

关于服务降级,相信很多小伙伴都听说过或者操作过。比如最近我们在 12306 上抢票回家,明明看到剩余的有票,可就是买不到,这就是很明显的一个(读)服务降级例子。再比如双十一时我们付款时偶尔出现付款失败,重新支付,也是(限流)服务降级的一种,也有许多其它降级的例子,大家可以自行搜索。具体来说就是:当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的降级,以此释放服务器资源以保证核心任务的正常运行 。服务降级的影响也很明显,用户体验不好。在这里就讲讲 Dubbo 的服务降级。

Dubbo 服务降级

Dubbo 中服务降级有两种选择

  • 屏蔽(force):在可预知的情况下执行,比如上次拼多多 100 元优惠券事件,如果在发现漏洞后及时限制支付服务,就不会造成所谓被薅200亿濒临倒闭的传闻(仅仅举例子,还是风控各种没做好)
  • 容错(fail):在服务超时,网络异常时等非业务异常时使用

服务降级配置

上面我们只使用了 return null,其实还有其它自定义配置

1、使用自定义mock类(接口名+Mock)

  • mock = default => DemoServiceMock

  • mock = true => DemoServiceMock

  • mock = fail => DemoServiceMock

  • mock = force => DemoServiceMock
    2、先普通执行,执行失败之后再执行相应的mock逻辑

  • mock = fail:throw => throw new RpcException(" mocked exception for Service degradation. ");

  • mock = fail:throw XxxException => throw new RpcException(RpcException.BIZ_EXCEPTION, XxxException);

  • mock = fail:return => return null

  • mock = fail:return xxx => return xxx

  • mock = fail:return empty => return new Car()
    3、直接执行相应的mock逻辑

  • mock = force:throw => throw new RpcException(" mocked exception for Service degradation. ");

  • mock = force:throw XxxException => throw new RpcException(RpcException.BIZ_EXCEPTION, XxxException);

  • mock = force:return => return null

  • mock = force:return xxx => return xxx

  • mock = force:return empty => return new Car()

屏蔽例子

我简单写了一个接口,共有两个方法,登录和注册

public interface UserService {

    JsonResponse<String> login(User user);

    JsonResponse<String> register(User user);
}

首先启动服务者,消费者我决定使用单元测试的方式。

在演示之前,为了加深大家的理解,我们连接上 ZooKeeper 去查看该服务的注册信息

在这里插入图片描述
从图片可以看出在我们提供的服务 /dubbo/com.dfire.service.UserService 下共有四个目录

  • consumers: 消费者的 URL 信息
  • configurators: 覆盖 URL 配置信息
  • routers: 路由配置信息
  • providers: 提供者的 URL 信息

此时由于我们的提供者已经启动,所以其 URL 信息已经展示,URL 解码后内容为

dubbo://10.1.87.92:20880/com.dfire.service.UserService?anyhost=true&application=xiaosuda_provider&dispatcher=all&dubbo=2.6.5&generic=false&interface=com.dfire.service.UserService&methods=login,register&pid=3316&side=provider&threadpool=cached&threads=50&timestamp=1548320426971&uptime=1548320426991

基本都是我们配置的信息。

接下来打开配置中心(好像 2.7.0+ 要使用 nacos,配置方法肯定大同小异) dubbo-admin

在这里插入图片描述

配置我们的注册方法 register 的服务降级策略为 屏蔽 (force) 返回的结果为 return null,点击保存

下面在看一下我们的 ZooKeeper 信息

在这里插入图片描述
发现 /dubbo/com.dfire.service.UserService/configurators 节点的数据内容从无变为

override://0.0.0.0/com.dfire.service.UserService?category=configurators&dynamic=false&enabled=true&register.mock=force:return null

是以 override 开头的协议,我们需要关注的参数:register.mock=force:return nul,解释为: com.dfire.service.UserService.register()方法的服务降级策略为屏蔽,直接返回 null
关于配置覆盖:

  • override:// 表示数据采用覆盖方式,支持 override 和 absent,可扩展,必填。
  • 0.0.0.0 表示对所有 IP 地址生效,如果只想覆盖某个 IP 的数据,请填入具体 IP,必填。
  • com.alibaba.dubbo.demo.DemoService表示只对指定服务生效,必填。
  • category=configurators 表示该数据为动态配置类型,必填。
  • dynamic=false 表示该数据为持久数据,当注册方退出时,数据依然保存在注册中心,必填。
  • enabled=true 覆盖规则是否生效,可不填,缺省生效。
  • application=demo-consumer 表示只对指定应用生效,可不填,表示对所有应用生效。
  • mock=force:return+null表示将满足以上条件的 mock 参数的值覆盖为 force:return+null。如果想覆盖其它参数,直接加在 overrideURL 参数上。

接下来执行我们的单元测试

@RunWith(SpringRunner.class)
@SpringBootTest
public class ConsumerApplicationTests {

	@Autowired
	private UserService userService;

	@Test
	public void testForceNullMock() {
		User user = new User();
		user.setUsername("张三");
		user.setPassword("张三");
        JsonResponse<String> response = userService.register(user);
        Assert.assertNull(response);
    }
}

执行后看到绿色的 Tests passed

容错例子

容错例子也很简单,就是在一次正常的服务调用失败后执行服务降级操作,如果这一次正常的服务执行成功则不会执行服务降级操作。具体配置与屏蔽类似

在这里插入图片描述

演示就不再演示了。

服务降级源码分析

服务降级的入口在 MockClusterInvoker.invoke()方法

public Result invoke(Invocation invocation) throws RpcException {
		Result result = null;
        // 判断方法名.mock 的值  即上面例子的:register.mock
        String value = directory.getUrl().getMethodParameter(invocation.getMethodName(), Constants.MOCK_KEY, Boolean.FALSE.toString()).trim(); 
        // 未设置mock 走正常 invoke
        if (value.length() == 0 || value.equalsIgnoreCase("false")){
        	//no mock
        	result = this.invoker.invoke(invocation);
        } else if (value.startsWith("force")) { 
        	if (logger.isWarnEnabled()) {
        		logger.info("force-mock: " + invocation.getMethodName() + " force-mock enabled , url : " +  directory.getUrl());
        	}
        	//屏蔽
        	result = doMockInvoke(invocation, null);
        } else {	
        	//容错
        	try {
        		//首先正常 invoke
        		result = this.invoker.invoke(invocation);
        	}catch (RpcException e) {
				if (e.isBiz()) {
					throw e;
				} else {
					if (logger.isWarnEnabled()) {
		        		logger.info("fail-mock: " + invocation.getMethodName() + " fail-mock enabled , url : " +  directory.getUrl(), e);
		        	}
		        	//异常后 走mock invoke
					result = doMockInvoke(invocation, e);
				}
			}
        }
        return result;
	}

需要注意的点

  • directory.getUrl()directory 对象为 RegistryDirectoryURL 会动态变化

doMockInvoke 方法

private Result doMockInvoke(Invocation invocation,RpcException e){
		Result result = null;
    	Invoker<T> minvoker ;
    	
    	List<Invoker<T>> mockInvokers = selectMockInvoker(invocation);
		if (mockInvokers == null || mockInvokers.size() == 0){
			minvoker = (Invoker<T>) new MockInvoker(directory.getUrl());
		} else {
			minvoker = mockInvokers.get(0);
		}
		try {
			result = minvoker.invoke(invocation);
		} catch (RpcException me) {
			if (me.isBiz()) {
				result = new RpcResult(me.getCause());
			} else {
				throw new RpcException(me.getCode(), getMockExceptionMessage(e, me), me.getCause());
			}
//			
		} catch (Throwable me) {
			throw new RpcException(getMockExceptionMessage(e, me), me.getCause());
		}
		return result;
    }

代码很简单,首先获得 mockInvokers 集合,如果该集合为空新建一个对象,否则以集合的第一个为准。
继续看selectMockInvoker

	/**
     * 返回MockInvoker
     * 契约:
     * directory根据invocation中是否有Constants.INVOCATION_NEED_MOCK,来判断获取的是一个normal invoker 还是一个 mock invoker
     * 如果directorylist 返回多个mock invoker,只使用第一个invoker.
     * @param invocation
     * @return 
     */
    private List<Invoker<T>> selectMockInvoker(Invocation invocation){
		List<Invoker<T>> invokers = null;
		//TODO generic invoker?
		if (invocation instanceof RpcInvocation) {
			//存在隐含契约(虽然在接口声明中增加描述,但扩展性会存在问题.同时放在attachement中的做法需要改进
			((RpcInvocation) invocation).setAttachment(Constants.INVOCATION_NEED_MOCK, Boolean.TRUE.toString());
			//directory根据invocation中attachment是否有Constants.INVOCATION_NEED_MOCK,来判断获取的是normal invokers or mock invokers
			try {
				invokers = directory.list(invocation);
			} catch (RpcException e) {
				if (logger.isInfoEnabled()) {
					logger.info("Exception when try to invoke mock. Get mock invokers error for service:"
							+ directory.getUrl().getServiceInterface() + ", method:" + invocation.getMethodName()
							+ ", will contruct a new mock with 'new MockInvoker()'.", e);
				}
			}
		}
		return invokers;
    }

判断 invocation 是否为 RpcInvocation 的实例,如果是为 invocation 添加 invocation.need.mock=true

然后在 RegistryDirectory 的抽象类中 AbstractDirectory 进行筛选 invokers

   public List<Invoker<T>> list(Invocation invocation) throws RpcException {
        if (destroyed){
            throw new RpcException("Directory already destroyed .url: "+ getUrl());
        }
        //模版方法  实例为RegistryDirectory 获得所有Invoker
        List<Invoker<T>> invokers = doList(invocation);
        // 开启时移除断流地址或者关闭时清空熔断信息 addCircuitBreaker
        CircuitBreakerManager circuitBreakerManager = ExtensionLoader.getExtensionLoader(CircuitBreakerManager.class).getAdaptiveExtension();
        invokers = circuitBreakerManager.filterCricuitBreakInvoker(invokers,invocation);
        //路由策略 默认会在routers集合中添加MockInvokersSelector
        List<Router> localRouters = this.routers; // local reference
        if (localRouters != null && localRouters.size() > 0) {
            for (Router router: localRouters){
                try {
                    if (router.getUrl() == null || router.getUrl().getParameter(Constants.RUNTIME_KEY, true)) {
                        invokers = router.route(invokers, getConsumerUrl(), invocation);
                    }
                } catch (Throwable t) {
                    logger.error("Failed to execute router: " + getUrl() + ", cause: " + t.getMessage(), t);
                }
            }
        }
        return invokers;
    }

上面代码作用:从服务字典中获得所有的服务,然后进行路由过滤把过滤后的服务列表返回。
主要看MockInvokersSelector类的 route 方法。

	public <T> List<Invoker<T>> route(final List<Invoker<T>> invokers,
			URL url, final Invocation invocation) throws RpcException {
		if (invocation.getAttachments() == null) {
			return getNormalInvokers(invokers);
		} else {
			// invocation.need.mock  就是在MockClusterInvoker.selectMockInvoker 中添加的
			String value = invocation.getAttachments().get(Constants.INVOCATION_NEED_MOCK);
			if (value == null) 
				return getNormalInvokers(invokers);
			else if (Boolean.TRUE.toString().equalsIgnoreCase(value)){
				return getMockedInvokers(invokers);
			} 
		}
		return invokers;
	}

主要就是判断 invocation.need.mock 的值是否为 true 。如果为 true 调用getMockedInvokers,否则调用getNormalInvokers

	private <T> List<Invoker<T>> getNormalInvokers(final List<Invoker<T>> invokers){
		//判断是否存在mock://协议的提供者  如果不存在 直接返回原提供者 
		if (! hasMockProviders(invokers)){
			return invokers;
		} else { //存在mock://协议的提供者  去掉mock://的提供者过滤后返回
			List<Invoker<T>> sInvokers = new ArrayList<Invoker<T>>(invokers.size());
			for (Invoker<T> invoker : invokers){
				if (! invoker.getUrl().getProtocol().equals(Constants.MOCK_PROTOCOL)){
					sInvokers.add(invoker);
				}
			}
			return sInvokers;
		}
	}
	private <T> List<Invoker<T>> getMockedInvokers(final List<Invoker<T>> invokers) {
	//判断是否存在mock://协议的提供者  如果不存在 直接返回null
		if (! hasMockProviders(invokers)){
			return null;
		}
		List<Invoker<T>> sInvokers = new ArrayList<Invoker<T>>(1);
		for (Invoker<T> invoker : invokers){
			// 过滤mock://的提供者 
			if (invoker.getUrl().getProtocol().equals(Constants.MOCK_PROTOCOL)){
				sInvokers.add(invoker);
			}
		}
		return sInvokers;
	}

调用层次比较深,大家最好 debug 查看。过滤完之后就又回到了 MockClusterInvoker.doMockInvoke 方法.

	private Result doMockInvoke(Invocation invocation,RpcException e){
		Result result = null;
    	Invoker<T> minvoker ;
    	
    	List<Invoker<T>> mockInvokers = selectMockInvoker(invocation);
    	//mockInvokers 结果返回 可能为空
		if (mockInvokers == null || mockInvokers.size() == 0){
			minvoker = (Invoker<T>) new MockInvoker(directory.getUrl());
		} else {
			minvoker = mockInvokers.get(0);
		}
		try {
			result = minvoker.invoke(invocation);
		} catch (RpcException me) {
			if (me.isBiz()) {
				result = new RpcResult(me.getCause());
			} else {
				throw new RpcException(me.getCode(), getMockExceptionMessage(e, me), me.getCause());
			}
//			
		} catch (Throwable me) {
			throw new RpcException(getMockExceptionMessage(e, me), me.getCause());
		}
		return result;
    }

下面就要进入 MockInvoker.invoke 方法了

	public Result invoke(Invocation invocation) throws RpcException {
		//该值即我们在dubbo-admin设置的  可能是 force:return null
    	String mock = getUrl().getParameter(invocation.getMethodName()+"."+Constants.MOCK_KEY);
    	if (invocation instanceof RpcInvocation) {
    		//设置invoke 为当前实例
    		((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));
    	}
    	//首先进行URL解码获得mock的具体要执行的结果 比如force:return null 执行normallizeMock后结果为return null
        mock = normallizeMock(URL.decode(mock));
        //配置 “return” 执行该语句
        if (Constants.RETURN_PREFIX.trim().equalsIgnoreCase(mock.trim())){
        	RpcResult result = new RpcResult();
        	result.setValue(null);
        	return result;
        } else if (mock.startsWith(Constants.RETURN_PREFIX)) {    //配置 “return *” *表示任务内容执行该语句
            mock = mock.substring(Constants.RETURN_PREFIX.length()).trim();
            mock = mock.replace('`', '"');
            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);
            }
        } else if (mock.startsWith(Constants.THROW_PREFIX)) {    //配置 “throw *” 执行该语句
        	mock = mock.substring(Constants.THROW_PREFIX.length()).trim();
            mock = mock.replace('`', '"');
            if (StringUtils.isBlank(mock)){
            	throw new RpcException(" mocked exception for Service degradation. ");
            } else { //用户自定义类
            	Throwable t = getThrowable(mock);
				throw new RpcException(RpcException.BIZ_EXCEPTION, t);
            }
        } else { //自定以mock实现执行该语句
             try {
                 Invoker<T> invoker = getInvoker(mock);
                 return invoker.invoke(invocation);
             } catch (Throwable t) {
                 throw new RpcException("Failed to create mock implementation class " + mock , t);
             }
        }
    }
    

MockInvoker.invoke 的功能就是处理mock的结果的地方,具体注释已经很清楚了,不再详述。

寒冬来了,祝大家都能找到心满意足的工作。

展开阅读全文
©️2020 CSDN 皮肤主题: 像素格子 设计师: CSDN官方博客 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值