在一个调用链很是长的功能中,若是想修改其中的一个特性,并进行测试,而又不影响该环境的其余用户使用现有功能、特性,例如:html
1. A、B、C、D之间经过Dubbo实现远程调用前端
2. 这些模块可能有一个或者多个实例java
3. 此环境由多我的员(包括开发、测试)同时使用git
此时若想修改B中的某个功能,增长一个特性(称为FAT1),而且也注册到此环境中,则会发生以下问题:程序员
当其余的用户从使用此功能时,从A发起的调用可能会因为Dubbo带的负载均衡算法等缘由,在带有FAT1和不带有FAT1的实例间来回切换,最后的表现可能就是某一个功能使用两次,产生的结果居然不同!github
解决这个问题最简单的方法就是给每一个功能特性(FAT)独立设置一个测试环境,例如这一期有20个功能特性上线,就部署20个环境好了。。。。等等,是否是哪里不对?部署20个环境?你是否感受到你BOSS站在你座位后面,随时准备把你扔出办公室?web
仔细分析这个问题,要解决的重点有两个:算法
1. 将不一样人员进行开发/测试的特性隔离开spring
2. 不修改的部分尽可能共享,以节省资源apache
综上,最好的解决方案应该是以下图所示:
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,未来由于某种缘由要换远程调用框架时,会变得轻松一些
问题(1)的解决方案(2019-04-30更新):
目前已经实现将工程中的后处理器、过滤器、拦截器打包到jar中,只须要在本身的工程引入便可,请参考:https://github.com/TTTTTAAAAAKKKKEEEENNNN/FATTest-modularization
下面是使用步骤:
1. 对于一个前端工程(使用了Spring MVC的工程)
请引入下述依赖:
<dependency> <groupId>com.fattest</groupId> <artifactId>FATtest-web</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency>
在工程的application.properties添加:featuretest=fattag,能够本身修改fattag为其余值
在Spring工程中添加:@ServletComponentScan({"com.fattest"})
2. 对于一个后端工程
请添加下述依赖:
<dependency> <groupId>com.fattest</groupId> <artifactId>FATtest-service</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency>
在工程的application.properties添加:dubbo.provider.filter=passFatTag
而且,在Spring工程添加:@ComponentScan({"com.fattest"})
请注意:
前端模块将引入:
Dubbo 2.7.0
javax.servlet-api 3.1.0(请适配本身工程合适的版本)
后端模块将会引入:Dubbo 2.7.0