【SpringCloud】Zuul在何種情況下使用Hystrix

首先萄喳,引入spring-cloud-starter-zuul之后會(huì)間接引入:

1542265942040.png


hystrix依賴已經(jīng)引入且警,那么何種情況下使用hystrix呢句旱?

在Zuul的自動(dòng)配置類ZuulServerAutoConfigurationZuulProxyAutoConfiguration中總共會(huì)向Spring容器注入3個(gè)Zuul的RouteFilter呻待,分別是

  • SimpleHostRoutingFilter

    簡(jiǎn)單路由薇组,通過(guò)HttpClient向預(yù)定的URL發(fā)送請(qǐng)求

    生效條件:

    RequestContext.getCurrentContext().getRouteHost() != null
    ? && RequestContext.getCurrentContext().sendZuulResponse()

    1、RequestContext中的routeHost不為空铸敏,routeHost就是URL缚忧,即使用URL直連

    2、RequestContext中的sendZuulResponse為true杈笔,即是否將response發(fā)送給客戶端闪水,默認(rèn)為true

  • RibbonRoutingFilter

    使用Ribbon、Hystrix和可插入的http客戶端發(fā)送請(qǐng)求

    生效條件:

    (RequestContext.getRouteHost() == null && RequestContext.get(SERVICE_ID_KEY) != null
    ? && RequestContext.sendZuulResponse())

    1蒙具、RequestContext中的routeHost為空球榆,即URL為空

    2、RequestContext中的serviceId不為空

    3禁筏、RequestContext中的sendZuulResponse為true持钉,即是否將response發(fā)送給客戶端,默認(rèn)為true

  • SendForwardFilter

    forward到本地URL

    生效條件:

    RequestContext.containsKey(FORWARD_TO_KEY)
    ? && !RequestContext.getBoolean(SEND_FORWARD_FILTER_RAN, false)

    1篱昔、RequestContext中包含F(xiàn)ORWARD_TO_KEY每强,即URL使用 forward: 映射

    2、RequestContext中SEND_FORWARD_FILTER_RAN為false州刽,SEND_FORWARD_FILTER_RAN意為“send forward是否運(yùn)行過(guò)了”空执,在SendForwardFilter#run()時(shí)會(huì)ctx.set(SEND_FORWARD_FILTER_RAN, true)

綜上所述,在使用serviceId映射的方法路由轉(zhuǎn)發(fā)的時(shí)候穗椅,會(huì)使用Ribbon+Hystrix


而哪種路由配置方式是“URL映射”辨绊,哪種配置方式又是“serviceId映射”呢?

Zuul有一個(gè)前置過(guò)濾器PreDecorationFilter用于通過(guò)RouteLocator路由定位器決定在何時(shí)以何種方式路由轉(zhuǎn)發(fā)

RouteLocator是用于通過(guò)請(qǐng)求地址匹配到Route路由的匹表,之后PreDecorationFilter再通過(guò)Route信息設(shè)置RequestContext上下文门坷,決定后續(xù)使用哪個(gè)RouteFilter做路由轉(zhuǎn)發(fā)

所以就引出以下問(wèn)題:

  • 什么是Route
  • RouteLocator路由定位器如何根據(jù)請(qǐng)求路徑匹配路由
  • 匹配到路由后,PreDecorationFilter如何設(shè)置RequestContext請(qǐng)求上下文


什么是Route

我總共見(jiàn)到兩個(gè)和Route相關(guān)的類

ZuulProperties.ZuulRoute袍镀,用于和zuul配置文件關(guān)聯(lián)默蚌,保存相關(guān)信息

org.springframework.cloud.netflix.zuul.filters.Route, RouteLocator找到的路由信息就是這個(gè)類苇羡,用于路由轉(zhuǎn)發(fā)

