1 场景导入
我们都知道,在微服务架构中,一般有着多个服务提供者,而且服务可能经常改变。 那我们外部的服务应该如何访问内部各种微服务呢?事实上,后端的服务往往不会直接开放给调用端,而是通过服务网关根据 url 路由到相应的服务。网关就通过对外暴露 API ,屏蔽内部微服务的微小变动,从而保持系统的稳定性。
2 什么是 Zuul?
Zuul 是 Spring Cloud 全家桶中的微服务 API 网关,所有请求都会先经过 Zuul 到达后端的应用程序。Zuul 提供了认证和安全,性能监测,动态路由,压力测试,负载卸载,静态资源处理等功能。
3 Zuul 的基础使用
我们先新建一个项目,命名为 zuul,其 pom.xml 如下,我们往里面添加了 zuul 和 eureka 的依赖。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.6.RELEASE</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>Zuul</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>Zuul</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
然后记得添加配置文件
spring.application.name=zuul
server.port=8086
# 设置与Eureka Server交互的地址,查询服务和注册服务都需要依赖这个地址
eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/
然后在启动类上添加注解 @EnableDiscoveryClient 和 @EnableZuulProxy,其中 @EnableZuulProxy 用于添加对 Zuul 的支持。
package com.example.Zuul;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
@SpringBootApplication
@EnableDiscoveryClient
@EnableZuulProxy
public class ZuulApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulApplication.class, args);
}
}
我们分别运行 Eureka,EurekaProducer,Zuul 三个项目,在浏览器输入 http://localhost:8761/
我们发现 Zuul ,EurekaProducer 均已注册至 Eureka。
然后在浏览器输入 http://localhost:8086/producer/helloWorld?id=3&saying=%E7%89%9B%E9%80%BC
事实上,我们输入的 http://localhost:8086/producer/helloWorld?id=3&saying=%E7%89%9B%E9%80%BC 可以等效为 http://localhost:8080/helloWorld?id=3&saying=牛逼,其中8080端口是 EurekaProducer 使用的端口。在实现微服务架构时,服务名与服务实例地址的关系在 Eureka 服务器中就存在了。在我们将 Zuul 注册到 Eureka 服务器中时,可以去发现其他服务并完成对 serviceId 的映射。Zuul 的默认路由规则如下:http://ZUUL_HOST:ZUUL_PORT/微服务在Eureka上的serviceId/** 将会被转发至 serviceId 对应的微服务。
4 Zuul 路由功能的实现
我们仔细观察之前使用 Zuul 进行访问的路径
http://localhost:8086/producer/helloWorld?id=3&saying=%E7%89%9B%E9%80%BC
我们发现,必须要知道项目的名称才能访问!!不知道的话访问不了!!那我们再认真地想一想,如果让用户知道项目的名称,使用 Zuul 就意义不大了,要知道使用原来的路径也是可以访问的啊。我们知道,Zuul 有代理的功能,不想让用户看到真实的实现,既然如此,我们可以为 Zuul 设置一些路由规则来隐藏真实的实现。
我们修改一下配置文件
# 通过/proxy/**来访问producer项目
zuul.routes.producer=/proxy/**
但是,仅仅这样好像不太行啊,我们用原来的方法照样可以访问啊!那么有没有一种方法可以忽略掉应用名称访问的呢?自然是有的,添加一行配置即可。
# 忽略producer的应用名称访问
zuul.ignored-services=producer
话说如果一个系统微服务很多的话,使用这种配置方式岂不是非常麻烦?其实最简单的是我们可以选择使用通配符模式来完成。
# 忽略全部应用名称访问
zuul.ignored-services="*"
所有访问都需要配置一个映射路径来完成,使用服务名称的信息访问将被禁止。
5 Zuul 中的 Filter
Filter 是 Zuul 的核心,其实现了 Zuul 的大部分功能,主要用来实现对外服务的控制。Filter 一共有 “PRE”,“ROUTING”,“POST”,“ERROR” 四个生命周期,对应四种过滤器类型。
- PRE:在请求被路由之前调用,我们可以使用其进行权限验证
- ROUTING:该过滤器将请求路由到微服务,我们可以用来构建发送给微服务的请求并请求微服务
- POST:在路由到微服务之后被调用,我们可以用来收集统计信息
- ERROR:在其他阶段发生错误时执行该过滤器。
下面我们演示如何自定义一个过滤器,实现一个功能:要求所有访问申请必须带有用户名信息,且我这里限定用户名必须为 Ling,只有用户名为 Ling 才可以继续访问下去,否则会直接打印 username error。下面我们就来实现这个过滤器。
我们实现一个继承 ZuulFilter 的类。需要覆盖其中四个方法。
package com.example.Zuul.filter;
import javax.servlet.http.HttpServletRequest;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
public class FirstFilter extends ZuulFilter{
//是否执行该过滤器
@Override
public boolean shouldFilter() {
return true;
}
//过滤器执行的具体操作
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
//获取参数
String username = request.getParameter("username");
//符合要求
if(username.equals("Ling")) {
ctx.setSendZuulResponse(true); //进行路由
ctx.setResponseStatusCode(200);
ctx.set("isSuccess", true);
}else {
ctx.setSendZuulResponse(false); //不进行路由
ctx.setResponseStatusCode(400);
ctx.setResponseBody("username error");
ctx.set("isSuccess", false);
}
return null;
}
//定义filter的类型,可选择pre、route、post、error四种
@Override
public String filterType() {
return "pre";
}
//定义filter的顺序,数字越小越先执行
@Override
public int filterOrder() {
return 0;
}
}
然后我们需要把我们自己实现的过滤器加入请求拦截队列中,在启动类添加如下代码:
@Bean
public FirstFilter firstFilter() {
return new FirstFilter();
}
我们测试一下过滤器的作用,先后运行 Eureka,EurekaProducer,Zuul 三个项目,在浏览器输入 http://localhost:8086/proxy/helloWorld?id=3&saying=%E7%89%9B%E9%80%BC&username=Ling1,显示如下:
我们发现这个请求由于用户名不为 Ling 而被拦截,说明这个过滤器是起到作用的。接下来我们访问 http://localhost:8086/proxy/helloWorld?id=3&saying=%E7%89%9B%E9%80%BC&username=Ling,由于访问请求可以到达微服务,说明过滤器已通过,请求正常响应了。
6 路由熔断
当后端服务出现异常时,我们不希望将异常抛出给最外层,期望服务可以自动进行降级。Zuul 给我们提供了类似的支持,当某个服务出现异常时,直接返回我们预设的信息。
我们通过自定义的方法,并且将其指定给某个 route 来实现该 route 访问出问题的熔断处理,主要通过实现 FallbackProvider 接口。我们先来看一下这个接口:
/**
* Provides fallback when a failure occurs on a route.
*
* @author Ryan Baxter
* @author Dominik Mostek
*/
public interface FallbackProvider {
/**
* The route this fallback will be used for.
* @return The route the fallback will be used for.
*/
public String getRoute();
/**
* Provides a fallback response based on the cause of the failed execution.
*
* @param route The route the fallback is for
* @param cause cause of the main method failure, may be <code>null</code>
* @return the fallback response
*/
ClientHttpResponse fallbackResponse(String route, Throwable cause);
}
getRoute 表示要拦截哪个服务,fallbackResponse 则表示 Zuul 断路时应该提供一个什么样的返回值。
我们创建一个类来实现 FallbackProvider 接口。
package com.example.Zuul.fallback;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;
/**
* zuul路由熔断的实现
* @author 30309
*
*/
@Component
public class MyFallback implements FallbackProvider{
//熔断拦截哪个服务
@Override
public String getRoute() {
//*表示为所有服务提供回退服务,当然也可以指定具体的服务
//return "*";
return "producer";
}
//定制返回内容
@Override
public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
if (cause != null && cause.getCause() != null) {
System.out.println("出错了!" + cause);
}
return response(HttpStatus.INTERNAL_SERVER_ERROR);
}
private ClientHttpResponse response(final HttpStatus status) {
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() {
return status;
}
@Override
public int getRawStatusCode() {
return status.value();
}
@Override
public String getStatusText() {
return status.getReasonPhrase();
}
@Override
public void close() {
}
@Override
public InputStream getBody() {
return new ByteArrayInputStream("The service is unavailable.".getBytes(StandardCharsets.UTF_8)); //返回前端的内容
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.APPLICATION_JSON_UTF8); //设置头
return httpHeaders;
}
};
}
}
现在我们修改一下 producer 项目的接口,让该服务在执行过程中休眠一段时间并确保其超时。
package com.example.EurekaProducer.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class HelloWorldController {
@RequestMapping("/helloWorld")
@ResponseBody
public String helloWorld(int id,String saying) {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "来自8080端口:序号为" + id + "的用户发布了一条新消息:" + saying;
}
}
然后我们运行 Eureka,EurekaProducer,Zuul 三个项目,输入 http://localhost:8086/proxy/helloWorld?id=3&saying=aa,结果发现 producer 服务已被熔断。
观察控制台,其打印如下,说明我们已经实现了 Zuul 的路由熔断功能。
出错了!com.netflix.client.ClientException
从上面的测试可见,Zuul 只支持服务级别的熔断,不支持具体 URL 级别的熔断。
7 路由重试
有时可能因为种种原因,服务暂时不可用,我们希望在这个时候对服务进行重试,Zuul 事实上也帮助我们实现了这个功能。
我们继续修改 Zuul 项目,增加 Spring Retry 依赖。
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
修改配置文件如下
spring.application.name=zuul
server.port=8086
# 设置与Eureka Server交互的地址,查询服务和注册服务都需要依赖这个地址
eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/
# 忽略全部应用名称访问
zuul.ignored-services="*"
# 通过/proxy/**来访问producer项目
zuul.routes.producer=/proxy/**
# 是否开启重试功能,默认为false
zuul.retryable=true
# 开启eureka负载均衡策略
ribbon.eureka.enabled=true
# 当前实例的重试次数,默认为一次
ribbon.MaxAutoRetries=3
# 切换实例的重试次数
ribbon.MaxAutoRetriesNextServer=0
# 是否所有操作都重试,为false时只对get请求重试,为true会对所有请求重试,慎用
ribbon.OkToRetryOnAllOperations=false
修改一下 producer 项目的接口
package com.example.EurekaProducer.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class HelloWorldController {
@RequestMapping("/helloWorld")
@ResponseBody
public String helloWorld(int id,String saying) {
System.out.println("Hello World!");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "来自8080端口:序号为" + id + "的用户发布了一条新消息:" + saying;
}
}
结果我们发现 Hello World 打印了四次,说明我们已经重试过三次了。我们的路由重试功能也实现成功。
8 超时配置
配置如下
# 使用serviceId进行路由,请求处理的超时时间
ribbon.ReadTimeout=60000
# 使用serviceId进行路由,请求连接的超时时间
ribbon.ConnectTimeout=60000
# 使用url进行路由,请求连接的超时时间
zuul.host.connect-timeout-millis=60000
# 使用url进行路由,请求处理的超时时间
zuul.host.socket-timeout-millis=60000
9 总结
在实际开发中,不同客户端会使用不同的负载将请求分发到 Zuul,Zuul 会通过 Eureka 调用后端服务,最后对外输出。
参考:【Spring Cloud 基础设施搭建系列】Spring Cloud Demo项目 Zuul的路由重试和路由熔断
springcloud(十一):服务网关Zuul高级篇
springcloud(十):服务网关zuul初级篇