SpringCloud中的灰度路由使用詳解
1 灰度路由的簡(jiǎn)介
在微服務(wù)中, 通常為了高可用, 同一個(gè)服務(wù)往往采用集群方式部署, 即同時(shí)存在幾個(gè)相同的服務(wù),而灰度的核心就 是路由, 通過(guò)我們特定的策略去調(diào)用目標(biāo)服務(wù)線(xiàn)路
灰度發(fā)布(又名金絲雀發(fā)布)是指在黑與白之間,能夠平滑過(guò)渡的一種發(fā)布方式。
在其上可以進(jìn)行A/B testing,即讓一部分用戶(hù)繼續(xù)用產(chǎn)品特性A,一部分用戶(hù)開(kāi)始用產(chǎn)品特性B,如果用戶(hù)對(duì)B沒(méi)有什么反對(duì)意見(jiàn),那么逐步擴(kuò)大范圍,把所有用戶(hù)都遷移到B上面來(lái)。
灰度發(fā)布可以保證整體系統(tǒng)的穩(wěn)定,在初始灰度的時(shí)候就可以發(fā)現(xiàn)、調(diào)整問(wèn)題,以保證其影響度.
關(guān)于SpringCloud微服務(wù)+nacos的灰度發(fā)布實(shí)現(xiàn), 首先微服務(wù)中之間的調(diào)用通常使用Feign方式和Resttemplate方式(較少使用),因此 , 我們需要指定服務(wù)之間的調(diào)用, 首先要給各個(gè)服務(wù)添加唯一標(biāo)識(shí), 我們可是使用一些特殊的標(biāo)記, 如版本號(hào)version等, 其次,要干預(yù)微服務(wù)中Ribbon的默認(rèn)輪詢(xún)調(diào)用機(jī)制, 我們需要根據(jù)微服務(wù)的版本等不同, 來(lái)進(jìn)行調(diào)用, 最后, 在服務(wù)之間, 需要傳遞調(diào)用鏈路的信息, 我們可以在請(qǐng)求頭中,添加調(diào)用鏈路的信息.
整理思路為:
- 在請(qǐng)求頭中添加調(diào)用鏈路信息
- 微服務(wù)之間調(diào)用時(shí),使用feign攔截器,增強(qiáng)請(qǐng)求頭
- 微服務(wù)調(diào)用選擇時(shí),根據(jù)指定的策略(如唯一標(biāo)識(shí)版本等)從nacos中獲取指定的服務(wù),調(diào)用
2 灰度路由的使用
基礎(chǔ)服務(wù)
一個(gè)父服務(wù),一個(gè)工具服務(wù)
父服務(wù)
pom依賴(lài)
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<!--spring cloud 版本-->
<spring-cloud.version>Greenwich.RELEASE</spring-cloud.version>
</properties>
<dependencies>
<!--nacos-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>0.2.2.RELEASE</version>
</dependency>
<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
<version>1.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
</dependency>
</dependencies>工具服務(wù)
feign攔截器
@Slf4j
public class FeignInterceptor implements RequestInterceptor {
/**
* feign接口攔截, 添加上灰度路由請(qǐng)求頭
* @param template
*/
@Override
public void apply(RequestTemplate template) {
String header = null;
try {
header = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
.getRequest().getHeader("gray-route");
if (null == header || header.isEmpty()) {
return;
}
} catch (Exception e) {
log.info("請(qǐng)求頭獲取失敗, 錯(cuò)誤信息為: {}", e.getMessage());
}
template.header("gray-route", header);
}
}灰度路由屬性類(lèi)
@Slf4j
public class FeignInterceptor implements RequestInterceptor {
/**
* feign接口攔截, 添加上灰度路由請(qǐng)求頭
* @param template
*/
@Override
public void apply(RequestTemplate template) {
String header = null;
try {
header = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
.getRequest().getHeader("gray-route");
if (null == header || header.isEmpty()) {
return;
}
} catch (Exception e) {
log.info("請(qǐng)求頭獲取失敗, 錯(cuò)誤信息為: {}", e.getMessage());
}
template.header("gray-route", header);
}
}路由屬性類(lèi)
@Data
@ConfigurationProperties(prefix = "spring.cloud.nacos.discovery.metadata.gray-route.route", ignoreUnknownFields = false)
@RefreshScope
public class RouteProp {
/**
* 本服務(wù)直接調(diào)用的所有服務(wù)的統(tǒng)一版本號(hào)
*/
private String all;
/**
* 指定調(diào)用服務(wù)的版本 serviceA:v1 表示在調(diào)用時(shí)只會(huì)調(diào)用v1版本服務(wù)
*/
private Map<String,String> custom;
}灰度路由規(guī)則類(lèi)(繼承ZoneAvoidanceRule類(lèi))
微服務(wù)在攔截處理后, Ribbon組件會(huì)從服務(wù)實(shí)例列表中獲取一個(gè)實(shí)現(xiàn)進(jìn)行轉(zhuǎn)發(fā), 且Ribbon默認(rèn)的規(guī)則是ZoneAvoidanceRule類(lèi), 我們定義自己的規(guī)則, 只需要繼承該類(lèi),重寫(xiě)choose方法即可.
@Slf4j
public class GrayRouteRule extends ZoneAvoidanceRule {
@Autowired
protected GrayRouteProp grayRouteProperties;
/**
* 參考 {@link PredicateBasedRule#choose(Object)}
*
*/
@Override
public Server choose(Object key) {
// 根據(jù)灰度路由規(guī)則,過(guò)濾出符合規(guī)則的服務(wù) this.getServers()
// 再根據(jù)負(fù)載均衡策略,過(guò)濾掉不可用和性能差的服務(wù),然后在剩下的服務(wù)中進(jìn)行輪詢(xún) getPredicate().chooseRoundRobinAfterFiltering()
Optional<Server> server = getPredicate()
.chooseRoundRobinAfterFiltering(this.getServers(), key);
return server.isPresent() ? server.get() : null;
}
/**
* 灰度路由過(guò)濾服務(wù)實(shí)例
*
* 如果設(shè)置了期望版本, 則過(guò)濾出所有的期望版本 ,然后再走默認(rèn)的輪詢(xún) 如果沒(méi)有一個(gè)期望的版本實(shí)例,則不過(guò)濾,降級(jí)為原有的規(guī)則,進(jìn)行所有的服務(wù)輪詢(xún)。(灰度路由失效) 如果沒(méi)有設(shè)置期望版本
* 則不走灰度路由,按原有輪詢(xún)機(jī)制輪詢(xún)所有
*/
protected List<Server> getServers() {
// 獲取spring cloud默認(rèn)負(fù)載均衡器
ZoneAwareLoadBalancer lb = (ZoneAwareLoadBalancer) getLoadBalancer();
// 獲取本次請(qǐng)求生效的灰度路由規(guī)則
RouteProp routeRule = this.getGrayRoute();
// 獲取本次請(qǐng)求期望的服務(wù)版本號(hào)
String version = getDesiredVersion(routeRule, lb.getName());
// 獲取所有待選的服務(wù)
List<Server> allServers = lb.getAllServers();
if (CollectionUtils.isEmpty(allServers)) {
return new ArrayList<>();
}
// 如果沒(méi)有設(shè)置要訪問(wèn)的版本,則不過(guò)濾,返回所有,走原有默認(rèn)的輪詢(xún)機(jī)制
if (StringUtils.isEmpty(version)) {
return allServers;
}
// 開(kāi)始灰度規(guī)則匹配過(guò)濾
List<Server> filterServer = new ArrayList<>();
for (Server server : allServers) {
// 獲取服務(wù)實(shí)例在注冊(cè)中心上的元數(shù)據(jù)
Map<String, String> metadata = ((NacosServer) server).getMetadata();
// 如果注冊(cè)中心上服務(wù)的版本標(biāo)簽和期望訪問(wèn)的版本一致,則灰度路由匹配成功
if (null != metadata && version.equals(metadata.get(GrayRouteProp.VERSION_KEY))) {
filterServer.add(server);
}
}
// 如果沒(méi)有匹配到期望的版本實(shí)例服務(wù),為了保證服務(wù)可用性,讓灰度規(guī)則失效,走原有的輪詢(xún)所有可用服務(wù)的機(jī)制
if (CollectionUtils.isEmpty(filterServer)) {
log.warn(String.format("沒(méi)有找到版本version[%s]的服務(wù)[%s],灰度路由規(guī)則降級(jí)為原有的輪詢(xún)機(jī)制!", version,
lb.getName()));
filterServer = allServers;
}
return filterServer;
}
/**
* 獲取本次請(qǐng)求 期望的服務(wù)版本號(hào)
*
* @param routeRule 生效的配置規(guī)則
* @param appName 服務(wù)名
*/
protected String getDesiredVersion(RouteProp routeRule, String appName) {
// 取路由規(guī)則里指定要訪問(wèn)的微服務(wù)的版本號(hào)
String version = null;
if (routeRule != null) {
if (routeRule.getCustom() != null) {
// 優(yōu)先取custom里指定版本
version = routeRule.getCustom().get(appName);
} else {
// custom里沒(méi)有指定就找all里面設(shè)置的統(tǒng)一版本
version = routeRule.getAll();
}
}
return version;
}
/**
* 獲取設(shè)置的灰度路由規(guī)則
*/
protected RouteProp getGrayRoute() {
// 確定路由規(guī)則(請(qǐng)求頭優(yōu)先,yml配置其次)
RouteProp routeRule;
String route_header = null;
try {
route_header = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
.getRequest().getHeader(GrayRouteProp.GRAY_ROUTE);
} catch (Exception e) {
log.error("灰度路由從上下文獲取路由請(qǐng)求頭異常!");
}
if (!StringUtils.isEmpty(route_header)) {//header
routeRule = JSONObject.parseObject(route_header, RouteProp.class);
} else {
// yml配置
routeRule = grayRouteProperties.getRoute();
}
return routeRule;
}
}業(yè)務(wù)服務(wù)
一個(gè)client服務(wù);兩個(gè)consumer服務(wù),分版本v1和v2;兩個(gè)provider服務(wù),分版本v1和v2
client服務(wù)
Controller控制器
@RestController
@Slf4j
public class ACliController {
@Autowired
private ConsumerFeign consumerFeign;
@GetMapping("/client")
public String list() {
String info = "我是客戶(hù)端,8000 ";
log.info(info);
String result = consumerFeign.list();
return JSON.toJSONString(info + result);
}
}Feign接口
@FeignClient(value = "consumer-a")
public interface ConsumerFeign {
@ResponseBody
@GetMapping("/consumer")
String list();
}Application啟動(dòng)器
@SpringBootApplication
@EnableFeignClients({"com.cf.client.feign"})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}application.yml
server:
port: 8000
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 # 配置nacos 服務(wù)端地址
namespace: public
metadata:
# gray-route是灰度路由配置的開(kāi)始
gray-route:
enable: true
version: v1
application:
name: client-test # 服務(wù)名稱(chēng)pom依賴(lài)
<!--自定義commons工具包-->
<dependencies>
<dependency>
<groupId>com.cf</groupId>
<artifactId>commons</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>consumer1服務(wù)
Controller控制器
@RestController
@Slf4j
public class AConController {
@Autowired
private ProviderFeign providerFeign;
@GetMapping("/consumer")
public String list() {
String info = "我是consumerA,8081 ";
log.info(info);
String result = providerFeign.list();
return JSON.toJSONString(info + result);
}
}Feign接口
@FeignClient(value = "provider-a")
public interface ProviderFeign {
@ResponseBody
@GetMapping("/provider")
String list();
}Application啟動(dòng)類(lèi)
@EnableDiscoveryClient
@SpringBootApplication
@EnableFeignClients({"com.cf.consumer.feign"})
public class AConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(AConsumerApplication.class, args);
}
}application.yml
server:
port: 8081
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 # 配置nacos 服務(wù)端地址
namespace: public
metadata:
# gray-route是灰度路由配置的開(kāi)始
gray-route:
enable: true
version: v1
application:
name: consumer-a # 服務(wù)名稱(chēng)pom依賴(lài)
<dependencies>
<dependency>
<groupId>com.cf</groupId>
<artifactId>commons</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>consumer2服務(wù)
consumer2服務(wù)和consumer1服務(wù)一樣,只是灰度路由版本不一樣(同一個(gè)服務(wù)器時(shí),其端口也不一致)
application.yml
server:
port: 8082
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 # 配置nacos 服務(wù)端地址
namespace: public
metadata:
# gray-route是灰度路由配置的開(kāi)始
gray-route:
enable: true
version: v2
application:
name: consumer-a # 服務(wù)名稱(chēng)provider1服務(wù)
Controller控制器
@RestController
@Slf4j
public class AProController {
@GetMapping("/provider")
public String list() {
String info = "我是 providerA,9091 ";
log.info(info);
return JSON.toJSONString(info);
}
}Application啟動(dòng)類(lèi)
@EnableDiscoveryClient
@SpringBootApplication
public class AProviderApplication {
public static void main(String[] args) {
SpringApplication.run(AProviderApplication.class, args);
}
}application.yml
server:
port: 9091
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 # 配置nacos 服務(wù)端地址
namespace: public
metadata:
# gray-route是灰度路由配置的開(kāi)始
gray-route:
enable: true
version: v1
application:
name: provider-a # 服務(wù)名稱(chēng)provider2服務(wù)
provider2服務(wù)和provider1服務(wù)相比, 就是灰度路由版本不一致(同一個(gè)服務(wù)器時(shí),其端口也不一致)
application.yml
server:
port: 9091
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 # 配置nacos 服務(wù)端地址
namespace: public
metadata:
# gray-route是灰度路由配置的開(kāi)始
gray-route:
enable: true
version: v2
application:
name: provider-a # 服務(wù)名稱(chēng)驗(yàn)證測(cè)試
- 啟動(dòng)本地nacos服務(wù)
- 啟動(dòng)五個(gè)項(xiàng)目服務(wù)
- 此時(shí),在nacos中,存在服務(wù)列表中存在三個(gè), 分別是client-test服務(wù)(1個(gè)),provider-a服務(wù)(2個(gè)實(shí)例),consumer-a服務(wù)(2個(gè)實(shí)例)
- 使用postman進(jìn)行測(cè)試
1 不指定請(qǐng)求頭灰度路由
"我是客戶(hù)端,8000 \"我是consumerA,8081 \\\"我是 providerA,9091 \\\"\""
"我是客戶(hù)端,8000 \"我是consumerB,8082 \\\"我是 providerA,9091 \\\"\""
"我是客戶(hù)端,8000 \"我是consumerA,8081 \\\"我是 providerB,9092 \\\"\""
"我是客戶(hù)端,8000 \"我是consumerB,8082 \\\"我是 providerB,9092 \\\"\""
調(diào)用四次, 采用的是Ribbon中默認(rèn)的輪詢(xún)策略.
2 指定請(qǐng)求頭灰度路由
請(qǐng)求頭中設(shè)置 gray-route = {"all":"v1"}
"我是客戶(hù)端,8000 \"我是consumerA,8081 \\\"我是 providerA,9091 \\\"\""
"我是客戶(hù)端,8000 \"我是consumerA,8081 \\\"我是 providerA,9091 \\\"\""
"我是客戶(hù)端,8000 \"我是consumerA,8081 \\\"我是 providerA,9091 \\\"\""
"我是客戶(hù)端,8000 \"我是consumerA,8081 \\\"我是 providerA,9091 \\\"\""
四次測(cè)試結(jié)果, 每個(gè)服務(wù)都是v1版本, 灰度路由生效.
請(qǐng)求頭中設(shè)置 {custom":{"consumer-a":"v1","provider-a":"v1"}}
"我是客戶(hù)端,8000 \"我是consumerA,8081 \\\"我是 providerA,9091 \\\"\""
"我是客戶(hù)端,8000 \"我是consumerA,8081 \\\"我是 providerA,9091 \\\"\""
"我是客戶(hù)端,8000 \"我是consumerA,8081 \\\"我是 providerA,9091 \\\"\""
"我是客戶(hù)端,8000 \"我是consumerA,8081 \\\"我是 providerA,9091 \\\"\""
四次測(cè)試結(jié)果, 每個(gè)服務(wù)都是v1版本, 灰度路由生效.
請(qǐng)求頭中設(shè)置 {custom":{"consumer-a":"v1","provider-a":"v2"}}
"我是客戶(hù)端,8000 \"我是consumerA,8081 \\\"我是 providerB,9092 \\\"\""
"我是客戶(hù)端,8000 \"我是consumerA,8081 \\\"我是 providerB,9092 \\\"\""
"我是客戶(hù)端,8000 \"我是consumerA,8081 \\\"我是 providerB,9092 \\\"\""
"我是客戶(hù)端,8000 \"我是consumerA,8081 \\\"我是 providerB,9092 \\\"\""
四次測(cè)試結(jié)果, consumer服務(wù)都是v1版本, provider服務(wù)都是版本2,灰度路由生效.
請(qǐng)求頭中設(shè)置{custom":{"consumer-a":"v1"}}
"我是客戶(hù)端,8000 \"我是consumerA,8081 \\\"我是 providerA,9091 \\\"\""
"我是客戶(hù)端,8000 \"我是consumerA,8081 \\\"我是 providerB,9092 \\\"\""
"我是客戶(hù)端,8000 \"我是consumerA,8081 \\\"我是 providerA,9091 \\\"\""
"我是客戶(hù)端,8000 \"我是consumerA,8081 \\\"我是 providerB,9092 \\\"\""
四次測(cè)試結(jié)果, consumer服務(wù)都是v1版本, provider服務(wù)沒(méi)有指定,所以采用默認(rèn)輪詢(xún)機(jī)制,灰度路由生效
到此這篇關(guān)于SpringCloud中的灰度路由使用詳解的文章就介紹到這了,更多相關(guān)SpringCloud灰度路由內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Eclipse中改變默認(rèn)的workspace的方法及說(shuō)明詳解
eclipse中改變默然的workspace的方法有哪幾種呢?接下來(lái)腳本之家小編給大家介紹Eclipse中改變默認(rèn)的workspace的方法及說(shuō)明,對(duì)eclipse改變workspace相關(guān)知識(shí)感興趣的朋友一起學(xué)習(xí)吧2016-04-04
一篇文章帶你了解Java中ThreadPool線(xiàn)程池
線(xiàn)程池可以控制運(yùn)行的線(xiàn)程數(shù)量,本文就線(xiàn)程池做了詳細(xì)的介紹,需要了解的小伙伴可以參考一下2021-08-08
Java協(xié)議字節(jié)操作工具類(lèi)詳情
這篇文章主要介紹了Java協(xié)議字節(jié)操作工具類(lèi)詳情,文章圍繞主題展開(kāi)詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-09-09
Logback日志基礎(chǔ)及自定義配置代碼實(shí)例
這篇文章主要介紹了Logback日志基礎(chǔ)及自定義配置代碼實(shí)例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-09-09
java實(shí)現(xiàn)計(jì)算器加法小程序(圖形化界面)
這篇文章主要介紹了Java實(shí)現(xiàn)圖形化界面的計(jì)算器加法小程序,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-05-05
springboot斷言異常封裝與統(tǒng)一異常處理實(shí)現(xiàn)代碼
異常處理其實(shí)一直都是項(xiàng)目開(kāi)發(fā)中的大頭,但關(guān)注異常處理的人一直都特別少,下面這篇文章主要給大家介紹了關(guān)于springboot斷言異常封裝與統(tǒng)一異常處理的相關(guān)資料,需要的朋友可以參考下2023-01-01
18個(gè)Java8日期處理的實(shí)踐(太有用了)
這篇文章主要介紹了18個(gè)Java8日期處理的實(shí)踐(太有用了),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-01-01
SpringSecurity安全框架在SpringBoot框架中的使用詳解
在Spring?Boot框架中,Spring?Security是一個(gè)非常重要的組件,它可以幫助我們實(shí)現(xiàn)應(yīng)用程序的安全性,本文將詳細(xì)介紹Spring?Security在Spring?Boot框架中的使用,包括如何配置Spring?Security、如何實(shí)現(xiàn)身份驗(yàn)證和授權(quán)、如何防止攻擊等2023-06-06

