基於Ribbon動態路由實現:調用鏈控制/版本控制/灰度發布
demo地址:https://gitee.com/kenwar/ribbon-chain-control-demo
有什么用
- 可實現一套調用鏈管理工具,將管理好的調用鏈保存於redis中,實現一個ServiceLancherHandler類,從redis中取指定服務的訪問tag,實現調用鏈管理
- 對服務添加自定義tag,例如blue/green,比如有一個服務A,線上運行版本為blue,如果需要升級服務,可先發布版本為green的服務A,然后在測試機中添加指定A:blue的header,可進行線上測試
如果測試沒問題,可通過調用鏈管理工具將redis中A:blue改為A:green,實現流量不停機切換,解決服務刷新延遲問題,如果切換后發現green版本有bug,可隨時切換為blue. - local環境無需搭建所有依賴服務,例如正在開發服務B的一些新本功能,可在啟動B時打上自己的tag,並注冊上dev環境的eureka,測試時可在header中指定B:myName,將自己發出的請求在訪問B服務時轉發到本機啟動的B服務.
- 解決eureka緩存以及ribbon緩存服務注冊表導致本地緩存刷新延遲從而在服務切換過程中多次請求已下線服務問題
- 徹底解決server.enable-self-preservation eureka保護模式導致應用無法下線問題
- 其他玩法請大膽想象...
灰度發布圖示:
實現原理及實戰
實現原理:ribbon支持自定義rule路由規則,且提供了基於eureka-instance-meta進行路由的實現,可以根據客戶端注冊在eureka是的tag實現動態路由
核心依賴:
// 該依賴將指定MetadataAwareRule路由策略
<dependency>
<groupId>io.jmnarloch</groupId>
<artifactId>ribbon-discovery-filter-spring-cloud-starter</artifactId>
<version>2.1.0</version>
</dependency>
核心類:RibbonFilterContextHolder, MetadataAwarePredicate.class
public class MetadataAwarePredicate extends DiscoveryEnabledPredicate {
public MetadataAwarePredicate() {
}
protected boolean apply(DiscoveryEnabledServer server) {
RibbonFilterContext context = RibbonFilterContextHolder.getCurrentContext();
Set<Entry<String, String>> attributes = Collections.unmodifiableSet(context.getAttributes().entrySet());
Map<String, String> metadata = server.getInstanceInfo().getMetadata();
return metadata.entrySet().containsAll(attributes);
}
}
// 在zuul轉發或者feign調用前寫入動態路由策略,指定轉發到有該tag-value的服務
RibbonFilterContextHolder.getCurrentContext()
.add("${tagName}", ${tagValue});
// 客戶端服務配置
eureka:
instance:
metadata-map:
tagName1: tagValue1
tagName2: tagValue2
public abstract class ServiceLancherHandler {
protected boolean handle(String serviceId) {
String serviceLancher = getServiceLancher(serviceId);
if (StringUtils.isEmpty(serviceLancher)){
return false;
}
RibbonFilterContextHolder.clearCurrentContext();
RibbonFilterContextHolder.getCurrentContext()
.add("lancher", serviceLancher);
return true;
}
/**
子類通過實現該方法可自定義訪問策略
*/
abstract String getServiceLancher(String serviceId);
}
- zuul網關轉發到指定tag的服務關鍵代碼(api-gateway)
// 配置指定tag的職責鏈,
// 例如可定義先從header中尋找,
// 再從redis中尋找,再從數據庫中尋找,
//可自行實現ServiceLancherHandler,本demo只實現了從header中尋找
@Bean
ServiceLancherHandlerChain serviceLancherHandlerChain(){
ServiceLancherHandlerChain chain = new ServiceLancherHandlerChain();
chain.addHandler(new RequestHeaderServiceLancherHandler());
return chain;
}
// AbFilter extends ZuulFilter
// 繼承zuulfilter,實現自定義zuul前置攔截器,在服務轉發前指定路由策略
// 在自定義的zuulfilter中,獲取到第一層轉發服務id后,調用指定tag的職責鏈方法
public Object run() throws ZuulException {
String serviceId = getServiceId();
if(StringUtils.isBlank(serviceId)){
return null;
}
serviceLancherHandlerChain.handle(serviceId);
return null;
}
- 服務間通過feign調用時轉發到指定tag服務關鍵代碼(serviceA\serviceB\serviceC\serviceD)
FeignRibbonFilterInterceptor.class
// 攔截 FeignClient 在feign遠程調用前指定調用指定tag的職責鏈方法,調用后清除
@Pointcut("@within(org.springframework.cloud.openfeign.FeignClient)")
// PS 需實現 RequestInterceptor,將需要的requestHead在feign進行服務間調用時轉發到下一級服務,否則服務間調用將損失前端過來的header
FeignHeadersInterceptor.class
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = servletRequestAttributes.getRequest();
Map<String, String> headers = HeaderUtil.getHeaders(request);
if (headers!=null && headers.size() > 0) {
Iterator<Entry<String, String>> iterator = headers.entrySet().iterator();
while (iterator.hasNext()) {
Entry<String, String> entry = iterator.next();
template.header(entry.getKey(), entry.getValue());
}
}
// 可根據實際業務需求對轉發的header進行增減
如何驗證
- 啟動eurekaserver
- 啟動api-gateway
- 啟動serviceA\serviceB\serviceC\serviceD(每個服務啟動兩個實栗,分別使用dev1和dev2的配置文件,通過該參數指定-Dspring.profiles.active=dev1-Dspring.profiles.active=dev2)
- 訪問localhost:8761 查看服務是否都已注冊上eureka
- 訪問http://localhost:8092/service-b/demo/hello,該接口會經由api-gateway服務轉發到serviceB->serviceC->serviceD-serviceA,將打印各個服務的tag信息,可觀測到每次請求在不同版本的服務中輪詢
- 在requestHeader中添加自定義headerribbon-lancher-map:{"service-a":"green","service-b":"blue","service-c":"blue","service-d":"blue"},可使用postman等調用工具,也可使用chrome的[Modify Header Value]插件
- 觀測到每次調用鏈與header中指定的一致
PS: header-key ribbon-lancher-map 定義在RequestHeaderServiceLancherHandler.class中