Tars | 第6篇 基於TarsGo Subset路由規則的Java JDK實現方式(下)

多氯環己烷發表於2021-09-12


前言

利開園導師用Go語言實現了Subset路由規則,並在中期彙報分享會裡介紹出來;這篇文章將基於利導師的實現方式,對Subset路由規則的細節做些理解與補充。

此篇文章為下半部分,將對上半部分提到的TarsGo對Subset路由規則的實現做一一分析,重點放在“如果開發語言是Java,對應功能將如何實現”問題上。

上下部分文章在目錄上一一對應,上半注重TarsGo分析,下半部分注重TarsJava實現方式。如上篇文章第一點修改.tars協議檔案記錄利導師在TarsGo的程式碼修改,下片文章第一點也是修改.tars協議檔案,側重點在如何用Java語言實現。上下文章相輔相成,建議對照學習。

一些資源連結如下:

上半部分文章連結
https://www.cnblogs.com/dlhjw/p/15245113.html

TarsJava 實現Subset路由規則JDK連結地址
https://github.com/TarsCloud/TarsJava/commit/cc2fe884ecbe8455a8e1f141e21341f4f3dd98a3

TarsGo 實現Subset路由規則JDK連結地址

https://github.com/defool/TarsGo/commit/136878e9551d68c4b54c402df564729f51f3dd9c#


1. 修改.tars協議檔案

需要修改兩處.tars協議檔案;

1.1 Java原始碼位置及邏輯分析

該部分的含義是:增加Subset配置增加獲取Subset資訊

通過上半文章的分析,增加的配置是在EndpointF.tarsQueryF.tars協議檔案裡面新增,而tars協議檔案在所有語言中是統一的,一樣的;在Java中,EndpointF協議檔案在src/main/resources/EndpointF.tars;QueryF協議檔案在src/main/resources/QueryF.tars;

因此,我們可以得到以下資訊:

  • 定位對應原始碼位置如下:
Go語言 Java
tars/protocol/res/EndpointF.tars TarsJava-1.7.x\core\src\main\resources\EndpointF.tars
tars/protocol/res/QueryF.tars TarsJava-1.7.x\core\src\main\resources\QueryF.tars
  • 直接新增subset配置即可;

1.2 Java語言實現方式

module tars
{
    /**
     * Port information
     */
    struct EndpointF
    {
        0  require  string host;
        1  require  int    port;
        2  require  int    timeout;
        3  require  int    istcp;
        4  require  int    grid;
        5  optional int    groupworkid;
        6  optional int    grouprealid;
        7  optional string setId;
        8  optional int    qos;
        9  optional int    bakFlag;
        11 optional int    weight;
        12 optional int    weightType;
        13 optional string subset;
    };
    key[EndpointF, host, port, timeout, istcp, grid, qos, weight, weightType];


};

QueryF.tars

1.3 通過協議檔案自動生成程式碼

Tars有個強大的功能,它能根據.tars裡的配置檔案自動生成相應Bean程式碼;

在Java語言裡,具體操作如下:

1. 在專案的pom.xml裡配置對應外掛

<build>
    <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
        <plugins>
            <!-- clean lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#clean_Lifecycle -->
            <plugin>
                <groupId>com.tencent.tars</groupId>
                <artifactId>tars-maven-plugin</artifactId>
                <version>1.7.2</version>
                <configuration>
                    <tars2JavaConfig>
                        <!-- tars檔案位置 -->
                        <tarsFiles>
                            <tarsFile>${basedir}/src/main/resources/EndpointF.tars</tarsFile>
                        </tarsFiles>
                        <!-- 原始檔編碼 -->
                        <tarsFileCharset>UTF-8</tarsFileCharset>
                        <!-- 生成服務端程式碼 -->
                        <servant>false</servant>
                        <!-- 生成原始碼編碼 -->
                        <charset>UTF-8</charset>
                        <!-- 生成的原始碼目錄 -->
                        <srcPath>${basedir}/src/main/java</srcPath>
                        <!-- 生成原始碼包字首 -->
                        <packagePrefixName>com.qq.tars.common.support.</packagePrefixName>
                    </tars2JavaConfig>
                </configuration>
            </plugin>
        </plugins>
    </pluginManagement>
</build>

我們僅需要修改的地方在 tars檔案位置生成原始碼包字首

2. 在專案根路徑下執行mvn tars:tars2java命令

專案根路徑輸入cmd
接著輸入mvn tars:tars2java命令後出現下面日誌則說明生成成功;

BUILD SUCCESS

3. 檢查生成程式碼

我們回到專案程式碼,經檢查,EndpointF類發生了修改,新增SubsetConf類。(因為筆者在第一步生成原始碼包字首沒有配置好,所有將生成後的程式碼直接複製黏貼到原始碼路徑裡了,影響不大。)

檢查程式碼
4. 用同樣的方法可以自動生成QueryF程式碼

1.4 變更程式碼的路徑

通過上述操作,以下路徑的程式碼發生改變,需要在github上提交:

  • TarsJava-1.7.x\core\src\main\resources\EndpointF.tars
  • TarsJava-1.7.x\core\src\main\resources\QueryF.tars
  • TarsJava-1.7.x\core\src\main\java\com\qq\tars\support\query\prx\EndpointF.java
  • TarsJava-1.7.x\core\src\main\java\com\qq\tars\support\query\prx\QueryFPrx.java
  • TarsJava-1.7.x\core\src\main\java\com\qq\tars\support\query\prx\QueryFPrxCallback.java

2. 【核心】增添Subset核心功能

這部分是核心功能,不需要在原始碼裡更改,屬於新增的內容。

2.1 Java原始碼位置及邏輯分析

該部分的含義是:增添Subset核心功能

