Spring Cloud灰度發(fā)布方案
關(guān)注我們,設(shè)為星標,每天7:30不見不散,架構(gòu)路上與您共享 回復(fù)"架構(gòu)師"獲取資源
前言
調(diào)用鏈分析
外部調(diào)用
請求==>zuul==>服務(wù)
zuul在轉(zhuǎn)發(fā)請求的時候,也會根據(jù) Ribbon從服務(wù)實例列表中選擇一個對應(yīng)的服務(wù),然后選擇轉(zhuǎn)發(fā).內(nèi)部調(diào)用
請求==>zuul==>服務(wù)Resttemplate調(diào)用==>服務(wù)
請求==>zuul==>服務(wù)Fegin調(diào)用==>服務(wù)
無論是通過Resttemplate還是Fegin的方式進行服務(wù)間的調(diào)用,他們都會從Ribbon選擇一個服務(wù)實例返回.
預(yù)備知識
eureka元數(shù)據(jù)
標準元數(shù)據(jù):主機名、IP地址、端口號、狀態(tài)頁和健康檢查等信息,這些信息都會被發(fā)布在服務(wù)注冊表中,用于服務(wù)之間的調(diào)用。
eureka RestFul接口
| 請求名稱 | 請求方式 | HTTP地址 | 請求描述 |
|---|---|---|---|
| 注冊新服務(wù) | POST | /eureka/apps/{appID} | 傳遞JSON或者XML格式參數(shù)內(nèi)容,HTTP code為204時表示成功 |
| 取消注冊服務(wù) | DELETE | /eureka/apps/{appID}/{instanceID} | HTTP code為200時表示成功 |
| 發(fā)送服務(wù)心跳 | PUT | /eureka/apps/{appID}/{instanceID} | HTTP code為200時表示成功 |
| 查詢所有服務(wù) | GET | /eureka/apps | HTTP code為200時表示成功,返回XML/JSON數(shù)據(jù)內(nèi)容 |
| 查詢指定appID的服務(wù)列表 | GET | /eureka/apps/{appID} | HTTP code為200時表示成功,返回XML/JSON數(shù)據(jù)內(nèi)容 |
| 查詢指定appID&instanceID | GET | /eureka/apps/{appID}/{instanceID} | 獲取指定appID以及InstanceId的服務(wù)信息,HTTP code為200時表示成功,返回XML/JSON數(shù)據(jù)內(nèi)容 |
| 查詢指定instanceID服務(wù)列表 | GET | /eureka/apps/instances/{instanceID} | 獲取指定instanceID的服務(wù)列表,HTTP code為200時表示成功,返回XML/JSON數(shù)據(jù)內(nèi)容 |
| 變更服務(wù)狀態(tài) | PUT | /eureka/apps/{appID}/{instanceID}/status?value=DOWN | 服務(wù)上線、服務(wù)下線等狀態(tài)變動,HTTP code為200時表示成功 |
| 變更元數(shù)據(jù) | PUT | /eureka/apps/{appID}/{instanceID}/metadata?key=value | HTTP code為200時表示成功 |
更改自定義元數(shù)據(jù)
eureka.instance.metadata-map.version = v1
PUT /eureka/apps/{appID}/{instanceID}/metadata?key=value
實現(xiàn)流程
用戶請求首先到達Nginx然后轉(zhuǎn)發(fā)到網(wǎng)關(guān)
zuul,此時zuul攔截器會根據(jù)用戶攜帶請求token解析出對應(yīng)的userId網(wǎng)關(guān)從Apollo配置中心拉取灰度用戶列表,然后根據(jù)灰度用戶策略判斷該用戶是否是灰度用戶。如是,則給該請求添加請求頭及線程變量添加信息
version=xxx;若不是,則不做任何處理放行在
zuul攔截器執(zhí)行完畢后,zuul在進行轉(zhuǎn)發(fā)請求時會通過負載均衡器Ribbon。負載均衡Ribbon被重寫。當請求到達時候,Ribbon會取出
zuul存入線程變量值version。于此同時,Ribbon還會取出所有緩存的服務(wù)列表(定期從eureka刷新獲取最新列表)及其該服務(wù)的metadata-map信息。然后取出服務(wù)metadata-map的version信息與線程變量version進行判斷對比,若值一直則選擇該服務(wù)作為返回。若所有服務(wù)列表的version信息與之不匹配,則返回null,此時Ribbon選取不到對應(yīng)的服務(wù)則會報錯!zuul通過Ribbon將請求轉(zhuǎn)發(fā)到consumer服務(wù)后,可能還會通過fegin或resttemplate調(diào)用其他服務(wù),如provider服務(wù)。但是無論是通過fegin還是resttemplate,他們最后在選取服務(wù)轉(zhuǎn)發(fā)的時候都會通過Ribbon。那么在通過
fegin或resttemplate調(diào)用另外一個服務(wù)的時候需要設(shè)置一個攔截器,將請求頭version=xxx給帶上,然后存入線程變量。在經(jīng)過
fegin或resttemplate的攔截器后最后會到Ribbon,Ribbon會從線程變量里面取出version信息。然后重復(fù)步驟(4)和(5)
設(shè)計思路
public abstract class PredicateBasedRule extends ClientConfigEnabledRoundRobinRule {
// 略....
@Override
public Server choose(Object key) {
ILoadBalancer lb = getLoadBalancer();
Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key);
if (server.isPresent()) {
return server.get();
} else {
return null;
}
}
}
public class GrayMetadataRule extends ZoneAvoidanceRule {
// 略....
@Override
public Server choose(Object key) {
//1.從線程變量獲取version信息
String version = HystrixRequestVariableDefault.get();
//2.獲取服務(wù)實例列表
List<Server> serverList = this.getPredicate().getEligibleServers(this.getLoadBalancer().getAllServers(), key);
//3.循環(huán)serverList,選擇version匹配的服務(wù)并返回
for (Server server : serverList) {
Map<String, String> metadata = ((DiscoveryEnabledServer) server).getInstanceInfo().getMetadata();
String metaVersion = metadata.get("version);
if (!StringUtils.isEmpty(metaVersion)) {
if (metaVersion.equals(hystrixVer)) {
return server;
}
}
}
}
}
此處,我們可以通過在 步驟2中,讓zuul添加添加線程變量的時候也在請求頭中添加信息。然后,再自定義HandlerInterceptorAdapter攔截器,使之在到達服務(wù)之前將請求頭中的信息存入到線程變量HystrixRequestVariableDefault中。public class CoreHttpRequestInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
HttpRequestWrapper requestWrapper = new HttpRequestWrapper(request);
String hystrixVer = CoreHeaderInterceptor.version.get();
requestWrapper.getHeaders().add(CoreHeaderInterceptor.HEADER_VERSION, hystrixVer);
return execution.execute(requestWrapper, body);
}
}
public class CoreFeignRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
String hystrixVer = CoreHeaderInterceptor.version.get();
logger.debug("====>fegin version:{} ",hystrixVer);
template.header(CoreHeaderInterceptor.HEADER_VERSION, hystrixVer);
}
}
yourServiceId.ribbon.NFLoadBalancerRuleClassName=自定義的負載均衡策略類
public PropertiesFactory() {
classToProperty.put(ILoadBalancer.class, "NFLoadBalancerClassName");
classToProperty.put(IPing.class, "NFLoadBalancerPingClassName");
classToProperty.put(IRule.class, "NFLoadBalancerRuleClassName");
classToProperty.put(ServerList.class, "NIWSServerListClassName");
classToProperty.put(ServerListFilter.class, "NIWSServerListFilterClassName");
}
灰度使用
spring.application.name = provide-test
server.port = 7770
eureka.client.service-url.defaultZone = http://localhost:1111/eureka/
#啟動后直接將該元數(shù)據(jù)信息注冊到eureka
#eureka.instance.metadata-map.version = v1
測試案例
[x] zuul-server
[x] provider-test
port:7770 version:無port: 7771 version:v1[x] consumer-test
通過此種方法更改server的元數(shù)據(jù)后,由于ribbon會緩存實力列表,所以在測試改變服務(wù)信息時,ribbon并不會立馬從eureka拉去最新信息m,這個拉取信息的時間可自行配置。
測試演示
用戶andy為灰度用戶。
1.測試灰度用戶andy,是否路由到灰度服務(wù)provider-test:7771
2.測試非灰度用戶andyaaa(任意用戶)是否能被路由到普通服務(wù)provider-test:7770
以同樣的方式再啟動兩個consumer-test服務(wù),這里不再截圖演示。
自動化配置
本文鏈接:
https://blog.csdn.net/dupengcheng1/article/details/89187452






