Spring Cloud Netflix 学习笔记(五)—服务网关(Zuul)
API网关是对外服务的一个入口,其隐藏了内部架构的实现,是微服务架构中必不可少的一个组件。API网关可以为我们管理大量的API接口,还可以对接客户、适配协议、进行安全认证、转发路由、限制流量、监控日志、防止爬虫、进行灰度发布等。
Zuul是一个基于JVM路由和服务端的负载均衡器,其参考GOF设计模式中的外观(Facade)模式,将细粒度的服务组合起来提供了一个粗粒度的服务,以便所有请求都导入一个统一的入口,整个服务只需要暴露一个API,对外屏蔽了服务端的实现细节。
Zuul对请求提供了路由和过滤器两个功能,其中,路由功能负责将外部请求转发到具体的微服务实例上,是实现外部访问统一入口的基础。过滤器功能则负责对请求的处理过程进行干预,是实现请求校验、服务聚合等功能的基础。
Zuul本身已经默认集成了Hystrix和Ribbon,所以Zuul天生就拥有线程隔离和服务容错的自我保护能力,以及对服务调用的客户端负载均衡功能。
通过Zuul组件,可以完成以下功能:
- 动态路由:Zuul路由服务器支持与Eureka服务器的整合,可以动态对注册到Eureka服务器中的微服务进行路由映射。另外,Zuul提供一系列的路由规则配置,可以针对生产中的实际情况进行配置,实现微服务路由的灵活控制。
- 请求监控:通过对一些特定的接口设置访问白名单、访问次数、访问频率等各类设置,可以在不影响微服务实现的情况下,对访问实施监控和审查处理。
- 认证鉴权:通过Zuul可以将认证的部分单独抽取出来,让微服务系统无须关注认证的逻辑,只需要关注业务本身即可。另外,可以统一在服务网关层增加一个额外的保护层来防止恶意攻击。
- 压力测试:通过Zuul所提供的过滤器功能可以逐渐增加对某一服务集群的流量,以了解服务性能,从而及早对服务运维架构做出调优。
- A/B测试:新版本、新功能可能都需要测试用户对其的反应,通过API服务网关,可以轻松控制部分用户访问服务实例,并且可以对用户行为进行记录和分析,以便对新版本及新功能进行评价,获取应用的最优方案。
- 服务迁移:通过Zuul代理可以处理来自旧端点的客户端上的所有流量,将一些请求重定向到新的端点,从而慢慢地用不同的实现来替换旧端点。
- 灰度发布:灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度。
1、搭建Zuul路由服务器
Zuul路由服务器是一个标准的Spring Boot应用。而Spring Cloud提供了@EnableZuulProxy注解来创建Zuul路由服务器,所创建的路由服务可以是嵌入式服务,也可以进行独立部署。
Zuul将默认从Eureka服务器中获取所注册的服务,然后将服务的ID作为请求路径中的一部分,然后将用户的请求自动转发到这些服务中。
1.1、新建maven工程
在父工程下,新建一个名为zuul-proxy的Spring Boot子模块
1.2、导入依赖
在pom.xml中添加Zuul和Eureka依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
1.3、编写启动类
创建一个启动类,在启动类上添加@EnableZuulProxy注解
@SpringBootApplication
@EnableZuulProxy
public class ZuulProxyApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulProxyApplication.class, args);
}
}
点击进入@EnableZuulProxy注解,可以看到它被@EnableCircuitBreaker注解修饰,也就是说Zuul本身已经默认集成了Hystrix和Ribbon,所以Zuul天生就拥有线程隔离和服务容错的自我保护能力,以及对服务调用的客户端负载均衡功能。
@EnableCircuitBreaker
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Import({ZuulProxyMarkerConfiguration.class})
public @interface EnableZuulProxy {
}
1.4、添加配置
在application.yml中,添加如下配置:
server:
port: 8600
spring:
application:
name: zuul-proxy
eureka:
client:
serviceUrl:
defaultZone: http://scott:scott@localhost:8761/eureka/,http://scott:scott@localhost:8762/eureka/
fetch-registry: true
register-with-eureka: true
1.5、测试访问
在浏览器输入 http://localhost:8600/emp/emp/all 可以看到Zuul路由服务器已经正确地将请求路由到相应的微服务中。
2、路由映射
Zuul提供了多种机制对请求路由进行映射,如下:
- 与Eureka服务器整合自动根据微服务的ID进行映射,这个是默认机制,也是之前示例中所使用的机制。
- 结合微服务ID通过自定义方式进行路由映射。
- 直接使用静态URL路径的方式对微服务进行路由映射。
- 添加全局路由映射。
- 通过自定义路由转换器,实现更灵活的路由映射。
2.1、Zuul路径映射信息页面
与Hystrix一样,Zuul通过actuator提供了一个查看路径映射的链接,在工程中导入如下依赖(已在父工程导入):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
并添加如下配置:
management:
endpoints:
web:
exposure:
include: "*"
在浏览器访问 http://localhost:8600/actuator/routes 可看到如下返回数据:
2.2、默认规则
Zuul设计之初就是默认与Spring Cloud相关产品进行集成的,也就是说当构建API服务网关时,如果有Eureka服务器时,那么Zuul会自动为注册到Eureka服务器上的每个服务都创建一个默认路由规则:访问路径的前缀为serviceId配置的服务名称。就会形成如下格式的访问路径:
http://[API网关地址]/[serviceID]/[接口URI]**
因此,当微服务部署架构中包含了Eureka服务时,在增加或移除一个服务时无须对Zuul进行任何修改,Zuul可以自动根据Eureka服务器中所注册的服务自动完成路由映射、负载均衡等处理。
2.3、自定义服务访问路径
可以在Zuul路由服务器配置文件中通过增加格式为zuul.routes.[微服务Id]=[指定路径]
的属性配置方式对访问路径进行控制。例如,自定义emp的访问路径:
zuul:
routes:
emp: ee/**
此时,路径映射信息如下:
注意:两个星号表示可以转发任意层级的URL,比如“/emp/emp/all”。如果只配置一个星号,那么就只能转发一级,比如“/emp/emp”。
可以看到,所返回的路由映射中emp这个服务有两个,一个是我们上面所配置的,另外一个则是Zuul默认的。
假如此时再通过之前的路径访问,发现依然是可以访问成功的。Zuul默认会将所有注册到Eureka服务器中的服务进行映射,假如你的微服务实例尚未注册到Eureka服务器中,如果通过routes端点来查看,是看不到相应路由映射的;但如果是通过自定义的方式进行路由映射配置,那么不论你的微服务是否已经注册到Eureka服务器中,routes端点都会返回该路由映射。
当停止emp后,routes信息中依然可看到自定义的路径映射,但默认配置的emp和dept已经看不到了:
2.4、忽略指定服务
Zuul路由服务器配置中提供了一个属性zuul.ignored-services
,通过设置该属性可以指定在默认映射中所要忽略的微服务,指定后Zuul的路由服务将不再代理该路径下的访问。参数的值可以设置多个服务的ID,如果需要忽略多个服务,那么服务ID之间需使用逗号隔开即可。
zuul:
ignored-services: emp
此时,访问routes信息链接,可发现,emp的默认映射已经不在了,但是,自定义的映射还存在,而且可以正常访问到。
2.5、设置路由前缀
我们在访问第三方服务时经常会发现在访问路径中有一个统一的前缀,而Zuul提供了zuul.prefix
属性可为所有的路由映射增加统一前缀:
zuul:
prefix: /demo
添加后,所有的路径映射前都有了“/demo”
此时,服务的访问路径变为:http://localhost:8600/demo/.../
2.6、灰度发布
灰度发布(又名金丝雀发布)是指在黑与白之间,能够平滑过渡的一种发布方式。在其上可以进行A/B testing,即让一部分用户继续用产品特性A,一部分用户开始用产品特性B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度。
使用Zuul实现灰度发布操作如下:
2.6.1、编写配置类
在Zuul工程中添加如下配置类:
@Configuration
public class ZuulConfig {
@Bean
public PatternServiceRouteMapper serviceRouteMapper() {
return new PatternServiceRouteMapper(
"(?<name>^.+)-(?<version>v.+$)",
"${version}/${name}");
}
}
这样,服务ID为users-v1的服务,就会被映射到路由为/v1/users/的路径上。但是对于所定义的命名组必须包括servicePattern和routePattern两部分。如果servicePattern没有匹配一个serviceId,那就会使用默认的。
2.6.2、提供不同版本服务
修改emp,提供v1和v2两个版本:
version: v1
spring:
application:
name: emp-${version}
修改EmpController:
@RestController
@RequestMapping("/emp")
public class EmpController {
private final DeptClient deptClient;
public EmpController(DeptClient deptClient) {
this.deptClient = deptClient;
}
@Value("${version}")
private String version;
@GetMapping("/all")
public String getAll() {
System.out.println("非Hystrix:" + Thread.currentThread().getName());
String result = deptClient.getAll();
return "emp: " + version + "\t" + result;
}
}
2.6.3、配置路由
zuul:
routes:
emp: /ee/**
guyuan:
path: /gy/**
serviceId: emp-v2
prefix: /demo
2.6.4、测试
访问:http://localhost:8600/demo/gy/emp/all
访问:http://localhost:8600/demo/v1/emp/emp/all
访问:http://localhost:8600/demo/v2/emp/emp/all
3、工作原理
Zuul是通过Servlet来实现的,Zuul通过自定义的ZuulServlet(类似于SpringMVC的DispatcServlet)来对请求进行控制。Zuul的核心是一系列过滤器,可以在Http请求的发起和响应返回期间执行一系列的过滤器。
Zuul包括以下4种过滤器。
- PRE过滤器(链):在请求被路由之前调用,可用来实现身份验证、在集群中选择请求的微服务、记录调试信息等。
- ROUTING过滤器:它用于将请求路由到具体的微服务实例。在默认情况下,它使用Http Client进行网络请求。通常可以用来处理一些动态路由。比如,A/B测试,在这里可以随机让部分用户访问指定版本的服务,然后通过用户体验数据的采集和分析来决定哪个版本更好。另外,还可以结合PRE过滤器实现不同版本服务之间的处理。
- POST过滤器(链):它是在请求已被路由到微服务后执行的。可以利用该过滤器实现为响应添加标准的HTTP Header、数据采集、统计信息和指标、审计日志处理等。
- ERROR过滤器:在处理请求过程中发生错误时被调用,可以使用该过滤器实现对异常、错误的统一处理,从而为客户端调用显示更加友好的界面。
Zuul的官方文档中描述的Zuul请求的生命周期如图所示:
当一个客户端Request请求进入Zuul网关服务时,网关先进入“pre filter”,进行一系列的验证、操作或者判断。然后交给“routing filter”进行路由转发,转发到具体的服务实例进行逻辑处理、返回数据。当具体的服务处理完后,最后由“postfilter”进行处理,该类型的处理器处理完之后,将Response信息返回给客户端。ZuulServlet是Zuul的核心Servlet。ZuulServlet的作用是初始化ZuulFilter,并编排这些ZuulFilter的执行顺序。该类中有一个service()方法,执行了过滤器执行的逻辑。
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
try {
this.init((HttpServletRequest)servletRequest, (HttpServletResponse)servletResponse);
RequestContext context = RequestContext.getCurrentContext();
context.setZuulEngineRan();
try {
this.preRoute();
} catch (ZuulException var12) {
this.error(var12);
this.postRoute();
return;
}
try {
this.route();
} catch (ZuulException var13) {
this.error(var13);
this.postRoute();
return;
}
try {
this.postRoute();
} catch (ZuulException var11) {
this.error(var11);
}
} catch (Throwable var14) {
this.error(new ZuulException(var14, 500, "UNHANDLED_EXCEPTION_" + var14.getClass().getName()));
} finally {
RequestContext.getCurrentContext().unset();
}
}
从上面的代码可知,首先执行preRoute()方法,这个方法执行的是PRE类型的过滤器的逻辑。如果执行这个方法时出错了,那么会执行error(e)和postRoute()。然后执行route()方法,该方法是执行ROUTING类型过滤器的逻辑。最后执行postRoute(),该方法执行了POST类型过滤器的逻辑。
Zuul过滤器具有以下关键特性。
- Type(类型):Zuul过滤器的类型,这个类型决定了过滤器在请求的哪个阶段起作用,例如Pre、Post阶段等。
- Execution Order(执行顺序):规定了过滤器的执行顺序,Order的值越小,越先执行。
- Criteria(标准):过滤器执行所需的条件。
- Action(行动):如果符合执行条件,则执行Action(即逻辑代码)。
4、自定义Zuul过滤器
自定义Zuul过滤器很简单,只需要继承ZuulFilter,并实现ZuulFilter中的抽象方法,其中,filterType()
即过滤器的类型,它有4种类型,分别是“pre”“post”“routing”和“error”。filterOrder()
是过滤顺序,它为一个Int类型的值,值越小,越早执行该过滤器。shouldFilter()
表示该过滤器是否过滤逻辑,如果为true,则执行run()方法;如果为false,则不执行run()方法。run()
方法写具体的过滤的逻辑。
4.1、继承ZuulFilter
定义一个POJO类,继承ZuulFilter,并交由Spring IOC 容器
@Component
public class MyPreFilter extends ZuulFilter {
@Override
public String filterType() {
return null;
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return false;
}
@Override
public Object run() throws ZuulException {
return null;
}
}
4.2、指定类型
重写filterType()
方法,指定过滤器类型
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
//return FilterConstants.ROUTE_TYPE;
//return FilterConstants.POST_TYPE;
//return FilterConstants.ERROR_TYPE;
}
4.3、指定执行顺序
重写filterOrder()
方法,指定过滤器的执行顺序,值越小,越先执行
@Override
public int filterOrder() {
return FilterConstants.PRE_DECORATION_FILTER_ORDER - 3;
}
4.4、指定是否开启
重写shouldFilter()
方法,指定是否开启
@Override
public boolean shouldFilter() {
return true;
}
4.5、实现业务
重写run()
方法,实现自定义的业务
@Override
public Object run() throws ZuulException {
System.out.println("MyPreFilter执行。。。");
RequestContext requestContext = RequestContext.getCurrentContext();
String contextPath = requestContext.getRequest().getContextPath();
System.out.println("当前请求路径:" + contextPath);
return null;
}
4.6、测试
在浏览器中访问:http://localhost:8600/demo/v1/emp/emp/all,看到控制台输出日志:
4.7、token校验
一个简单的token校验过滤器:
@Component
public class MyTokenFilter 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() {
return true;
}
@Override
public Object run() throws ZuulException {
System.out.println("MyTokenFilter执行。。。");
RequestContext requestContext = RequestContext.getCurrentContext();
HttpServletRequest request = requestContext.getRequest();
String token = request.getParameter("token");
if (null == token || !"abc".equals(token)) {
requestContext.setSendZuulResponse(false);
requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
}
return null;
}
}
http://localhost:8600/demo/v1/emp/emp/all?token=a
5、动态路由
@Component
public class DynamicRoutingFilter extends ZuulFilter {
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return FilterConstants.PRE_DECORATION_FILTER_ORDER + 4;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext requestContext = RequestContext.getCurrentContext();
HttpServletRequest request = requestContext.getRequest();
String routeKey = request.getParameter("routeKey");
if (null != routeKey && "emp".equalsIgnoreCase(routeKey)) {
requestContext.put(FilterConstants.SERVICE_ID_KEY, "emp-v1");
requestContext.put(FilterConstants.REQUEST_URI_KEY, "/emp/all");
} else if (null != routeKey && "dept".equalsIgnoreCase(routeKey)) {
requestContext.put(FilterConstants.SERVICE_ID_KEY, "dept");
requestContext.put(FilterConstants.REQUEST_URI_KEY, "/dept/all");
}
return null;
}
}