Spring Cloud - Zuul 原理解析

321 阅读6分钟

1、基本配置

1.1 请求头:

默认有些敏感的请求头不会转发给后端,比如 cookie,set-cookie , authorization ,也可以自己配置敏感请求头

zuul:
sensitiveHeaders: accept-language,cookie
    routes:
    demo:
     sensitiveHeaders:cookie

1.2 路由映射:

引入 actuator ,在配置文件中,配置 management.security.enabled 设置为 false ,就可以访问 /routes 地址,然后可以看到路由的映射信息

management:
  security:
    enabled: false
    
 // result
 
{
  "/demo/**": "https://www.baidu.com"
}

1.3 Hystrix 配置

与 ribbon 整合转发时,会使用 RibbonRoutingFilter ,转发会使用 hystrix 包裹请求,如果失败,执行降级逻辑

public class ServivceBFallbackProvider implements ZuulFallbackProvider {
    @Override
    public String getRoute() {
        return null;
    }

    @Override
    public ClientHttpResponse fallbackResponse() {
        return null;
    }
}

1.4 ribbon 客户端预加载

在默认情况下,第一次请求 zuul 才会初始化 ribbon 客户端,所以可以配置预加载,这样第一次就基本不会超时了。

zuul:
    ribbon:
    eager-load:
        enabled: true

1. 5、超时配置

zuul 使用 ribbon + hystrix 那套东西,所以,超时要考虑 hystrix 和 ribbon ,而且 hystrix 的超时要考虑 riibon 的重试次数和单次超时时间

hystrix 超时计算公示: (ribbon.connectTimeOut + ribbon.ReadTimeOut) * (ribbon.MaxAutoRetires + 1) * (ribbon.MaxAutoRetriesNextServer + 1)

ribbon:
    ReadTimeOut: 100
  ConnectTimeOut: 500
  MaxAutoRetries: 1
  MaxAutoRetriesNextServer: 1
  
# 如果不配置 ribbon 的超时时间,默认的 hystrix 超时时间是 4000 ms

2、高级配置

2.1 过滤器优先级

pre 过滤器

-3 :ServletDetectionFilter

-2:Servlet30WrapperFilter

-1:FromBodyWrapperFilter

1:DebugFilter

5: PreDecorationFilter

routing 过滤器

10: RibbonRoutingFilter

100: SimpleHostRoutingFilter

500: SendForwardFilter : 负责路由跳转

post 过滤器

900 : locationRewriteFilter

1000: SendResponseFilter

error 过滤器

0: SendErrorFilter

2.2 自定义过滤器

public class MyFilterClass extends ZuulFilter {
    @Override
    public String filterType() {
        // 在哪个阶段执行过滤其
        return FilterConstants.ROUTE_TYPE;
    }

    @Override
    public int filterOrder() {
        // 设置过滤器,优先级
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        // 是否执行过滤其
        return false;
    }

    @Override
    public Object run() {
        // 执行过滤器
        return null;
    }
}

@Configuration
public class FilterConfigClass {
    @Bean
    public MyFilterClass myFilter() {
        return new MyFilterClass();   
    }
}
    

2.3 动态加载过滤器

<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>2.4.12</version>
</dependency>
// 在Application类里
@PostConstruct 
public void zuulInit() {
    FilterLoader.getInstance().setCompiler(new GroovyCompiler());
    String scriptRoot = System.getProperty(“zuul.filter.root”, “groovy/filters”);
    String refreshInterval = System.getProperty(“zuul.filter.refreshInterval”, “5”);
    if(scriptRoot.length() > 0) {
        scriptRoot = scriptRoot + File.separator;
    }
    try {
        FilterFileManager.setFilenameFilter(new GroovyFileFilter());
        FilterFileManager.init(Integer.parseInt(refreshInterval), scriptRoot + “pre“, scriptRoot + “route”, scriptRoot + “post”);
    } catch(Exception e) {
        throw new RuntimeException(e);
    }
}

yml 文件中,配置相关属性

zuul:
    filter:
        root: “groovy/filters”  # 设置扫描路径
        refreshInterval: 5    # 设置间隔时间

在src/main/java/groovy/filters中,放一个MyFilter.groovy

class MyFilter extends ZuulFilter {
    public boolean shouldFilter() {
        return true;
    }
    public Object run() {
        System.out.println(“过滤器”);
        return null;
    }
    public String filterType() {
        return FilterConstants.ROUTE_TYPE;
    }
    public int filterOrder() {
        return 1;
    }
}

先启动网关项目,然后将这个过滤器放到指定目录,过几秒钟就会生效

2.4 禁用过滤器

zuul:
    SendForwardFilter:
        route:
            disable: true

2.5 RequestContext

在过滤器中,使用RequestContext.getCurrentContext(),可以获取到serviceId、requestURI等各种东西