public static class ZuulRoute {
    private String id;    //ZuulRoute的id
    private String path;  //路由的pattern敏簿,如 /foo/**
    private String serviceId;  //要映射到此路由的服務(wù)id
    private String url;   //要映射到路由的完整物理URL
    private boolean stripPrefix = true;  //用于確定在轉(zhuǎn)發(fā)之前是否應(yīng)剝離此路由前綴的標(biāo)志位
    private Boolean retryable;  //此路由是否可以重試,通常重試需要serviceId和ribbon
    private Set<String> sensitiveHeaders = new LinkedHashSet(); //不會(huì)傳遞給下游請(qǐng)求的敏感標(biāo)頭列表
    private boolean customSensitiveHeaders = false; //是否自定義了敏感頭列表
}
public class Route {
    private String id;
    private String fullPath;
    private String path;
    private String location;  //可能是 url 或 serviceId
    private String prefix;
    private Boolean retryable;
    private Set<String> sensitiveHeaders = new LinkedHashSet<>();
    private boolean customSensitiveHeaders;
}

可以看到org.springframework.cloud.netflix.zuul.filters.RouteZuulProperties.ZuulRoute基本一致宣虾,只是Route用于路由轉(zhuǎn)發(fā)定位的屬性location根據(jù)不同的情況,可能是一個(gè)具體的URL温数,可能是一個(gè)serviceId


RouteLocator路由定位器如何根據(jù)請(qǐng)求路徑匹配路由

Zuul在自動(dòng)配置加載時(shí)注入了2個(gè)RouteLocator

  • CompositeRouteLocator: 組合的RouteLocator绣硝,在getMatchingRoute()時(shí)會(huì)依次調(diào)用其它的RouteLocator,先找到先返回撑刺;CompositeRouteLocator的routeLocators集合中只有DiscoveryClientRouteLocator
  • DiscoveryClientRouteLocator: 可以將靜態(tài)的鹉胖、已配置的路由與來(lái)自DiscoveryClient服務(wù)發(fā)現(xiàn)的路由組合在一起,來(lái)自DiscoveryClient的路由優(yōu)先;SimpleRouteLocator的子類(SimpleRouteLocator 基于加載到ZuulProperties中的配置定位Route路由信息)

其中CompositeRouteLocator是 @Primary 的甫菠,它是組合多個(gè)RouteLocator的Locator挠铲,其getMatchingRoute()方法會(huì)分別調(diào)用其它所有RouteLocator的getMatchingRoute()方法,通過(guò)請(qǐng)求路徑匹配路由信息寂诱,只要匹配到了就馬上返回

默認(rèn)CompositeRouteLocator混合路由定位器的routeLocators只有一個(gè)DiscoveryClientRouteLocator拂苹,故只需分析DiscoveryClientRouteLocator#getMatchingRoute(path)

//----------DiscoveryClientRouteLocator是SimpleRouteLocator子類,其實(shí)是調(diào)用的SimpleRouteLocator##getMatchingRoute(path)
@Override
public Route getMatchingRoute(final String path) {
    return getSimpleMatchingRoute(path);
}

protected Route getSimpleMatchingRoute(final String path) {
    if (log.isDebugEnabled()) {
        log.debug("Finding route for path: " + path);
    }

    // routes是保存路由信息的map痰洒,如果此時(shí)還未加載瓢棒,調(diào)用locateRoutes()
    if (this.routes.get() == null) {
        this.routes.set(locateRoutes());
    }

    if (log.isDebugEnabled()) {
        log.debug("servletPath=" + this.dispatcherServletPath);
        log.debug("zuulServletPath=" + this.zuulServletPath);
        log.debug("RequestUtils.isDispatcherServletRequest()="
                + RequestUtils.isDispatcherServletRequest());
        log.debug("RequestUtils.isZuulServletRequest()="
                + RequestUtils.isZuulServletRequest());
    }

    /**
     * 下面的方法主要是先對(duì)path做微調(diào)
     * 再根據(jù)path到routes中匹配到ZuulRoute
     * 最后根據(jù) ZuulRoute 和 adjustedPath 生成 Route
     */
    String adjustedPath = adjustPath(path);

    ZuulRoute route = getZuulRoute(adjustedPath);

    return getRoute(route, adjustedPath);
}

下面我們來(lái)看看locateRoutes()是如何加載靜態(tài)的、已配置的路由與來(lái)自DiscoveryClient服務(wù)發(fā)現(xiàn)的路由的