由於Subset路由業務與客戶端相關,在Tars中的地位是:Tars支援(support)的功能之一,因此,筆者打算在參照原來的專案結構,在TarsJava-1.7.x\core\src\main\java\com\qq\tars\client路徑下新建包subset,包內實現以下功能:

新增型別 新增內容
結構體 新增Subset配置項的結構體 subsetConf
結構體 新增路由規則配置項的結構體ratioConfig
結構體 新增染色路徑的結構體keyRoute
結構體 新增染色配置項的結構體keyConfig
結構體 新增subset管理者的結構體subsetManager
方法 新增獲取subset配置項的方法getSubsetConfig
方法 新增獲取比例 / 染色路由配置項的方法getSubset
方法 新增根據subset規則過濾節點的方法subsetEndpointFilter
方法 新增根據一致hash的subset規則過濾節點的方法subsetHashEpFilter
方法 新增按比例路由路由路徑的方法findSubet
方法 新增按預設路由路徑findSubet

因此,我們可以得到以下資訊:

  • 定位對應原始碼位置如下:
Go語言 Java
tars/subset.go TarsJava-1.7.x\core\src\main\java\com\qq\tars\client\subset\

2.2 Java語言實現方式

筆者的理解是五個結構體各自新建成一個類,此外新建Subset類;根據TarsGo實現邏輯:

  • SubsetConf類裡定義一些屬性,並生成對應getter與setter方法;
  • RatioConfig類裡實現findSubet()方法;
    • *在KeyRoute類裡實現getRouteKey()setRouteKey()setRouteKeyToRequest()方法;
    • 這裡提到的方法請見《3. 新增常量與獲取染色key的方法》與《5. 實現透傳染色Key功能》分析;
  • KeyConfig類裡實現findSubet()方法;
  • SubsetManager類裡實現getSubsetConfig()getSubset()方法;
  • Subset類裡實現subsetEndpointFilter()subsetHashEpFilter()方法

具體的實現程式碼如下:

SubsetConf

public class SubsetConf {

    private boolean enanle;
    private String ruleType;
    private RatioConfig ratioConf;
    private KeyConfig keyConf;

    private Instant lastUpdate;

    public SubsetConf() {
        lastUpdate =  Instant.now();
    }


    public SubsetConf(boolean enanle, String ruleType, RatioConfig ratioConf, KeyConfig keyConf) {
        this.enanle = enanle;
        this.ruleType = ruleType;
        this.ratioConf = ratioConf;
        this.keyConf = keyConf;
        lastUpdate =  Instant.now();
    }

    public boolean isEnanle() {
        return enanle;
    }

    public void setEnanle(boolean enanle) {
        this.enanle = enanle;
    }

    public String getRuleType() {
        return ruleType;
    }

    public void setRuleType(String ruleType) {
        this.ruleType = ruleType;
    }

    public RatioConfig getRatioConf() {
        return ratioConf;
    }

    public void setRatioConf(RatioConfig ratioConf) {
        this.ratioConf = ratioConf;
    }

    public KeyConfig getKeyConf() {
        return keyConf;
    }

    public void setKeyConf(KeyConfig keyConf) {
        this.keyConf = keyConf;
    }

    public Instant getLastUpdate() {
        return lastUpdate;
    }

    public void setLastUpdate(Instant lastUpdate) {
        this.lastUpdate = lastUpdate;
    }
}

RatioConfig

public class RatioConfig {

    private Map<String, Integer> rules;


    //進行路由規則的具體實現,返回subset欄位
    public String findSubet(String routeKey){
        //routeKey為空時隨機
        if( "".equals(routeKey) ){
            //賦值routeKey為獲取的隨機值
            Random random = new Random();
            int r = random.nextInt( rules.size() );
            routeKey = String.valueOf(r);
            int i = 0;
            for (String key : rules.keySet()) {
                if(i == r){
                    return key;
                }
                i++;
            }
        }

        //routeKey不為空時實現按比例演算法
        int totalWeight = 0;
        int supWeight = 0;
        String subset = null;
        //獲得總權重
        for (Integer value : rules.values()) {
            totalWeight+=value;
        }
        //獲取隨機數
        Random random = new Random();
        int r = random.nextInt(totalWeight);
        //根據隨機數找到subset
        for (Map.Entry<String, Integer> entry : rules.entrySet()){
            supWeight+=entry.getValue();
            if( r < supWeight){
                subset = entry.getKey();
                return subset;
            }
        }
        return null;
    }

    public Map<String, Integer> getRules() {
        return rules;
    }

    public void setRules(Map<String, Integer> rules) {
        this.rules = rules;
    }
}

KeyRoute

  • 這裡提到的方法請見《3. 新增常量與獲取染色key的方法》分析;
public class KeyRoute {

    private String action = null;
    private String value = null;
    private String route = null;

    public static final String TARS_ROUTE_KEY = "TARS_ROUTE_KEY";

    private static final Logger logger = LoggerFactory.getClientLogger();


    //根據分散式上下文資訊獲取KeyRoute
    public static String getRouteKey(DistributedContext distributedContext){
        if( distributedContext == null ){
            logger.info("無分散式上下文資訊distributedContext");
        }
        String routeValue = "";
        if(distributedContext != null){
            TarsServantRequest tarsServantRequest = distributedContext.get(DyeingSwitch.REQ);
            if( tarsServantRequest != null){
                routeValue = tarsServantRequest.getStatus().get(TARS_ROUTE_KEY);
            }
        }
        return routeValue;
    }

    //根據分散式上下文資訊設定KeyRoute
    public static void setRouteKey(DistributedContext distributedContext, String routeKey){

        if(distributedContext != null && routeKey != null ){
            TarsServantRequest tarsServantRequest = distributedContext.get(DyeingSwitch.REQ);
            tarsServantRequest.getStatus().put(TARS_ROUTE_KEY, routeKey);
        }
    }

