图文+源码分析实现dubbo 3.x灰度(附源码)

这里是weihubeats ,觉得文章不错可以关注公众号小奏技术

背景

最近在做全链路灰度,服务之间调用有用到dubbo,所以需要做一下dubbo的灰度

环境

设计

网关入口流量染色

灰度的设计也有两种方案,第一种是全链路在路口进行灰度流量计算。

比如这种

在网关进行全局的灰度规则计算,然后进行流量染色,比如发现命中规则则调用product的灰度,然后product会调用order由于这条流量已经被染色,所以还是会继续调用order的灰度,一条路走到黑

如果是product没有灰度就是下面这种情况

不管流量有没有染色,都是调用正常的prodcuct,因为prodcuct没有灰度,而下一跳到order的时候就根据入口的流量有没有染色,如果染色就调用order灰度,否则调用正常的order

全局染色

上面的方案有一个明显问题。

比如我order想要开启灰度30%,正常应该是所有调用到order的流量都应该是30%的概率进灰度,实际情况是,这里的概率受限于网关入口的染色规则。不符合我们预期,如果要符合预期就需要每一跳都进行流量规则判断,重新进行流量染色

两种方案的核心在于路由规则的计算是全局在网关入口计算还是各个服务还要单独计算

本次我们选用第一种方案进行简单实现

实现

服务扩展

首先我们看看dubbo官方给我的服务扩展的图片

消费端的工作流程如下:

  • 通过 Stub 接收来自用户的请求,并且封装在 Invocation 对象中
  • 将 Invocation 对象传递给 ClusterFilter(扩展点)做选址前的请求预处理,如请求参数的转换、请求日志记录、限流等操作都是在此阶段进行的
  • Invocation 对象传递给 Cluster(扩展点)进行集群调用逻辑的决策,如快速失败模式、安全失败模式等决策都是在此阶段进行的
    • Cluster 调用 Directory 获取所有可用的服务端地址信息
    • Directory 调用 StateRouter(扩展点,推荐使用) 和 Router(扩展点) 对服务端的地址信息进行路由筛选,此阶段主要是从全量的地址信息中筛选出本次调用允许调用到的目标,如基于打标的流量路由就是在此阶段进行的
    • Cluster 获得从 Directory 提供的可用服务端信息后,会调用 LoadBalance (扩展点)从多个地址中选择出一个本次调用的目标,如随机调用、轮询调用、一致性哈希等策略都是在此阶段进行的
    • Cluster 获得目标的 Invoker 以后将 Invocation 传递给对应的 Invoker,并等待返回结果,如果出现报错则执行对应的决策(如快速失败、安全失败等)
  • 经过上面的处理,得到了带有目标地址信息的 Invoker,会再调用 Filter(扩展点)进行选址后的请求处理(由于在消费端侧创建的 Filter 数量级和服务端地址量级一致,如无特殊需要建议使用 ClusterFilter 进行扩展拦截,以提高性能)
  • 最后 Invocation 会被通过网络发送给服务端

服务端的工作流程如下:

  • 服务端通信层收到请求以后,会将请求传递给协议层构建出 Invocation
  • 将 Invocation 对象传递给 Filter (扩展点)做服务端请求的预处理,如服务端鉴权、日志记录、限流等操作都是在此阶段进行的
  • 将 Invocation 对象传递给动态代理做真实的服务端调用

来自官网

Router扩展

所以路由相关的扩展其实可以通过Router进行扩展。

扩展接口

  • org.apache.dubbo.rpc.cluster.RouterFactory
  • org.apache.dubbo.rpc.cluster.Router

以下官方是自带一些Router扩展的。

  • org.apache.dubbo.rpc.cluster.router.ScriptRouterFactory
  • org.apache.dubbo.rpc.cluster.router.FileRouterFactory
  • org.apache.dubbo.rpc.cluster.router.condition.config.AppRouterFactory
  • org.apache.dubbo.rpc.cluster.CacheableRouterFactory
  • org.apache.dubbo.rpc.cluster.router.condition.ConditionRouterFactory
  • org.apache.dubbo.rpc.cluster.router.mock.MockRouterFactory
  • org.apache.dubbo.rpc.cluster.router.condition.config.ServiceRouterFactory
  • org.apache.dubbo.rpc.cluster.router.tag.TagRouterFactory

