前言
关于服务降级,相信很多小伙伴都听说过或者操作过。比如最近我们在 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×tamp=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®ister.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
。如果想覆盖其它参数,直接加在override
的URL
参数上。
接下来执行我们的单元测试
@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
对象为RegistryDirectory
,URL
会动态变化
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
的结果的地方,具体注释已经很清楚了,不再详述。
寒冬来了,祝大家都能找到心满意足的工作。