    public static void setRouteKeyToRequest(DistributedContext distributedContext, TarsServantRequest request){
        if( distributedContext == null ){
            logger.info("無分散式上下文資訊distributedContext");
        }
        String routeValue = KeyRoute.getRouteKey(distributedContext);
        if( routeValue != null && !"".equals(routeValue)){
            if(request.getStatus() != null){
                request.getStatus().put(KeyRoute.TARS_ROUTE_KEY ,routeValue);
            } else {
                HashMap<String, String> status = new HashMap<>();
                status.put(KeyRoute.TARS_ROUTE_KEY ,routeValue);
                request.setStatus(status);
            }
        }
    }

    //將分散式上下文資訊的routeValue 設定到KeyRoute.value
    public void setValue(DistributedContext distributedContext){
        String routeKey = getRouteKey(distributedContext);
        if( !"".equals(routeKey) && routeKey != null){
            this.value = routeKey;
        }
    }

    public KeyRoute() {
    }

    public KeyRoute(String action, String value, String route) {
        this.action = action;
        this.value = value;
        this.route = route;
    }

    public String getValue() {
        return value;
    }

    public String getAction() {
        return action;
    }

    public void setAction(String action) {
        this.action = action;
    }

    public String getRoute() {
        return route;
    }

    public void setRoute(String route) {
        this.route = route;
    }
}

KeyConfig

  • 因為這裡涉及正則匹配,所有在StringUtils工具類裡有正則演算法的實現,詳情見《8. 正則演算法的實現》;
public class KeyConfig {

    private String defaultRoute;

    private List<KeyRoute> rules;

    private DistributedContext distributedContext = DistributedContextManager.getDistributedContext();

    private static final Logger logger = LoggerFactory.getClientLogger();

    public String findSubet(String routeKey){
        //非空校驗
        if( routeKey == null || "".equals(routeKey) || rules == null){
            return null;
        }
        for ( KeyRoute rule: rules) {
            //根據根據分散式上下文資訊獲取 “請求的染色的key”
            String routeKeyReq;
            if( distributedContext != null){
                routeKeyReq = KeyRoute.getRouteKey(distributedContext);
            } else {
                logger.info("無分散式上下文資訊distributedContext");
                return null;
            }
            //精確匹配
            if( "match".equals(rule.getAction())  ){
                if( routeKeyReq.equals(rule.getValue()) ){
                    return rule.getRoute();
                } else {
                    logger.info("染色key匹配不上,請求的染色key為:" + routeKeyReq + "; 規則的染色key為:" + rule.getValue());
                }
            }
            //正則匹配
            if( "equal".equals(rule.getAction()) ){
                if( StringUtils.matches(routeKeyReq, rule.getValue()) ){
                    return rule.getRoute();
                } else {
                    logger.info("正則匹配失敗,請求的染色key為:" + routeKeyReq + "; 規則的染色key為:" + rule.getValue());
                }

            }
            //預設匹配
            if( "default".equals(rule.getAction()) ){
                //預設路由無需考慮染色key
                return rule.getRoute();
            }
        }
        return null;
    }

    public KeyConfig() {
    }

    public KeyConfig(String defaultRoute, List<KeyRoute> rules) {
        this.defaultRoute = defaultRoute;
        this.rules = rules;
    }

    public String getDefaultRoute() {
        return defaultRoute;
    }

    public void setDefaultRoute(String defaultRoute) {
        this.defaultRoute = defaultRoute;
    }

    public List<KeyRoute> getRules() {
        return rules;
    }

    public void setRules(List<KeyRoute> rules) {
        this.rules = rules;
    }
}

SubsetManager

public class SubsetManager {

    private Map<String, SubsetConf> cache = new HashMap<>();

    private QueryFPrx queryProxy;

    //獲取Subset路由規則,並存到subsetConf配置項
    public SubsetConf getSubsetConfig(String servantName){
        SubsetConf subsetConf = new SubsetConf();
        if( cache.containsKey(servantName) ){
            subsetConf = cache.get(servantName);

            //小於10秒從快取中取
            if( Duration.between(subsetConf.getLastUpdate() , Instant.now()).toMillis() < 1000 ){
                return subsetConf;
            }
        }
        // get config from registry
        Holder<SubsetConf> subsetConfHolder = new Holder<SubsetConf>(subsetConf);
        int ret = queryProxy.findSubsetConfigById(servantName, subsetConfHolder);
        SubsetConf newSubsetConf = subsetConfHolder.getValue();
        if( ret == TarsHelper.SERVERSUCCESS ){
            return newSubsetConf;
        }
        //從registry中獲取失敗時,更新subsetConf新增進快取
        subsetConf.setRuleType( newSubsetConf.getRuleType() );
        subsetConf.setLastUpdate( Instant.now() );
        cache.put(servantName, subsetConf);
        //解析subsetConf
        if( !newSubsetConf.isEnanle() ){
            subsetConf.setEnanle(false);
            return subsetConf;
        }
        if( "ratio".equals(newSubsetConf.getRuleType())){
            subsetConf.setRatioConf( newSubsetConf.getRatioConf() );
        } else {
            //按引數匹配
            KeyConfig newKeyConf = newSubsetConf.getKeyConf();
            List<KeyRoute> keyRoutes = newKeyConf.getRules();
            for ( KeyRoute kr: keyRoutes) {
                KeyConfig keyConf = new KeyConfig();
                //預設
                if("default".equals(kr.getAction())){
                    keyConf.setDefaultRoute(newKeyConf.getDefaultRoute());
                    subsetConf.setKeyConf(keyConf);
                }
                //精確匹配
                if("match".equals(kr.getAction())){
                    List<KeyRoute> rule = new ArrayList<>();
                    rule.add(new KeyRoute("match", kr.getValue() , kr.getRoute()));
                    keyConf.setRules( rule );
                }
                //正則匹配
                if("equal".equals(kr.getAction())){
                    List<KeyRoute> rule = new ArrayList<>();
                    rule.add(new KeyRoute("equal", kr.getValue() , kr.getRoute()));
                    keyConf.setRules( rule );
                }
            }
            subsetConf.setKeyConf(newKeyConf);
        }
        return subsetConf;
    }

