如何使用Dubbo 2.7.0和Spring boot實現FAT測試(Feature Acceptance Test)


在一個調用鏈非常長的功能中,如果想修改其中的一個特性,並進行測試,而又不影響該環境的其他用戶使用現有功能、特性,例如:

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>
View Code

這個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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM