背景
最近项目中使用dubbo遇到了这样一个问题,dubbo invoke 要通过请求参数来动态决定,即不同的参数对应 dubbo 请求转到指定的机器上去。
集群策略源码
要实现背景中提到的问题,就必须先了解一下 dubbo 的源码
1、首先从配置入手,如果配置中可以灵活设置自定义的集群策略,就可以通过扩展源码来实现,下面看一下配置:
dubbo配置:
dubbo 源码:
通过上述图可以得出,dubbo 配置中可以使用任意的集群策略,那么dubbo中具体有哪些集群策略呢?下面通过源码看一下
2、dubbo 源码集群策略
Failover Cluster
这是dubbo中默认的集群容错模式,失败自动切换,当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。可通过retries=”2”来设置重试次数(不含第一次)
Failfast Cluster
快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。
Failsafe Cluster
失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。
Failback Cluster
失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。
Forking Cluster
并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过forks=”2”来设置最大并行数。
Broadcast Cluster
广播调用所有提供者,逐个调用,任意一台报错则报错。(2.1.0开始支持)通常用于通知所有提供者更新缓存或日志等本地资源信息。
Failover 是dubbo 的默认集群策略,通过 Cluster 的 @SPI 注解可以看出,如下:
3、分析学习一下 FailoverCluster 的源码
FailoverCluster 实现了 Cluster 接口,其具体实现是在 FailoverClusterInvoker 中的 doInvoke 方法中
这里最重要的就是选择某种策略获取到具体的 invoker
解决方案
通过阅读了 dubbo 中的部分源码,我们有了启发,只需要在消费者端自定义实现一个 Cluster 和对应的 ClusterInvoker,重写 ClusterInvoker 中的 doInvoke 方法即可
下面通过在消费者端拓展 Cluster 来解决此问题
1、先在resources 目录下创建 META-INF/dubbo 目录,该目录下创建 com.alibaba.dubbo.rpc.cluster.Cluster 文件,此文件产生参照dubbo源码中的 META-INF/dubbo/interal/com.alibaba.dubbo.rpc.cluster.Cluster 文件,内容如下:
custom=com.springboot.dubbo.cluster.CustomCluster
如下图所示:
2、创建 CustomCluster 和 CustomClusterInvoker 类,代码分别如下:
CustomCluster 对象:
package com.springboot.dubbo.cluster;
import com.alibaba.dubbo.rpc.Invoker;
import com.alibaba.dubbo.rpc.RpcException;
import com.alibaba.dubbo.rpc.cluster.Cluster;
import com.alibaba.dubbo.rpc.cluster.Directory;
/**
* 自定义集群策略
**/
public class CustomCluster implements Cluster {
public final static String NAME = "custom";
@Override
public <T> Invoker<T> join(Directory<T> directory) throws RpcException {
return new CustomClusterInvoker<>(directory);
}
}
CustomClusterInvoker 对象:
package com.springboot.dubbo.cluster;
import com.alibaba.dubbo.common.Constants;
import com.alibaba.dubbo.common.Version;
import com.alibaba.dubbo.common.utils.NetUtils;
import com.alibaba.dubbo.rpc.Invocation;
import com.alibaba.dubbo.rpc.Invoker;
import com.alibaba.dubbo.rpc.Result;
import com.alibaba.dubbo.rpc.RpcContext;
import com.alibaba.dubbo.rpc.RpcException;
import com.alibaba.dubbo.rpc.cluster.Directory;
import com.alibaba.dubbo.rpc.cluster.LoadBalance;
import com.alibaba.dubbo.rpc.cluster.support.AbstractClusterInvoker;
import com.springboot.common.util.StringUtil;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import lombok.extern.log4j.Log4j2;
/**
* 自定义集群invoker
**/
@Log4j2
public class CustomClusterInvoker<T> extends AbstractClusterInvoker<T> {
public static final String BIND_IP_KEY = "bind_ip_key";
public CustomClusterInvoker(Directory<T> directory) {
super(directory);
}
@Override
protected Result doInvoke(Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
List<Invoker<T>> copyinvokers = invokers;
checkInvokers(copyinvokers, invocation);
int len = getUrl().getMethodParameter(invocation.getMethodName(), Constants.RETRIES_KEY, Constants.DEFAULT_RETRIES) + 1;
if (len <= 0) {
len = 1;
}
// retry loop.
RpcException le = null;
// last exception.
List<Invoker<T>> invoked = new ArrayList<>(copyinvokers.size());
// invoked invokers.
Set<String> providers = new HashSet<>(len);
for (int i = 0; i < len; i++) {
//Reselect before retry to avoid a change of candidate `invokers`.
//NOTE: if `invokers` changed, then `invoked` also lose accuracy.
if (i > 0) {
checkWhetherDestroyed();
copyinvokers = list(invocation);
// check again
checkInvokers(copyinvokers, invocation);
}
Invoker<T> invoker = getInvokerByParam(loadbalance,invocation,copyinvokers,invoked,invokers);
invoked.add(invoker);
RpcContext.getContext().setInvokers((List) invoked);
try {
Result result = invoker.invoke(invocation);
if (le != null && log.isWarnEnabled()) {
log.warn("Although retry the method " + invocation.getMethodName()
+ " in the service " + getInterface().getName()
+ " was successful by the provider " + invoker.getUrl().getAddress()
+ ", but there have been failed providers " + providers
+ " (" + providers.size() + "/" + copyinvokers.size()
+ ") from the registry " + directory.getUrl().getAddress()
+ " on the consumer " + NetUtils.getLocalHost()
+ " using the dubbo version " + Version.getVersion() + ". Last error is: "
+ le.getMessage(), le);
}
return result;
} catch (RpcException e) {
if (e.isBiz()) {
// biz exception.
throw e;
}
le = e;
} catch (Throwable e) {
le = new RpcException(e.getMessage(), e);
} finally {
providers.add(invoker.getUrl().getAddress());
}
}
throw new RpcException(le != null ? le.getCode() : 0, "Failed to invoke the method "
+ invocation.getMethodName() + " in the service " + getInterface().getName()
+ ". Tried " + len + " times of the providers " + providers
+ " (" + providers.size() + "/" + copyinvokers.size()
+ ") from the registry " + directory.getUrl().getAddress()
+ " on the consumer " + NetUtils.getLocalHost() + " using the dubbo version "
+ Version.getVersion() + ". Last error is: "
+ (le != null ? le.getMessage() : ""), le != null && le.getCause() != null ? le.getCause() : le);
}
/**
* 根据参数动态获取invoker
*
* @param loadbalance
* @param invocation
* @param copyinvokers
* @param invoked
* @param originInvokers
* @return
*/
private Invoker<T> getInvokerByParam(LoadBalance loadbalance, Invocation invocation, List<Invoker<T>> copyinvokers, List<Invoker<T>> invoked, List<Invoker<T>> originInvokers) {
// 获取消费者处设置的参数
String host = RpcContext.getContext().getAttachment(BIND_IP_KEY);
if (StringUtil.isEmpty(host)) {
return select(loadbalance, invocation, copyinvokers, invoked);
}
// 移除绑定的参数
RpcContext.getContext().removeAttachment(BIND_IP_KEY);
for (Invoker<T> invoker : originInvokers) {
if (host.equals(invoker.getUrl().getHost())) {
// 获取到了匹配的host的invoker
return invoker;
}
}
return select(loadbalance, invocation, copyinvokers, invoked);
}
}
3、dubbo配置中设置自定义集群策略,如下图:
4、具体的消费者处设置dubbo上下文参数,以便于 CustomClusterInvoker 的 doInvoke 方法解析选择host
具体的host对应关系我保存到了 zookeeper 中,如下图:
请求id 若是奇数,则 host 为 192.168.1.235 ,若是偶数, 则 host 为 192.168.1.247
具体消费者处设置的dubbo上下文参数代码如下:
package com.springboot.dubbo.consumer;
import com.alibaba.dubbo.rpc.RpcContext;
import com.alibaba.fastjson.JSONObject;
import com.springboot.common.util.StringUtil;
import com.springboot.dubbo.cluster.CustomClusterInvoker;
import com.springboot.dubbo.param.request.QueryUserParam;
import com.springboot.dubbo.param.request.UserInParam;
import com.springboot.dubbo.param.response.UserOutParam;
import com.springboot.dubbo.service.UserService;
import com.springboot.zookeeper.ZookeeperConfigService;
import java.util.List;
import java.util.Map;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* 用户消费者
**/
@Log4j2
@Service
public class UserConsumer {
@Autowired
private UserService userService;
@Autowired
private ZookeeperConfigService zookeeperConfigService;
public List<UserOutParam> getUsers(Integer id) {
QueryUserParam param = new QueryUserParam();
param.setId(id);
log.warn("userServiceProvider selectUsers 请求参数:{}", param);
String host = getHostByUserId(id);
if (StringUtil.isNotEmpty(host)) {
RpcContext.getContext().setAttachment(CustomClusterInvoker.BIND_IP_KEY, host);
}
List<UserOutParam> userList = userService.selectUsers(param);
log.warn("userServiceProvider selectUsers 请求结果:{}", JSONObject.toJSONString(userList));
return userList;
}
private String getHostByUserId(Integer id) {
String id2Host = zookeeperConfigService.getValue("id_to_host");
if (StringUtil.isEmpty(id2Host) || id == null) {
return null;
}
Map<String, String> hostMap = JSONObject.parseObject(id2Host, Map.class);
if (id % 2 == 0) {
// 偶数
return hostMap.get("evenNumber");
}
// 奇数
return hostMap.get("oddNumber");
}
}
5、代码写好之后,消费者项目启动之后,通过 consumer 的 url 可以看出是否应用了我们自定义的集群策略,如下图:
上图中可以看出 consumer 的 URL 中有 cluster=custom 这句话,表明了当前使用的是我们自定义的集群策略了
6、请求代码测试
请求id=2 时,rpc 请求落到了 192.168.1.247 这个机器上,如下图:
请求id=3 时,rpc 请求落到了 192.168.1.235 这个机器上,如下图:
总结
当我们使用第三方包做开发时,遇到了第三方包目前无法解决我们的需求使用时,只能靠阅读第三方包的部分源码,阅读源码之后通过扩展方式来实现自己的项目需求。
这也是阅读源码的好处之一