官方文档地址:cn.dubbo.apache.org/zh-cn/overv...

这里我们要实现上面的流量染色后自动调用到灰度机器,我们可以使用官方自带的TagRouterFactory

代码实现

公共接口

java 复制代码
public interface DemoService {

    String sayHello(String name);
}

生产者

  • 配置文件
yml 复制代码
dubbo:
  application:
    name: weihubeats-provider
  protocol:
    name: dubbo
    port: -1
  registry:
    address: zookeeper://${zookeeper.address:127.0.0.1}:2181
java 复制代码
@SpringBootApplication
@EnableDubbo
public class ProviderApplication {
    public static void main(String[] args) {
        SpringApplication.run(ProviderApplication.class, args);
    }
}

服务提供我们这里启动两个来区分,一个带灰度tag,一个不带

java 复制代码
@DubboService
public class DemoServiceImpl implements DemoService {

    @Override
    public String sayHello(String name) {
        return "no tag" + name;
    }
}
java 复制代码
@DubboService(tag = "weihubeats-tag")
public class DemoServiceImpl implements DemoService {

    @Override
    public String sayHello(String name) {
        return "weihubeats-tag" + name;
    }
    
}

tag的设置我们这是是通过@DubboService设置的,实际我们还可以通过环境变量设置比如

java 复制代码
System.setProperty("dubbo.provider.tag", "weihubeats-tag")

如果是xml方式暴露服务就是如下方式

xml 复制代码
<dubbo:service tag="gray"/>

或者

xml 复制代码
<dubbo:provider tag="gray"/>

也可以自定义环境变量,然后用如下方式

xml 复制代码
<dubbo:provider tag="${PROVIDER-TAG}"/>

参考:cn.dubbo.apache.org/zh-cn/overv...

我们通过连接zookeeper就可以看到两个已经注册上去的节点了

元数据我们可以看到已经包含tag了

消费者

  • 配置
yml 复制代码
dubbo:
  application:
    name: weihubeats-provider-consumer
    qos-port: 33333
  protocol:
    name: dubbo
    port: -1
  registry:
    address: zookeeper://${zookeeper.address:127.0.0.1}:2181
  • 服务消费
java 复制代码
@Component
public class Task implements CommandLineRunner {
    @DubboReference
    private DemoService demoService;

    @Override
    public void run(String... args) throws Exception {
        String result = demoService.sayHello("world");
        System.out.println("Receive result ======> " + result);

        new Thread(()-> {
            while (true) {
                try {
                    Thread.sleep(1000);
                    System.out.println(new Date() + " Receive result ======> " + demoService.sayHello("world"));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    Thread.currentThread().interrupt();
                }
            }
        }).start();
    }
}

可以看到一只是调用没有tag的服务提供者

如果我们需要实现gray调用,也很简单。我们添加一个ClusterFilter扩展

  • TagFilter
java 复制代码
@Activate(group = {CommonConstants.CONSUMER})
public class TagFilter implements ClusterFilter {
    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        invocation.setAttachment(CommonConstants.TAG_KEY, "weihubeats-tag");
        return invoker.invoke(invocation);

    }
}

需要使TagFilter生效我们还要通过spi的方式。在resources文件夹下面添加META-INF.dubbo文件夹然后添加文件

org.apache.dubbo.rpc.cluster.filter.ClusterFilter

文件内容:

ini 复制代码
tag=com.spring.boot.duubo.consumer.TagFilter

这里添加ommonConstants.TAG_KEY代表我们需要调用带有weihubeats-tag标签的服务。

