This is an automated email from the ASF dual-hosted git repository.
liubao pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/servicecomb-java-chassis.git
The following commit(s) were added to refs/heads/master by this push:
new eef99bf3a [#4132] router support set fallback route as downgrading
routing strategy (#4134)
eef99bf3a is described below
commit eef99bf3afff34ec71d0157733ab2c3a2bfebf2a
Author: Cheng YouLing <[email protected]>
AuthorDate: Fri Dec 15 09:07:45 2023 +0800
[#4132] router support set fallback route as downgrading routing strategy
(#4134)
---
.../consumer/src/main/resources/application.yml | 17 +++
.../apache/servicecomb/samples/HelloWorldIT.java | 47 ++++++
.../distribute/AbstractRouterDistributor.java | 89 +++++++----
.../servicecomb/router/model/PolicyRuleItem.java | 22 +++
.../servicecomb/router/model/ServiceInfoCache.java | 27 +++-
.../RouterDistributorFileWithFallbackTest.java | 162 +++++++++++++++++++++
governance/src/test/resources/application.yaml | 52 ++++++-
7 files changed, 381 insertions(+), 35 deletions(-)
diff --git a/demo/demo-cse-v1/consumer/src/main/resources/application.yml
b/demo/demo-cse-v1/consumer/src/main/resources/application.yml
index e56621ef6..3708895be 100644
--- a/demo/demo-cse-v1/consumer/src/main/resources/application.yml
+++ b/demo/demo-cse-v1/consumer/src/main/resources/application.yml
@@ -59,6 +59,23 @@ servicecomb:
- weight: 100
tags:
version: 0.0.1
+ - precedence: 3
+ match:
+ headers:
+ canary:
+ exact: fallback
+ route:
+ - weight: 100
+ tags:
+ version: 0.0.3
+ fallback:
+ - weight: 20
+ tags:
+ version: 0.0.1
+ - weight: 80
+ tags:
+ version: 0.0.2
+
router:
type: router
header: canary
diff --git
a/demo/demo-cse-v1/test-client/src/main/java/org/apache/servicecomb/samples/HelloWorldIT.java
b/demo/demo-cse-v1/test-client/src/main/java/org/apache/servicecomb/samples/HelloWorldIT.java
index c49295d11..7657b5971 100644
---
a/demo/demo-cse-v1/test-client/src/main/java/org/apache/servicecomb/samples/HelloWorldIT.java
+++
b/demo/demo-cse-v1/test-client/src/main/java/org/apache/servicecomb/samples/HelloWorldIT.java
@@ -34,6 +34,8 @@ public class HelloWorldIT implements CategorizedTestCase {
@Override
public void testRestTransport() throws Exception {
+ testHelloWorldFallback();
+ testHelloWorldNoHeader();
testHelloWorld();
testHelloWorldCanary();
}
@@ -72,4 +74,49 @@ public class HelloWorldIT implements CategorizedTestCase {
double ratio = oldCount / (float) (oldCount + newCount);
TestMgr.check(ratio > 0.1 && ratio < 0.3, true);
}
+
+ private void testHelloWorldFallback() {
+ int oldCount = 0;
+ int newCount = 0;
+
+ for (int i = 0; i < 20; i++) {
+ MultiValueMap<String, String> headers = new HttpHeaders();
+ headers.add("canary", "fallback");
+ HttpEntity<Object> entity = new HttpEntity<>(headers);
+ String result = template
+ .exchange(Config.GATEWAY_URL + "/sayHelloCanary?name=World",
HttpMethod.GET, entity, String.class).getBody();
+ if (result.equals("\"Hello Canary World\"")) {
+ oldCount++;
+ } else if (result.equals("\"Hello Canary in canary World\"")) {
+ newCount++;
+ } else {
+ TestMgr.fail("not expected result testHelloWorldCanary");
+ return;
+ }
+ }
+
+ double ratio = oldCount / (float) (oldCount + newCount);
+ TestMgr.check(ratio > 0.1 && ratio < 0.3, true);
+ }
+
+ private void testHelloWorldNoHeader() {
+ int oldCount = 0;
+ int newCount = 0;
+
+ for (int i = 0; i < 20; i++) {
+ String result = template
+ .getForObject(Config.GATEWAY_URL + "/sayHelloCanary?name=World",
String.class);
+ if (result.equals("\"Hello Canary World\"")) {
+ oldCount++;
+ } else if (result.equals("\"Hello Canary in canary World\"")) {
+ newCount++;
+ } else {
+ TestMgr.fail("not expected result testHelloWorldCanary");
+ return;
+ }
+ }
+
+ double ratio = oldCount / (float) (oldCount + newCount);
+ TestMgr.check(ratio == 0.5, true);
+ }
}
diff --git
a/governance/src/main/java/org/apache/servicecomb/router/distribute/AbstractRouterDistributor.java
b/governance/src/main/java/org/apache/servicecomb/router/distribute/AbstractRouterDistributor.java
index 731d05d97..8e5b1af1f 100644
---
a/governance/src/main/java/org/apache/servicecomb/router/distribute/AbstractRouterDistributor.java
+++
b/governance/src/main/java/org/apache/servicecomb/router/distribute/AbstractRouterDistributor.java
@@ -52,22 +52,30 @@ public abstract class AbstractRouterDistributor<INSTANCE>
implements
protected AbstractRouterDistributor() {
}
+ /**
+ * distribute logic:
+ * 1、First according to the set route rules to choose target instances, if
have just return.
+ * 2、if route rules not match instance, check if fallback rules are set, if
set and match instances then return.
+ * 3、if route and fallback routes all have not match instance, then if route
rules weight count less 100, return
+ * unset instances, otherwise return all instances.
+ * @param targetServiceName
+ * @param list
+ * @param invokeRule
+ * @return
+ */
@Override
public List<INSTANCE> distribute(String targetServiceName, List<INSTANCE>
list, PolicyRuleItem invokeRule) {
-
invokeRule.check();
// unSetTags instance list
List<INSTANCE> unSetTagInstances = new ArrayList<>();
- // get tag list
- Map<TagItem, List<INSTANCE>> versionServerMap =
getDistributList(targetServiceName, list, invokeRule, unSetTagInstances);
+ // record fallback router targItem instance
+ Map<TagItem, List<INSTANCE>> fallbackVersionServerMap = new HashMap<>();
- if (CollectionUtils.isEmpty(versionServerMap)) {
- LOGGER.debug("route management can not match any rule and route the
latest version");
- // rule note matched instance babel, all instance return, select
instance for load balancing later
- return list;
- }
+ // get tag instance map, fallbackVersionServerMap, unSetTagInstances
+ Map<TagItem, List<INSTANCE>> versionServerMap =
getDistributList(targetServiceName, list, invokeRule,
+ unSetTagInstances, fallbackVersionServerMap);
// weight calculation to obtain the next tags instance
TagItem targetTag = getFiltedServerTagItem(invokeRule, targetServiceName);
@@ -75,8 +83,16 @@ public abstract class AbstractRouterDistributor<INSTANCE>
implements
return versionServerMap.get(targetTag);
}
- // has weightLess situation
- if (invokeRule.isWeightLess() && unSetTagInstances.size() > 0) {
+ if (!fallbackVersionServerMap.isEmpty()) {
+ // weight calculation to obtain the next fallback tags instance
+ TagItem fallbackTargetTag = getFallbackFiltedServerTagItem(invokeRule,
targetServiceName);
+ if (fallbackTargetTag != null &&
fallbackVersionServerMap.containsKey(fallbackTargetTag)) {
+ return fallbackVersionServerMap.get(fallbackTargetTag);
+ }
+ }
+
+ // has weightLess situation and unSetTagInstances has values
+ if (invokeRule.isWeightLess() && !unSetTagInstances.isEmpty()) {
return unSetTagInstances;
}
return list;
@@ -96,33 +112,26 @@ public abstract class AbstractRouterDistributor<INSTANCE>
implements
.getNextInvokeVersion(rule);
}
+ public TagItem getFallbackFiltedServerTagItem(PolicyRuleItem rule, String
targetServiceName) {
+ return routerRuleCache.getServiceInfoCacheMap().get(targetServiceName)
+ .getFallbackNextInvokeVersion(rule);
+ }
+
/**
- * 1.filter targetService
+ * 1.filter set route rules targetService, build fallback targetService map
and unSetTagInstances list.
* 2.establish map is a more complicate way than direct traversal, because
of multiple matches.
*
* the method getProperties() contains other field that we don't need.
*/
- private Map<TagItem, List<INSTANCE>> getDistributList(String serviceName,
- List<INSTANCE> list, PolicyRuleItem invokeRule, List<INSTANCE>
unSetTagInstances) {
+ private Map<TagItem, List<INSTANCE>> getDistributList(String serviceName,
List<INSTANCE> list,
+ PolicyRuleItem invokeRule, List<INSTANCE> unSetTagInstances,
Map<TagItem, List<INSTANCE>> fallbackVersionMap) {
Map<TagItem, List<INSTANCE>> versionServerMap = new HashMap<>();
for (INSTANCE instance : list) {
//get server
if (getServerName.apply(instance).equals(serviceName)) {
- //most matching
TagItem tagItem = new TagItem(getVersion.apply(instance),
getProperties.apply(instance));
- TagItem targetTag = null;
- int maxMatch = 0;
- // obtain the rule with the most parameter matches
- for (RouteItem entry : invokeRule.getRoute()) {
- if (entry.getTagitem() == null){
- continue;
- }
- int nowMatch = entry.getTagitem().matchNum(tagItem);
- if (nowMatch > maxMatch) {
- maxMatch = nowMatch;
- targetTag = entry.getTagitem();
- }
- }
+ // route most matching TagItem
+ TagItem targetTag = buildTargetTag(invokeRule.getRoute(), tagItem);
if (targetTag != null) {
if (!versionServerMap.containsKey(targetTag)) {
versionServerMap.put(targetTag, new ArrayList<>());
@@ -132,8 +141,34 @@ public abstract class AbstractRouterDistributor<INSTANCE>
implements
// not matched, placed in the unset tag instances collection
unSetTagInstances.add(instance);
}
+ // ensure the tags can build when set for both route and fallback at
the same time
+ if (!CollectionUtils.isEmpty(invokeRule.getFallback())) {
+ // fallback most matching TagItem
+ TagItem targetTagFallback = buildTargetTag(invokeRule.getFallback(),
tagItem);
+ if (!fallbackVersionMap.containsKey(targetTagFallback)) {
+ fallbackVersionMap.put(targetTagFallback, new ArrayList<>());
+ }
+ fallbackVersionMap.get(targetTagFallback).add(instance);
+ }
}
}
return versionServerMap;
}
+
+ private TagItem buildTargetTag(List<RouteItem> route, TagItem tagItem) {
+ int maxMatch = 0;
+ TagItem targetTag = null;
+ // obtain the rule with the most parameter matches
+ for (RouteItem entry : route) {
+ if (entry.getTagitem() == null){
+ continue;
+ }
+ int nowMatch = entry.getTagitem().matchNum(tagItem);
+ if (nowMatch > maxMatch) {
+ maxMatch = nowMatch;
+ targetTag = entry.getTagitem();
+ }
+ }
+ return targetTag;
+ }
}
diff --git
a/governance/src/main/java/org/apache/servicecomb/router/model/PolicyRuleItem.java
b/governance/src/main/java/org/apache/servicecomb/router/model/PolicyRuleItem.java
index 769b07bdc..11c95099f 100644
---
a/governance/src/main/java/org/apache/servicecomb/router/model/PolicyRuleItem.java
+++
b/governance/src/main/java/org/apache/servicecomb/router/model/PolicyRuleItem.java
@@ -42,6 +42,10 @@ public class PolicyRuleItem implements
Comparable<PolicyRuleItem> {
private boolean weightLess = false;
+ private List<RouteItem> fallback;
+
+ private Integer fallbackTotal;
+
public PolicyRuleItem() {
}
@@ -113,6 +117,22 @@ public class PolicyRuleItem implements
Comparable<PolicyRuleItem> {
this.weightLess = weightLess;
}
+ public List<RouteItem> getFallback() {
+ return fallback;
+ }
+
+ public void setFallback(List<RouteItem> fallback) {
+ this.fallback = fallback;
+ }
+
+ public Integer getFallbackTotal() {
+ return fallbackTotal;
+ }
+
+ public void setFallbackTotal(Integer fallbackTotal) {
+ this.fallbackTotal = fallbackTotal;
+ }
+
@Override
public String toString() {
return "PolicyRuleItem{" +
@@ -121,6 +141,8 @@ public class PolicyRuleItem implements
Comparable<PolicyRuleItem> {
", route=" + route +
", total=" + total +
", weightLess=" + weightLess +
+ ", fallback=" + fallback +
+ ", fallbackTotal=" + fallbackTotal +
'}';
}
}
diff --git
a/governance/src/main/java/org/apache/servicecomb/router/model/ServiceInfoCache.java
b/governance/src/main/java/org/apache/servicecomb/router/model/ServiceInfoCache.java
index 15cc08c94..676403206 100644
---
a/governance/src/main/java/org/apache/servicecomb/router/model/ServiceInfoCache.java
+++
b/governance/src/main/java/org/apache/servicecomb/router/model/ServiceInfoCache.java
@@ -19,6 +19,8 @@ package org.apache.servicecomb.router.model;
import java.util.List;
import java.util.stream.Collectors;
+import org.springframework.util.CollectionUtils;
+
/**
* @Author GuoYl123
* @Date 2019/10/17
@@ -34,9 +36,12 @@ public class ServiceInfoCache {
public ServiceInfoCache(List<PolicyRuleItem> policyRuleItemList) {
this.allrule =
policyRuleItemList.stream().sorted().collect(Collectors.toList());
- this.getAllrule().forEach(rule ->
- rule.getRoute().forEach(RouteItem::initTagItem)
- );
+ this.getAllrule().forEach(rule -> {
+ rule.getRoute().forEach(RouteItem::initTagItem);
+ if (!CollectionUtils.isEmpty(rule.getFallback())) {
+ rule.getFallback().forEach(RouteItem::initTagItem);
+ }
+ });
}
public TagItem getNextInvokeVersion(PolicyRuleItem policyRuleItem) {
@@ -44,7 +49,19 @@ public class ServiceInfoCache {
if (policyRuleItem.getTotal() == null) {
policyRuleItem.setTotal(rule.stream().mapToInt(RouteItem::getWeight).sum());
}
- rule.stream().forEach(RouteItem::addCurrentWeight);
+ return calculateWeight(rule, policyRuleItem.getTotal());
+ }
+
+ public TagItem getFallbackNextInvokeVersion(PolicyRuleItem policyRuleItem) {
+ List<RouteItem> rule = policyRuleItem.getFallback();
+ if (policyRuleItem.getFallbackTotal() == null) {
+
policyRuleItem.setFallbackTotal(rule.stream().mapToInt(RouteItem::getWeight).sum());
+ }
+ return calculateWeight(rule, policyRuleItem.getFallbackTotal());
+ }
+
+ private TagItem calculateWeight(List<RouteItem> rule, int total) {
+ rule.forEach(RouteItem::addCurrentWeight);
int maxIndex = 0, maxWeight = -1;
for (int i = 0; i < rule.size(); i++) {
if (maxWeight < rule.get(i).getCurrentWeight()) {
@@ -52,7 +69,7 @@ public class ServiceInfoCache {
maxWeight = rule.get(i).getCurrentWeight();
}
}
- rule.get(maxIndex).reduceCurrentWeight(policyRuleItem.getTotal());
+ rule.get(maxIndex).reduceCurrentWeight(total);
return rule.get(maxIndex).getTagitem();
}
diff --git
a/governance/src/test/java/org/apache/servicecomb/router/RouterDistributorFileWithFallbackTest.java
b/governance/src/test/java/org/apache/servicecomb/router/RouterDistributorFileWithFallbackTest.java
new file mode 100644
index 000000000..2d7a10645
--- /dev/null
+++
b/governance/src/test/java/org/apache/servicecomb/router/RouterDistributorFileWithFallbackTest.java
@@ -0,0 +1,162 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.servicecomb.router;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+import org.apache.servicecomb.governance.marker.GovernanceRequest;
+import org.apache.servicecomb.router.distribute.RouterDistributor;
+import org.junit.Test;
+import org.junit.jupiter.api.Assertions;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import
org.springframework.boot.test.context.ConfigDataApplicationContextInitializer;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringRunner;
+
+@RunWith(SpringRunner.class)
+@ContextConfiguration(locations = "classpath:META-INF/spring/*.xml",
initializers = ConfigDataApplicationContextInitializer.class)
+public class RouterDistributorFileWithFallbackTest {
+ private static final String TARGET_SERVICE_NAME_WITHOUT_FALLBACK =
"test_server3";
+
+ private static final String TARGET_SERVICE_NAME_WITH_FALLBACK =
"test_server4";
+
+ private static final String TARGET_SERVICE_NAME_ROUTE_FALLBACK =
"test_server5";
+
+ private RouterFilter routerFilter;
+
+ private RouterDistributor<ServiceIns> routerDistributor;
+
+ @Autowired
+ public void setRouterFilter(RouterFilter routerFilter) {
+ this.routerFilter = routerFilter;
+ }
+
+ @Autowired
+ public void setRouterDistributor(RouterDistributor<ServiceIns>
routerDistributor) {
+ this.routerDistributor = routerDistributor;
+ }
+
+ @Test
+ public void testDistributeWithoutFallback() {
+ List<ServiceIns> list =
initServiceList(TARGET_SERVICE_NAME_WITHOUT_FALLBACK);
+ HashMap<String, String> header = new HashMap<>();
+ header.put("canary", "canary");
+ GovernanceRequest governanceRequest = new GovernanceRequest();
+ governanceRequest.setHeaders(header);
+ List<ServiceIns> listOfServers = routerFilter
+ .getFilteredListOfServers(list, TARGET_SERVICE_NAME_WITHOUT_FALLBACK,
governanceRequest, routerDistributor);
+ Assertions.assertNotNull(listOfServers);
+ for (ServiceIns server : listOfServers) {
+ Assertions.assertEquals(TARGET_SERVICE_NAME_WITHOUT_FALLBACK,
server.getServerName());
+ }
+ int serverNum1 = 0;
+ int serverNum2 = 0;
+
+ for (int i = 0; i < 10; i++) {
+ List<ServiceIns> serverList = routerFilter
+ .getFilteredListOfServers(list,
TARGET_SERVICE_NAME_WITHOUT_FALLBACK, governanceRequest, routerDistributor);
+ for (ServiceIns serviceIns : serverList) {
+ if ("01".equals(serviceIns.getId())) {
+ serverNum1++;
+ } else if ("02".equals(serviceIns.getId())) {
+ serverNum2++;
+ }
+ }
+ }
+ Assertions.assertTrue(serverNum2 == serverNum1);
+ }
+
+ @Test
+ public void testDistributeWithFallback() {
+ List<ServiceIns> list = initServiceList(TARGET_SERVICE_NAME_WITH_FALLBACK);
+ HashMap<String, String> header = new HashMap<>();
+ header.put("canary", "canary");
+ GovernanceRequest governanceRequest = new GovernanceRequest();
+ governanceRequest.setHeaders(header);
+ List<ServiceIns> listOfServers = routerFilter
+ .getFilteredListOfServers(list, TARGET_SERVICE_NAME_WITH_FALLBACK,
governanceRequest, routerDistributor);
+ Assertions.assertNotNull(listOfServers);
+ for (ServiceIns server : listOfServers) {
+ Assertions.assertEquals(TARGET_SERVICE_NAME_WITH_FALLBACK,
server.getServerName());
+ }
+ int serverNum1 = 0;
+ int serverNum2 = 0;
+
+ for (int i = 0; i < 10; i++) {
+ List<ServiceIns> serverList = routerFilter
+ .getFilteredListOfServers(list, TARGET_SERVICE_NAME_WITH_FALLBACK,
governanceRequest, routerDistributor);
+ for (ServiceIns serviceIns : serverList) {
+ if ("01".equals(serviceIns.getId())) {
+ serverNum1++;
+ } else if ("02".equals(serviceIns.getId())) {
+ serverNum2++;
+ }
+ }
+ }
+ Assertions.assertTrue((serverNum2 + serverNum1) == serverNum1);
+ }
+
+ @Test
+ public void testDistributeRouteAndFallbackHaveSame() {
+ List<ServiceIns> list =
initServiceList(TARGET_SERVICE_NAME_ROUTE_FALLBACK);
+ HashMap<String, String> header = new HashMap<>();
+ header.put("canary", "canary");
+ GovernanceRequest governanceRequest = new GovernanceRequest();
+ governanceRequest.setHeaders(header);
+ List<ServiceIns> listOfServers = routerFilter
+ .getFilteredListOfServers(list, TARGET_SERVICE_NAME_ROUTE_FALLBACK,
governanceRequest, routerDistributor);
+ Assertions.assertNotNull(listOfServers);
+ for (ServiceIns server : listOfServers) {
+ Assertions.assertEquals(TARGET_SERVICE_NAME_ROUTE_FALLBACK,
server.getServerName());
+ }
+ int serverNum1 = 0;
+ int serverNum2 = 0;
+
+ for (int i = 0; i < 20; i++) {
+ List<ServiceIns> serverList = routerFilter
+ .getFilteredListOfServers(list, TARGET_SERVICE_NAME_ROUTE_FALLBACK,
governanceRequest, routerDistributor);
+ for (ServiceIns serviceIns : serverList) {
+ if ("01".equals(serviceIns.getId())) {
+ serverNum1++;
+ } else if ("02".equals(serviceIns.getId())) {
+ serverNum2++;
+ }
+ }
+ }
+ boolean flag = false;
+ if (Math.round(serverNum1 * 1.0 / serverNum2) == 3) {
+ flag = true;
+ }
+ Assertions.assertTrue(flag);
+ }
+
+ List<ServiceIns> initServiceList(String serviceName) {
+ ServiceIns serviceIns1 = new ServiceIns("01", serviceName);
+ ServiceIns serviceIns2 = new ServiceIns("02", serviceName);
+ serviceIns1.setVersion("1.0");
+ serviceIns2.setVersion("2.0");
+ serviceIns1.addTags("x-group", "red");
+ serviceIns2.addTags("x-group", "green");
+ List<ServiceIns> list = new ArrayList<>();
+ list.add(serviceIns1);
+ list.add(serviceIns2);
+ return list;
+ }
+}
diff --git a/governance/src/test/resources/application.yaml
b/governance/src/test/resources/application.yaml
index 96b614075..ad9c68599 100644
--- a/governance/src/test/resources/application.yaml
+++ b/governance/src/test/resources/application.yaml
@@ -223,10 +223,8 @@ servicecomb:
headers: # header 匹配
region: # 如果配置了多个 header,那么所有的 header
规则都必须和请求匹配
exact: 'providerRegion'
- caseInsensitive: false # 不区分大小写
type:
regex: gray_[a-z]+ # java 正则表达式匹配
- caseInsensitive: true # 区分大小写
route: # 路由规则
- weight: 20 # 权重值
tags:
@@ -249,4 +247,52 @@ servicecomb:
route:
- weight: 20
tags:
- x-group: red
\ No newline at end of file
+ x-group: red
+
+ test_server3: | # 服务名
+ - precedence: 2
+ match:
+ headers:
+ canary:
+ exact: 'canary'
+ route:
+ - weight: 20
+ tags:
+ version: 1.0.0
+ - weight: 80
+ tags:
+ version: 2.0.0
+ test_server4: | # 服务名
+ - precedence: 2
+ match:
+ headers:
+ canary:
+ exact: 'canary'
+ route:
+ - weight: 20
+ tags:
+ version: 1.0.0
+ - weight: 80
+ tags:
+ version: 2.0.0
+ fallback:
+ - weight: 100
+ tags:
+ x-group: red
+ test_server5: | # 服务名
+ - precedence: 2
+ match:
+ headers:
+ canary:
+ exact: 'canary'
+ route:
+ - weight: 50
+ tags:
+ x-group: red
+ fallback:
+ - weight: 50
+ tags:
+ x-group: red
+ - weight: 50
+ tags:
+ x-group: green
\ No newline at end of file