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

首先,引入 spring-cloud-starter-zuul 之後會間接引入:

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

hystrix依賴已經引入,那麼何種情況下使用hystrix呢?
在Zuul的自動配置類 ZuulServerAutoConfiguration 和 ZuulProxyAutoConfiguration 中總共會向Spring容器注入3個Zuul的RouteFilter,分別是
SimpleHostRoutingFilter
簡單路由,通過HttpClient向預定的URL發送請求
生效條件:
RequestContext.getCurrentContext().getRouteHost() != null
​ && RequestContext.getCurrentContext().sendZuulResponse()
1、RequestContext中的routeHost不爲空,routeHost就是URL,即使用URL直連
2、RequestContext中的sendZuulResponse爲true,即是否將response發送給客戶端,默認爲true
RibbonRoutingFilter
使用Ribbon、Hystrix和可插入的http客戶端發送請求
生效條件:
(RequestContext.getRouteHost() == null && RequestContext.get(SERVICE_ID_KEY) != null
​ && RequestContext.sendZuulResponse())
1、RequestContext中的routeHost爲空,即URL爲空
2、RequestContext中的serviceId不爲空
3、RequestContext中的sendZuulResponse爲true,即是否將response發送給客戶端,默認爲true
SendForwardFilter
forward到本地URL
生效條件:
RequestContext.containsKey(FORWARD_TO_KEY)
​ && !RequestContext.getBoolean(SEND_FORWARD_FILTER_RAN, false)
1、RequestContext中包含FORWARD_TO_KEY,即URL使用 forward: 映射
2、RequestContext中SEND_FORWARD_FILTER_RAN爲false,SEND_FORWARD_FILTER_RAN意爲“send forward是否運行過了”,在SendForwardFilter#run()時會 ctx.set(SEND_FORWARD_FILTER_RAN, true)
綜上所述,在使用serviceId映射的方法路由轉發的時候,會使用Ribbon+Hystrix
而哪種路由配置方式是“URL映射”,哪種配置方式又是“serviceId映射”呢?
Zuul有一個前置過濾器 PreDecorationFilter 用於通過 RouteLocator路由定位器 決定在何時以何種方式路由轉發
RouteLocator是用於通過請求地址匹配到Route路由的,之後 PreDecorationFilter 再通過Route信息設置RequestContext上下文,決定後續使用哪個RouteFilter做路由轉發
所以就引出以下問題:
什麼是Route
RouteLocator路由定位器如何根據請求路徑匹配路由
匹配到路由後,PreDecorationFilter如何設置RequestContext請求上下文
什麼是Route
我總共見到兩個和Route相關的類
ZuulProperties.ZuulRoute ,用於和zuul配置文件關聯,保存相關信息
org.springframework.cloud.netflix.zuul.filters.Route , RouteLocator找到的路由信息就是這個類,用於路由轉發
public static class ZuulRoute {
private String id; //ZuulRoute的id
private String path; //路由的pattern,如 /foo/**
private String serviceId; //要映射到此路由的服務id
private String url; //要映射到路由的完整物理URL
private boolean stripPrefix = true; //用於確定在轉發之前是否應剝離此路由前綴的標誌位
private Boolean retryable; //此路由是否可以重試,通常重試需要serviceId和ribbon
private Set<String> sensitiveHeaders = new LinkedHashSet(); //不會傳遞給下游請求的敏感標頭列表
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.Route 和 ZuulProperties.ZuulRoute 基本一致,只是Route用於路由轉發定位的屬性location根據不同的情況,可能是一個具體的URL,可能是一個serviceId
RouteLocator路由定位器如何根據請求路徑匹配路由
Zuul在自動配置加載時注入了2個RouteLocator
CompositeRouteLocator : 組合的RouteLocator,在 getMatchingRoute() 時會依次調用其它的RouteLocator,先找到先返回;CompositeRouteLocator的routeLocators集合中只有DiscoveryClientRouteLocator
DiscoveryClientRouteLocator : 可以將靜態的、已配置的路由與來自DiscoveryClient服務發現的路由組合在一起,來自DiscoveryClient的路由優先;SimpleRouteLocator的子類(SimpleRouteLocator 基於加載到 ZuulProperties 中的配置定位Route路由信息)
其中CompositeRouteLocator是 @Primary 的,它是組合多個RouteLocator的Locator,其 getMatchingRoute() 方法會分別調用其它所有RouteLocator的getMatchingRoute()方法,通過請求路徑匹配路由信息,只要匹配到了就馬上返回
默認CompositeRouteLocator混合路由定位器的routeLocators只有一個DiscoveryClientRouteLocator,故只需分析 DiscoveryClientRouteLocator#getMatchingRoute(path)br/>//----------DiscoveryClientRouteLocator是SimpleRouteLocator子類,其實是調用的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,如果此時還未加載,調用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());
}

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

