dubbo自定义集群策略扩展

本文介绍如何在Dubbo框架中实现自定义集群策略,通过扩展源码解决动态请求路由问题,实现根据请求参数选择不同服务节点。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

背景

最近项目中使用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 这个机器上,如下图:

总结

当我们使用第三方包做开发时,遇到了第三方包目前无法解决我们的需求使用时,只能靠阅读第三方包的部分源码,阅读源码之后通过扩展方式来实现自己的项目需求。

这也是阅读源码的好处之一

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值