    // 根據路由規則先獲取到比例 / 染色路由的配置,再通過配置獲取String的subset欄位
    public String getSubset(String servantName, String routeKey){
        //check subset config exists
        SubsetConf subsetConf = getSubsetConfig(servantName);
        if( subsetConf == null ){
            return null;
        }
        // route key to subset
        if("ratio".equals(subsetConf.getRuleType())){
            RatioConfig ratioConf = subsetConf.getRatioConf();
            if(ratioConf != null){
                return ratioConf.findSubet(routeKey);
            }
        }
        KeyConfig keyConf = subsetConf.getKeyConf();
        if ( keyConf != null ){
            return keyConf.findSubet(routeKey);
        }
        return null;
    }

    public SubsetManager() {
    }

    public SubsetManager(Map<String, SubsetConf> cache) {
        if(cache == null){
            this.cache = new HashMap<>();
        }
    }

    public Map<String, SubsetConf> getCache() {
        return cache;
    }

    public void setCache(Map<String, SubsetConf> cache) {
        this.cache = cache;
    }

}

Subset

public class Subset {

    private String hashString;

    private SubsetConf subsetConf;

    private KeyConfig keyConfig;
    private KeyRoute keyRoute;
    private RatioConfig ratioConfig;

    private SubsetManager subsetManager;


    //獲取到規則後的subset,與節點的subset比較,過濾不匹配節點
    public Holder<List<EndpointF>> subsetEndpointFilter(String servantName, String routeKey, Holder<List<EndpointF>> eps){

        if( subsetConf==null || !subsetConf.isEnanle() ){
            return eps;
        }

        if(eps.value == null || eps.value.isEmpty()){
            return eps;
        }

        //呼叫subsetManager,根據比例/匹配等規則獲取到路由規則的subset
        String subset = subsetManager.getSubset(servantName, routeKey);
        if( "".equals(subset) || subset == null){
            return eps;
        }
        //和每一個eps的subset比較,淘汰不符合要求的

        Holder<List<EndpointF>> epsFilter = new Holder<>(new ArrayList<EndpointF>());
        for (EndpointF ep : eps.value) {
            if( subset.equals(ep.getSubset())){
                epsFilter.getValue().add(ep);
            }
        }
        return epsFilter;
    }

    public Subset() {
    }

    public Subset(String hashString, SubsetConf subsetConf, KeyConfig keyConfig, KeyRoute keyRoute, RatioConfig ratioConfig) {
        this.hashString = hashString;
        this.subsetConf = subsetConf;
        this.keyConfig = keyConfig;
        this.keyRoute = keyRoute;
        this.ratioConfig = ratioConfig;
    }

    public String getHashString() {
        return hashString;
    }

    public void setHashString(String hashString) {
        this.hashString = hashString;
    }

    public SubsetConf getSubsetConf() {
        return subsetConf;
    }

    public void setSubsetConf(SubsetConf subsetConf) {
        this.subsetConf = subsetConf;
    }

    public KeyConfig getKeyConfig() {
        return keyConfig;
    }

    public void setKeyConfig(KeyConfig keyConfig) {
        this.keyConfig = keyConfig;
    }

    public KeyRoute getKeyRoute() {
        return keyRoute;
    }

    public void setKeyRoute(KeyRoute keyRoute) {
        this.keyRoute = keyRoute;
    }

    public RatioConfig getRatioConfig() {
        return ratioConfig;
    }

    public void setRatioConfig(RatioConfig ratioConfig) {
        this.ratioConfig = ratioConfig;
    }

    public SubsetManager getSubsetManager() {
        return subsetManager;
    }

    public void setSubsetManager(SubsetManager subsetManager) {
        this.subsetManager = subsetManager;
    }
}

2.3 變更程式碼的路徑

通過上述操作,新增了以下程式碼,需要在github上提交:

  • TarsJava-1.7.x\core\src\main\java\com\qq\tars\client\subset\SubsetConf.java
  • TarsJava-1.7.x\core\src\main\java\com\qq\tars\client\subset\KeyConfig.java
  • TarsJava-1.7.x\core\src\main\java\com\qq\tars\client\subset\KeyRoute.java
  • TarsJava-1.7.x\core\src\main\java\com\qq\tars\client\subset\RatioConfig.java
  • TarsJava-1.7.x\core\src\main\java\com\qq\tars\client\subset\Subset.java
  • TarsJava-1.7.x\core\src\main\java\com\qq\tars\client\subset\SubsetManager.java

3. 新增常量與獲取染色key的方法

3.1 Java原始碼位置及邏輯分析

該部分的含義是:新增常量新增獲取染色key的方法

在TarsJava中,染色相關的邏輯在DyeingKeyCacheDyeingSwitch類裡;但我們新增的TARS_ROUTE_KEY染色key與原染色邏輯相關性不大,這裡的TARS_ROUTE_KEY是隨著Tars的請求體TarsServantRequest裡的中獲取status引數(map型別)傳遞而來的;

  • Tars的請求體路徑:TarsJava-1.7.x\core\src\main\java\com\qq\tars\rpc\protocol\tars\TarsServantRequest.java

因此設定 / 獲取染色key的邏輯應該是:通過分散式上下文資訊DistributedContext獲取到TarsServantRequest請求體,再從請求體裡的statusmap資料設定 / 獲取染色key相關;