注意时机灰度实现肯定不能写死,可能需要通过skywalking之类的全链路标签透传进行获取流量标签

我们运行就会发现自动调度到带有灰度标签的节点

如果我们设置了灰度标签没有灰度tag会如何呢?

答案是自动调用到没有灰度标签的服务。

这一点我们可以在下面的源码分析看到

TagStateRouter源码分析

核心路由实现的方法在org.apache.dubbo.rpc.cluster.router.tag.TagStateRouter#doRoute方法

需要注意的是这里有两种过滤规则 一个是动态标签

java 复制代码
final TagRouterRule tagRouterRuleCopy = tagRouterRule;

这里的标签设置是通过dubbo-admin进行设置的。

由于我们是静态标签设置,即项目启动的时候通过环境变量设置的。所以我们这里主要看看filterUsingStaticTag的逻辑

java 复制代码
return filterUsingStaticTag(invokers, url, invocation);
java 复制代码
private <T> BitList<Invoker<T>> filterUsingStaticTag(BitList<Invoker<T>> invokers, URL url, Invocation invocation) {
        BitList<Invoker<T>> result;
        // Dynamic param
        String tag = StringUtils.isEmpty(invocation.getAttachment(TAG_KEY)) ? url.getParameter(TAG_KEY) :
            invocation.getAttachment(TAG_KEY);
        // Tag request
        if (!StringUtils.isEmpty(tag)) {
            result = filterInvoker(invokers, invoker -> tag.equals(invoker.getUrl().getParameter(TAG_KEY)));
            if (CollectionUtils.isEmpty(result) && !isForceUseTag(invocation)) {
                result = filterInvoker(invokers, invoker -> StringUtils.isEmpty(invoker.getUrl().getParameter(TAG_KEY)));
            }
        } else {
            result = filterInvoker(invokers, invoker -> StringUtils.isEmpty(invoker.getUrl().getParameter(TAG_KEY)));
        }
        return result;
    }

这里的逻辑有两个

  1. 过滤掉不包含指定标签的Invoker
java 复制代码
result = filterInvoker(invokers, invoker -> tag.equals(invoker.getUrl().getParameter(TAG_KEY)));
  1. 如果过滤完后没有Invoker满足则看是否开启了强制使用带有tag的Invoker,默认false
java 复制代码
private boolean isForceUseTag(Invocation invocation) {
        return Boolean.parseBoolean(invocation.getAttachment(FORCE_USE_TAG, this.getUrl().getParameter(FORCE_USE_TAG, "false")));
    }
  1. 如果不是则调用没有tag的Invoker
java 复制代码
result = filterInvoker(invokers, invoker -> StringUtils.isEmpty(invoker.getUrl().getParameter(TAG_KEY)));

总结

总得来说dubbo实现流量灰度还是非常简单的,官方提供了原生的扩展我们直接使用就好。如果需要实现dubbo服务的动态标签就需要结合dubbo admin使用

参考

相关推荐
桦说编程20 分钟前
Java 中如何创建不可变类型
java·后端·函数式编程
IT毕设实战小研23 分钟前
基于Spring Boot 4s店车辆管理系统 租车管理系统 停车位管理系统 智慧车辆管理系统
java·开发语言·spring boot·后端·spring·毕业设计·课程设计
wyiyiyi1 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
阿华的代码王国2 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端
Jimmy2 小时前
AI 代理是什么,其有助于我们实现更智能编程
前端·后端·ai编程
AntBlack2 小时前
不当韭菜V1.1 :增强能力 ,辅助构建自己的交易规则
后端·python·pyqt
bobz9653 小时前
pip install 已经不再安全
后端
寻月隐君3 小时前
硬核实战:从零到一,用 Rust 和 Axum 构建高性能聊天服务后端
后端·rust·github
Pitayafruit5 小时前
Spring AI 进阶之路03:集成RAG构建高效知识库
spring boot·后端·llm
我叫黑大帅5 小时前
【CustomTkinter】 python可以写前端?😆
后端·python