Zuul
不管是来自于客户端(PC或移动端)的请求,还是服务内部调用。一切对服务的请求都会经过Zuul这个网关,然后再由网关来实现 鉴权、动态路由等等操作。Zuul就是我们服务的统一入口。
入门
新建项目
新建一个新的模块 gateway,导入相关依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
编写启动类
@SpringBootApplication
@EnableZuulProxy // 开启Zuul的网关功能
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class);
}
}
编写配置
server:
port: 10010 #服务端口
spring:
application:
name: api-gateway #指定服务名
编写路由规则
我们需要用Zuul来代理user-service服务,先看一下控制面板中的服务状态:
- ip为:127.0.0.1
- 端口为:8888
映射规则:
zuul:
routes:
user-service: # 这里是路由id,随意写
path: /user-service/** # 这里是映射路径
url: http://127.0.0.1:8888 # 映射路径对应的实际url地址
我们将符合path
规则的一切请求,都代理到 url
参数指定的地址
本例中,我们将 /user-service/**
开头的请求,代理到http://127.0.0.1:8888
启动测试
访问的路径中需要加上配置规则的映射路径,我们访问:http://127.0.0.1:10010/user-service/user/5
面向服务的路由
在刚才的路由规则中,我们把路径对应的服务地址写死了!如果同一服务有多个实例的话,这样做显然就不合理了。
我们应该根据服务的名称,去Eureka注册中心查找 服务对应的所有实例列表,然后进行动态路由才对!
添加Eureka客户端依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
修改配置
zuul:
routes:
user-service: # 这里是路由id,随意写
path: /user-service/** # 这里是映射路径
serviceId: user-service # 指定服务名称
# url: http://127.0.0.1:8888 # 映射路径对应的实际url地址
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
启动测试
再次启动,这次Zuul进行代理时,会利用Ribbon进行负载均衡访问:
日志中可以看到使用了负载均衡器:
简化路由配置
在刚才的配置中,我们的规则是这样的:
zuul.routes.<route>.path=/xxx/**
: 来指定映射路径。<route>
是自定义的路由名zuul.routes.<route>.serviceId=/user-service
:来指定服务名。
而大多数情况下,我们的<route>
路由名称往往和 服务名会写成一样的。因此Zuul就提供了一种简化的配置语法:zuul.routes.<serviceId>=<path>
比方说上面我们关于user-service的配置可以简化为一条:
zuul:
routes:
user-service: /user-service/** # 这里是映射路径
省去了对服务名称的配置。
默认的路由规则
- 默认情况下,一切服务的映射路径就是服务名本身。
- 例如服务名为:
user-service
,则默认的映射路径就是:/user-service/**
- 例如服务名为:
Zuul注册到Eureka后,会默认给所有服务都添加一个默认配置 就比如: 我们之前在配置里并没有配置consumer相关的配置 但是我们访问http://127.0.0.1:10010/consumer_server/consumer/1
却可以成功
当然我们也可以选择禁用掉默认配置,只允许它服务间调用
zuul:
ignored-services:
- consumer_server
也可以一次性禁用掉所有的默认配置
zuul:
ignored-services: "*"
这个时候我们再去访问刚才的路径http://127.0.0.1:10010/consumer_server/consumer/1
就成功不了了
值得注意的是
- 当你简化了路由配置后,就比如简化成这样
user-service: /user/**
,这个时候,Zuul的默认配置依旧会生效 - 意思就是说,你可以使用
http://127.0.0.1:10010/user-service/user/1
访问,也可以使用http://127.0.0.1:10010/user/user/1
访问
路由前缀
zuul:
prefix: /api # 添加路由前缀
routes:
user-service: # 这里是路由id,随意写
path: /user-service/** # 这里是映射路径
service-id: user-service # 指定服务名称
我们通过zuul.prefix=/api
来指定了路由的前缀,这样在发起请求时,路径就要以/api开头。
路径/api/user-service/user/1
将会被代理到/user-service/user/1
可以添加前缀必然也可以去除前缀
就比如我们上面说可以使用http://127.0.0.1:10010/user/user/1
访问
那我们也可以使用http://127.0.0.1:10010/user/1
访问
zuul:
routes:
user-service: # 这里是路由id,随意写
path: /user-service/** # 这里是映射路径
serviceId: user-service
strip-prefix: false #去除前缀
# url: http://127.0.0.1:8888 # 映射路径对应的实际url地址
ignored-services:
- consumer_server
重启项目访问成功
strip-prefix: false
去除前缀默认为true,跟在zuul后面就是全局去除(此时对于路由id所配置的路径就无效),跟路由id就是局部去除- 我们虽然使用
http://127.0.0.1:10010/user/1
访问成功,少写了一个/user
- 但是
yml
配置文件里却多写了很多,有得有失,所以具体情况具体对待
过滤器
Zuul作为网关的其中一个重要功能,就是实现请求的鉴权。而这个动作我们往往是通过Zuul提供的过滤器来实现的。
ZuulFilter
ZuulFilter是过滤器的顶级父类。在这里我们看一下其中定义的4个最重要的方法:
public abstract ZuulFilter implements IZuulFilter{
abstract public String filterType(); //过滤器类型
abstract public int filterOrder(); //过滤器顺序
boolean shouldFilter();// 来自IZuulFilter 要不要过滤
Object run() throws ZuulException;// IZuulFilter 过滤逻辑
}
shouldFilter
:返回一个Boolean
值,判断该过滤器是否需要执行。返回true执行,返回false不执行。run
:过滤器的具体业务逻辑。filterType
:返回字符串,代表过滤器的类型。包含以下4种:pre
:请求在被路由之前执行routing
:在路由请求时调用post
:在routing和errror过滤器之后调用error
:处理请求时发生错误调用
filterOrder
:通过返回的int值来定义过滤器的执行顺序,数字越小优先级越高。
过滤器执行生命周期
这张是Zuul官网提供的请求生命周期图,清晰的表现了一个请求在各个过滤器的执行顺序。
- 正常流程:
- 请求到达首先会经过pre类型过滤器,而后到达routing类型,进行路由,请求就到达真正的服务提供者,执行请求,返回结果后,会到达post过滤器。而后返回响应。
- 异常流程:
- 整个过程中,pre或者routing过滤器出现异常,都会直接进入error过滤器,再error处理完毕后,会将请求交给POST过滤器,最后返回给用户。
- 如果是error过滤器自己出现异常,最终也会进入POST过滤器,而后返回。
- 如果是POST过滤器出现异常,会跳转到error过滤器,但是与pre和routing不同的时,请求不会再到达POST过滤器了。
所有内置过滤器列表:
使用场景:
- 请求鉴权:一般放在pre类型,如果发现没有访问权限,直接就拦截了
- 异常处理:一般会在error类型和post类型过滤器中结合来处理。
- 服务调用时长统计:pre和post结合使用。
自定义过滤器
定义过滤器类
package cn.itcast.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
/**
* @Author: 萧一旬
* @Description:
* @Date: Create in 14:38 2019/4/13
*/
@Component
public class LoginFilter extends ZuulFilter {
@Override
public String filterType() {
// 登录校验,肯定是在前置拦截
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return FilterConstants.PRE_DECORATION_FILTER_ORDER - 1;
}
@Override
public boolean shouldFilter() {
// 返回true,代表过滤器生效。
return true;
}
@Override
public Object run() throws ZuulException {
//获取请求上下文
RequestContext context = RequestContext.getCurrentContext();
//获取request
HttpServletRequest request = context.getRequest();
//获取请求参数
String token = request.getParameter("access-token");
//判断是否存在
if (StringUtils.isBlank(token)) {
//不存在,未登录 拦截
context.setSendZuulResponse(false);
//返回403
context.setResponseStatusCode(HttpStatus.FORBIDDEN.value());
}
return null;
}
}
测试
运行项目 不携带Token访问
携带Token访问
熔断和负载均衡
Zuul中默认就已经集成了Ribbon负载均衡和Hystix熔断机制。但是所有的超时策略都是走的默认值,比如熔断超时时间只有1S,很容易就触发了。因此建议我们手动进行配置:
zuul:
retryable: true
ribbon:
ConnectTimeout: 250 # 连接超时时间(ms)
ReadTimeout: 2000 # 通信超时时间(ms)
OkToRetryOnAllOperations: true # 是否对所有操作重试
MaxAutoRetriesNextServer: 2 # 同一服务不同实例的重试次数
MaxAutoRetries: 1 # 同一实例的重试次数
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMillisecond: 6000 # 熔断超时时长:6000ms
Ribbon的超时时长,真实值是(read + connect) * 2 ,必须小于Hystrix的时长,否则会报警告,虽然不会影响运行
计算公式
ribbonTimeout = (ribbonReadTimeout + ribbonConnectTimeout) * (maxAutoRetries + 1) * (maxAutoRetriesNextServer + 1);
- 不知是何原因,或许是我设置问题,无论我在
yml
里ribbon.ConnectTimeout
值为多少,ribbonConnectTimeout
恒等于1000
一些常见的BUG
com.netflix.zuul.exception.ZuulException: Hystrix Readed time out
这个错误是应为zuul的默认超时时间比较小,我们配置下zuul的超时时间,因zuul启用了ribbon的负载均衡,还需要设置ribbon的超时时间,注意ribbon的超时时间要小于zuul超时时间 。
zuul:
host:
connect-timeout-millis: 15000 #HTTP连接超时要比Hystrix的大
socket-timeout-millis: 60000 #socket超时
ribbon:
ReadTimeout: 10000
ConnectTimeout: 10000
springcloud 加入spring session通过zuul请求session不一致问题
对应的添加一条配置即可
zuul:
prefix: /api # 添加路由前缀
routes:
user-service: # 这里是路由id,随意写
path: /user-service/** # 这里是映射路径
service-id: user-service # 指定服务名称
# 加入以下配置即可
sensitiveHeaders: "*"