因此,我們可以得到以下資訊:

  • 定位對應原始碼位置如下:
Go語言 Java
tars/subset.go TarsJava-1.7.x\core\src\main\java\com\qq\tars\client\subset\KeyRoute.java

3.2 Java語言實現方式

跟《2.2 Java語言實現方式》中的KeyRoute一樣

3.3 變更程式碼的路徑

通過上述操作,改變了以下程式碼,需要在github上提交:

  • TarsJava-1.7.x\core\src\main\java\com\qq\tars\client\subset\KeyRoute.java

4.【核心】修改獲取服務IP規則

4.1 Java原始碼位置及邏輯分析

該部分的含義是:節點管理

在Go語言中,我們點進去tars/endpointmanager.go檢視原始碼發現該程式碼的作用是:建立一個結點管理器,通過管理器可以實現檢視節點狀態checkEpStatus()、更新節點資訊updateEndpoints()等功能。

修改的方法為SelectAdapterProxy()選擇介面卡代理,原邏輯為獲取服務端節點列表,新增邏輯為subsetEndpointFilter()為根據subset規則過濾節點;

而在Java語言中,類似功能在ObjectProxyFactory類裡,該類的功能主要是:建立代理物件,通過代理物件實現更新節點updateServantEndpoints()、建立服務代理配置項createServantProxyConfig()等功能。

其中在updateServantEndpoints()方法裡涉及到更新服務節點列表,但在Java中使用了一個QueryHelper查詢工具,裡面有個getServerNodes()方法獲取服務端節點列表,我們要修改的地方就在這裡。

因此,我們可以得到以下資訊:

  • 定位對應原始碼位置如下:
Go語言 Java語言
tars/endpointmanager.go TarsJava-1.7.x\core\src\main\java\com\qq\tars\support\query\QueryHelper.java
  • 增加的方法邏輯

由於在java裡節點的儲存是使用Holder<List<EndpointF>>物件而並不是LIst,因此對應引數型別改成Holder;

專案 說明
方法名 subsetEndpointFilter
實現邏輯 根據subset規則過濾節點
傳入引數 服務名String、染色狀態String、存活的節點Holder
返回引數 過濾後的節點Holder

這裡的染色邏輯

新新增的獲取染色key的方法與原來染色邏輯類似,可以參照相應實現邏輯;

在TarsGo裡,通過msg.Req.狀態[current.STATUS_ROUTE_KEY]獲取routeKey欄位;通過msg.Req.SServantName獲取服務名;

而在TarsJava裡,通過ServantProxyConfig.getSimpleObjectName()獲取服務名,獲取routeKey欄位則比較複雜;我們需要的最終染色欄位在Tars請求體TarsServantRequest裡的status引數(map型別);

獲取的邏輯是:通過分散式上下文資訊DistributedContext獲取到TarsServantRequest請求體,再從請求體裡的statusmap獲取染色key;

4.2 Java語言實現方式

public String getServerNodes(ServantProxyConfig config) {
    QueryFPrx queryProxy = getPrx();

    //【新增】通過KeyRoute類與分散式上下文資訊獲取routeKey
    String routeKey = getRouteKeyByContext();
    String name = config.getSimpleObjectName();

    //存活的節點
    Holder<List<EndpointF>> activeEp = new Holder<List<EndpointF>>(new ArrayList<EndpointF>());
    //掛掉的節點
    Holder<List<EndpointF>> inactiveEp = new Holder<List<EndpointF>>(new ArrayList<EndpointF>());
    int ret = TarsHelper.SERVERSUCCESS;
    //判斷是否為啟用集
    if (config.isEnableSet()) {
        ret = queryProxy.findObjectByIdInSameSet(name, config.getSetDivision(), activeEp, inactiveEp);
    } else {
        ret = queryProxy.findObjectByIdInSameGroup(name, activeEp, inactiveEp);
    }

    if (ret != TarsHelper.SERVERSUCCESS) {
        return null;
    }
    Collections.sort(activeEp.getValue());
    
    //【新增】根據Subset規則過濾節點
    Holder<List<EndpointF>> activeEpFilter = subset.subsetEndpointFilter(name, routeKey, activeEp);
    
    //將獲取到的節點列表格式化為一個字串格式
    StringBuilder value = new StringBuilder();
    if (activeEpFilter.value != null && !activeEpFilter.value.isEmpty()) {
        for (EndpointF endpointF : activeEpFilter.value) {
            if (value.length() > 0) {
                value.append(":");
            }
            value.append(ParseTools.toFormatString(endpointF, true));
        }
    }
    //個格式化後的字串加上Tars的服務名
    if (value.length() < 1) {
        return null;
    }
    value.insert(0, Constants.TARS_AT);
    value.insert(0, name);
    return value.toString();
}

//【新增】根據分散式上下文資訊獲取RouteKey
public String getRouteKeyByContext(){
    KeyRoute routeKey = new KeyRoute();
    return KeyRoute.getRouteKey(DistributedContextManager.getDistributedContext())
}

4.3 變更程式碼的路徑

通過上述操作,改變了以下程式碼,需要在github上提交:

  • TarsJava-1.7.x\core\src\main\java\com\qq\tars\support\query\QueryHelper.java

5. 實現透傳染色Key功能(客戶端)

5.1 Java原始碼位置及邏輯分析

該部分的含義是:透傳染色Key

是指染色key和value放到tars請求結構體的status引數,需要透傳給下游。這裡討論客戶端。

在TarsGo裡,這部分程式碼位置在tars/servant.go,通過閱讀原始碼上下文,我們可以得知這個類主要圍繞ServantProxy服務代理器而工作的;透傳染色Key是在ServantProxyTars_invoke方法裡實現的,invoke方法一般是最終要執行的方法;

