Spring Cloud體系實現標簽路由
如果你正在使用Spring Cloud體系,在實際使用過程中正遇到以下問題,可以閱讀本文章的內容作為後續你解決這些問題的參考,文章內容不保證無錯,請務必仔細思考之後再進行實踐。
問題:
1,本地連上開發或測試環境的集群連調,正常測試請求可能會請求到本地,被自己的debug阻塞。
2,測試環境維護時,多項目並發提測,維護多個相同的集群進行測試是否必要,是否有更好的方案。
一般,我們在使用Spring Cloud全家桶的時候,會選擇zuul作為網關,Ribbon作為負載均衡器,Feign作為遠程服務調用模版。使用過Spring Cloud的同學對這些組件的作用必然非常熟悉。這裏就拿這些組件組合成的微服務集群來實現標簽路由的功能。
實現的效果如圖所示,在頭上帶上標簽的請求會在經過網關和各個應用時進行標簽判斷流量應該打到哪一個去,而每一個應用自己本身的標簽是通過eureka上的matedate實現的。
如下圖可以構想動態修改標簽控制應用所能承接的請求,這裏暫時不描述mq部分的功能:
答案:
實現一個ZoneAvoidanceRule的繼承類,重寫getPredicate方法:
@Override public AbstractServerPredicate getPredicate() { OfflineEnvMetadataAwarePredicate offlineEnvMetadataAwarePredicate = new OfflineEnvMetadataAwarePredicate(); offlineEnvMetadataAwarePredicate.setEnv(env); return offlineEnvMetadataAwarePredicate; }
Predicate的實現屏蔽了開發測試環境中非這個環境網段啟動的應用,並且比對請求的標簽和本地的標簽,來控制路由給哪一個服務器。
/** * 線下環境路由策略具體邏輯 */ public class OfflineEnvMetadataAwarePredicate extends AbstractServerPredicate { private String env; public void setEnv(String env) { this.env = env; } @Override public boolean apply(PredicateKey predicateKey) { if(predicateKey == null || !(predicateKey.getServer() instanceof DiscoveryEnabledServer)){ return true; } DiscoveryEnabledServer server = (DiscoveryEnabledServer) predicateKey.getServer(); String serverZone = server.getInstanceInfo().getMetadata().get("zone"); String requestZone = RequestZoneLabelContext.getRequestZone(); // dev || sit 環境 本地不允許直接連調 if(env.equals("sit") || env.equals("dev")){ if(StringUtils.isBlank(requestZone) && !server.getHost().startsWith("10.0")){ return false; } } if(StringUtils.isNotBlank(serverZone)) { return serverZone.equals(requestZone); }else if(StringUtils.isNotBlank(requestZone)){ return requestZone.equals(serverZone); } return true; } }
那麽我們註意到請求頭上的標簽要在初始時就拿到,所以需要一個ServletRequestListener,將拿到的zone放入RequestZoneLabelContext。我們知道在一個請求中如果是一個io線程執行到底,我們只需要利用threadlocal來存儲線程變量,可是如果一個請求中會產生不定的子線程完成,數據在線程間的傳遞就成為問題,這裏使用了InheritableThreadLocal來決解,在RequestZoneLabelContext中可以看到。
public class RequestZoneLabelContextListener implements ServletRequestListener {
private static final String ZONE_LABEL_NAME = "zone";
@Override
public void requestDestroyed(ServletRequestEvent sre) {
RequestZoneLabelContext.remove();
}
@Override
public void requestInitialized(ServletRequestEvent requestEvent) {
HttpServletRequest request = (HttpServletRequest)requestEvent.getServletRequest();
String lbZone = request.getHeader(ZONE_LABEL_NAME);
if(StringUtils.isNotBlank(lbZone)){
RequestZoneLabelContext.setZone(lbZone);
}
}
}
/**
* 從request header上傳遞label到feign請求
*/
public class RequestZoneLabelContext {
private static InheritableThreadLocal<String> zoneLabelThreadLocal = new InheritableThreadLocal<>();
public static void setZone(String zone){
zoneLabelThreadLocal.set(zone);
}
public static String getRequestZone(){
return zoneLabelThreadLocal.get();
}
public static void remove(){
zoneLabelThreadLocal.remove();
}
}
那麽在應用之間調用的feign中我們是需要繼續把這個zone通過header傳遞下去的,所以又擴展了RequestInterceptor:
public class FeignZoneHeaderInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
String requestZone = RequestZoneLabelContext.getRequestZone();
if(StringUtils.isNotBlank(requestZone)){
template.header("zone", requestZone);
}
}
}
至此就基本實現了最初的想法。
這個實現方式僅供參考,如有跟好的方式,多多指教哈~
Spring Cloud體系實現標簽路由