Zuul 入门教程 - 高级篇
大纲
前言
版本说明
在本文中,默认使用的 Spring Cloud 版本是 Finchley.RELEASE,对应的 Spring Boot 版本是 2.0.3,Zuul 版本是 1.x,特别声明除外。
Zuul 多层负载
痛点场景
在 Spring Cloud 微服务架构体系中,所有请求的前门的网关 Zuul 承担着请求转发的主要功能,对后端服务起着举足轻重的作用。当业务体量猛增之后,得益于 Spring Cloud 的横向扩展能力,往往加节点、加机器就可以使得系统支撑性获得大大提升,但是仅仅加服务而不加网关是会有性能瓶颈的,单一 Zuul 节点的处理能力十分有限。因此扩张节点往往是微服务连带 Zuul 一起扩张,一般会部署一个 Zuul 集群来横向扩展微服务应用,然后再在请求上层加一层软负载,通常是使用 Nginx 均分请求到 Zuul 集群(如下图)。此时若其中一台 Zuul 服务挂掉了,由于从 Nginx 到 Zuul 其实是没有什么关联性,如果 Zuul 服务宕掉,Nginx 还是会把请求导向到 Zuul 服务,导致从 Nginx 到这 Zuul 节点的请求会全部失效,在 Nginx 没有采取相关应对措施的情况下,这是十分严重的问题。
解决方案
OpenResty 整合了 Nginx 与 Lua,实现了可伸缩的 Web 服务器,内部集成了大量精良的 Lua 库、第三方模块以及多数的依赖项,能够非常快捷地搭建处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。开发者可以使用 Lua 脚本模块与注册中心构建一个服务动态增减的机制,通过 Lua 获取注册中心状态为 UP 的服务,动态地加入到 Nginx 的负载均衡列表中去,由于这种架构模式涉及了不止一个负载均衡器,一般称其为 “多层负载”(如下图)。
目前 Spring Cloud 中国社区针对这一场景开源了相关的 Lua 插件源码,GitHub 地址在这里,核心配置如下。实现原理是使用 Lua 脚本定时根据配置的服务名与 Eureka 地址,去拉取该服务的信息,在 Eureka 里面提供 /eureka/apps/(serviceld)
端点,返回服务的注册信息,所以只需要取用状态为 UP
的服务,将它的地址加入 Nginx 负载列表即可。此项目使得 Nginx 与 Zuul 之间 拥有一个动态感知能力,不用手动配置 Nginx 负载与 Zuul 负载,这样对于应用弹性扩展是极其友好的。
1 | http { |
Zuul 应用优化
概述
Zuul(这里指 Zuul1.0 版本,Zuul2.0 版本之后使用了 Netty 的异步非阻塞模型)在给微服务体系带来诸多便利的同时,也饱受着性能的争议,这一切还要从它的底层架构说起。Zuul 是建立在 Servlet 的同步阻塞架构基础上,所以在处理逻辑上面是和线程密不可分的,每一次请求都需要从线程池获取一个线程来维持 I/O 操作,路由转发的时候又需要从 HTTP 客户端获取线程来维持连接,这就会导致一个组件占用两个线程资源的情况。所以,在 Zuul 的使用中,对这部分的优化是很有必要的,一个好的优化体系会使得应用支撑的业务体量更大,也能最大化利用服务器资源。在这里,将对 Zuul 的优化分为以下几个类型:
- 容器优化:内置容器 Tomcat 与 Undertow 的比较与参数设置
- 组件优化:内部集成的组件优化,如 Hystrix 线程隔离、Ribbon. HttpClient 与 OkHttp 选择
- JVM 参数优化:适用于网关应用的 JVM 参数建议
- 内部优化:一些内部原生参数,或者内部源码,以一种更恰当的方式重写它们
容器优化
关于 Spring Boot 优化的文章,网上有很多,不过大部分都会提到把默认的内嵌容器 Tomcat 替换成 Undertow。其中 Undertow 翻译为” 暗流”,即平静的湖面下暗藏着波涛汹涌,所以 JBoss 公司取其意,为它的轻量级高性能容器命名。Undertow 提供阻塞或基于 XNIO 的非阻塞机制,它的包大小不足 1MB,内嵌模式运行时的堆内存占用只有 4MB 左右。要使用 Undertow ,只需要在配置文件中移除 Tomcat,添加 Undertow 的依赖 即可:
1 | <dependency> |
Undertow 的主要配置参数如下:
组件优化
在 Spring Cloud 微服务体系中,Zuul 是一个容易被忽略优化,但是集成组件最多,功能最强大的组件。Zuul 网关主要用于智能路由,同时也支持认证、区域和内容感知路由,将多个底层服务聚合成统一的对外 API。所以要更好地使用 Zuul,就免不了要对它集成的组件进行优化,使它可以更好地支撑服务集群。
Hystrix 优化
由于 Zuul 默认集成了 Hystrix 熔断器,使得网关应用具有弹性、容错的能力。但是如果使用缺省的配置,可能会遇到种种问题,其中最常见的问题就是当启动 Zuul 应用之后,第一次请求往往会失败。根本原因是 Hystrix 默认的超时时间是 1 秒,如果超过这个时间尚未作出响应,将会进入 fallback 代码。由于在处理第一次请求的时候,Zuul 内部要初始化很多类信息,这是十分耗时的,如果这个响应时间超过 1 秒,就会出现请求失败的问题。解决方式有以下两种:
- 禁用 Hystrix 的超时时间:
hystrix.command.default.execution.timeout.enabled=false
- 加大 Hystrix 的超时时间:
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=5000
Zuul 中关于 Hystrix 的配置还有一个很重要的点,那就是 Hystrix 的线程隔离模式的选择,包括线程池隔离模式(THREAD)或者信号量隔离模式(SEMAPHORE)。在网关中,对资源的使用是应该受到严格控制的,如果不加限制,会导致资源滥用,在恶劣的线上环境下就容易引起服务雪崩。两种隔离模式对比,如下图所示。
Hystrix 切换隔离模式的配置方式:
1 | hystrix.command.default.execution.isolation.strategy=Thread | Semaphore |
Hystrix 隔离模式选择总结,当应用需要与外网交互,由于网络开销比较大与请求比较耗时,这时选用线程隔离策略,可以保证有剩余的容器(Tomcat & Undertow & Jetty)线程可用,而不会由于外部原因使得线程一直处于阻塞或等待状态,可以快速失败返回。但当微服务应用只在内网交互,并且体量比较大,这时使用信号量隔离策略就比较好,因为这类应用的响应通常会非常快(由于在内网),不会占用容器线程太长时间,可以减少线程上下文切换的开销,提高应用运转的效率,也可以起到对请求进行全局限流的作用。
Ribbon 优化
这里主要是讲 Ribbon 的超时重试优化,在 Spring Cloud 中有多种发送 HTTP 请求的方式可以与 Zuul 结合,RestTemplate、Ribbon 或者 Feign,但是无论选择哪种,都可能出现请求失败的情况,这在复杂的互联网环境是不可避免的。Zuul 作为一个网关中间件,在出现偶然请求失败时进行适当的重试是十分必要的,重试可以有效地避免一些突发原因引起的请求丢失。Zuul 中的重试机制是配合 Spring Retry 与 Ribbon 来使用的。
在 pom.xml
引入 Spring Retry 的依赖包:
1 | <dependency> |
在 application.yml
里添加重试相关的配置内容:
1 | #Zuul开启重试,D版之后默认为false,需要手动开启 |
配置当中的 ConnectTimeout
与 ReadTimeou
是当 HTTP 客户端使用 Apache HttpClient 的时候生效的,这个超时时间最终会被设置到 Apache HttpClient 中去。在设置的时候要结合 Hystrix 的超时时间来综合考虑,针对不同的应用场景,设置太小会导致很多请求失败,设置太大会导致熔断功能控制性变差,所以需要经过压力测试得来。Zuul 同时也支持对单个映射规则进行重试 zuul.routes.<route>.retryable=true
,需要注意的是,在某些对幂等要求比较高的使用场景下,要慎用重试机制,因为如果没有相关处理的话,出现幂等问题是十分有可能的。
内部优化
在官方文档中,Zuul 部分开篇讲了 zuul.max.host.connections
属性拆解成了 zuul.host.maxTotalConnections
(服务 HTTP 客户端最大连接数)与 zuul.host.maxPerRouteConnections
(每个路由规则 HTTP 客户端最大连接数),默认值分别为 200 与 20,如果使用 Apache HttpClient 的时候这两个配置参数则有效,如果使用 OkHttp 则无效。在 Zuul 中还有一个超时时间,使用 serviceld 映射与 url 映射的设置是不一样的,如果使用 serviceld 映射,ribbon.ReadTimeout
与 ribbon.SocketTimeout
生效;如果使用 url 映射,应该设置 zuul.host.connect-timeout-millis
与 zuul.host.socket-timeout-millis
参数。