一 spring cloud Zuul底层架构
spring cloud zuul融合的是netflix 的zuul 1.x版本
1.1 netflix 的1.x的线程模型
zuul1.x采用的是线程阻塞模型,也就是我们常说的BIO,每来一个请求就会从线程池中分配一个线程去处理。这里导致的问题就是如果每一次请求的耗时很长,i/o操作时间很长,就会导致大量线程被挂起,利用率不仅低,很容易耗尽容器线程池内的线程,造成容器无法接受新的请求。所以对于这种io密集型,大请求来说,是不合适的。他适合那种小请求,cpu密集型,如果每一次请求只需要0.0几秒,一个线程1s钟也可以处理上百次请求,再加上简单,所以对于流量不是特别大,请求时间很短的场景是很实用的。
应用场景:
- cpu密集型任务
- 简单操作的需求
- 开发简单的需求
- 实时请求高的
1.2 zuul 2.x 的线程模型
可以简单理解为有一个队列专门负责处理用户请求(如果连接量大,可以开个线程组来处理),后端有个队列专门负责处理后台服务调用(如果连接量大,可以开个线程组来处理),中间有个事件环线程(Event Loop Thread),它同时监听前后两个队列上的事件,有事件就触发回调函数处理事件。这种模式下需要的线程比较少,基本上每个CPU核上只需要一个事件环处理线程,前端的连接数可以很多,连接来了只需要进队列,不需要启动线程,事件环线程由事件触发,没有多线程阻塞问题。但是zuul2.x带来的问题就是开发成本大,对于小请求来说他的性能提升不明显。
应用场景:
- io密集的任务
- 大请求或者大文件
- 队列的流式数据
- 超大量的连接
1.3 Zulu 1.x 的架构
每一次请求都会通过servlet,然后进入各种过滤器,最后再返回给客户端。而各个filters之间不会直接进行通信的。zuul 1.x使用RequestContext来实现各个filters之间共享数据,而RequestContext采用ConcurrentHashMap和ThreadLocal实现线程安全。
1.4 Zuul 1.x requestLifeCycle
一次请求的生命周期就如上图所示。可见最重要的一环就是各种过滤器。
过滤器类型:
PRE:这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
ROUTING:这种过滤器将请求路由到微服务。这种过滤器用于构建发送给微服务的请求,并使用Apache HttpClient或Netfilx Ribbon请求微服务。
POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
-
ERROR:在其他阶段发生错误时执行该过滤器
对于我们的业务来说,我们可以结合我们的业务来定制适合自己的过滤器。
定制自己的过滤器其实也很简单,只继成ZuulFilter,实现
String filterType();
int filterOrder();
boolean shouldFilter();
-
Object run();
其中:
filterType:返回过滤器的类型。有pre、route、post、error等几种取值,分别对应上文的几种过滤器
filterOrder:返回一个int值来指定过滤器的执行顺序,不同的过滤器允许返回相同的数字。
shouldFilter:返回一个boolean值来判断该过滤器是否要执行,true表示执行,false表示不执行。可以在此添加一些条件
run:过滤器的具体逻辑。一个对token进行check过滤器的例子:
* @author zhaokai008@ke.com
* @date 2019-05-11 21:45
*/
public class TokenFilters {
private static Logger LOGGER = LoggerFactory.getLogger(TokenFilter.class);
@Override
public String filterType() {
return PRE_TYPE;
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
//todo 什么时候需要验证token
return true;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String url = request.getRequestURI();
String token = request.getHeader("token");
checkToken(url,token);
return null;
}
private boolean checkToken(String url, String token) {
//todo dosomething
return true;
}
}
二 配置详解
zuul:
SendErrorFilter:
error:
disable: true
sensitive-headers:
#ignored-services: '*'
# 限流
ratelimit:
enabled: true
fallback: true
repository: in_memory
# 全局限流
default-policy-list:
- limit: 10000 #optional - request number limit per refresh interval window
quota: 300 #optional - request time limit per refresh interval window (in seconds)
refresh-interval: 60 #default value (in seconds)
type: #optional
- url
# 分route限流
policy-list:
test1:
- limit: 1 #optional - request number limit per refresh interval window
quota: 10 #optional - request time limit per refresh interval window (in seconds)
refresh-interval: 1 #default value (in seconds)
type: #optional
- url
routes:
test1:
path: /api/10/search/**
serviceId: common
stripPrefix: false
token: 10
test2:
path: /api/10/search/**
serviceId: on
stripPrefix: false
token: 10
test3:
path: /api/data/10/**
serviceId: DAS
stripPrefix: false
token: d10
test4:
path: /api/data/**
serviceId: no
stripPrefix: false
token: d10
test5:
path: /api/search/**
serviceId: no
stripPrefix: true
token: d10
test6:
path: /api/search/**
url: http://host/api/data/**
stripPrefix: false
token: d10
test7:
path: /*
url: http://host/api/data/**
stripPrefix: false
token: d10
# hystrix
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 6000
host:
max-per-route-connections: 20 #默认20
max-total-connections: 200 #默认200
ribbon:
eager-load:
enabled: true
ConnectTimeout: 15 #默认1000
ReadTimeout: 15 #默认2000
2.1 请求路由
zuul.ignored-services: 默认情况下所有Eureka上的服务都会被Zuul自动创建映射关系进行路由,一般情况下需要 将其 置为 * ,表示不需要为所有服务自动创建路规则。这个默认情况下是需要设置为 * ,因为很多时候我们不想让我们的服务都对外暴露
2.1.1 设置路由
有两种方式,URL和serviceId.都是通过匹配到网关的path然后映射到对应的服务(url或者serviceId)上去。其中test5是url式,其余的都是serviceId式。但是要注意的是url方式没有 Hystrix、Ribbon 特性。
- stripPrefix: 为true时(默认为true),会忽略path的路径,如test5,当访问http://host/api/data路径时,请求将会被转发到http://host/路径,要正常访问的话:http://host/api/data/api/data.
2.1.2 存取路由顺序
- 所有的Route保存在一个map里面,而map的key是path,所有如果配置有两个path一致的话就会有一个被覆盖
- 而对于yaml文件的来说,存入的顺序是按照先后顺序存入的。
示例:
比如上面的配置,首先存入的是test1,但是后来发现最后test2的path和test1一致,最终我们的routes里面包含的route就只有test2而没有test1
2.1.3 读取路由规则
1,使用路由规则匹配请求路径的时候是通过线性遍历的方式,在请求路径获取到第一个匹配的路由规则之后就会返回并结束匹配过程。所以当存在多个匹配的路由规则时,匹配结果完全取决于路由规则的保存顺序。
2, 在yaml存入是按照顺序存入的,左边第一个图当我们访问/api/data/10的时候,因为第一个匹配到的是/api/data/10/**,所以会映射到test3的DAS,而不会映射到test4 的no服务
2.2 限流
- 分为全局限流(default-policy-list)和分route(policy-list)限流:
- Limit:单位时间内允许访问的次数
- Quota:单位时间内允许访问的总时间(统计每次请求的时间综合)
- refresh-interval:单位时间设置,默认60s
- Type限流方式:ORIGIN, USER, URL
policy-list.test1配置的意思是:在一个时间窗口 1s 内,最多允许 12 次访问,或者总请求时间小于 120s
2.3 超时,熔断
- ribbon. ConnectTimeout,ribbon.ReadTimeout
- zuul.host.connect-timeout-millis, zuul.host.socket-timeout-millis
- hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 6000
单位:ms,区别在于,如果路由方式是serviceId的方式,那么ribbon的生效,如果是url的方式,则zuul.host开头的生效,ribbon 和hystrix谁小,谁生效。Hystrix是熔断,力度可基于全局配置和基于某个服务,但是不能基于route
2.4 线程池大小
- max-per-route-connections:表示为每一个route分配的最大线程数,只是限制了线程数而不是限流,和qps没多大关系
- max-total-connections:表示所有route的最大线程数
三 Zuul 使用的坑:
- stripPrefix:默认为true,需要注意
- 过动态刷新的配置,对于路由规则的变动,只能新增和修改,不能删除(所以动态删除会刷新不生效的错觉);
- 新增的规则在匹配顺序上,位于老规则的后(如上面的test7配置生效之后,因为/*已经表示所有的路由,导致后面所有新加的所以路由都会打到test7之上,以后想要再添加其他的路由,是相当于不生效的。)
- 一定要要将超时时间设置小一点,负责如果一瞬间上来都是2s或者更长的请求,就会导致在这2s内的这些线程得不到释放,就不能处理更多的请求,服务器性能大幅度降低。