//----------DiscoveryClientRouteLocator#locateRoutes()  服務(wù)發(fā)現(xiàn)路由定位器的locateRoutes()
@Override
protected LinkedHashMap<String, ZuulRoute> locateRoutes() {
    //保存ZuulRoute的LinkedHashMap
    LinkedHashMap<String, ZuulRoute> routesMap = new LinkedHashMap<String, ZuulRoute>();
    
    //調(diào)用父類SimpleRouteLocator#locateRoutes()
    //加載ZuulProperties中的所有配置文件中的路由信息
    routesMap.putAll(super.locateRoutes());
    
    //如果服務(wù)發(fā)現(xiàn)客戶端discovery存在
    if (this.discovery != null) {
        //將routesMap已經(jīng)存在的配置文件中的ZuulRoute放入staticServices<serviceId, ZuulRoute>
        Map<String, ZuulRoute> staticServices = new LinkedHashMap<String, ZuulRoute>();
        for (ZuulRoute route : routesMap.values()) {
            String serviceId = route.getServiceId();
            
            //如果serviceId為null丘喻,以id作為serviceId脯宿,此情況適合 zuul.routes.xxxx=/xxxx/** 的情況
            if (serviceId == null) {
                serviceId = route.getId();
            }
            if (serviceId != null) {
                staticServices.put(serviceId, route);
            }
        }
        
        
        // Add routes for discovery services by default
        List<String> services = this.discovery.getServices(); //到注冊(cè)中心找到所有service
        String[] ignored = this.properties.getIgnoredServices()
                .toArray(new String[0]);
        
        //遍歷services
        for (String serviceId : services) {
            // Ignore specifically ignored services and those that were manually
            // configured
            String key = "/" + mapRouteToService(serviceId) + "/**";
            
            //如果注冊(cè)中心的serviceId在staticServices集合中,并且此路由沒(méi)有配置URL
            //那么泉粉,更新路由的location為serviceId
            if (staticServices.containsKey(serviceId)
                    && staticServices.get(serviceId).getUrl() == null) {
                // Explicitly configured with no URL, cannot be ignored
                // all static routes are already in routesMap
                // Update location using serviceId if location is null
                ZuulRoute staticRoute = staticServices.get(serviceId);
                if (!StringUtils.hasText(staticRoute.getLocation())) {
                    staticRoute.setLocation(serviceId);
                }
            }
            
            //如果注冊(cè)中心的serviceId不在忽略范圍內(nèi)连霉,且routesMap中還沒(méi)有包含,添加到routesMap
            if (!PatternMatchUtils.simpleMatch(ignored, serviceId)
                    && !routesMap.containsKey(key)) {
                // Not ignored
                routesMap.put(key, new ZuulRoute(key, serviceId));
            }
        }
    }
    
    // 如果routesMap中有 /** 的默認(rèn)路由配置
    if (routesMap.get(DEFAULT_ROUTE) != null) {
        ZuulRoute defaultRoute = routesMap.get(DEFAULT_ROUTE);
        // Move the defaultServiceId to the end
        routesMap.remove(DEFAULT_ROUTE);
        routesMap.put(DEFAULT_ROUTE, defaultRoute);
    }
    
    //將routesMap中的數(shù)據(jù)微調(diào)后嗡靡,放到values<String, ZuulRoute>跺撼,返回
    LinkedHashMap<String, ZuulRoute> values = new LinkedHashMap<>();
    for (Entry<String, ZuulRoute> entry : routesMap.entrySet()) {
        String path = entry.getKey();
        // Prepend with slash if not already present.
        if (!path.startsWith("/")) {
            path = "/" + path;
        }
        if (StringUtils.hasText(this.properties.getPrefix())) {
            path = this.properties.getPrefix() + path;
            if (!path.startsWith("/")) {
                path = "/" + path;
            }
        }
        values.put(path, entry.getValue());
    }
    
    return values;
}

此方法運(yùn)行后就已經(jīng)加載了配置文件中所有路由信息,以及注冊(cè)中心中的服務(wù)路由信息叽躯,有的通過(guò)URL路由财边,有的通過(guò)serviceId路由

只需根據(jù)本次請(qǐng)求的requestURI與 路由的pattern匹配找到對(duì)應(yīng)的路由


匹配到路由后,PreDecorationFilter如何設(shè)置RequestContext請(qǐng)求上下文