在TarsJava裡,對Tars_invoke類似的方法進行了層層封裝;通過之前分析的客戶端負載均衡原始碼分析可知,最終的執行方法在TarsInvoker類的doInvokeServant方法裡,而該方法又對非同步呼叫、同步呼叫、協程呼叫三種形式,這三個呼叫才是最終執行方法。

因此,我們可以得到以下資訊:

  • 定位對應原始碼位置如下:
Go語言 Java
tars/servant.go TarsJava-1.7.x\core\src\main\java\com\qq\tars\client\rpc\tars\TarsInvoker.java

5.2 Java語言實現方式

在KeyRoute類裡新增一個靜態方法setRouteKeyToRequest(),邏輯是通過分散式上下文資訊,判斷Tars請求體的status(map型別)是否存在TARS_ROUTE_KEY鍵值對,存在則設定到Tars的響應體透傳給下游,不存在則不處理;

之所以新增到KeyRoute類,是因為該方法需要在多處地方重用,如《6.2 Java語言實現方式》;

public static void KeyRoute.setRouteKeyToRequest(DistributedContext distributedContext, TarsServantRequest request){
    String routeKey = KeyRoute.getRouteKey(distributedContext);
    if( routeKey != null && !"".equals(routeKey)){
        if(request.getStatus() != null){
            request.getStatus().put(KeyRoute.TARS_ROUTE_KEY ,routeKey);
        } else {
            HashMap<String, String> status = new HashMap<>();
            status.put(KeyRoute.TARS_ROUTE_KEY ,routeKey);
            request.setStatus(status);
        }
    }
}

然後在同步呼叫方法invokeWithSync()、非同步呼叫方法invokeWithAsync()和協程呼叫方法invokeWithPromiseFuture()裡,呼叫上述方法即可。

呼叫setRouteKey方法(客戶端)

5.3 變更程式碼的路徑

通過上述操作,改變了以下程式碼,需要在github上提交:

  • TarsJava-1.7.x\core\src\main\java\com\qq\tars\client\rpc\tars\TarsInvoker.java
  • TarsJava-1.7.x\core\src\main\java\com\qq\tars\client\subset\KeyRoute.java

6. 實現透傳染色Key功能(服務端)

6.1 Java原始碼位置及邏輯分析

該部分的含義是:透傳染色Key

是指染色key和value放到tars請求結構體的status引數,需要透傳給下游。這裡討論服務端。

在TarsGo裡,這部分程式碼位置在tars/tarsprotocol.go,通過閱讀原始碼上下文,我們可以得知這個類主要圍繞TarsProtocolTars服務端協議而工作的;透傳染色Key是在TarsProtocolInvoke方法裡實現的,其主要功能是將request請求作為位元組陣列,呼叫dispather,然後以位元組陣列返回response響應;

在TarsJava中,Tars服務處理器為TarsServantProcessor,其中的process()方法邏輯是處理request請求到response響應轉換;

因此,我們可以得到以下資訊:

  • 定位對應原始碼位置如下:
Go語言 Java
tars/tarsprotocol.go TarsJava-1.7.x\core\src\main\java\com\qq\tars\server\core\TarsServantProcessor.java

6.2 Java語言實現方式

直接呼叫setRouteKeyToRequest()方法即可;

呼叫setRouteKey方法(服務端)

6.3 變更程式碼的路徑

通過上述操作,改變了以下程式碼,需要在github上提交:

  • TarsJava-1.7.x\core\src\main\java\com\qq\tars\server\core\TarsServantProcessor.java

7. 給節點資訊增添Subset欄位

7.1 Java原始碼位置及邏輯分析

該部分的含義是:增添Subset欄位

在TarsGo中,這部分程式碼位置在endpoint.go,比較簡單,增加了一個String型別的Subset欄位屬性;

在TarsJava中,endpoint的原始碼位置很容易找到,直接修改即可;主要修改兩處,增加一個subset欄位以及修改解析方法;

因此,我們可以得到以下資訊:

  • 定位對應原始碼位置如下:
Go語言 Java
tars/util/endpoint/endpoint.go和tars/util/endpoint/convert.go TarsJava-1.7.x\core\src\main\java\com\qq\tars\common\support\Endpoint.java

7.2 Java語言實現方式

public class Endpoint {

    private final String type;
    private final String host; 
    private final int port; 

    private final int timeout;
    private final int grid; 
    private final int qos; 
    private final String setDivision;
    //新增
    private String subset;
    ……
}

7.3 變更程式碼的路徑

通過上述操作,改變了以下程式碼,需要在github上提交:

  • TarsJava-1.7.x\core\src\main\java\com\qq\tars\common\support\Endpoint.java

* 8. 正則演算法的實現

8.1 Java原始碼位置及邏輯分析

該部分的含義是:正則演算法匹配

因為在引數匹配裡要求正則匹配,因此在String工具類裡新增一個演算法實現正則匹配;

8.2 Java語言實現方式

public static Boolean matches(String regex, String input){
    //非空校驗
    if(regex==null || "".equals(regex) || input == null){
        return false;
    }
    char[] chars = regex.toCharArray();
    boolean flage = true;
    if( chars[0] == '*'){
        //如果regex是*開頭,如:*d123等。從d往後匹配;
        if( regex.length() < 2){
            return true;
        }
        int i;
        flage = false;
        for (i = 0; i < input.length(); i++) {
            if( input.charAt(i) == regex.charAt(1)){
                flage = true;
                for (int j = 1; j < regex.length(); j++) {

                    if( i > input.length() -1 && regex.charAt(j) != '*' ){
                        return false;
                    }

                    if( regex.charAt(j) == '*' || input.charAt(i) == regex.charAt(j)  ){
                        i++;
                    } else {
                        flage = false;
                    }


                }
            }
        }
    }else {
        if( chars[chars.length-1] == '*'){
            //如果regex是*結尾,如uid12*。從第一個字元開始匹配
            for (int i = 0; i < Math.min(regex.length(), input.length()); i++) {
                if(regex.charAt(i) == input.charAt(i) || regex.charAt(i) == '*'){
                    if( i == Math.min(regex.length(), input.length()) -1 && regex.length() > input.length()+1 ){
                        flage = false;
                    }

                } else {
                    flage = false;
                }
            }
        } else {
            //如果沒有*,如uid123。
            flage = regex.equals(input);
        }
    }

    return flage;
}

