【中】Dubbo灰度实践
2025/11/3大约 8 分钟
开始
UAT环境下,调用链路是 DubboA > DubboB > DubboC,现在在本地启动了一个服务DubboB1,如何在不修改UAT环境,让 UAT的DubboA 调用到本地的DubboB1: DubboA > DubboB1 > DubboC (PS: 网络畅通)
这其实就是一个Dubbo灰度的实践策略
一、实现思路
- 给每个服务的提供者打一个标签 (使用 parameters 自定义参数实现)
- 前端在请求头里面加一个 灰度标签,使用 ServletFilter 把标签存入 ThreadLocal
- 自定义 DubboFilter(生产者消费者都要) 把灰度标签,继续向下传递
- 自定义 Router,基于标签,进行 服务选择
1-1、parameters
这个很简单,直接在Dubbo配置文件里面去定义一个参数即可
gray-version 是自定义的key,随便写什么都可以,取的时候对应上即可
灰度环境
dubbo:
application:
name: dubbo3-provider
# 服务环境标识:灰度环境
parameters:
gray-version: gray正常环境
其实正常环境不配也可以,毕竟除了灰度就是正常的
dubbo:
application:
name: dubbo3-provider
# 服务环境标识:正式环境
parameters:
gray-version: prod1-2、解析Header 灰度标签
正常请求一般都是从前端发起的,所以这个 灰度标签的开始也应该是从HTTP请求开始的
GrayWebFilter
import com.xdx97.dubbo3.gray.constant.GrayConstant;
import com.xdx97.dubbo3.gray.context.GrayContext;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
/**
* Web 请求灰度标签解析 Filter
* 从 HTTP 请求头中获取灰度标签,并设置到上下文中
*/
public class GrayWebFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
// 从请求头获取灰度标签
String grayTag = httpRequest.getHeader(GrayConstant.GRAY_TAG);
if (grayTag != null) {
GrayContext.setGrayTag(grayTag);
System.out.println("=== Web灰度Filter === 设置灰度标签: " + grayTag + " 请求路径: " + httpRequest.getRequestURI());
}
try {
chain.doFilter(request, response);
} finally {
// 请求完成后清理上下文
GrayContext.clear();
}
}
} GrayContext
import com.xdx97.dubbo3.gray.constant.GrayConstant;
/**
* 灰度上下文 - 用于在同一线程中传递灰度标签
*/
public class GrayContext {
private static final ThreadLocal<String> GRAY_TAG_HOLDER = new ThreadLocal<>();
/**
* 设置灰度标签
*/
public static void setGrayTag(String tag) {
GRAY_TAG_HOLDER.set(tag);
}
/**
* 获取灰度标签
*/
public static String getGrayTag() {
return GRAY_TAG_HOLDER.get();
}
/**
* 清除灰度标签
*/
public static void clear() {
GRAY_TAG_HOLDER.remove();
}
/**
* 判断是否是灰度流量
*/
public static boolean isGray() {
String tag = getGrayTag();
return GrayConstant.GRAY_ENV_TAG.equals(tag);
}
}1-3、自定义 DubboFilter
关于DubboFilter更详细的解释参看:Dubbo自定义过滤器,过滤器源码详解
GrayConsumerFilter
Dubbo消费者过滤器
import com.xdx97.dubbo3.gray.constant.GrayConstant;
import com.xdx97.dubbo3.gray.context.GrayContext;
import org.apache.dubbo.common.constants.CommonConstants;
import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.rpc.*;
/**
* 灰度标签透传 Filter - Consumer端
* 将灰度标签添加到 RPC 调用的 attachment 中,实现标签透传
*
* 注意:此Filter不清理GrayContext,因为:
* 1. 一个HTTP请求可能会产生多个Dubbo调用链路
* 2. GrayContext的清理由GrayWebFilter统一在HTTP请求结束时处理
* 3. 这样可以保证整个调用链路的灰度标签一致性
*/
@Activate(group = {CommonConstants.CONSUMER}, order = -10000)
public class GrayConsumerFilter implements Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
// 从上下文获取灰度标签
String grayTag = GrayContext.getGrayTag();
if (grayTag != null) {
// 将灰度标签添加到 RPC 调用的 attachment 中,实现跨服务传递
invocation.setAttachment(GrayConstant.GRAY_TAG, grayTag);
System.out.println("=== Consumer灰度Filter === 透传灰度标签: " + grayTag);
}
// 不需要清理上下文,由GrayWebFilter统一清理
// 这样可以支持一个请求中的多次Dubbo调用都使用同一个灰度标签
return invoker.invoke(invocation);
}
}GrayProviderFilter
Dubbo生产者过滤器
import com.xdx97.dubbo3.gray.constant.GrayConstant;
import com.xdx97.dubbo3.gray.context.GrayContext;
import org.apache.dubbo.common.constants.CommonConstants;
import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.rpc.*;
/**
* 灰度标签接收 Filter - Provider端
* 接收并设置灰度标签到上下文中
*
* 关键设计:
* 1. 记录当前线程是否已经有灰度标签(说明是调用链的中间节点)
* 2. 如果是第一次设置标签,则在finally中清理(说明是调用链的起点)
* 3. 如果已经有标签,则不清理(说明会继续向下游传递)
*
* 这样可以支持:
* - Consumer → Provider(单层调用)
* - Consumer → ProviderA → ProviderB → ProviderC(多层调用链)
*/
@Activate(group = {CommonConstants.PROVIDER}, order = -10000)
public class GrayProviderFilter implements Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
// 从 attachment 中获取灰度标签
String grayTag = invocation.getAttachment(GrayConstant.GRAY_TAG);
// 检查当前线程是否已经有灰度标签(说明是调用链的中间节点)
String existingTag = GrayContext.getGrayTag();
boolean shouldCleanup = (existingTag == null);
if (grayTag != null) {
// 设置到上下文中,供业务代码使用
GrayContext.setGrayTag(grayTag);
System.out.println("=== Provider灰度Filter === 接收灰度标签: " + grayTag
+ " (已有标签: " + existingTag + ", 需要清理: " + shouldCleanup + ")");
}
try {
return invoker.invoke(invocation);
} finally {
// 只有当前Filter设置的标签才清理,避免影响调用链
// 如果是调用链的中间节点(已经有标签),则不清理,让后续调用继续使用
if (shouldCleanup) {
GrayContext.clear();
System.out.println("=== Provider灰度Filter === 清理灰度标签");
}
}
}
}1-4、自定义Router
上面做了给服务打标签,标签逐层传递,但并没有使用标签,现在来看怎么基于 Router来做调用
在之前的学习知道Dubbo每一个服务的提供者都会被解析成一个 invoker,然后由负载均衡策略去选择一个 invoker。现在可以用Router来使用自定义参数选择想要的 invoker
GrayRouter
代码有点长,但逻辑很简单,就是通过 parameters 对invoker进行区分
import com.xdx97.dubbo3.gray.constant.GrayConstant;
import com.xdx97.dubbo3.gray.context.GrayContext;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.rpc.Invocation;
import org.apache.dubbo.rpc.Invoker;
import org.apache.dubbo.rpc.RpcException;
import org.apache.dubbo.rpc.cluster.router.AbstractRouter;
import org.apache.dubbo.rpc.cluster.router.RouterResult;
import java.util.ArrayList;
import java.util.List;
/**
* 灰度路由器 - 根据灰度标签选择合适的服务提供者
* 使用 Dubbo 3.x 新的 RouterResult API
*/
public class GrayRouter extends AbstractRouter {
public GrayRouter(URL url) {
super(url);
}
@Override
public int getPriority() {
return 100; // 设置较高优先级
}
/**
* Dubbo 3.x 新方法 - 返回 RouterResult
* 这是推荐的实现方式,支持更丰富的路由信息
*/
@Override
public <T> RouterResult<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation, boolean needToPrintMessage) throws RpcException {
if (invokers == null || invokers.isEmpty()) {
return new RouterResult<>(invokers);
}
String grayTag = getGrayTag(invocation);
logGrayTag(grayTag, needToPrintMessage);
InvokerGroup<T> group = partitionInvokers(invokers, needToPrintMessage);
List<Invoker<T>> result = selectInvokers(grayTag, group, invokers, needToPrintMessage);
return new RouterResult<>(result);
}
/**
* 获取灰度标签
*/
private String getGrayTag(Invocation invocation) {
String grayTag = GrayContext.getGrayTag();
return grayTag != null ? grayTag : invocation.getAttachment(GrayConstant.GRAY_TAG);
}
/**
* 记录灰度标签日志
*/
private void logGrayTag(String grayTag, boolean needToPrintMessage) {
if (needToPrintMessage) {
System.out.println("=== 灰度路由 === 当前灰度标签: " + grayTag);
}
}
/**
* 分离灰度服务和正式服务
*/
private <T> InvokerGroup<T> partitionInvokers(List<Invoker<T>> invokers, boolean needToPrintMessage) {
List<Invoker<T>> grayInvokers = new ArrayList<>();
List<Invoker<T>> prodInvokers = new ArrayList<>();
for (Invoker<T> invoker : invokers) {
String version = invoker.getUrl().getParameter(GrayConstant.GRAY_VERSION);
logInvoker(invoker, version, needToPrintMessage);
if (GrayConstant.GRAY_ENV_TAG.equals(version)) {
grayInvokers.add(invoker);
} else {
prodInvokers.add(invoker);
}
}
return new InvokerGroup<>(grayInvokers, prodInvokers);
}
/**
* 记录提供者信息
*/
private <T> void logInvoker(Invoker<T> invoker, String version, boolean needToPrintMessage) {
if (needToPrintMessage) {
System.out.println(" 提供者: " + invoker.getUrl().getAddress() + " 版本: " + version);
}
}
/**
* 根据灰度标签选择对应的服务提供者
*/
private <T> List<Invoker<T>> selectInvokers(String grayTag, InvokerGroup<T> group,
List<Invoker<T>> allInvokers, boolean needToPrintMessage) {
if (isGrayRouting(grayTag, group)) {
return logAndReturn(group.grayInvokers, "灰度环境", needToPrintMessage);
}
List<Invoker<T>> result = group.prodInvokers.isEmpty() ? allInvokers : group.prodInvokers;
return logAndReturn(result, "正式环境", needToPrintMessage);
}
/**
* 判断是否路由到灰度环境
*/
private <T> boolean isGrayRouting(String grayTag, InvokerGroup<T> group) {
return GrayConstant.GRAY_ENV_TAG.equals(grayTag) && !group.grayInvokers.isEmpty();
}
/**
* 记录路由结果并返回
*/
private <T> List<Invoker<T>> logAndReturn(List<Invoker<T>> invokers, String env, boolean needToPrintMessage) {
if (needToPrintMessage) {
System.out.println(" → 路由到" + env + ",提供者数量: " + invokers.size());
}
return invokers;
}
/**
* 服务提供者分组
*/
private static class InvokerGroup<T> {
final List<Invoker<T>> grayInvokers;
final List<Invoker<T>> prodInvokers;
InvokerGroup(List<Invoker<T>> grayInvokers, List<Invoker<T>> prodInvokers) {
this.grayInvokers = grayInvokers;
this.prodInvokers = prodInvokers;
}
}
}二、最佳实践
2-1、场景解析
HTTPA > DubboB > DubboC&DubboD > DubboE
大部分场景是这样的链路,来解析一下灰度参数是如何逐级传递的
- 首先前端在header里面传递了
gray-tag,GrayWebFilter 读取到了,并把它设置到 ThreadLocal - 发起调用 DubboB,会经过 GrayConsumerFilter,从ThreadLocal读取,并设置到 invocation 的参数里面
- GrayRouter 会通过invocation的参数,找到灰度的 DubboB
- 请求达到 灰度DubboB,会经过 GrayProviderFilter,继续把
gray-tag添加到 GrayContext中 - 继续调用DubboC&DubboD,经过GrayConsumerFilter,从ThreadLocal读取,并设置到 invocation 的参数里面
- DubboB 里面GrayProviderFilter 的finally里面会清空
GrayContext.clear() - GrayWebFilter 的finally里面会清空
GrayContext.clear()
2-2、自定义 starter
上面定义了那么多代码,但在实际使用的时候,服务是很多的,肯定是不希望代码到处复制,很简单定义一个 starter就好了,引入即可

starter获取,关注公众号:小道仙97,回复关键字:dubbo3-gray-starter
三、REST灰度、MQ灰度
3-1、怎么返回灰度标签?以及REST怎么做灰度
- 可以在用户信息接口里面返回灰度tag,前端判断如果这个tag有值,就设置到请求头里面
- 通过网关基于 请求头灰度到不同的服务 基于Ingress的全链路灰度
3-2、RocketMQ灰度
- 灰度服务和正常服务使用不同的 topic,生产者和消费者都要部署
- 基于消息队列RocketMQ版实现全链路灰度
四、简单灰度
如果有很多服务,但是呢,你只是修改了A 调用B,这两个服务,而且服务中并没有灰度,简单的做法就是本地启动A、B两个服务,在消费者和生产者都打上一个标签即可
@DubboReference(version = "1.0.0", tag = "aaaa")
@DubboService(version = "1.0.0", timeout = 10000, retries = 0, tag = "aaaa")