在一個調用鏈非常長的功能中,如果想修改其中的一個特性,並進行測試,而又不影響該環境的其他用戶使用現有功能、特性,例如:
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,將來因為某種原因要換遠程調用框架時,會變得輕松一些
問題(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