在微服务架构中,需要几个基础的服务治理组件,包括服务注册与发现、服务消费、负载均衡、断路器、智能路由、配置管理等,由这几个基础组件相互协作,共同组建了一个简单的微服务系统。一个简答的微服务系统如下图:

在Spring Cloud微服务系统中,一种常见的负载均衡方式是,客户端的请求首先经过负载均衡(zuul、Ngnix),再到达服务网关(zuul集群),然后再到具体的服。,服务统一注册到高可用的服务注册中心集群,服务的所有的配置文件由配置服务管理(下一篇文章讲述),配置服务的配置文件放在git仓库,方便开发人员随时改配置。
一、Zuul简介
Zuul的主要功能是路由转发和过滤器。路由功能是微服务的一部分,比如api/user转发到到user服务,/api/order转发到到order服务。zuul默认和Ribbon结合实现了负载均衡的功能。
- SpringCloud Zuul通过与SpringCloud Eureka进行整合,将自身注册为Eureka服务治理下的应用,同时从Eureka中获得了所有其他微服务的实例信息。外层调用都必须通过API网关,使得将维护服务实例的工作交给了服务治理框架自动完成。
- 在API网关服务上进行统一调用来对微服务接口做前置过滤,以实现对微服务接口的拦截和校验。
Zuul天生就拥有线程隔离和断路器的自我保护功能,以及对服务调用的客户端负载均衡功能。也就是说:Zuul也是支持Hystrix和Ribbon。
zuul有以下功能:
- Authentication
- Insights
- Stress Testing
- Canary Testing
- Dynamic Routing
- Service Migration
- Load Shedding
- Security
- Static Response handling
- Active/Active traffic management
1.1、可能对Zuul的疑问
Zuul支持Ribbon和Hystrix,也能够实现客户端的负载均衡。我们的Feign不也是实现客户端的负载均衡和Hystrix的吗?既然Zuul已经能够实现了,那我们的Feign还有必要吗?
或者可以这样理解:
- zuul是对外暴露的唯一接口相当于路由的是controller的请求,而Ribbonhe和Fegin路由了service的请求
- zuul做最外层请求的负载均衡 ,而Ribbon和Fegin做的是系统内部各个微服务的service的调用的负载均衡
有了Zuul,还需要Nginx吗?他俩可以一起使用吗?
- 我的理解:Zuul和Nginx是可以一起使用的(毕竟我们的Zuul也是可以搭成集群来实现高可用的),要不要一起使用得看架构的复杂度了(业务)~~~
二、实战
2.1、创建zuul工程
其pom.xml文件如下:
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring-cloud-test</artifactId>
<groupId>com.dukun.study</groupId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>zuul</artifactId>
<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>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
在其入口applicaton类加上注解@EnableZuulProxy,开启zuul的功能:
package com.dukun.study.zuul;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
/**
* zuul网关
*
* @Author: dukun0210
* @Date: 2021/1/12 14:17
*/
@SpringBootApplication
@EnableZuulProxy
public class AppZuul {
public static void main(String[] args) {
SpringApplication.run(AppZuul.class);
}
}
yml文件:
server:
port: 9000
eureka:
client:
serviceUrl:
defaultZone: http://localhost:3000/eureka/ #eureka服务端提供的注册地址 参考服务端配置的这个路径
instance:
instance-id: zuul-0 #此实例注册到eureka服务端的唯一的实例ID
prefer-ip-address: true #是否显示IP地址
leaseRenewalIntervalInSeconds: 20 #eureka客户需要多长时间发送心跳给eureka服务器,表明它仍然活着,默认为30 秒 (与下面配置的单位都是秒)
leaseExpirationDurationInSeconds: 60 #Eureka服务器在接收到实例的最后一次发出的心跳后,需要等待多久才可以将此实例删除,默认为90秒
spring:
application:
name: zuul #此实例注册到eureka服务端的name
zuul:
prefix: /api
ignored-services: "*"
# stripPrefix: false
routes:
order:
serviceId: server-order
path: /order/**
user:
serviceId: client-user
path: /user/**
power:
serviceId: server-power
path: /power/**
注意/ **代表是所有层级 / * 是代表一层。 如果是/ * 的话 /power/admin/getUser.do 就不会被路由 。
这样 简单的zuul就搭建好了, 启动项目 我们就可以通过zuul然后加上对应的访问微服务:
以/api/order/** 开头的请求都转发给server-order服务;以/api/user/**开头的请求都转发给client-user服务;
http://localhost:9000/api/user/getOrderAndPowerrFeign.do?name=8848 就可以访问到 user服务中的 /getOrderAndPowerrFeign.do 方法。
2.2、相关配置
2.2.1、统一前缀
这个很简单,就是我们可以在前面加一个统一的前缀,这个时候我们在
yaml
配置文件中添加如下。
zuul:
prefix: /zuul
2.2.2、路由策略配置
你会发现前面的访问方式(直接使用服务名),需要将微服务名称暴露给用户,会存在安全性问题。所以,可以自定义路径来替代微服务名称,即自定义路由策略。
zuul: prefix: /api routes: order: serviceId: server-order path: /order/**
2.2.3、服务名屏蔽
这个时候你别以为你好了,你可以试试,在你配置完路由策略之后使用微服务名称还是可以访问的,这个时候你需要将服务名屏蔽。
zuul:
ignore-services: "*"
2.2.4、路径屏蔽
Zuul
还可以指定屏蔽掉的路径 URI,即只要用户请求中包含指定的 URI 路径,那么该请求将无法访问到指定的服务。通过该方式可以限制用户的权限。
zuul:
ignore-patterns: **/auto/**
2.3、Zuul 的过滤功能
如果说,路由功能是
Zuul
的基操的话,那么过滤器就是
Zuul
的利器了。毕竟所有请求都经过网关(Zuul),那么我们可以进行各种过滤,这样我们就能实现限流,灰度发布,权限控制等等。
先解释一下关于过滤器的一些注意点:
zuul中定义了4种标准过滤器类型,这 些过滤器类型对应于请求的典型生命周期。
过滤器类型:
- PRE:这种过滤器在请求被路由之前调用。可利用这种过滤器实现身份 验证、在 集群中选择请求的微服务、记录调试信息等。
- ROUTING:这种过滤器将请求路由到微服务。这种过滤器 用于构建发送给微服 务的请求,并使用 Apache HttpCIient或 Netfilx Ribbon请求微服务
- POST:这种过滤器在路由 到微服务以后执行。这种过滤器可用来为响应添加标准 的 HTTP Header、收集统计信息和指标、将响应从微服务 发送给客户端等。
- ERROR:在其他阶段发生错误时执行该过滤器。
2.3.1、简单实现一个请求访问记录打印
/**
* ZUUL 网关的过滤器
* 添加日志功能
*
* @Author: dukun0210
* @Date: 2021/1/13 10:24
*/
@Component
public class LogFilter extends ZuulFilter {
//Pre、Routing、Post。
// 前置Pre就是在请求之前进行过滤,
// Routing路由过滤器就是我们上面所讲的路由策略,
// 而Post后置过滤器就是在Response之前进行过滤的过滤器。
@Override
public String filterType() {
return FilterConstants.POST_TYPE;
}
// 指定过滤顺序 越小越先执行,这里第一个执行
// 当然不是只真正第一个 在Zuul内置中有其他过滤器会先执行
// 那是写死的 比如 SERVLET_DETECTION_FILTER_ORDER = -3
@Override
public int filterOrder() {
return FilterConstants.PRE_DECORATION_FILTER_ORDER;
}
// 什么时候该进行过滤
// 这里我们可以进行一些判断,这样我们就可以过滤掉一些不符合规定的请求等等
@Override
public boolean shouldFilter() {
return true;
}
// 如果过滤器允许通过则怎么进行处理
@Override
public Object run() throws ZuulException {
RequestContext currentContext = RequestContext.getCurrentContext();
HttpServletRequest request = currentContext.getRequest();
String remoteAddr = request.getRemoteAddr();
System.out.println("访问者IP:"+remoteAddr+"访问地址:"+request.getRequestURI());
return null;
}
}
由代码可知,自定义的 zuul Filter需实现以下几个方法。
filterType:返回过滤器的类型。有 pre、 route、 post、 error等几种取值,分别对应上文的几种过滤器。 详细可以参考 com.netflix.zuul.ZuulFilter.filterType()中的注释。
filter0rder:返回一个 int值来指定过滤器的执行顺序,不同的过滤器允许返回相同的数字。
shouldFilter:返回一个 boolean值来判断该过滤器是否要执行, true表示执行, false表示不执行。
run:过滤器的具体逻辑。 禁用zuul过滤器 Spring Cloud默认为Zuul编写并启用了一些过滤器,例如DebugFilter、 FormBodyWrapperFilter 等,这些过滤器都存放在spring-cloud-netflix-core这个jar包 里,一些场景下,想要禁用掉部分过滤器,该怎么办 呢? 只需在application.yml里设置zuul...disable=true 例如,要禁用上面我们写的过滤器,这样配置就行了: zuul.LogFilter.pre.disable=true
2.3.2、令牌桶限流
当然不仅仅是令牌桶限流方式,
Zuul
只要是限流的活它都能干,这里我只是简单举个例子。
我先来解释一下什么是令牌桶限流吧。
首先我们会有个桶,如果里面没有满那么就会以一定固定的速率会往里面放令牌,一个请求过来首先要从桶中获取令牌,如果没有获取到,那么这个请求就拒绝,如果获取到那么就放行。很简单吧,啊哈哈、
下面我们就通过
Zuul
的前置过滤器来实现一下令牌桶限流。
@Component
@Slf4j
public class RouteFilter extends ZuulFilter {
// 定义一个令牌桶,每秒产生2个令牌,即每秒最多处理2个请求
private static final RateLimiter RATE_LIMITER = RateLimiter.create(2);
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return -5;
}
@Override
public Object run() throws ZuulException {
log.info("放行");
return null;
}
@Override
public boolean shouldFilter() {
RequestContext context = RequestContext.getCurrentContext();
if(!RATE_LIMITER.tryAcquire()) {
log.warn("访问量超载");
// 指定当前请求未通过过滤
context.setSendZuulResponse(false);
// 向客户端返回响应码429,请求数量过多
context.setResponseStatusCode(429);
return false;
}
return true;
}
}
这样我们就能将请求数量控制在一秒两个,有没有觉得很酷?
2.3.3、zuul容错与回退
zuul默认是整合了hystrix和ribbon的, 提供降级回退,那如何来使用hystrix呢?
我们自行写一个类,继承FallbackProvider 类 然后重写里面的方法。
package com.dukun.study.zuul.provider;
import com.netflix.hystrix.exception.HystrixTimeoutException;
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;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* zuul网关的 容错
*
* @Author: dukun0210
* @Date: 2021/1/13 11:10
*/
@Component
public class FallBackProvider implements FallbackProvider {
@Override
public String getRoute() {
//制定为哪个微服务提供回退(这里写微服务名 写*代表所有微服务)
return "*";
}
//此方法需要返回一个ClientHttpResponse对象 ClientHttpResponse是一个接口,具体的回退逻辑要实 现此接口
//route:出错的微服务名 cause:出错的异常对象
@Override
public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
//这里可以判断根据不同的异常来做不同的处理, 也可以不判断
//完了之后调用response方法并根据异常类型传入HttpStatus
if (cause instanceof HystrixTimeoutException) {
return response(HttpStatus.GATEWAY_TIMEOUT);
} else {
return response(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
private ClientHttpResponse response(final HttpStatus status) {
//这里返回一个ClientHttpResponse对象 并实现其中的方法,关于回退逻辑的详细,便在下面的方法中
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
返回一个HttpStatus对象 这个对象是个枚举对象, 里面包含了一个status code 和
//reasonPhrase信息
return status;
}
@Override
public int getRawStatusCode() throws IOException {
//返回status的code 比如 404,500等
return status.value();
}
@Override
public String getStatusText() throws IOException {
//返回一个HttpStatus对象的reasonPhrase信息
return status.getReasonPhrase();
}
@Override
public void close() {
//close的时候调用的方法, 讲白了就是当降级信息全部响应完了之后调用的方法
}
@Override
public InputStream getBody() throws IOException {
//吧降级信息响应回前端
return new ByteArrayInputStream("系统繁忙,稍后再试".getBytes());
}
@Override
public HttpHeaders getHeaders() {
//需要对响应报头设置的话可以在此设置
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
};
}
}
关于 Zuul 的其他
Zuul
的过滤器的功能肯定不止上面我所实现的两种,它还可以实现权限校验,包括我上面提到的灰度发布等等。
当然,
Zuul
作为网关肯定也存在单点问题,如果我们要保证
Zuul
的高可用,我们就需要进行
Zuul
的集群配置,这个时候可以借助额外的一些负载均衡器比如
Nginx
。