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);
        }
    }
}

至此就基本實現了最初的想法。

這個實現方式僅供參考,如有跟好的方式,多多指教哈~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章