【SpringCloud】Zuul在何种情况下使用Hystrix

首先,引入 spring-cloud-starter-zuul 之后会间接引入:

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

SendForwardFilterforward到本地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)

//----------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

欢迎工作一到五年的Java工程师朋友们加入Java架构开发: 855835163

群内提供免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)合理利用自己每一分每一秒的时间来学习提升自己,不要再用"没有时间“来掩饰自己思想上的懒惰!趁年轻,使劲拼,给未来的自己一个交代!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容