2.6 @EnableZuulServer

这个的话,就是自动禁用掉PreDecorationFilter、RibbonRoutingFilter、SimpleHostRoutingFilter等过滤器。。。

2.7 error过滤器

在自定义过滤器里搞一个异常抛出来,ZuulException

然后写一个MyErrorController,继承BasicErrorController,统一处理异常,打印一些信息,这就是统一异常处理

统一异常处理

统一认证

统一限流

统一降级

3、核心原理

基本流程.png

具体的包在 package org.springframework.cloud:spring-cloud-netfix-core

image.png

3.1 EnableZuulProxy 注解

通过 import ZuulProxyMarkerConfiguration ,触发 ZuulProxyAutoConfiguration 的执行

@EnableCircuitBreaker  // 显然,这开启了熔断
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(ZuulProxyMarkerConfiguration.class)
public @interface EnableZuulProxy {
}

@ConditionalOnBean(ZuulProxyMarkerConfiguration.Marker.class) ,这个意思是说,必须有一个 ZuulProxyMarkerConfiguration.Marker 作为 Spring 容器中的 bean ,然后才能触发 ZuulProxyAutoConfiguration 的执行

@Configuration
@Import({ RibbonCommandFactoryConfiguration.RestClientRibbonConfiguration.class,
        RibbonCommandFactoryConfiguration.OkHttpRibbonConfiguration.class,
        RibbonCommandFactoryConfiguration.HttpClientRibbonConfiguration.class,
        HttpClientConfiguration.class })
@ConditionalOnBean(ZuulProxyMarkerConfiguration.Marker.class)
public class ZuulProxyAutoConfiguration extends ZuulServerAutoConfiguration {

}
    

3.2 入口类 ZuulServlet

@Override
    public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
        try {
            init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);

            // Marks this request as having passed through the "Zuul engine", as opposed to servlets
            // explicitly bound in web.xml, for which requests will not have the same data attached
            RequestContext context = RequestContext.getCurrentContext();
            context.setZuulEngineRan();

            try {
                preRoute();
            } catch (ZuulException e) {
                error(e);
                postRoute();
                return;
            }
            try {
                route();
            } catch (ZuulException e) {
                error(e);
                postRoute();
                return;
            }
            try {
                postRoute();
            } catch (ZuulException e) {
                error(e);
                return;
            }

        } catch (Throwable e) {
            error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));
        } finally {
            RequestContext.getCurrentContext().unset();
        }
    }

这里的话,会根据每次请求,都构建一个 RequestContext 对象,这个的话,为了保证每个请求相互隔离,采用了 threadLocal ,然后将原生的 servletRequest 交给 RequestContext ,将原生的 servletResponse 进行下包装,交给 RequestContext

public void init(HttpServletRequest servletRequest, HttpServletResponse servletResponse) {

        RequestContext ctx = RequestContext.getCurrentContext();
        if (bufferRequests) {
            ctx.setRequest(new HttpServletRequestWrapper(servletRequest));
        } else {
            ctx.setRequest(servletRequest);
        }

        ctx.setResponse(new HttpServletResponseWrapper(servletResponse));
    }

入口流程.png

3.3 pre 过滤器

首先在 servlet 类中,会首先去执行 pre 中的5个过滤器,拿到 pre 中的5个过滤器,并且按照执行的优先级进行排序。

public Object runFilters(String sType) throws Throwable {
        if (RequestContext.getCurrentContext().debugRouting()) {
            Debug.addRoutingDebug("Invoking {" + sType + "} type filters");
        }
        boolean bResult = false;
        // 这个就是负责拿到对应过滤器中的 filter ,并且是按照优先级排好序的 。
        List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType);
        if (list != null) {
            for (int i = 0; i < list.size(); i++) {
                ZuulFilter zuulFilter = list.get(i);
                Object result = processZuulFilter(zuulFilter);
                if (result != null && result instanceof Boolean) {
                    bResult |= ((Boolean) result);
                }
            }
        }
        return bResult;
    }

首先要执行的是 ServletDetectionFilter 过滤器,这个逻辑很简单,就是在 requestContext 中设置 IS_DISPATCHER_SERVLET_REQUEST_KEY 属性为 true。

public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        if (!(request instanceof HttpServletRequestWrapper) 
                && isDispatcherServletRequest(request)) {
            ctx.set(IS_DISPATCHER_SERVLET_REQUEST_KEY, true);
        } else {
            ctx.set(IS_DISPATCHER_SERVLET_REQUEST_KEY, false);
        }

        return null;
    }

然后会执行 Servlet30WrapperFilter 过滤器,这个逻辑很简单,在上一个过滤器给那个属性设置了 true ,所以它这的话,就是简单的构建了一个 Servlet30RequestWrapper 实例,放到 requestContext 对象中

