这里是weihubeats ,觉得文章不错可以关注公众号小奏技术
背景
最近在做全链路灰度,服务之间调用有用到dubbo,所以需要做一下dubbo的灰度
环境
- dubbo-version:3.3.0-beta.1
- spring-boot: 2.7.8
- 源码地址:github.com/weihubeats/...
设计
网关入口流量染色
灰度的设计也有两种方案,第一种是全链路在路口进行灰度流量计算。
比如这种
在网关进行全局的灰度规则计算,然后进行流量染色,比如发现命中规则则调用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
这里我们要实现上面的流量染色后自动调用到灰度机器,我们可以使用官方自带的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}"/>
我们通过连接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;
}
这里的逻辑有两个
- 过滤掉不包含指定标签的Invoker
java
result = filterInvoker(invokers, invoker -> tag.equals(invoker.getUrl().getParameter(TAG_KEY)));
- 如果过滤完后没有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")));
}
- 如果不是则调用没有tag的
Invoker
java
result = filterInvoker(invokers, invoker -> StringUtils.isEmpty(invoker.getUrl().getParameter(TAG_KEY)));
总结
总得来说dubbo实现流量灰度还是非常简单的,官方提供了原生的扩展我们直接使用就好。如果需要实现dubbo服务的动态标签就需要结合dubbo admin
使用