Spring Cloud 学习(25) --- Zuul(七) Zuul 优化、Zuul 工作原理

Zuul 作为一个网关中间件,需要应付各种复杂场景,整合的组件非常繁杂。在受益于其丰富的功能时,也需要面对很多问题。如:与上层负载均衡器(Nginx等)、性能、调优等。

Zuul 应用优化

Zuul 是建立在 Servlet 上的同步阻塞架构,所有在处理逻辑上面是和线程密不可分,每一次请求都需要在线程池获取一个线程来维护 I/O 操作,路由转发的时候又需要从 http 客户端获取线程来维持连接,这样会导致一个组件占用两个线程资源的情况。所以在 Zuul 的使用中,对这部分的优化很有必要。

Zuul 的优化分为以下几个类型:

  • 容器优化:内置容器 tomcat 与 undertow 的比较与参数设置
  • 组件优化:内部集成的组件优化,如 Hystrix 线程隔离、Ribbon、HttpClient、OkHttp 选择等
  • JVM 参数优化:适用于网关应用的 JVM 参数建议
  • 内部优化:内部原生参数,内部源码,重写等

容器优化

把 tomcat 替换为 undertow。undertow 翻译为“暗流”,是一个轻量级、高性能容器。undertow 提供阻塞或基于 XNIO 的非阻塞机制,包大小不足 1M,内嵌模式运行时的堆内存占用只有 4M。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
server:
  undertow:
    io-threads: 10
    worker-threads: 10
    direct-buffers: true
    buffer-size: 1024 # 字节数
