在一個調用鏈很是長的功能中,若是想修改其中的一個特性,並進行測試,而又不影響該環境的其餘用戶使用現有功能、特性,例如: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