8.3 變更程式碼的路徑

通過上述操作,改變了以下程式碼,需要在github上提交:

  • TarsJava-1.7.x\core\src\main\java\com\qq\tars\common\util\StringUtils.java

* 9. 新增測試程式碼

9.1 Java原始碼位置及邏輯分析

該部分的含義是:主要流量路由規則測試

測試中包含按比例路由單次測試、按比例路由多次測試、按引數精確路由測試、按引數路由正則測試,以及registry測試;

由於其他同學部分的相關registry介面功能還未完成,故registry測試會失敗。

9.2 Java語言實現方式

public class TestSubset {

    //建立Subset過濾器
    Subset subsetFilter = new Subset();

    //模擬objectName
    String objectName = "objectName";

    //模擬routeKey
    String routeKey = "routeKey";

    //存活節點list列表
    List<EndpointF> endpointFList = new ArrayList<EndpointF>();
    Holder<List<EndpointF>> activeEp = new Holder<List<EndpointF>>(new ArrayList<EndpointF>());

    //定義一個Session域,用來構建Tars請求體
    Session session;


    /**
     * 按比例路由規則 - 單次測試
     * 沒有測試registry獲取subsetConf功能
     */
    @Test
    public void testRatioOnce() {

        //1. 給過濾器設定過濾規則
        //1.1 建立SubsetManager管理器
        SubsetManager subsetManager = new SubsetManager();


        //1.1 設定比例路由規則
        RatioConfig ratioConf = new RatioConfig();
        Map<String , Integer> map = new HashMap<>();
        map.put("v1",20);
        map.put("v2",80);
        //map.put("v3",20);
        ratioConf.setRules(map);

        //1.2 設定subsetConf,並加入快取
        SubsetConf subsetConf = new SubsetConf();
        subsetConf.setEnanle(true);
        subsetConf.setRuleType("ratio");
        subsetConf.setRatioConf(ratioConf);
        subsetConf.setLastUpdate( Instant.now() );

        Map<String, SubsetConf> cache = new HashMap<>();
        cache.put(objectName,subsetConf);
        subsetManager.setCache(cache);

        //1.3 給過濾器設定過濾規則和管理者
        subsetFilter.setSubsetConf(subsetConf);
        subsetFilter.setSubsetManager(subsetManager);


        //2. 模擬存活節點
        endpointFList.add(new EndpointF("host1",1,2,3,4,5,6,"setId1",7,8,9,10,"v1"));
        endpointFList.add(new EndpointF("host2",1,2,3,4,5,6,"setId2",7,8,9,10,"v1"));
        endpointFList.add(new EndpointF("host3",1,2,3,4,5,6,"setId3",7,8,9,10,"v2"));
        endpointFList.add(new EndpointF("host4",1,2,3,4,5,6,"setId4",7,8,9,10,"v2"));
        endpointFList.add(new EndpointF("host5",1,2,3,4,5,6,"setId5",7,8,9,10,"v2"));
        endpointFList.add(new EndpointF("host5",1,2,3,4,5,6,"setId5",7,8,9,10,"v3"));
        activeEp.setValue(endpointFList);


        //3. 輸出過濾前資訊
        System.out.println("過濾前節點資訊如下:");
        for( EndpointF endpoint : endpointFList){
            System.out.println(endpoint.toString());
        }

        //4. 對存活節點按subset規則過濾
        Holder<List<EndpointF>> filterActiveEp = subsetFilter.subsetEndpointFilter(objectName, routeKey, activeEp);

        //5. 輸出過濾結果

        System.out.println("過濾後節點資訊如下:");
        for( EndpointF endpoint : filterActiveEp.getValue() ){
            System.out.println(endpoint.toString());
        }
    }