配置项 默认值 说明
server.undertow.io-threads Math.max(Runtime.getRuntime().availableProcessors(), 2) 设置 IO 线程数,它主要执行非阻塞的任务,它们负责多个连接,默认设置每个 CPU 核心有一个线程。不要设置太大,否则启动项目会报错:打开文件数过多
server.undertow.worker-threads io-threads * 8 阻塞任务线程数,当执行类型 Servlet 请求阻塞 IO 操作,undertow 会从这个线程池中取得线程。值设置取决于系统线程执行任务的阻塞系统,默认是 IO 线程数 * 8
server.undertow.direct-buffers 取决于 JVM 最大可用内存大小Runtime.getRuntime().maxMemory(),小于 64MB 默认为 false,其余默认为 true 是否分配直接内存(NIO 直接分配的堆外内存
server.undertow.buffer-size 最大可用内存 <64MB:512 字节;64MB< 最大可用内存 <128MB:1024 字节;128MB < 最大可用内存:1024*16 - 20 字节 每块 buffer 的空间大小,空间越小利用越充分,设置太大会影响其他应用
server.undertow.buffers-per-region 最大可用内存 <128MB:10;128MB < 最大可用内存:20 每个区域分配的 buffer 数量,pool 大小是 buffer-size * buffer-per-region

组件优化

Hystrix

在 Zuul 中默认集成了 Hystrix 熔断器,使得网关应用具有弹性、容错的能力。但是如果使用默认配置,可能会遇到问题。如:第一次请求失败。这是因为第一次请求的时候,zuul 内部需要初始化很多信息,十分耗时。而 hystrix 默认超时时间是一秒,可能会不够。

解决方式:

  • 加大超时时间
hystrix:
 command:
   default: 
     execution:
       isolation:
         thread:
           timeoutInMilliseconds: 5000 
  • 禁用 hystrix 超时
hystrix:
 command:
   default: 
     execution:
       timeout:
         enabled: false    

Zuul 中关于 Hystrix 的配置还有一个很重要的点:Hystrix 线程隔离策略

线程池模式(THREAD) 信号量模式(SEMAPHORE)
官方推荐
线程 与请求线程分离 与请求线程公用
开销 上下文切换频繁,较大 较小
异步 支持 不支持
应对并发量
适用场景 外网交互 内网交互

如果应用需要与外网交互,由于网络开销比较大、请求比较耗时,选用线程隔离,可以保证有剩余容器(tomcat 等)线程可用,不会由于外部原因使线程一直在阻塞或等待状态,可以快速返回失败
如果应用不需要与外网交互,并且体量较大,使用信号量隔离,这类应用响应通常非常快,不会占用容器线程太长时间,使用信号量线程上下文就会成为一个瓶颈,可以减少线程切换的开销,提高应用运转的效率,也可以气到对请求进行全局限流的作用。

Ribbon

ribbon:
  ConnectTimeout: 3000
  ReadTimeout: 60000
  MaxAutoRetries: 1 # 对第一次请求的发我的重试次数
  MaxAutoRetriesNextServer: 1 # 要重试的下一个服务的最大数量(不包括第一个服务)
  OkToRetryOnAllOperations: true

ConnectTimeoutReadTimeout 是当前 HTTP 客户端使用 HttpClient 的时候生效的,这个超时时间最终会被设置到 HttpClient 中。在设置的时候要结合 Hystrix 超时时间综合考虑。设置太小会导致请求失败,设置太大会导致 Hystrix 熔断控制变差。

JVM 参数优化

根据实际情况,调整 JVM 参数

内有优化

在官方文档中,zuul 部分将 zuul.max.host.coonnections 属性拆分成了 zuul.host.maxTotalConnectionszuul.host.maxPerRouteConnections,默认值分别为 200、20。
需要注意:这个配置只在使用 HttpClient 时有效,使用 OkHttp 无效。

zuul 中还有一个超时时间,使用 serviceId 映射与 url 映射的设置是不一样的,如果使用 serviceId 映射,ribbon.ReadTimeoutribbon.SocketTimeout 生效;如果使用 url 映射,zuul.host.connect-timeout-milliszuul.host.socket-timeout-millis 生效


Zuul 原理、核心

zuul 官方提供了一张架构图,很好的描述了 Zuul 工作原理

Zuul 架构图

Zuul Servlet 通过 RequestContext 通关着由许多 Filter 组成的核心组件,所有操作都与 Filter 息息相关。请求、ZuulServlet、Filter 共同构建器 Zuul 的运行时声明周期

zuul life cycle

Zuul 的请求来自于 DispatcherServlet,然后交给 ZuulHandlerMapping 处理初始化得来的路由定位器RouteLocator,为后续的请求分发做好准备,同时整合了基于事件从服务中心拉取服务列表的机制;
进入 ZuulController,主要职责是初始化 ZuulServlet 以及集成 ServletWrappingController,通过重写 handleRequest 方法来将 ZuulServlet 引入声明周期,之后所有的请求都会经过 ZuulServlet;
当请求进入 ZuulServlet 之后,第一次调用会初始化 ZuulRunner,非第一次调用就按照 Filter 链的 order 顺序执行;
ZuulRunner 中将请求和响应初始化为 RequestContext,包装成 FilterProcessor 转换为为调用 preRoute、route、postRoute、error 方法;
最后再 Filter 链中经过种种变换,得到预期结果。

EnableZuulProxy、EnableZuulServer

对比 EnableZuulProxyEnableZuulServer

@EnableCircuitBreaker
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(ZuulProxyMarkerConfiguration.class)
public @interface EnableZuulProxy {
}
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({ZuulServerMarkerConfiguration.class})
public @interface EnableZuulServer {
}

这两个注解的区别在于 @Import 中的配置类不一样。查看两个配置类的源码:

/**
 * Responsible for adding in a marker bean to trigger activation of 
 * {@link ZuulProxyAutoConfiguration}
 *
 * @author Biju Kunjummen
 */

@Configuration
public class ZuulProxyMarkerConfiguration {
    @Bean
    public Marker zuulProxyMarkerBean() {
        return new Marker();
    }

    class Marker {
    }
}
/**
 * Responsible for adding in a marker bean to trigger activation of 
 * {@link ZuulServerAutoConfiguration}
 *
 * @author Biju Kunjummen
 */

@Configuration
public class ZuulServerMarkerConfiguration {
    @Bean
    public Marker zuulServerMarkerBean() {
        return new Marker();
    }

    class Marker {
    }
}

可以看到,这两个配置类的源码一致,区别在于类的注释上 @link 指向的自动装配类不一样,ZuulProxyMarkerConfiguration 对应的是 ZuulProxyAutoConfigurationZuulServerMarkerConfiguration 对应的是 ZuulServerAutoConfiguration

查看 ZuulProxyAutoConfigurationZuulServerAutoConfiguration 的类注解

@Configuration
@Import({ RibbonCommandFactoryConfiguration.RestClientRibbonConfiguration.class,
        RibbonCommandFactoryConfiguration.OkHttpRibbonConfiguration.class,
        RibbonCommandFactoryConfiguration.HttpClientRibbonConfiguration.class,
        HttpClientConfiguration.class })
@ConditionalOnBean(ZuulProxyMarkerConfiguration.Marker.class)
public class ZuulProxyAutoConfiguration extends ZuulServerAutoConfiguratio
}
@Configuration
@EnableConfigurationProperties({ ZuulProperties.class })
@ConditionalOnClass({ZuulServlet.class, ZuulServletFilter.class})
@ConditionalOnBean(ZuulServerMarkerConfiguration.Marker.class)
public class ZuulServerAutoConfiguration {
}

