1. 程式人生 > >【SpringCloud】Zuul在何種情況下使用Hystrix

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

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


hystrix依賴已經引入,那麼何種情況下使用hystrix呢?

在Zuul的自動配置類ZuulServerAutoConfigurationZuulProxyAutoConfiguration中總共會向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.RouteZuulProperties.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)

//----------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()
@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匹配找到對應的路由


匹配到路由後,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