public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        if (request instanceof HttpServletRequestWrapper) {
            request = (HttpServletRequest) ReflectionUtils.getField(this.requestField,
                    request);
            ctx.setRequest(new Servlet30RequestWrapper(request));
        }
        else if (RequestUtils.isDispatcherServletRequest()) {
            // If it's going through the dispatcher we need to buffer the body
            ctx.setRequest(new Servlet30RequestWrapper(request));
        }
        return null;
    }

之后会走 FormBodyWrapperFilter 过滤器,这里面就是简单通过 FormBodyRequestWrapper 给包装了一下,在放到 requestContext 中,默认的话,这个是不执行的,除非 MediaType 类型是 APPLICATION_FORM_URLENCODED 或者 MULTIPART_FORM_DATA 才会执行

public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        FormBodyRequestWrapper wrapper = null;
        if (request instanceof HttpServletRequestWrapper) {
            HttpServletRequest wrapped = (HttpServletRequest) ReflectionUtils
                    .getField(this.requestField, request);
            wrapper = new FormBodyRequestWrapper(wrapped);
            ReflectionUtils.setField(this.requestField, request, wrapper);
            if (request instanceof ServletRequestWrapper) {
                ReflectionUtils.setField(this.servletRequestField, request, wrapper);
            }
        }
        else {
            wrapper = new FormBodyRequestWrapper(request);
            ctx.setRequest(wrapper);
        }
        if (wrapper != null) {
            ctx.getZuulRequestHeaders().put("content-type", wrapper.getContentType());
        }
        return null;
    }

public boolean shouldFilter() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        String contentType = request.getContentType();
        // Don't use this filter on GET method
        if (contentType == null) {
            return false;
        }
        // Only use this filter for form data and only for multipart data in a
        // DispatcherServlet handler
        try {
            MediaType mediaType = MediaType.valueOf(contentType);
            return MediaType.APPLICATION_FORM_URLENCODED.includes(mediaType)
                    || (isDispatcherServletRequest(request)
                            && MediaType.MULTIPART_FORM_DATA.includes(mediaType));
        }
        catch (InvalidMediaTypeException ex) {
            return false;
        }
    }

然后是 DebugFilter 过滤器,这个的话,就是设置 degu 属性,会在后面多打印一些 debug 日志,这个的话,默认也是不执行的,除非 设置了 debug 属性 为 true ,才会执行

public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        ctx.setDebugRouting(true);
        ctx.setDebugRequest(true);
        return null;
    }


public boolean shouldFilter() {
        HttpServletRequest request = RequestContext.getCurrentContext().getRequest();
        if ("true".equals(request.getParameter(DEBUG_PARAMETER.get()))) {
            return true;
        }
        return ROUTING_DEBUG.get();
    }

最后是 PreDecorationFilter 过滤器,这个逻辑比较复杂,主要的核心就是 获取到请求地址,和配置文件中配置的路由规则进行匹配,然后往 requestContext 里面设置相关属性,比如是否重试,比如 host 地址, location 请求地址等等

public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        final String requestURI = this.urlPathHelper.getPathWithinApplication(ctx.getRequest());
        Route route = this.routeLocator.getMatchingRoute(requestURI);
        if (route != null) {
            String location = route.getLocation();
            if (location != null) {
                ctx.put(REQUEST_URI_KEY, route.getPath());
                ctx.put(PROXY_KEY, route.getId());
                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());
                }

                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);
                }
                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);
                }
                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;
    }

3.4 PreDecorationFilter 路由解析