可以发现,这两个类是通过 ZuulServerMarkerConfigurationZuulProxyMarkerConfiguration 中 Marker 类是否存在,当做是否进行自动装配的开关。
对比两个 AutoCOnfiguration 的具体源码实现,经过对比,可以分析出:
ZuulServerAutoConfiguration 的功能是:

  • 初始化配置加载器
  • 初始化路由定位器
  • 初始化路由映射器
  • 初始化配置刷新监听器
  • 初始化 ZuulServlet 加载器
  • 初始化 ZuulController
  • 初始化 Filter 执行解析器
  • 初始化部分 Filter
  • 初始化 Metrix 监控

ZuulProxyAutoConfiguration 的功能是:

  • 初始化服务注册、发现监听器
  • 初始化服务列表监听器
  • 初始化 zuul 自定义的 endpoint
  • 初始化一些 ZuulServerAutoConfiguration 中没有的 filter'
  • 引入 http 客户端的两种方式:HttpClient、OkHttp

Filter 链

filter 装载

zuul 中的 Filter 必须经过初始化装载,才能在请求中发挥作用,其过程如下


zuul-filter-life-cycle

zuul filter 连初始化过程

public class ZuulServerAutoConfiguration {

  // 省略其他代码

  @Configuration
  protected static class ZuulFilterConfiguration {
    @Autowired
    private Map<String, ZuulFilter> filters;
    @Bean
    public ZuulFilterInitializer zuulFilterInitializer(
      CounterFactory counterFactory, TracerFactory tracerFactory) {
      FilterLoader filterLoader = FilterLoader.getInstance();
      FilterRegistry filterRegistry = FilterRegistry.instance();
      return new ZuulFilterInitializer(this.filters, counterFactory, tracerFactory, filterLoader, filterRegistry);
    }
  }
}
public class ZuulFilterInitializer {

  // 省略其他代码

  // @PostConstruct:表明在 Bean 初始化之前,就把 Filter 的信息保存到 FilterRegistry
    @PostConstruct
    public void contextInitialized() {
        log.info("Starting filter initializer");

        TracerFactory.initialize(tracerFactory);
        CounterFactory.initialize(counterFactory);

        for (Map.Entry<String, ZuulFilter> entry : this.filters.entrySet()) {
            filterRegistry.put(entry.getKey(), entry.getValue());
        }
    }

  // @PreDestroy:表明在 bean 销毁之前清空 filterRegistry 与 FilterLoader。filterloader 可以通过 filter 名、filter class、filter 类型来查询得到相应的 filter
    @PreDestroy
    public void contextDestroyed() {
        log.info("Stopping filter initializer");
        for (Map.Entry<String, ZuulFilter> entry : this.filters.entrySet()) {
            filterRegistry.remove(entry.getKey());
        }
        clearLoaderCache();

        TracerFactory.initialize(null);
        CounterFactory.initialize(null);
    }

  private void clearLoaderCache() {
        Field field = ReflectionUtils.findField(FilterLoader.class, "hashFiltersByType");
        ReflectionUtils.makeAccessible(field);
        @SuppressWarnings("rawtypes")
        Map cache = (Map) ReflectionUtils.getField(field, filterLoader);
        cache.clear();
    }
}

zuul filter 请求调用过

public class ZuulServletFilter implements Filter {

    private ZuulRunner zuulRunner;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

        String bufferReqsStr = filterConfig.getInitParameter("buffer-requests");
        boolean bufferReqs = bufferReqsStr != null && bufferReqsStr.equals("true") ? true : false;

        zuulRunner = new ZuulRunner(bufferReqs);
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        try {
            init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);
            try {
                preRouting();
            } catch (ZuulException e) {
                error(e);
                postRouting();
                return;
            }
            
            // Only forward onto to the chain if a zuul response is not being sent
            if (!RequestContext.getCurrentContext().sendZuulResponse()) {
                filterChain.doFilter(servletRequest, servletResponse);
                return;
            }
            
            try {
                routing();
            } catch (ZuulException e) {
                error(e);
                postRouting();
                return;
            }
            try {
                postRouting();
            } catch (ZuulException e) {
                error(e);
                return;
            }
        } catch (Throwable e) {
            error(new ZuulException(e, 500, "UNCAUGHT_EXCEPTION_FROM_FILTER_" + e.getClass().getName()));
        } finally {
            RequestContext.getCurrentContext().unset();
        }
    }

    void postRouting() throws ZuulException {
        zuulRunner.postRoute();
    }
    
    // 省略其他代码
}
public class ZuulRunn{

