zoukankan      html  css  js  c++  java
  • 【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
  • 相关阅读:
    VSS使用
    Delphi简单数据库连接程序
    为表增加字段与拷贝数据到另一个表
    VSTS 使用
    Delphi实现个相似的功能界面共用一个窗体
    看代码笔记
    数据库安全管理
    函数
    【USACO】Ordered Fractions 顺序的分数
    C# 专业数据库连接配置界面
  • 原文地址:https://www.cnblogs.com/trust-freedom/p/9982680.html
Copyright © 2011-2022 走看看