前言
利開園導師用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.tars
與QueryF.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];
};
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命令
接著輸入mvn tars:tars2java
命令後出現下面日誌則說明生成成功;
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中,染色相關的邏輯在DyeingKeyCache
和DyeingSwitch
類裡;但我們新增的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
請求體,再從請求體裡的status
map資料設定 / 獲取染色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
請求體,再從請求體裡的status
map獲取染色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是在ServantProxy
的 Tars_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()
裡,呼叫上述方法即可。
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,通過閱讀原始碼上下文,我們可以得知這個類主要圍繞TarsProtocol
Tars服務端協議而工作的;透傳染色Key是在TarsProtocol
的 Invoke
方法裡實現的,其主要功能是將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()
方法即可;
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