    /**
     * 按比例路由規則 - 多次測試
     * 沒有測試registry獲取subsetConf功能
     */
    @Test
    public void testRatioTimes() {

        //1. 給過濾器設定過濾規則
        //1.1 建立SubsetManager管理器
        SubsetManager subsetManager = new SubsetManager();


        //1.1 設定比例路由規則
        RatioConfig ratioConf = new RatioConfig();
        Map<String , Integer> map = new HashMap<>();
        map.put("v1",20);
        map.put("v2",80);
        map.put("v3",20);
        ratioConf.setRules(map);

        //1.2 設定subsetConf,並加入快取
        SubsetConf subsetConf = new SubsetConf();
        subsetConf.setEnanle(true);
        subsetConf.setRuleType("ratio");
        subsetConf.setRatioConf(ratioConf);
        subsetConf.setLastUpdate( Instant.now() );

        Map<String, SubsetConf> cache = new HashMap<>();
        cache.put(objectName,subsetConf);
        subsetManager.setCache(cache);

        //1.3 給過濾器設定過濾規則和管理者
        subsetFilter.setSubsetConf(subsetConf);
        subsetFilter.setSubsetManager(subsetManager);


        //2. 模擬存活節點
        endpointFList.add(new EndpointF("host1",1,2,3,4,5,6,"setId1",7,8,9,10,"v1"));
        endpointFList.add(new EndpointF("host2",1,2,3,4,5,6,"setId2",7,8,9,10,"v1"));
        endpointFList.add(new EndpointF("host3",1,2,3,4,5,6,"setId3",7,8,9,10,"v2"));
        endpointFList.add(new EndpointF("host4",1,2,3,4,5,6,"setId4",7,8,9,10,"v2"));
        endpointFList.add(new EndpointF("host5",1,2,3,4,5,6,"setId5",7,8,9,10,"v2"));
        endpointFList.add(new EndpointF("host5",1,2,3,4,5,6,"setId5",7,8,9,10,"v3"));
        activeEp.setValue(endpointFList);


        //3. 迴圈times次
        int times = 1000000;
        int v1Times = 0;
        int v2Times = 0;
        int v3Times = 0;
        int errTimes = 0;
        for (int i = 0; i < times; i++) {
            //對存活節點按subset規則過濾
            Holder<List<EndpointF>> filterActiveEp = subsetFilter.subsetEndpointFilter(objectName, routeKey, activeEp);
            String subsetValue = filterActiveEp.getValue().get(0).getSubset();
            if("v1".equals(subsetValue)){
                v1Times++;
            } else if("v2".equals(subsetValue)){
                v2Times++;
            } else if("v3".equals(subsetValue)){
                v3Times++;
            } else {
                errTimes++;
            }

        }
        //輸出結果
        System.out.println("一共迴圈次數:" + times);
        System.out.println("路由到v1次數:" + v1Times);
        System.out.println("路由到v2次數:" + v2Times);
        System.out.println("路由到v3次數:" + v3Times);
        System.out.println("路由異常次數:" + errTimes);
    }


    /**
     * 測試引數匹配 - 精確匹配
     * 沒有測試registry獲取subsetConf功能
     * 注意要成功必須routeKey和match匹配上
     */
    @Test
    public void testMatch() {

        //1. 給過濾器設定過濾規則
        //1.1 建立SubsetManager管理器
        SubsetManager subsetManager = new SubsetManager();


        //1.1 設定引數路由規則,這裡的KeyRoute的value為 “規則的染色key”
        KeyConfig keyConf = new KeyConfig();
        List<KeyRoute> krs = new LinkedList<>();
        krs.add(new KeyRoute("match","routeKey","v1"));
        keyConf.setRules(krs);

        //1.2 設定subsetConf,並加入快取
        SubsetConf subsetConf = new SubsetConf();
        subsetConf.setEnanle(true);
        subsetConf.setRuleType("key");
        subsetConf.setKeyConf(keyConf);
        subsetConf.setLastUpdate( Instant.now() );

        Map<String, SubsetConf> cache = new HashMap<>();
        cache.put(objectName,subsetConf);
        subsetManager.setCache(cache);

        //1.3 給過濾器設定過濾規則和管理者
        subsetFilter.setSubsetConf(subsetConf);
        subsetFilter.setSubsetManager(subsetManager);

        //1.4 模擬Tars “請求的染色key” TARS_ROUTE_KEY,但請求染色key和規則染色key匹配時,才能精確路由
        //1.4.1 建立Tars的請求體TarsServantRequest
        TarsServantRequest request = new TarsServantRequest( session );
        //1.4.2 往請求體的status新增{TARS_ROUTE_KEY, "routeKey"}鍵值對
        Map<String, String> status = new HashMap<>();
        status.put("TARS_ROUTE_KEY", "routeKey");
        request.setStatus(status);
        //1.4.3 構建分散式上下文資訊,將請求放入分散式上下文資訊中,因為getSubset()的邏輯是從分散式上下文資訊中取
        DistributedContext distributedContext = new DistributedContextImpl();
        distributedContext.put(DyeingSwitch.REQ,request);

        //2. 模擬存活節點
        endpointFList.add(new EndpointF("host1",1,2,3,4,5,6,"setId1",7,8,9,10,"v1"));
        endpointFList.add(new EndpointF("host2",1,2,3,4,5,6,"setId2",7,8,9,10,"v1"));
        endpointFList.add(new EndpointF("host3",1,2,3,4,5,6,"setId3",7,8,9,10,"v2"));
        endpointFList.add(new EndpointF("host4",1,2,3,4,5,6,"setId4",7,8,9,10,"v2"));
        endpointFList.add(new EndpointF("host5",1,2,3,4,5,6,"setId5",7,8,9,10,"v2"));
        endpointFList.add(new EndpointF("host5",1,2,3,4,5,6,"setId5",7,8,9,10,"v3"));
        activeEp.setValue(endpointFList);


        //3. 輸出過濾前資訊
        System.out.println("過濾前節點資訊如下:");
        for( EndpointF endpoint : endpointFList){
            System.out.println(endpoint.toString());
        }

        //4. 對存活節點按subset規則過濾
        Holder<List<EndpointF>> filterActiveEp = subsetFilter.subsetEndpointFilter(objectName, routeKey, activeEp);

        //5. 輸出過濾結果

        System.out.println("過濾後節點資訊如下:");
        for( EndpointF endpoint : filterActiveEp.getValue() ){
            System.out.println(endpoint.toString());
        }
    }


    /**
     * 測試引數匹配 - 正則匹配
     * 沒有測試registry獲取subsetConf功能
     * 注意要成功必須routeKey和match匹配上
     */
    @Test
    public void testEqual() {

        //1. 給過濾器設定過濾規則
        //1.1 建立SubsetManager管理器

9.3 變更程式碼的路徑

通過上述操作,改變了以下程式碼,需要在github上提交:

  • TarsJava-1.7.x\core\src\test\java\com\qq\tars\client\subset\TestSubset.java

最後

新人制作,如有錯誤,歡迎指出,感激不盡!
歡迎關注公眾號,會分享一些更日常的東西!
如需轉載,請標註出處!
Tars | 第6篇 基於TarsGo Subset路由規則的Java JDK實現方式(下)

相關文章