Spring Cloud + Nacos + 負(fù)載均衡器實(shí)現(xiàn)全鏈路灰度發(fā)布實(shí)戰(zhàn)
共 36822字,需瀏覽 74分鐘
·
2024-06-17 14:25
來源:https://blog.csdn.net/weixin_44606481
?? 歡迎加入小哈的星球 ,你將獲得: 專屬的項(xiàng)目實(shí)戰(zhàn) / Java 學(xué)習(xí)路線 / 一對(duì)一提問 / 學(xué)習(xí)打卡 / 每月贈(zèng)書
新項(xiàng)目:仿小紅書(微服務(wù)架構(gòu))正在更新中... 。全棧前后端分離博客項(xiàng)目 2.0 版本完結(jié)啦, 演示鏈接:http://116.62.199.48/ 。全程手摸手,后端 + 前端全棧開發(fā),從 0 到 1 講解每個(gè)功能點(diǎn)開發(fā)步驟,1v1 答疑,直到項(xiàng)目上線。目前已更新了287小節(jié),累計(jì)45w+字,講解圖:2008張,還在持續(xù)爆肝中.. 后續(xù)還會(huì)上新更多項(xiàng)目,目標(biāo)是將Java領(lǐng)域典型的項(xiàng)目都整一波,如秒殺系統(tǒng), 在線商城, IM即時(shí)通訊,Spring Cloud Alibaba 等等,戳我加入學(xué)習(xí),已有1600+小伙伴加入(早鳥價(jià)超低)
概念
灰度發(fā)布, 也叫金絲雀發(fā)布。是指在黑與白之間,能夠平滑過渡的一種發(fā)布方式。AB test就是一種灰度發(fā)布方式,讓一部分用戶繼續(xù)用A,一部分用戶開始用B,如果用戶對(duì)B沒有什么反對(duì)意見,那么逐步擴(kuò)大范圍,把所有用戶都遷移到B上面來。
灰度發(fā)布可以保證整體系統(tǒng)的穩(wěn)定,在初始灰度的時(shí)候就可以發(fā)現(xiàn)、調(diào)整問題,以保證其影響度,而我們平常所說的金絲雀部署也就是灰度發(fā)布的一種方式。
具體到服務(wù)器上,實(shí)際操作中還可以做更多控制,譬如說,給最初更新的10臺(tái)服務(wù)器設(shè)置較低的權(quán)重、控制發(fā)送給這10臺(tái)服務(wù)器的請(qǐng)求數(shù),然后逐漸提高權(quán)重、增加請(qǐng)求數(shù)。一種平滑過渡的思路, 這個(gè)控制叫做“流量切分”。
組件版本說明
我們這項(xiàng)目已經(jīng)練習(xí)了兩年半了使用的版本不是很新,我這里的Demo也會(huì)使用這個(gè)版本,有感情了,使用新版本的朋友自己調(diào)整一下就行,實(shí)現(xiàn)思路是一樣的只是這些框架源碼可能會(huì)有變化。
-
spring-boot: 2.3.12.RELEASE -
spring-cloud-dependencies: Hoxton.SR12 -
spring-cloud-alibaba-dependencies: 2.2.9.RELEASE
spring-cloud 對(duì)應(yīng)版本關(guān)系圖
?
https://blog.csdn.net/weixin_44606481/article/details/131726688
?
核心組件說明
-
注冊(cè)中心: Nacos -
網(wǎng)關(guān): SpringCloudGateway -
負(fù)載均衡器: Ribbon (使用SpringCloudLoadBalancer實(shí)現(xiàn)也是類似的) -
服務(wù)間RPC調(diào)用: OpenFeign
灰度發(fā)布代碼實(shí)現(xiàn)
要實(shí)現(xiàn)Spring Cloud項(xiàng)目灰度發(fā)布技術(shù)方案有很多,重點(diǎn)在于服務(wù)發(fā)現(xiàn),怎么將灰度流量只請(qǐng)求到灰度服務(wù),這里我們會(huì)使用Nacos作為注冊(cè)中心和配置中心,核心就是利用Nacos的Metadata設(shè)置一個(gè)version值,在調(diào)用下游服務(wù)是通過version值來區(qū)分要調(diào)用那個(gè)版本,這里會(huì)省略一些流程,文章末尾提供了源碼地址需要自提。
代碼設(shè)計(jì)結(jié)構(gòu)
這個(gè)是demo項(xiàng)目,結(jié)構(gòu)都按最簡(jiǎn)單的來。
spring-cloud-gray-example // 父工程
kerwin-common // 項(xiàng)目公共模塊
kerwin-gateway // 微服務(wù)網(wǎng)關(guān)
kerwin-order // 訂單模塊
order-app // 訂單業(yè)務(wù)服務(wù)
kerwin-starter // 自定義springboot starter模塊
spring-cloud-starter-kerwin-gray // 灰度發(fā)布starter包 (核心代碼都在這里)
kerwin-user // 用戶模塊
user-app // 用戶業(yè)務(wù)服務(wù)
user-client // 用戶client(Feign和DTO)
核心包spring-cloud-starter-kerwin-gray結(jié)構(gòu)介紹
入口Spring Cloud Gateway實(shí)現(xiàn)灰度發(fā)布設(shè)計(jì)(一些基礎(chǔ)信息類在下面)
在請(qǐng)求進(jìn)入網(wǎng)關(guān)時(shí)開始對(duì)是否要請(qǐng)求灰度版本進(jìn)行判斷,通過Spring Cloud Gateway的過濾器實(shí)現(xiàn),在調(diào)用下游服務(wù)時(shí)重寫一個(gè)Ribbon的負(fù)載均衡器實(shí)現(xiàn)調(diào)用時(shí)對(duì)灰度狀態(tài)進(jìn)行判斷。
存取請(qǐng)求灰度標(biāo)記Holder(業(yè)務(wù)服務(wù)也是使用的這個(gè))
使用ThreadLocal記錄每個(gè)請(qǐng)求線程的灰度標(biāo)記,會(huì)在前置過濾器中將標(biāo)記設(shè)置到ThreadLocal中。
public class GrayFlagRequestHolder {
/**
* 標(biāo)記是否使用灰度版本
* 具體描述請(qǐng)查看 {@link com.kerwin.gray.enums.GrayStatusEnum}
*/
private static final ThreadLocal<GrayStatusEnum> grayFlag = new ThreadLocal<>();
public static void setGrayTag(final GrayStatusEnum tag) {
grayFlag.set(tag);
}
public static GrayStatusEnum getGrayTag() {
return grayFlag.get();
}
public static void remove() {
grayFlag.remove();
}
}
前置過濾器
在前置過濾器中會(huì)對(duì)請(qǐng)求是否要使用灰度版本進(jìn)行判斷,并且會(huì)將灰度狀態(tài)枚舉GrayStatusEnum設(shè)置到GrayRequestContextHolder中存儲(chǔ)這一個(gè)請(qǐng)求的灰度狀態(tài)枚舉,在負(fù)載均衡器中會(huì)取出灰度狀態(tài)枚舉判斷要調(diào)用那個(gè)版本的服務(wù),同時(shí)這里還實(shí)現(xiàn)了Ordered 接口會(huì)對(duì)網(wǎng)關(guān)的過濾器進(jìn)行的排序,這里我們將這個(gè)過濾器的排序設(shè)置為Ordered.HIGHEST_PRECEDENCE int的最小值,保證這個(gè)過濾器最先執(zhí)行。
public class GrayGatewayBeginFilter implements GlobalFilter, Ordered {
@Autowired
private GrayGatewayProperties grayGatewayProperties;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
GrayStatusEnum grayStatusEnum = GrayStatusEnum.ALL;
// 當(dāng)灰度開關(guān)打開時(shí)才進(jìn)行請(qǐng)求頭判斷
if (grayGatewayProperties.getEnabled()) {
grayStatusEnum = GrayStatusEnum.PROD;
// 判斷是否需要調(diào)用灰度版本
if (checkGray(exchange.getRequest())) {
grayStatusEnum = GrayStatusEnum.GRAY;
}
}
GrayFlagRequestHolder.setGrayTag(grayStatusEnum);
ServerHttpRequest newRequest = exchange.getRequest().mutate()
.header(GrayConstant.GRAY_HEADER, grayStatusEnum.getVal())
.build();
ServerWebExchange newExchange = exchange.mutate()
.request(newRequest)
.build();
return chain.filter(newExchange);
}
/**
* 校驗(yàn)是否使用灰度版本
*/
private boolean checkGray(ServerHttpRequest request) {
if (checkGrayHeadKey(request) || checkGrayIPList(request) || checkGrayCiryList(request) || checkGrayUserNoList(request)) {
return true;
}
return false;
}
/**
* 校驗(yàn)自定義灰度版本請(qǐng)求頭判斷是否需要調(diào)用灰度版本
*/
private boolean checkGrayHeadKey(ServerHttpRequest request) {
HttpHeaders headers = request.getHeaders();
if (headers.containsKey(grayGatewayProperties.getGrayHeadKey())) {
List<String> grayValues = headers.get(grayGatewayProperties.getGrayHeadKey());
if (!Objects.isNull(grayValues)
&& grayValues.size() > 0
&& grayGatewayProperties.getGrayHeadValue().equals(grayValues.get(0))) {
return true;
}
}
return false;
}
/**
* 校驗(yàn)自定義灰度版本IP數(shù)組判斷是否需要調(diào)用灰度版本
*/
private boolean checkGrayIPList(ServerHttpRequest request) {
List<String> grayIPList = grayGatewayProperties.getGrayIPList();
if (CollectionUtils.isEmpty(grayIPList)) {
return false;
}
String realIP = request.getHeaders().getFirst("X-Real-IP");
if (realIP == null || realIP.isEmpty()) {
realIP = request.getRemoteAddress().getAddress().getHostAddress();
}
if (realIP != null && CollectionUtils.contains(grayIPList.iterator(), realIP)) {
return true;
}
return false;
}
/**
* 校驗(yàn)自定義灰度版本城市數(shù)組判斷是否需要調(diào)用灰度版本
*/
private boolean checkGrayCiryList(ServerHttpRequest request) {
List<String> grayCityList = grayGatewayProperties.getGrayCityList();
if (CollectionUtils.isEmpty(grayCityList)) {
return false;
}
String realIP = request.getHeaders().getFirst("X-Real-IP");
if (realIP == null || realIP.isEmpty()) {
realIP = request.getRemoteAddress().getAddress().getHostAddress();
}
// 通過IP獲取當(dāng)前城市名稱
// 這里篇幅比較長(zhǎng)不具體實(shí)現(xiàn)了,想要實(shí)現(xiàn)的可以使用ip2region.xdb,這里寫死cityName = "本地"
String cityName = "本地";
if (cityName != null && CollectionUtils.contains(grayCityList.iterator(), cityName)) {
return true;
}
return false;
}
/**
* 校驗(yàn)自定義灰度版本用戶編號(hào)數(shù)組(我們系統(tǒng)不會(huì)在網(wǎng)關(guān)獲取用戶編號(hào)這種方法如果需要可以自己實(shí)現(xiàn)一下)
*/
private boolean checkGrayUserNoList(ServerHttpRequest request) {
List<String> grayUserNoList = grayGatewayProperties.getGrayUserNoList();
if (CollectionUtils.isEmpty(grayUserNoList)) {
return false;
}
return false;
}
@Override
public int getOrder() {
// 設(shè)置過濾器的執(zhí)行順序,值越小越先執(zhí)行
return Ordered.HIGHEST_PRECEDENCE;
}
}
后置過濾器
后置過濾器是為了在調(diào)用完下游業(yè)務(wù)服務(wù)后在響應(yīng)之前將 GrayFlagRequestHolder 中的 ThreadLocal 清除避免照成內(nèi)存泄漏。
public class GrayGatewayAfterFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 請(qǐng)求執(zhí)行完必須要remore當(dāng)前線程的ThreadLocal
GrayFlagRequestHolder.remove();
return chain.filter(exchange);
}
@Override
public int getOrder() {
// 設(shè)置過濾器的執(zhí)行順序,值越小越先執(zhí)行
return Ordered.LOWEST_PRECEDENCE;
}
}
全局異常處理器
全局異常處理器是為了處理異常情況下將 GrayFlagRequestHolder 中的 ThreadLocal 清除避免照成內(nèi)存泄漏,如果在調(diào)用下游業(yè)務(wù)服務(wù)時(shí)出現(xiàn)了異常就無法進(jìn)入后置過濾器。
public class GrayGatewayExceptionHandler implements WebExceptionHandler, Ordered {
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
// 請(qǐng)求執(zhí)行完必須要remore當(dāng)前線程的ThreadLocal
GrayFlagRequestHolder.remove();
ServerHttpResponse response = exchange.getResponse();
if (ex instanceof ResponseStatusException) {
// 處理 ResponseStatusException 異常
ResponseStatusException responseStatusException = (ResponseStatusException) ex;
response.setStatusCode(responseStatusException.getStatus());
// 可以根據(jù)需要設(shè)置響應(yīng)頭等
return response.setComplete();
} else {
// 處理其他異常
response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
// 可以根據(jù)需要設(shè)置響應(yīng)頭等
return response.setComplete();
}
}
@Override
public int getOrder() {
// 設(shè)置過濾器的執(zhí)行順序,值越小越先執(zhí)行
return Ordered.HIGHEST_PRECEDENCE;
}
}
自定義Ribbon負(fù)載均衡路由(業(yè)務(wù)服務(wù)也是使用的這個(gè))
「灰度Ribbon負(fù)載均衡路由抽象類:」 這里提供了兩個(gè)獲取服務(wù)列表的方法,會(huì)對(duì)GrayFlagRequestHolder 中存儲(chǔ)的當(dāng)前線程灰度狀態(tài)枚舉進(jìn)行判斷,如果枚舉值為GrayStatusEnum.ALL則響應(yīng)全部服務(wù)列表不區(qū)分版本,如果枚舉值為GrayStatusEnum.PROD則返回生產(chǎn)版本的服務(wù)列表,如果枚舉值為GrayStatusEnum.GRAY則返回灰度版本的服務(wù)列表,版本號(hào)會(huì)在GrayVersionProperties 中配置,通過服務(wù)列表中在Nacos的metadata中設(shè)置的version和GrayVersionProperties的版本號(hào)進(jìn)行匹配出對(duì)應(yīng)版本的服務(wù)列表。
public abstract class AbstractGrayLoadBalancerRule extends AbstractLoadBalancerRule {
@Autowired
private GrayVersionProperties grayVersionProperties;
@Value("${spring.cloud.nacos.discovery.metadata.version}")
private String metaVersion;
/**
* 只有已啟動(dòng)且可訪問的服務(wù)器,并對(duì)灰度標(biāo)識(shí)進(jìn)行判斷
*/
public List<Server> getReachableServers() {
ILoadBalancer lb = getLoadBalancer();
if (lb == null) {
return new ArrayList<>();
}
List<Server> reachableServers = lb.getReachableServers();
return getGrayServers(reachableServers);
}
/**
* 所有已知的服務(wù)器,可訪問和不可訪問,并對(duì)灰度標(biāo)識(shí)進(jìn)行判斷
*/
public List<Server> getAllServers() {
ILoadBalancer lb = getLoadBalancer();
if (lb == null) {
return new ArrayList<>();
}
List<Server> allServers = lb.getAllServers();
return getGrayServers(allServers);
}
/**
* 獲取灰度版本服務(wù)列表
*/
protected List<Server> getGrayServers(List<Server> servers) {
List<Server> result = new ArrayList<>();
if (servers == null) {
return result;
}
String currentVersion = metaVersion;
GrayStatusEnum grayStatusEnum = GrayFlagRequestHolder.getGrayTag();
if (grayStatusEnum != null) {
switch (grayStatusEnum) {
case ALL:
return servers;
case PROD:
currentVersion = grayVersionProperties.getProdVersion();
break;
case GRAY:
currentVersion = grayVersionProperties.getGrayVersion();
break;
}
}
for (Server server : servers) {
NacosServer nacosServer = (NacosServer) server;
Map<String, String> metadata = nacosServer.getMetadata();
String version = metadata.get("version");
// 判斷服務(wù)metadata下的version是否于設(shè)置的請(qǐng)求版本一致
if (version != null && version.equals(currentVersion)) {
result.add(server);
}
}
return result;
}
}
「自定義輪詢算法實(shí)現(xiàn)GrayRoundRobinRule:」 代碼篇幅太長(zhǎng)了這里只截取代碼片段,我這里是直接拷貝了Ribbon的輪詢算法,將里面獲取服務(wù)列表的方法換成了自定義AbstractGrayLoadBalancerRule 中的方法,其它算法也可以通過類似的方式實(shí)現(xiàn)。
業(yè)務(wù)服務(wù)實(shí)現(xiàn)灰度發(fā)布設(shè)計(jì)
自定義SpringMVC請(qǐng)求攔截器
自定義SpringMVC請(qǐng)求攔截器獲取上游服務(wù)的灰度請(qǐng)求頭,如果獲取到則設(shè)置到GrayFlagRequestHolder 中,之后如果有后續(xù)的RPC調(diào)用同樣的將灰度標(biāo)記傳遞下去。
@SuppressWarnings("all")
public class GrayMvcHandlerInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String grayTag = request.getHeader(GrayConstant.GRAY_HEADER);
// 如果HttpHeader中灰度標(biāo)記存在,則將灰度標(biāo)記放到holder中,如果需要就傳遞下去
if (grayTag!= null) {
GrayFlagRequestHolder.setGrayTag(GrayStatusEnum.getByVal(grayTag));
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
GrayFlagRequestHolder.remove();
}
}
自定義OpenFeign請(qǐng)求攔截器
自定義OpenFeign請(qǐng)求攔截器,取出自定義SpringMVC請(qǐng)求攔截器中設(shè)置到GrayFlagRequestHolder中的灰度標(biāo)識(shí),并且放到調(diào)用下游服務(wù)的請(qǐng)求頭中,將灰度標(biāo)記傳遞下去。
public class GrayFeignRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
// 如果灰度標(biāo)記存在,將灰度標(biāo)記通過HttpHeader傳遞下去
GrayStatusEnum grayStatusEnum = GrayFlagRequestHolder.getGrayTag();
if (grayStatusEnum != null ) {
template.header(GrayConstant.GRAY_HEADER, Collections.singleton(grayStatusEnum.getVal()));
}
}
}
基礎(chǔ)信息設(shè)計(jì)
這里會(huì)定義一些基礎(chǔ)參數(shù),比如是否開啟灰度還有什么請(qǐng)求需要使用灰度版本等,為后續(xù)業(yè)務(wù)做準(zhǔn)備。
-
調(diào)用業(yè)務(wù)服務(wù)時(shí)設(shè)置的灰度統(tǒng)一請(qǐng)求頭
public interface GrayConstant {
/**
* 灰度統(tǒng)一請(qǐng)求頭
*/
String GRAY_HEADER="gray";
}
-
灰度版本狀態(tài)枚舉
public enum GrayStatusEnum {
ALL("ALL","可以調(diào)用全部版本的服務(wù)"),
PROD("PROD","只能調(diào)用生產(chǎn)版本的服務(wù)"),
GRAY("GRAY","只能調(diào)用灰度版本的服務(wù)");
GrayStatusEnum(String val, String desc) {
this.val = val;
this.desc = desc;
}
private String val;
private String desc;
public String getVal() {
return val;
}
public static GrayStatusEnum getByVal(String val){
if(val == null){
return null;
}
for (GrayStatusEnum value : values()) {
if(value.val.equals(val)){
return value;
}
}
return null;
}
}
-
網(wǎng)關(guān)灰度配置信息類
@Data
@Configuration
@RefreshScope
@ConfigurationProperties("kerwin.tool.gray.gateway")
public class GrayGatewayProperties {
/**
* 灰度開關(guān)(如果開啟灰度開關(guān)則進(jìn)行灰度邏輯處理,如果關(guān)閉則走正常處理邏輯)
* PS:一般在灰度發(fā)布測(cè)試完成以后會(huì)將線上版本都切換成灰度版本完成全部升級(jí),這時(shí)候應(yīng)該關(guān)閉灰度邏輯判斷
*/
private Boolean enabled = false;
/**
* 自定義灰度版本請(qǐng)求頭 (通過grayHeadValue來匹配請(qǐng)求頭中的值如果一致就去調(diào)用灰度版本,用于公司測(cè)試)
*/
private String grayHeadKey="gray";
/**
* 自定義灰度版本請(qǐng)求頭匹配值
*/
private String grayHeadValue="gray-996";
/**
* 使用灰度版本IP數(shù)組
*/
private List<String> grayIPList = new ArrayList<>();
/**
* 使用灰度版本城市數(shù)組
*/
private List<String> grayCityList = new ArrayList<>();
/**
* 使用灰度版本用戶編號(hào)數(shù)組(我們系統(tǒng)不會(huì)在網(wǎng)關(guān)獲取用戶編號(hào)這種方法如果需要可以自己實(shí)現(xiàn)一下)
*/
private List<String> grayUserNoList = new ArrayList<>();
}
-
全局版本配置信息類
@Data
@Configuration
@RefreshScope
@ConfigurationProperties("kerwin.tool.gray.version")
public class GrayVersionProperties {
/**
* 當(dāng)前線上版本號(hào)
*/
private String prodVersion;
/**
* 灰度版本號(hào)
*/
private String grayVersion;
}
-
全局自動(dòng)配置類
@Configuration
// 可以通過@ConditionalOnProperty設(shè)置是否開啟灰度自動(dòng)配置 默認(rèn)是不加載的
@ConditionalOnProperty(value = "kerwin.tool.gray.load",havingValue = "true")
@EnableConfigurationProperties(GrayVersionProperties.class)
public class GrayAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(value = GlobalFilter.class)
@EnableConfigurationProperties(GrayGatewayProperties.class)
static class GrayGatewayFilterAutoConfiguration {
@Bean
public GrayGatewayBeginFilter grayGatewayBeginFilter() {
return new GrayGatewayBeginFilter();
}
@Bean
public GrayGatewayAfterFilter grayGatewayAfterFilter() {
return new GrayGatewayAfterFilter();
}
@Bean
public GrayGatewayExceptionHandler grayGatewayExceptionHandler(){
return new GrayGatewayExceptionHandler();
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(value = WebMvcConfigurer.class)
static class GrayWebMvcAutoConfiguration {
/**
* Spring MVC 請(qǐng)求攔截器
* @return WebMvcConfigurer
*/
@Bean
public WebMvcConfigurer webMvcConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new GrayMvcHandlerInterceptor());
}
};
}
}
@Configuration
@ConditionalOnClass(value = RequestInterceptor.class)
static class GrayFeignInterceptorAutoConfiguration {
/**
* Feign攔截器
* @return GrayFeignRequestInterceptor
*/
@Bean
public GrayFeignRequestInterceptor grayFeignRequestInterceptor() {
return new GrayFeignRequestInterceptor();
}
}
}
項(xiàng)目運(yùn)行配置
這里我會(huì)啟動(dòng)五個(gè)服務(wù),一個(gè)網(wǎng)關(guān)服務(wù)、一個(gè)用戶服務(wù)V1版本、一個(gè)訂單服務(wù)V1版本、一個(gè)用戶服務(wù)V2版本、一個(gè)訂單服務(wù)V2版本,來演示灰度發(fā)布效果。
?
PS:Nacos的命名空間我這里叫spring-cloud-gray-example可以自己創(chuàng)建一個(gè)也可以換成自己的命名空間,源碼里面配置都是存在的,有問題看源碼就行
?
配置Nacos全局配置文件(common-config.yaml)
所有服務(wù)都會(huì)使用到這個(gè)配置
kerwin:
tool:
gray:
## 配置是否加載灰度自動(dòng)配置類,如果不配置那么默認(rèn)不加載
load: true
## 配置生產(chǎn)版本和灰度版本號(hào)
version:
prodVersion: V1
grayVersion: V2
## 配置Ribbon調(diào)用user-app和order-app服務(wù)時(shí)使用我們自定義灰度輪詢算法
user-app:
ribbon:
NFLoadBalancerRuleClassName: com.kerwin.gray.loadbalancer.GrayRoundRobinRule
order-app:
ribbon:
NFLoadBalancerRuleClassName: com.kerwin.gray.loadbalancer.GrayRoundRobinRule
配置網(wǎng)關(guān)Nacos配置文件(gateway-app.yaml)
kerwin:
tool:
gray:
gateway:
## 是否開啟灰度發(fā)布功能
enabled: true
## 自定義灰度版本請(qǐng)求頭
grayHeadKey: gray
## 自定義灰度版本請(qǐng)求頭匹配值
grayHeadValue: gray-996
## 使用灰度版本IP數(shù)組
grayIPList:
- '127.0.0.1'
## 使用灰度版本城市數(shù)組
grayCityList:
- 本地
啟動(dòng)網(wǎng)關(guān)服務(wù)
網(wǎng)關(guān)服務(wù)啟動(dòng)一個(gè)就行,直接Debug啟動(dòng)即可,方便調(diào)試源碼
啟動(dòng)業(yè)務(wù)服務(wù)V1 和 V2版本(用戶服務(wù)和訂單服務(wù)都用這種方式啟動(dòng))
先直接Debug啟動(dòng)會(huì)在IDEA這個(gè)位置看到一個(gè)對(duì)應(yīng)啟動(dòng)類名稱的信息
點(diǎn)擊Edit編輯這個(gè)啟動(dòng)配置
復(fù)制一個(gè)對(duì)應(yīng)啟動(dòng)配置作為V2版本,自己將Name改成自己能區(qū)分的即可
配置啟動(dòng)參數(shù),第一步點(diǎn)擊Modify options 然后第二步將Add VM options勾選上,第三步填寫對(duì)應(yīng)服務(wù)的啟動(dòng)端口和Nacos的metadata.version,我這里用戶服務(wù)V1版本配置為-Dserver.port=7201 -Dspring.cloud.nacos.discovery.metadata.version=V1,用戶服務(wù)V2版本配置為-Dserver.port=7202 -Dspring.cloud.nacos.discovery.metadata.version=V2,訂單服務(wù)配置類似,配置好后點(diǎn)Apply。
最后啟動(dòng)好的服務(wù)信息
灰度效果演示
源碼中的user-app提供了一個(gè)獲取用戶信息的接口并且會(huì)攜帶當(dāng)前服務(wù)的端口和版本信息,order-app服務(wù)提供了一個(gè)獲取訂單信息的接口,會(huì)去遠(yuǎn)程調(diào)用user-app獲取訂單關(guān)聯(lián)的用戶信息,并且也會(huì)攜帶當(dāng)前服務(wù)的端口和版本信息響應(yīng)。
場(chǎng)景一(關(guān)閉灰度開關(guān):不區(qū)分調(diào)用服務(wù)版本)
關(guān)閉灰度開關(guān)有兩個(gè)配置可以實(shí)現(xiàn)
1、在項(xiàng)目啟動(dòng)之前修改Nacos全局配置文件中的kerwin.tool.gray.load 配置是否加載灰度自動(dòng)配置類,只要配置不為true就不會(huì)加載整個(gè)灰度相關(guān)類
2、關(guān)閉網(wǎng)關(guān)灰度開關(guān),修改網(wǎng)關(guān)Nacos配置文件中的kerwin.tool.gray.gateway.enabled ,只要配置不為true就不會(huì)進(jìn)行灰度判斷。
調(diào)用演示
這里調(diào)用不一定就是Order服務(wù)版本為V1 User服務(wù)版本也為V1,也有可能Order服務(wù)版本為V1 User服務(wù)版本也為V2.
-
第一次調(diào)用,Order服務(wù)版本為V1,User服務(wù)版本也為V1
-
第二次調(diào)用,Order服務(wù)版本為V2,User服務(wù)版本也為V2
場(chǎng)景二(開啟灰度開關(guān):只調(diào)用生產(chǎn)版本)
修改網(wǎng)關(guān)Nacos配置文件中的kerwin.tool.gray.gateway.enabled 設(shè)置為true,其它灰度IP數(shù)組和城市數(shù)組配置匹配不上就行,這樣怎么調(diào)用都是V1版本,因?yàn)樵?code style="font-size: 14px;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: Operator Mono, Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(40, 202, 113);">GrayVersionProperties版本配置中設(shè)置的生產(chǎn)版本就是為V1灰度版本為V2。
場(chǎng)景三(開啟灰度開關(guān):通過請(qǐng)求頭、ip、城市匹配調(diào)用灰度版本)
這里通過請(qǐng)求頭測(cè)試,攜帶請(qǐng)求頭gray=gray-996訪問網(wǎng)關(guān)那么流量就會(huì)都進(jìn)入灰度版本V2。
源碼
?
https://gitee.com/kerwin_code/spring-cloud-gray-example
?
存在問題
1、如果項(xiàng)目中使用到了分布式任務(wù)調(diào)度那怎么區(qū)分灰度版本
這里其實(shí)挺好解決的,就拿xxl-job來說,注冊(cè)不同的執(zhí)行器就行,在發(fā)布灰度版本時(shí)注冊(cè)到灰度版本的執(zhí)行器即可。
2、如果項(xiàng)目中使用的了MQ我們收發(fā)消息怎么控制灰度
這里和解決分布式任務(wù)調(diào)度思想是一樣的灰度版本的服務(wù)發(fā)送消息的時(shí)候投遞到另外一個(gè)MQ的服務(wù)端,就是弄兩套MQ服務(wù)端,生產(chǎn)的服務(wù)使用生產(chǎn)的MQ,灰度發(fā)布使用灰度的MQ
3、這里整個(gè)實(shí)現(xiàn)流程不是很復(fù)雜,但也是很沒必要,只是提供一種實(shí)現(xiàn)方案可以參考
其實(shí)通過Nginx + Lua腳本方式直接路由網(wǎng)關(guān),然后給灰度整套服務(wù)都使用一個(gè)Nacos灰度的命名空間,生產(chǎn)的使用生產(chǎn)的命名空間,這樣就能將兩套服務(wù)都隔離了,分布式任務(wù)調(diào)度、MQ等配置都可以獨(dú)立在自己命名空間的配置文件中豈不美哉
?? 歡迎加入小哈的星球 ,你將獲得: 專屬的項(xiàng)目實(shí)戰(zhàn) / Java 學(xué)習(xí)路線 / 一對(duì)一提問 / 學(xué)習(xí)打卡 / 每月贈(zèng)書
新項(xiàng)目:仿小紅書(微服務(wù)架構(gòu))正在更新中... 。全棧前后端分離博客項(xiàng)目 2.0 版本完結(jié)啦, 演示鏈接:http://116.62.199.48/ 。全程手摸手,后端 + 前端全棧開發(fā),從 0 到 1 講解每個(gè)功能點(diǎn)開發(fā)步驟,1v1 答疑,直到項(xiàng)目上線。目前已更新了287小節(jié),累計(jì)45w+字,講解圖:2008張,還在持續(xù)爆肝中.. 后續(xù)還會(huì)上新更多項(xiàng)目,目標(biāo)是將Java領(lǐng)域典型的項(xiàng)目都整一波,如秒殺系統(tǒng), 在線商城, IM即時(shí)通訊,Spring Cloud Alibaba 等等,戳我加入學(xué)習(xí),已有1600+小伙伴加入(早鳥價(jià)超低)
2. 面試官:如何實(shí)現(xiàn)一個(gè)合格的分布式鎖?
最近面試BAT,整理一份面試資料《Java面試BATJ通關(guān)手冊(cè)》,覆蓋了Java核心技術(shù)、JVM、Java并發(fā)、SSM、微服務(wù)、數(shù)據(jù)庫、數(shù)據(jù)結(jié)構(gòu)等等。
獲取方式:點(diǎn)“在看”,關(guān)注公眾號(hào)并回復(fù) Java 領(lǐng)取,更多內(nèi)容陸續(xù)奉上。
PS:因公眾號(hào)平臺(tái)更改了推送規(guī)則,如果不想錯(cuò)過內(nèi)容,記得讀完點(diǎn)一下“在看”,加個(gè)“星標(biāo)”,這樣每次新文章推送才會(huì)第一時(shí)間出現(xiàn)在你的訂閱列表里。
點(diǎn)“在看”支持小哈呀,謝謝啦
