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 没有采取相关应对措施的情况下,这是十分严重的问题。

zuul-nginx

解决方案

OpenResty 整合了 Nginx 与 Lua,实现了可伸缩的 Web 服务器,内部集成了大量精良的 Lua 库、第三方模块以及多数的依赖项,能够非常快捷地搭建处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。开发者可以使用 Lua 脚本模块与注册中心构建一个服务动态增减的机制,通过 Lua 获取注册中心状态为 UP 的服务,动态地加入到 Nginx 的负载均衡列表中去,由于这种架构模式涉及了不止一个负载均衡器,一般称其为 “多层负载”(如下图)。

zuul-openresty

目前 Spring Cloud 中国社区针对这一场景开源了相关的 Lua 插件源码,GitHub 地址在这里,核心配置如下。实现原理是使用 Lua 脚本定时根据配置的服务名与 Eureka 地址,去拉取该服务的信息,在 Eureka 里面提供 /eureka/apps/(serviceld) 端点,返回服务的注册信息,所以只需要取用状态为 UP 的服务,将它的地址加入 Nginx 负载列表即可。此项目使得 Nginx 与 Zuul 之间 拥有一个动态感知能力,不用手动配置 Nginx 负载与 Zuul 负载,这样对于应用弹性扩展是极其友好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
http {
#sharing cache area
lua_shared_dict dynamic_eureka_balancer 128m;

init_worker_by_lua_block {
-- init eureka balancer
local file = require "resty.dynamic_eureka_balancer"
local balancer = file:new({dict_name="dynamic_eureka_balancer"})

--eureka server list
balancer.set_eureka_service_url({"127.0.0.1:8888", "127.0.0.1:9999"})

--eureka basic authentication
--use this setting if eureka has enabled basic authentication.
--note: basic authentication must use BASE64 encryption in `user:password` format
--balancer.set_eureka_service_basic_authentication("")

--The service name that needs to be monitored
balancer.watch_service({"zuul", "client"})
}

upstream springcloud_cn {
server 127.0.0.1:666; # Required, because empty upstream block is rejected by nginx (nginx+ can use 'zone' instead)

balancer_by_lua_block {

--The zuul name that needs to be monitored
local service_name = "zuul"

local file = require "resty.dynamic_eureka_balancer"
local balancer = file:new({dict_name="dynamic_eureka_balancer"})

--balancer.ip_hash(service_name) --IP Hash LB
balancer.round_robin(service_name) --Round Robin LB
}
}

server {
listen 80;
server_name localhost;

location / {
proxy_pass http://springcloud_cn/;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}

error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupld>org.springframework.boot</groupld>
<artifactld>spring-boot-starter-tomcat</artifactld>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupld>org.springframework.boot</groupld>
<artifactld>spring-boot-starter-undertow</artifactld>
</dependency>

Undertow 的主要配置参数如下:

undertow-config

组件优化

在 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-isolation

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
2
3
4
<dependency>
<groupld>org.springframework.retry</groupld>
<artifactld>spring-retry</artifactld>
</dependency>

application.yml 里添加重试相关的配置内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#Zuul开启重试,D版之后默认为false,需要手动开启
zuul:
retryable: true

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

#SpringCloud内部默认已开启负载均衡重试,这里列出来说明这个参数比较重要
spring:
cloud:
loadbalancer:
retry:
enabled: true

配置当中的 ConnectTimeoutReadTimeou 是当 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.ReadTimeoutribbon.SocketTimeout 生效;如果使用 url 映射,应该设置 zuul.host.connect-timeout-milliszuul.host.socket-timeout-millis 参数。

Zuul 源码解析(待续)