//----------PreDecorationFilter前置過(guò)濾器
@Override
public Object run() {
    RequestContext ctx = RequestContext.getCurrentContext();
    final String requestURI = this.urlPathHelper.getPathWithinApplication(ctx.getRequest());
    Route route = this.routeLocator.getMatchingRoute(requestURI); //找到匹配的路由
    //----------------到上面為止是已經(jīng)分析過(guò)的点骑,根據(jù)requestURI找到匹配的Route信息
    
    // ==== 匹配到路由信息
    if (route != null) {
        String location = route.getLocation();
        if (location != null) {
            ctx.put(REQUEST_URI_KEY, route.getPath());//RequestContext設(shè)置 requestURI:路由的pattern路徑
            ctx.put(PROXY_KEY, route.getId());//RequestContext設(shè)置 proxy:路由id
            
            //設(shè)置需要忽略的敏感頭信息酣难,要么用全局默認(rèn)的,要么用路由自定義的
            if (!route.isCustomSensitiveHeaders()) {
                this.proxyRequestHelper
                        .addIgnoredHeaders(this.properties.getSensitiveHeaders().toArray(new String[0]));
            }
            else {
                this.proxyRequestHelper.addIgnoredHeaders(route.getSensitiveHeaders().toArray(new String[0]));
            }

            //設(shè)置重試信息
            if (route.getRetryable() != null) {
                ctx.put(RETRYABLE_KEY, route.getRetryable());
            }

            //如果location是 http/https開(kāi)頭的黑滴,RequestContext設(shè)置 routeHost:URL
            //如果location是 forward:開(kāi)頭的憨募,RequestContext設(shè)置 forward信息、routeHost:null
            //其它 RequestContext設(shè)置 serviceId袁辈、routeHost:null菜谣、X-Zuul-ServiceId
            if (location.startsWith(HTTP_SCHEME+":") || location.startsWith(HTTPS_SCHEME+":")) {
                ctx.setRouteHost(getUrl(location));
                ctx.addOriginResponseHeader(SERVICE_HEADER, location);
            }
            else if (location.startsWith(FORWARD_LOCATION_PREFIX)) {
                ctx.set(FORWARD_TO_KEY,
                        StringUtils.cleanPath(location.substring(FORWARD_LOCATION_PREFIX.length()) + route.getPath()));
                ctx.setRouteHost(null);
                return null;
            }
            else {
                // set serviceId for use in filters.route.RibbonRequest
                ctx.set(SERVICE_ID_KEY, location);
                ctx.setRouteHost(null);
                ctx.addOriginResponseHeader(SERVICE_ID_HEADER, location);
            }
            
            //是否添加代理頭信息 X-Forwarded-For
            if (this.properties.isAddProxyHeaders()) {
                addProxyHeaders(ctx, route);
                String xforwardedfor = ctx.getRequest().getHeader(X_FORWARDED_FOR_HEADER);
                String remoteAddr = ctx.getRequest().getRemoteAddr();
                if (xforwardedfor == null) {
                    xforwardedfor = remoteAddr;
                }
                else if (!xforwardedfor.contains(remoteAddr)) { // Prevent duplicates
                    xforwardedfor += ", " + remoteAddr;
                }
                ctx.addZuulRequestHeader(X_FORWARDED_FOR_HEADER, xforwardedfor);
            }
            
            //是否添加Host頭信息
            if (this.properties.isAddHostHeader()) {
                ctx.addZuulRequestHeader(HttpHeaders.HOST, toHostHeader(ctx.getRequest()));
            }
        }
    }
    // ==== 沒(méi)有匹配到路由信息
    else {
        log.warn("No route found for uri: " + requestURI);

        String fallBackUri = requestURI;
        String fallbackPrefix = this.dispatcherServletPath; // default fallback
                                                            // servlet is
                                                            // DispatcherServlet

        if (RequestUtils.isZuulServletRequest()) {
            // remove the Zuul servletPath from the requestUri
            log.debug("zuulServletPath=" + this.properties.getServletPath());
            fallBackUri = fallBackUri.replaceFirst(this.properties.getServletPath(), "");
            log.debug("Replaced Zuul servlet path:" + fallBackUri);
        }
        else {
            // remove the DispatcherServlet servletPath from the requestUri
            log.debug("dispatcherServletPath=" + this.dispatcherServletPath);
            fallBackUri = fallBackUri.replaceFirst(this.dispatcherServletPath, "");
            log.debug("Replaced DispatcherServlet servlet path:" + fallBackUri);
        }
        if (!fallBackUri.startsWith("/")) {
            fallBackUri = "/" + fallBackUri;
        }
        String forwardURI = fallbackPrefix + fallBackUri;
        forwardURI = forwardURI.replaceAll("http://", "/");
        ctx.set(FORWARD_TO_KEY, forwardURI);
    }
    return null;
}


