Zuul 是 Netflix OSS 中的一员,是一个基于 JVM 路由和服务端的负载均衡器 。 提供路由 、 监控 、 弹性、安全等方面的服务框架 。 Zuul 能够与 Eureka 、 Ribbon 、 Hystrix 等组件配合使用 。
Zuul 的核心是过滤器:
- 动态路由:动态的将客户端的请求路由到后端的不同的服务,做一些逻辑处理,例如聚合多个服务的数据返回
- 请求监控:可以对整个系统的请求进行监控,记录详细的请求响应日志,可以实时统计出当前系统的访问量以及监控状态
- 认证鉴权:对每一个访问的请求做认证,拒绝非法请求,保护后端服务
- 压力测试:可以动态的将请求转发到后端服务集群中,还可以识别测试流量和真实流量,从而做一些特殊处理
- 灰度发布:可以保证整体系统的稳定,在初始灰度时候就可以发现,调整问题,保证其影响度
使用 Zuul 构建微服务网关
// Zuul Api网关
compile group: 'org.springframework.cloud', name: 'spring-cloud-starter-netflix-zuul', version: '2.2.2.RELEASE'
Zuul 进行路由的转发
属性文件中增加配置信息
# Zuul
zuul.routes.zwt.path=/zwt/**
zuul.routes.zwt.url=https://www.baidu.com
通过 zuul.routes 来配置路由转发, zwt是自定义的名称,当访问 http://localhost:8080/zwt 开始的地址时,就会跳转到 https://www.baidu.com 上
开启路由代理功能,在启动类上添加 @EnableZuulProxy 注解
@EnableDiscoveryClient
@SpringBootApplication
//@RibbonClient(name = "hello-client", configuration = MySelfRule.class)
@EnableFeignClients(basePackages = "com.zwt.helloclient")
@EnableHystrix
@EnableHystrixDashboard
@EnableZuulProxy
public class HelloApplication {
public static void main(String[] args) {
SpringApplication.run(HelloApplication.class, args);
}
}
集成 Eureka
一般通常是用 Zuul 来代理请求转发到内部的服务上去,统一为外部提供服务
前面已经添加了 Eureka 相关的依赖,所以启动类不需要修改(@EnableZuulProxy 已经自带了@EnableDiscoveryClient),属性文件中增加配置信息
zuul.routes.hello-client.service-id=hello-client
访问规则是" API 网关地址+访问的服务名称+接口 URI "
Zuul 路由配置
网关配置方式有多种,默认、URL、服务名称、排除|忽略、前缀
1.指定具体服务路由
属性文件中增加配置信息
zuul.routes.hello-client.path=/api-hello/**
将 hello-client 服务的路由地址配置成了 api-hello ,当需要访问 hello-client 中的接口时,通过 api-hello/house/hello 来进行,就是将服务名称变成了我们自定义的名称,注意前面配置 zuul.routes.hello-client.service-id=hello-client
如果配置成 zuul.routes.zzz.service-id=hello-client
也就是说要访问 http://localhost:8080/zzz/house/hello 也是可以访问到的,也就是说zzz和api-hello都是hello-client的一个别名,起了别名后原服务器的名字就不能用了,方便调用
2. 路由前缀
有的时候想在 API 前面配置一个统一的前缀
zuul.prefix=/rest
访问路径变成:http://localhost:8080/rest/api-hello/house/hello
3. 本地跳转
Zuul 的 API 路由还提供了本地跳转功能,通过 forward 就可以实现
zuul.routes.fsh-substitution.path=/api/**
zuul.routes.fsh-substitution.url=forward:/house/callhello
访问 api/hello 的时候会路由到本地的 /house/callhello/hello 上去
Zuul 过滤器
Zuul 中的过滤器总共有 4 种类型:
- pre:可以在请求被路由之前调用,适用于身份认证的场景,认证通过后再继续执行下面的流程 。
- route:在路由请求时被调用,适用于灰度发布场景,在将要路由的时候可以做一些自定义的逻辑 。
- post:在 route 和 error 过滤器之后被调用,这种过滤器将请求路由到达具体的服务之后执行 。 适用于需要添加响应头,记录响应日志等应用场景 。
- error:处理请求时发生错误时被调用,在执行过程中发送错误时会进入 error 过滤器,可以用来统一记录错误信息 。
请求生命周期
请求发过来首先到 pre 过滤器,再到routing 过滤器 ,最后到 post 过滤器,任何一个过滤器有异常都会进入 error 过滤器
public class IpFilter extends ZuulFilter {
public IpFilter(){
super();
}
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
System.out.println("IpFilter拦截Ip");
ctx.setSendZuulResponse(false);
ctx.setResponseBody("出错,非法调用");
ctx.getResponse().setContentType("application/json; charset=utf-8");
}
return null;
}
}
- shouldFilter:是否执行该过滤器, true 为执行, false 为不执行,这个也可以利用配置中心来实现,达到动态的开启和关闭过滤器
- filterType:过滤器类型,可选值有 pre、route、post、error
- filterOrder:过滤器的执行顺序,数值越小,优先级越高
- run:执行自己的业务逻辑
ctx.setSendZuulResponse(false),告诉 Zuul 不需要将当前请求转发到后端的服务。 通过 setResponseBody 返回数据给客户端
配置过滤器
@Configuration
public class FilterConfig {
@Bean
public IpFilter ipFilter(){
return new IpFilter();
}
}
最后运行,访问 http://localhost:8080/rest/api-hello/house/hello
过滤器禁用
- 利用 shouldFilter 方法中的 return false 让过滤器不再执行
- 通过配置方式来禁用过滤器,格式为“ zuul.过滤器的类名.过滤器类型.disable=true”
例如禁用上面就可以zuul.IpFilter.pre.disable=true
过滤器中传递数据
一个项目往往会存在很多的过滤器,执行的顺序是根据 filterOrder 决定的,要是第一个过滤器需要告诉第二个过滤器一些信息,就需要第一个过滤器传递信息给第二个过滤器
RequestContext ctx = RequestContext.getCurrentContext();
ctx.set ("msg","你好");
后面的过滤就可以通过 RequestContext 的 get 方法来获取数据:
RequestContext ctx = RequestContext.getCurrentContext();
ctx.get("msg");
过滤器拦截请求
拦截和返回结果核心只需要 4 行代码即可实现
RequestContext ctx = RequestContext.getCurrentContext();
ctx.setSendZuulResponse(false);
ctx.setResponseBody("返回信息");
return null;
ctx.setSendZuulResponse(false);
告诉 Zuul 不需要将当前请求转发到后端的服务,但当项目中有多个过滤器的时候,发现非法请求,然后进行拦截返回结果,前面已经出错后面的过滤器就没有必要执行,但 Zuul 通过 ctx.setSendZuulResponse(false);
设置了不路由到服务,并且返回 null,这只是当前的过滤器执行完成了,后面还有很多过滤器在等着执行。
在com.netflix.zuul.http.ZuulServlet
中的service 方法中执行对应的 Filter
从上面也可以看出过滤器执行的顺序,pre 过滤器——routing 过滤器——post 过滤器
preRoute 中会通过 zuulRunner 来执行,zuulRunner 中通过调用 FilterProcessor 来执行 Filter
FilterProcessor 通过过滤器类型获取所有过滤器,并循环执行,上面说到只有在 shouldFilter()
返回true的时候过滤器才会执行,那么可以通过第一个过滤器给第二个过滤器传值改变 shouldFilter()
返回值的方式通知是否继续执行下面其他的过滤器。
RequestContext ctx = RequestContext.getCurrentContext();
ctx.set("isSuccess", false);
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
Boolean success = (Boolean)ctx.get("isSuccess");
return success == null?true:success;
}
过滤器中异常处理
过滤器中的异常主要发生在 run 方法中,可以用 try catch 来处理 。 Zuul 中也为我们提供了一个异常处理的过滤器,当过滤器在执行
过程中发生异常,若没有被捕获到,就会进入 error 过滤器,可以定义一个 error 过滤器来记录异常信息
public class ErrorFilter extends ZuulFilter {
@Override
public boolean shouldFilter() {
return true;
}
@Override
public String filterType() {
return "error";
}
@Override
public int filterOrder() {
return 100;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
Throwable throwable = ctx.getThrowable();
System.err.println("Filter Error:"+throwable.getCause().getMessage());
return null;
}
}
在上面 ip 过滤器中模拟一个异常信息
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
System.out.println("IpFilter拦截Ip");
//异常信息
System.out.println(2/0);
ctx.setSendZuulResponse(false);
ctx.setResponseBody("出错,非法调用");
ctx.getResponse().setContentType("application/json; charset=utf-8");
}
return null;
}
Zuul 容错和回退
Zuul 主要功能就是转发,在转发过程中你无法保证被转发的服务是可用的,这个时候需要容错机制及回退机制 。
容错机制
就是当某个服务不可用时,能够切换到其他可用的服务上去,也就是需要有重试机制
中添加 spring-retry 的依赖
compile group: 'org.springframework.retry', name: 'spring-retry', version: '1.3.0'
在属性文件中开启重试机制以及配置重试次数
zuul.retryable=true # 开启重试
hello-client.ribbon.ConnectTimeout=500 #请求连接超时时间
hello-client.ribbon.ReadTimeout=1000 #请求处理的超时时间
hello-client.ribbon.OkToRetryOnAllOperations=true #对所有请求都进行重试
hello-client.ribbon.MaxAutoRetriesNextServer=2 #切换实例的重试次数
hello-client.ribbon.MaxAutoRetries=1 #对当前实例的重试次数
默认 Ribbon 的转发规则是轮询,当请求接口的时候要是被转发到挂掉的服务上去的,返回的是异常信息,但当加入了重试机制后,可以循环请求接口,这个时候不会返回异常信息,因为 Ribbon 会根据重试配置进行重试,当请求失败后会将请求重新转发到可用的服务上去
回退机制
Zuul 默认整合了 Hystrix,当后端服务异常时可以为 Zuul 添加回退功能,返回默认的数据给客户端,实现回退机制需要实现 FallbackProvider 接口
@Component
public class MyFallbackProvider implements FallbackProvider {
private Logger log = LoggerFactory.getLogger(MyFallbackProvider.class);
@Override
public String getRoute() {
//指定为哪个微服务提供回退(这里写微服务名 写*代表所有微服务)注意这个名称一定要是注册到 Eureka 中的名称
return "*";
}
@Override
public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
//判断根据不同的异常来做不同的处理
if (cause instanceof HystrixTimeoutException) {
return response(HttpStatus.GATEWAY_TIMEOUT);
}else{
return response(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
private ClientHttpResponse response(final HttpStatus status) {
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
return status;
}
@Override
public int getRawStatusCode() throws IOException {
return status.value();
}
@Override
public String getStatusText() throws IOException {
return status.getReasonPhrase();
}
@Override
public void close() {
//close的时候调用的方法, 讲白了就是当降级信息全部响应完了之后调用的方法
}
@Override
public InputStream getBody() throws IOException {
RequestContext ctx = RequestContext.getCurrentContext();
Throwable throwable = ctx.getThrowable();
if(throwable != null){
log.error("", throwable.getCause());
}
return new ByteArrayInputStream("服务器内部错误,降级信息".getBytes());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
MediaType mt = new MediaType("application", "json", Charset.forName("utf-8"));
headers.setContentType(mt);
return headers;
}
};
}
}
在要调用的接口制造异常信息
Zuul 高可用
业务相关的服务都是注册到 Eureka 中,通过 Ribbon 来进行负载均衡,服务可以通过水平扩展来实现高可用,API 网关这层往往是给 APP 、 Webapp 、客户来调用接口的,如果将 Zuul 也注册到 Eureka 中是达不到高可用的,因为不可能让客户也去操作注册中心可以用额外的负载均衡器来实现 Zuul 的高可用,比如常用的 Nginx,或者 HAProxy、FS