  // 省略其他代码

  public void postRoute() throws ZuulException {
        FilterProcessor.getInstance().postRoute();
  }

  // 省略其他代码
}
public class FilterProcessor {
    public void postRoute() throws ZuulException {
        try {
            runFilters("post");
        } catch (ZuulException e) {
            throw e;
        } catch (Throwable e) {
            throw new ZuulException(e, 500, "UNCAUGHT_EXCEPTION_IN_POST_FILTER_" + e.getClass().getName());
        }
    }

    public Object runFilters(String sType) throws Throwable {
        if (RequestContext.getCurrentContext().debugRouting()) {
            Debug.addRoutingDebug("Invoking {" + sType + "} type filters");
        }
        boolean bResult = false;
        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;
    }

    // 省略其他代码
}

核心路由的实现

Zuul 的路由有一个顶级接口 RouteLocator。所有关于路由的功能都是由此而来,其中定义了三个方法:获取忽略的 path 集合、获取路由列表、根据 path 获取路由信息。

Route 类图

SimpleRouteLocator 是一个基本实现,主要功能是对 ZuulServer 的配置文件中路由规则的维护,实现了 Ordered 接口,可以对定位器优先级进行设置。
Spring 是一个大量使用策略模式的框架,在策略模式下,接口的实现类有一个优先级问题,Spring 通过 Ordered 接口实现优先级。

ZuulProperties$ZuulRoute 类就是维护路由规则的类,具体属性如下:

public static class ZuulRoute {

        private String id;

        private String path;

        private String serviceId;

        private String url;

        private boolean stripPrefix = true;
    
        private Boolean retryable;

        private Set<String> sensitiveHeaders = new LinkedHashSet<>();

        private boolean customSensitiveHeaders = false;
}

RefreshableRouteLocator 扩展了 RouteLocator 接口,在 ZuulHandlerMapping 中才实质性生效:凡是实现了 RefreshableRouteLocator,都会被时间监听器所刷新:

public void setDirty(boolean dirty) {
        this.dirty = dirty;
        if (this.routeLocator instanceof RefreshableRouteLocator) {
            ((RefreshableRouteLocator) this.routeLocator).refresh();
        }
    }

DiscoveryClientRouteLocator 实现了 RefreshableRouteLocator,扩展了 SimpleRouteLocator。其作用是整合配置文件与注册中心的路由信息。

CompositeRouteLocator,在 ZuulServerAutoConfiguration 中配置加载时,有一个很重要的注解:@Primary,表示所有的 RouteLocator 中,优先加载它,也就是说,所有的定位器都要在这里装配,可以看做其他路由定位器的处理器。
zuul 通过它来将请求域路由规则进行关联,这个操作在 ZuulHandlerMapping 中:

public class ZuulHandlerMapping extends AbstractUrlHandlerMapping {

  // 省略其他代码

    private final RouteLocator routeLocator;

    private final ZuulController zuul;

  public ZuulHandlerMapping(RouteLocator routeLocator, ZuulController zuul) {
        this.routeLocator = routeLocator;
        this.zuul = zuul;
        setOrder(-200);
    }

  @Override
    protected Object lookupHandler(String urlPath, HttpServletRequest request) throws Exception {
        if (this.errorController != null && urlPath.equals(this.errorController.getErrorPath())) {
            return null;
        }
        if (isIgnoredPath(urlPath, this.routeLocator.getIgnoredPaths())) return null;
        RequestContext ctx = RequestContext.getCurrentContext();
        if (ctx.containsKey("forward.to")) {
            return null;
        }
        if (this.dirty) {
            synchronized (this) {
                if (this.dirty) {
                    registerHandlers();
                    this.dirty = false;
                }
            }
        }
        return super.lookupHandler(urlPath, request);
    }

  private void registerHandlers() {
        Collection<Route> routes = this.routeLocator.getRoutes();
        if (routes.isEmpty()) {
            this.logger.warn("No routes found from RouteLocator");
        }
        else {
            for (Route route : routes) {
                registerHandler(route.getFullPath(), this.zuul);
            }
        }
    }

}

ZuulHandlerMapping 将映射规则交给 ZuulController 处理,而 ZuulController 又到 ZuulServlet 中处理,最后到达异域或源服务发送 http 请求的 route 类型的 filter 中,默认有三种发送 http 请求的 filter

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

推荐阅读更多精彩内容