zuul:
  routes:
    ServiceB:
      path: /demo/**

Map<String, ZuulRoute>

key = /demo/**

value = ZuulRoute(serviceId = “ServiceB”)

/demo/**

/demo/ServiceB/user/sayHello/1

是否匹配,如果匹配的话,就直接返回ZuulRoute,就是路由规则

ZuulRoute{id='ServiceB', path='/demo/**', serviceId='ServiceB', url='null', stripPrefix=true, retryable=null, sensitiveHeaders=[], customSensitiveHeaders=false, }

targetPath = /ServiceB/user/sayHello/1

prefix = /demo

return new Route(route.getId(), targetPath, route.getLocation(), prefix,
                retryable,
                route.isCustomSensitiveHeaders() ? route.getSensitiveHeaders() : null, 
                route.isStripPrefix());

创建了一个Route对象,先搞到了一个ZuulRoute的这么一个东西,这个东西里面封装了一些基本的路由规则,然后对这个ZuulRoute再次进行了解析,以及一些转换,尤其是处理出来了几个数据,封装了一个Route对象

**Route{id='ServiceB', fullPath='/demo/ServiceB/user/sayHello/1', path='/ServiceB/user/sayHello/1', location='ServiceB', prefix='/demo', retryable=false, sensitiveHeaders=[], customSensitiveHeaders=false, prefixStripped=true},**接下来就是将Route路由规则中的各种信息给放在了RequestContext中了,给下一个阶段route过滤器来使用

入口流程 (2).png

3.5 rout 过滤器

在上面的 pre 过滤器执行完毕之后,就会去执行 rout 过滤器,首先是会去执行 RibbonRoutingFilter 过滤器,这个的话,比较关键,在这就通过 ribbon 进行负载均衡发送请求到对应的服务了

public Object run() {
        RequestContext context = RequestContext.getCurrentContext();
        this.helper.addIgnoredHeaders();
        try {
            RibbonCommandContext commandContext = buildCommandContext(context);
            ClientHttpResponse response = forward(commandContext);
            setResponse(response);
            return response;
        }
        catch (ZuulException ex) {
            throw new ZuulRuntimeException(ex);
        }
        catch (Exception ex) {
            throw new ZuulRuntimeException(ex);
        }
    }

然后会执行 SimpleHostRoutingFilter , 将请求转发到某个 rul 地址,默认不执行,最后 SendForwardFilter , 将请求转发到 zuul 自己的地址,默认不执行。

3.6 RibbonRoutingFilter 服务转发

这里的话,主要是执行 forward() 方法的执行,构建出来 RibbonCommand,这个的话,本质上来说就是 HystrixCommand ,那么他在执行的时候,就是在执行 Hystrix Command 进行执行,熔断限流等等操作。

protected ClientHttpResponse forward(RibbonCommandContext context) throws Exception {
        Map<String, Object> info = this.helper.debug(context.getMethod(),
                context.getUri(), context.getHeaders(), context.getParams(),
                context.getRequestEntity());

        RibbonCommand command = this.ribbonCommandFactory.create(context);
        try {
            ClientHttpResponse response = command.execute();
            this.helper.appendDebug(info, response.getRawStatusCode(), response.getHeaders());
            return response;
        }
        catch (HystrixRuntimeException ex) {
            return handleException(info, ex);
        }
    }

在创建 RibbonCommand 的时候,会初始化 ribbon 相关的东西,通过 ribbon 进行负载均衡的操作,由 ZoneAwareLoadBaclancer 中的组件从 eureka client 中拉取注册表,根据服务对应的服务列表,通过负载均衡算法(chooseService) 选择一个要请求的服务地址出来,交由 hystrix 进行执行。

public HttpClientRibbonCommand create(final RibbonCommandContext context) {
        ZuulFallbackProvider zuulFallbackProvider = getFallbackProvider(context.getServiceId());
        final String serviceId = context.getServiceId();
        final RibbonLoadBalancingHttpClient client = this.clientFactory.getClient(
                serviceId, RibbonLoadBalancingHttpClient.class);
        client.setLoadBalancer(this.clientFactory.getLoadBalancer(serviceId));

        return new HttpClientRibbonCommand(serviceId, client, context, zuulProperties, zuulFallbackProvider,
                clientFactory.getClientConfig(serviceId));
    }

入口流程 (3).png

3.7 post 过滤器

post 的话,会执行两个过滤器,LocationRewriteFilter,SendResponseFilter,前者的话,只有在需要进行重定向也就是 3xx 状态码的时候,才会执行,将结果进行重定向,后者就是直接将请求结果写给浏览器。。

入口流程 (4).png

3.8 error 过滤器

在上述中任意的过滤器抛出异常,都会执行这个过滤器,SendErrorFilter

public boolean shouldFilter() {
        RequestContext ctx = RequestContext.getCurrentContext();
        // only forward to errorPath if it hasn't been forwarded to already
        return ctx.getThrowable() != null
                && !ctx.getBoolean(SEND_ERROR_FILTER_RAN, false);
    }
    public Object run() {
        try {
            RequestContext ctx = RequestContext.getCurrentContext();
            ZuulException exception = findZuulException(ctx.getThrowable());
            HttpServletRequest request = ctx.getRequest();

            request.setAttribute("javax.servlet.error.status_code", exception.nStatusCode);

            log.warn("Error during filtering", exception);
            request.setAttribute("javax.servlet.error.exception", exception);

            if (StringUtils.hasText(exception.errorCause)) {
                request.setAttribute("javax.servlet.error.message", exception.errorCause);
            }

            RequestDispatcher dispatcher = request.getRequestDispatcher(
                    this.errorPath);
            if (dispatcher != null) {
                ctx.set(SEND_ERROR_FILTER_RAN, true);
                if (!ctx.getResponse().isCommitted()) {
                    ctx.setResponseStatusCode(exception.nStatusCode);
                    dispatcher.forward(request, ctx.getResponse());
                }
            }
        }
        catch (Exception ex) {
            ReflectionUtils.rethrowRuntimeException(ex);
        }
        return null;
    }

入口流程 (5).png