在一个调用链非常长的功能中,如果想修改其中的一个特性,并进行测试,而又不影响该环境的其他用户使用现有功能、特性,例如:
1. A、B、C、D之间通过Dubbo实现远程调用
2. 这些模块可能有一个或者多个实例
3. 此环境由多个人员(包括开发、测试)同时使用
此时若想修改B中的某个功能,增加一个特性(称为FAT1),并且也注册到此环境中,则会发生如下问题:
当其他的用户从使用此功能时,从A发起的调用可能会由于Dubbo带的负载均衡算法等原因,在带有FAT1和不带有FAT1的实例间来回切换,最后的表现可能就是某一个功能使用两次,产生的结果竟然不一样!
解决这个问题最简单的方法就是给每个功能特性(FAT)独立设置一个测试环境,例如这一期有20个功能特性上线,就部署20个环境好了。。。。等等,是不是哪里不对?部署20个环境?你是否感觉到你BOSS站在你座位后面,随时准备把你扔出办公室?
仔细分析这个问题,要解决的重点有两个:
1. 将不同人员进行开发/测试的特性隔离开
2. 不修改的部分尽量共享,以节省资源
综上,最好的解决方案应该是如下图所示:
1. 建立一个Baseline环境,该环境包含了应用程序所需的所有组件、数据集等
2. 对于不同的功能特性,为该特性修改的组件独立发布一个实例,称之为一个Feature,对应的测试场称之为FAT+编号,例如Feature 1的测试环境称为FAT1
3. 开发和测试某个功能特性(例如Feature 1)时,利用路由功能让上游模块自动选择正确的下游模块,便于开发人员调试以及测试人员查看效果
通过对Dubbo文档的探索(http://dubbo.apache.org/zh-cn/docs/user/demos/routing-rule.html),发现实现此功能的方案有如下几种:
1. 使用条件路由规则
2. 使用动态标签功能
3. 使用静态标签功能
经过对上述三种方法的分析,发现各自的优缺点如下:
1. 如果使用条件路由:
优点是需求明晰,如果我想设计一个FAT测试场,其中A、B是待测试组件,可以使用路由规则host != A => host !=B和host = A => host = B
缺点是:
A. 需要使用Dubbo控制台修改路由规则,对于一般的开发/测试来说,权限太大了
B. 如果组件A、B、D同时修改了,当请求从A->B->C传递时,C不一定知道这个请求是否应该传到D,使用条件路由无法实现
2. 如果使用动态标签,1中的问题B能够得到解决,因为标签在整个调用链路中都会以Attachment的形式被传递,但是A问题依然无法解决
综上,要实现此功能,最好是使用3. 静态标签功能,根据官方文档,Dubbo的标签路由功能是2.7.0开始才可用的(坑巨多,下面会一一说明),所以我们需要使用这个版本。
为了简化(偷)步骤(懒),我们把问题变为A->B->C这种三模块调用过程,本质上设计的调用路由问题还是一样的。
先建立三个spring boot工程:组件svcA、svcB和svcC
两个模块间调用使用的facade工程,以及他们所共享的父工程,总共六个工程如下图:
他们之间的关系如下:
其中callfromsvcA2svcB是A调用B使用的facade,而callfromsvcB2svcC是从B调用C时的facade,取名方式略暴力,品位低,敬请理解
下面进入踩坑之旅:
1. 导入Dubbo 2.7.0
因为Dubbo 2.7.0才支持tag路由功能,所以我们必须先导入它到工程,但是当你实践时,你会发现。。。。。。网上的教程(包括官方文档):都!是!骗!人!的!
官方的说明是:http://dubbo.apache.org/zh-cn/docs/user/versions/version-270.html
<properties> <dubbo.version>2.7.0</dubbo.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo-dependencies-bom</artifactId> <version>${dubbo.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo</artifactId> <version>${dubbo.version}</version> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> </dependency> </dependencies>
这个bom和spring boot(2.1.4.RELEASE)是冲突的,启动时会报错:
Exception in thread "main" java.lang.AbstractMethodError: org.springframework.boot.context.config.ConfigFileApplicationListener.supportsSourceType(Ljava/lang/Class;)Z
(天哪,鬼知道这是啥错)
当然,如果不使用spring boot,可能会没有问题,不过现在建工程貌似都是用spring boot为主流
所以只能手动引用Dubbo。
经过反复尝试(内心:mmp),得到如下能够正常工作的pom清单:
<dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo</artifactId> <version>2.7.0</version> </dependency> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-framework</artifactId> <version>4.2.0</version> <exclusions> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> </exclusion> <exclusion> <groupId>log4j</groupId> <artifactId>log4j</artifactId> </exclusion> </exclusions> </dependency> <!-- https://mvnrepository.com/artifact/org.apache.curator/curator-recipes --> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-recipes</artifactId> <version>4.2.0</version> </dependency>
注意:
1. 从2.7.0开始,Dubbo已经从alibaba的项目转为了apache的了,所以命名空间会发生改变,小心不要踩坑。
2. 引用com.alibaba.xxx下面的对象会因为这些对象都是Deprecated导致这些对象有删除线,解决办法就是把对应的import删掉,重新引用,你会发现有两个一摸一样的,一个在alibaba的名称空间下面,另外一个在apache里面,引用apache的那个即可。
3. 切记,千万不要在一个工程里面既引用alibaba空间下面的注解,又引用apache下面的注解,这会直接导致注解失效。
下面开始处理最困难的部分:给服务打上标签:
首先我们在svcA中建立两个properties文件,用于模拟普通测试和FAT测试,代码如下:
application.properties:
spring.application.name=svcB
dubbo.application.name=svcB
dubbo.registry.protocol=zookeeper
dubbo.registry.address=127.0.0.1:2181
dubbo.protocol.name=dubbo
dubbo.monitor.protocol=registry
dubbo.protocol.port=20881
server.port=55557
application-fat1.properties(请注意标红的属性):
#fat1
spring.application.name=svcB
dubbo.application.name=svcB
dubbo.registry.protocol=zookeeper
dubbo.registry.address=127.0.0.1:2181
dubbo.protocol.name=dubbo
dubbo.monitor.protocol=registry
dubbo.protocol.port=20882
server.port=55558
featuretest=fat1
我们假设svcA是前端,从用户处得到请求调用后续的服务的,在这个服务中,我们嵌入一个WebFilter,实现将FAT的TAG打到Dubbo调用中,代码如下:
package com.dubbotest.svcA.filters; import java.io.IOException; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.annotation.WebFilter; import org.apache.dubbo.common.Constants; import org.apache.dubbo.rpc.RpcContext; import org.springframework.beans.factory.annotation.Value; @WebFilter public class FatTagFilter implements Filter { @Value("${featuretest:#{null}}") private String feature; @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException { if (feature != null) { RpcContext.getContext().setAttachment(Constants.TAG_KEY, feature); } chain.doFilter(req, resp); } }
这段代码的作用就是从环境中查找名为featuretest的变量,如果找到了,就放到Dubbo中名为TAG的attachment中。
顺便吐槽一下,Dubbo官网上的文档(http://dubbo.apache.org/zh-cn/docs/user/demos/routing-rule.html)中的范例代码:
RpcContext.getContext().setAttachment(Constants.REQUEST_TAG_KEY,"tag1");
是有问题的,2.7.0中,Constants里面已经没有名为REQUEST_TAG_KEY的常量了,只有TAG_KEY,其次,静态打标:
java -jar xxx-provider.jar -Ddubbo.provider.tag={the tag you want, may come from OS ENV}
是不起作用的,我看了下Dubbo源码,没有相关的内容
有了上述代码后,前端就实现了当设定了featuretest变量时,这个变量会被当成TAG存放到RPC调用的Attachment中,而根据阿里的文档,这个Attachment是能够存续在整个RPC调用过程的,但是,但是!事实证明这又是坑爹的!
还是拿前面的例子:
A->B->C
当前端传递Attachment到B时,B能够看到数据,但是不知为何,B却没能将这个数据传送到C,导致这个数据在后面调用全部失效
所以只好自己写一个过滤器放在服务B中,将这个变量传递下去:
1. 先在resources\META-INF\dubbo目录添加com.alibaba.dubbo.rpc.Filter,内容如下:
passFatTag=com.dubbotest.svcB.filters.PassFatTagFilter
然后再在服务B的application.properties中添加:
dubbo.provider.filter=passFatTag
最后,添加下述Java代码:
package com.dubbotest.svcB.filters; import org.apache.dubbo.common.Constants; import org.apache.dubbo.rpc.Filter; import org.apache.dubbo.rpc.Invocation; import org.apache.dubbo.rpc.Invoker; import org.apache.dubbo.rpc.Result; import org.apache.dubbo.rpc.RpcContext; import org.apache.dubbo.rpc.RpcException; public class PassFatTagFilter implements Filter { @Override public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException { String fatTag = invocation.getAttachment(Constants.TAG_KEY); if (fatTag != null) { RpcContext.getContext().setAttachment(Constants.TAG_KEY, fatTag); } Result result = invoker.invoke(invocation); return result; } }
请注意,这些代码在所有的下游服务器都要添加,例如本例中的B、C。如果后面还有更多的服务,也要添加,目的是让Attachment传递下去。
上面这些工作只是对前端到服务的调用进行了打标,下一步将进行对服务提供者进行打标:
对于application.properties的处理大同小异,无非是增加了一个FAT测试标签的变量,但是如何把这个标签弄到服务提供者上,恭喜你,遇到了史前巨坑:
前面已经说过了,下述方法对服务提供者打标是无效的:
java -jar xxx-provider.jar -Ddubbo.provider.tag={the tag you want, may come from OS ENV}
所以要想办法,只能在Service上面想办法,例如svcB提供的服务,代码可以这么写:
package com.dubbotest.svcB.impl; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.dubbo.config.annotation.Reference; import org.apache.dubbo.config.annotation.Service; import com.facade.callfromsvcA2svcB.callfromA2B; import com.facade.callfromsvcB2svcC.callfromB2C; @Service(tag="fat1") public class ServiceBimpl implements callfromA2B { Logger logger = Logger.getLogger(ServiceBimpl.class.getName()); @Reference private callfromB2C svcC; @Override public String getNamefromSvcB(String source) { logger.log(Level.INFO, "Source:"+ source); if (source == null) { return "no name, since source is empty"; } String name = source+source.length(); return name + " hash:"+svcC.getIDfromName(name); } }
转眼你就会发现这个做法的坑爹之处:
1. FAT测试特性的代码侵入了业务逻辑
2. 无法随时修改特性测试的名称(fat1)
3. 我提供了100个服务,是不是100个服务都要添加打标的代码?如果我要修改呢?(996程序员的内心:mmp)
似乎问题到此陷入了僵局,不过不妨先看下打标的功能是怎么实现的:
我们先通过tag作为关键词直接搜索dubbo的jar:
我的搜索方法是这样的:用Java Search,查找All occurrences,Search for中每一个都试一遍(哪位大神如果有更好的方法,麻烦推荐)
最后找到有价值的东西:
猜想如下:Spring在加载ServiceBean的时候,通过注解拿到属性,并且调用setTag配置好,最后服务调用的时候就会使用这个tag,我们先在Service注解中放一个tag,并且对setTag打一个断点,最后启动服务,发现调用栈如下:
不出所料,果然断在了setTag上,这是调用getBean实例化对象时,对Bean对象属性填充时设定的(请看populateBean和applyPropertyValues这两个栈帧)。
这给我们了一个启发,我们可以使用一个BeanPostProcessor后处理器,在Bean实例化后对它进行设定,将tag直接设置上去,代码如下:
package com.dubbotest.svcB.postprocessors; import org.apache.dubbo.config.spring.ServiceBean; import org.springframework.beans.BeansException; import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.stereotype.Component; @Component public class FeatureTestPostProcessor implements BeanPostProcessor { @Value("${featuretest:#{null}}") private String featuretest; @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (featuretest != null) { if (bean instanceof ServiceBean) { ServiceBean thebean = (ServiceBean) bean; thebean.setTag(featuretest); } } return bean; } }
因为Dubbo导出服务时,先会在Spring容器中注册一个ServiceBean,所以我们可以在此ServiceBean初始化完毕后,将我们想要的属性注入。
为何不使用postProcessBeforeInitialization?如果使用Before,可能Bean本身初始化属性时又会将我们设定的属性覆盖。
逻辑很简单,无非就是当设定了featuretest时,将这个属性注入到ServiceBean的tag中。
这个代码也会造成一点问题:
如果以后Dubbo升级,可能Bean的类型会改变,属性也会改变
考虑到我们的代码并没有和业务代码耦合,如果以后发生改变,我们修改下后处理器就可以了,这不会是什么问题
因为B、和C都是服务提供者,所以C也应该添加上述后处理器以及用于传递消息的Dubbo过滤器
测试效果:
我们搭建一个基础服务器组和一个FAT测试场,命名为fat1:
其中基础服务器组入口是:127.0.0.1:8088
fat1入口是:127.0.0.1:8089
先启动基础服务组和FAT1的前端入口:
可以发现,基础服务和FAT1组使用的都是默认feature:
此时我们如果启动fat1中的某个服务,例如C:
服务启动情况如图:
运行结果:
可见,实现了对不同特性进行隔离的功能,fat1的使用者可以独立于Baseline环境进行开发测试。
如果此时有另外一个开发组想要开发客户提出的新需求fat2,只需要将application.properties中的featuretest改为fat2然后在本机或者服务器上发布进行测试即可,不同环境完全隔离,互不影响。
上述工程的git路径:https://github.com/TTTTTAAAAAKKKKEEEENNNN/FATtestDemo
对于工程需要改进的地方,有如下几点思考:
1. 实现FAT使用的过滤器、后处理器需要在每个工程中独立添加,还是不够方便,如果能封装成一个jar在其他工程中引入,将会更加方便
2. 工程中引入Dubbo服务是直接使用的Dubbo注解@Service,如果能在中间嵌入一层,让工程通过Spring间接引用Dubbo,将来因为某种原因要换远程调用框架时,会变得轻松一些
原文地址:https://www.cnblogs.com/nullifier/p/10742957.html