ZuulRoute route = getZuulRoute(adjustedPath);

return getRoute(route, adjustedPath);

}
下面我們來看看 locateRoutes() 是如何加載靜態的、已配置的路由與來自DiscoveryClient服務發現的路由的
//----------DiscoveryClientRouteLocator#locateRoutes() 服務發現路由定位器的locateRoutes()br/>@Override
protected LinkedHashMap<String, ZuulRoute> locateRoutes() {
//保存ZuulRoute的LinkedHashMap
LinkedHashMap<String, ZuulRoute> routesMap = new LinkedHashMap<String, ZuulRoute>();

//調用父類SimpleRouteLocator#locateRoutes()
//加載ZuulProperties中的所有配置文件中的路由信息
routesMap.putAll(super.locateRoutes());

//如果服務發現客戶端discovery存在
if (this.discovery != null) {
    //將routesMap已經存在的配置文件中的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(); //到註冊中心找到所有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) + "/**";

        //如果註冊中心的serviceId在staticServices集合中,並且此路由沒有配置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);
            }
        }

        //如果註冊中心的serviceId不在忽略範圍內,且routesMap中還沒有包含,添加到routesMap
        if (!PatternMatchUtils.simpleMatch(ignored, serviceId)
                && !routesMap.containsKey(key)) {
            // Not ignored
            routesMap.put(key, new ZuulRoute(key, serviceId));
        }
    }
}

// 如果routesMap中有 /** 的默認路由配置
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中的數據微調後,放到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;

}
此方法運行後就已經加載了配置文件中所有路由信息,以及註冊中心中的服務路由信息,有的通過URL路由,有的通過serviceId路由
只需根據本次請求的requestURI與 路由的pattern匹配找到對應的路由br/>匹配到路由後,PreDecorationFilter如何設置RequestContext請求上下文
//----------PreDecorationFilter前置過濾器
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
final String requestURI = this.urlPathHelper.getPathWithinApplication(ctx.getRequest());
Route route = this.routeLocator.getMatchingRoute(requestURI); //找到匹配的路由
//----------------到上面爲止是已經分析過的,根據requestURI找到匹配的Route信息

// ==== 匹配到路由信息
if (route != null) {
    String location = route.getLocation();
    if (location != null) {
        ctx.put(REQUEST_URI_KEY, route.getPath());//RequestContext設置 requestURI:路由的pattern路徑
        ctx.put(PROXY_KEY, route.getId());//RequestContext設置 proxy:路由id

        //設置需要忽略的敏感頭信息,要麼用全局默認的,要麼用路由自定義的
        if (!route.isCustomSensitiveHeaders()) {
            this.proxyRequestHelper
                    .addIgnoredHeaders(this.properties.getSensitiveHeaders().toArray(new String[0]));
        }
        else {
            this.proxyRequestHelper.addIgnoredHeaders(route.getSensitiveHeaders().toArray(new String[0]));
        }

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

        //如果location是 http/https開頭的,RequestContext設置 routeHost:URL
        //如果location是 forward:開頭的,RequestContext設置 forward信息、routeHost:null
        //其它 RequestContext設置 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()));
        }
    }
}
// ==== 沒有匹配到路由信息
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("//", "/");
    ctx.set(FORWARD_TO_KEY, forwardURI);
}
return null;

}
總結:

只要引入了spring-cloud-starter-zuul就會間接引入Ribbon、Hystrix
路由信息可能是從配置文件中加載的,也可能是通過DiscoveryClient從註冊中心加載的
zuul是通過前置過濾器PreDecorationFilter找到與當前requestURI匹配的路由信息,並在RequestContext中設置相關屬性的,後續的Route Filter會根據RequestContext中的這些屬性判斷如何路由轉發
Route Filter主要使用 SimpleHostRoutingFilter 和 RibbonRoutingFilter
當RequestContext請求上下文中存在routeHost,即URL直連信息時,使用SimpleHostRoutingFilter簡單Host路由
當RequestContext請求上下文中存在serviceId,即服務id時(可能會與註冊中心關聯獲取服務列表,或者讀取配置文件中serviceId.ribbon.listOfServers的服務列表),使用RibbonRoutingFilter,會使用Ribbon、Hystrix

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