總結(jié):

  • 只要引入了spring-cloud-starter-zuul就會(huì)間接引入Ribbon、Hystrix
  • 路由信息可能是從配置文件中加載的晚缩,也可能是通過(guò)DiscoveryClient從注冊(cè)中心加載的
  • zuul是通過(guò)前置過(guò)濾器PreDecorationFilter找到與當(dāng)前requestURI匹配的路由信息尾膊,并在RequestContext中設(shè)置相關(guān)屬性的,后續(xù)的Route Filter會(huì)根據(jù)RequestContext中的這些屬性判斷如何路由轉(zhuǎn)發(fā)
  • Route Filter主要使用 SimpleHostRoutingFilter 和 RibbonRoutingFilter
  • 當(dāng)RequestContext請(qǐng)求上下文中存在routeHost荞彼,即URL直連信息時(shí)冈敛,使用SimpleHostRoutingFilter簡(jiǎn)單Host路由
  • 當(dāng)RequestContext請(qǐng)求上下文中存在serviceId,即服務(wù)id時(shí)(可能會(huì)與注冊(cè)中心關(guān)聯(lián)獲取服務(wù)列表鸣皂,或者讀取配置文件中serviceId.ribbon.listOfServers的服務(wù)列表)抓谴,使用RibbonRoutingFilter暮蹂,會(huì)使用Ribbon、Hystrix
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末癌压,一起剝皮案震驚了整個(gè)濱河市仰泻,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌滩届,老刑警劉巖集侯,帶你破解...
    沈念sama閱讀 211,265評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異丐吓,居然都是意外死亡浅悉,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門(mén)券犁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)术健,“玉大人,你說(shuō)我怎么就攤上這事粘衬≤窆溃” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,852評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵稚新,是天一觀的道長(zhǎng)勘伺。 經(jīng)常有香客問(wèn)我,道長(zhǎng)褂删,這世上最難降的妖魔是什么飞醉? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,408評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮屯阀,結(jié)果婚禮上缅帘,老公的妹妹穿的比我還像新娘。我一直安慰自己难衰,他們只是感情好钦无,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,445評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著盖袭,像睡著了一般失暂。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上鳄虱,一...
    開(kāi)封第一講書(shū)人閱讀 49,772評(píng)論 1 290
  • 那天弟塞,我揣著相機(jī)與錄音,去河邊找鬼拙已。 笑死宣肚,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的悠栓。 我是一名探鬼主播霉涨,決...
    沈念sama閱讀 38,921評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼惭适!你這毒婦竟也來(lái)了笙瑟?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,688評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤癞志,失蹤者是張志新(化名)和其女友劉穎往枷,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體凄杯,經(jīng)...
    沈念sama閱讀 44,130評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡错洁,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,467評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了戒突。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片屯碴。...
    茶點(diǎn)故事閱讀 38,617評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖膊存,靈堂內(nèi)的尸體忽然破棺而出导而,到底是詐尸還是另有隱情,我是刑警寧澤隔崎,帶...
    沈念sama閱讀 34,276評(píng)論 4 329
  • 正文 年R本政府宣布今艺,位于F島的核電站,受9級(jí)特大地震影響爵卒,放射性物質(zhì)發(fā)生泄漏虚缎。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,882評(píng)論 3 312
  • 文/蒙蒙 一钓株、第九天 我趴在偏房一處隱蔽的房頂上張望实牡。 院中可真熱鬧,春花似錦享幽、人聲如沸铲掐。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,740評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)摆霉。三九已至,卻和暖如春奔坟,著一層夾襖步出監(jiān)牢的瞬間携栋,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,967評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工咳秉, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留婉支,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,315評(píng)論 2 360
  • 正文 我出身青樓澜建,卻偏偏與公主長(zhǎng)得像向挖,于是被迫代替她去往敵國(guó)和親蝌以。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,486評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容