Spring-Cloud 学习笔记-(7)路由网关Zuul
Spring-Cloud 学习笔记-(7)路由网关Zuul
1、简介
2、Zuul加入后的架构
-
所有的服务启动后回想Eureka注册。
-
Zuul作为请求的入口,先进行一顿筛选,对有权限的请求去Eureka注册中心拉去列表,获取服务实例,然后路由到制定的服务上。
-
服务之间调用同样也可以先经过网关,由网关获取对应实例去路由。
最主要的功能,鉴权和限流
3、快速入门
3.1、新建一个Model
3.1.1、pom文件
<?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>cloud-demo</artifactId>
<groupId>com.bigfly</groupId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.bigfly</groupId>
<artifactId>gateway</artifactId>
<dependencies>
<!-- zuul依赖。里面涵盖由web依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
</dependencies>
</project>
3.1.2、启动类
//GateWayApplication类
package com.bigfly;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
@SpringBootApplication
@EnableZuulProxy//开启Zuul注解
public class GateWayApplication {
public static void main(String[] args) {
SpringApplication.run(GateWayApplication.class);
}
}
3.1.3、application.yml
#端口号
server:
port: 10000
#应用名称
spring:
application:
name: gateway
3.2、Zuul的路由
3.2.1、方式一
#方式一:
zuul:
routes: #路由的规则
hehe: #(规则式key-value形式,key只要不重复就行)
path: /user-service/** #用来表示以什么开头的路径
url: http://127.0.0.1:8771 #路由到的url
测试:
分别启动eureka-server和user-service两个服务,然后访问http://127.0.0.1:8771/api/v1/user/2
然后启动gateway(如果启动报错,可能是版本不兼容,把springboot版本降到2.0.6就可以了),访问http://127.0.0.1:10000/user-service/api/v1/user/2
这就证明了,通过上面的配置,我们把所有访问gateway的请求中,只要以ser-service开头的请求全部路由到http://127.0.0.1:8771上。
3.2.2、方式二
上面的方式存在的问题:请求路径匹配写死了。如果后期,用户服务路径修改,没办法及时调整,而且如果用户服务集群搭建,没有办法负载均衡。所以我们就应该把zuul注册到eureka注册中心。****
-
加依赖
<!-- eureka-client --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
-
加配置
# 注册中心 eureka: client: service-url: defaultZone: http://127.0.0.1:8761/eureka
-
修改规则
zuul: routes: #路由的规则 hehe: #(规则式key-value形式,key只要不重复就行) path: /user-service/** #用来表示以什么开头的路径 serviceId: user-service #路由到的serviceId
-
同样我们访问http://127.0.0.1:10000/user-service/api/v1/user/2
虽然表面上看是一样的,但是本质已经发生了改变,用户请求到网关后,网关根据路由规则拿到serviceId,然后根据serviceId去注册中心拉去服务列表,通过负载均衡得到最后的实例,最后调用接口。
3.2.3、方式三
zuul:
routes: #路由的规则
user-service: /user-service/** #key:服务的id,value:服务的映射路径
这种方式本质就是跟方式二一样的,只是简写了而已,因为在方式二中,我们路由的id是任意的,可以叫hehe,也可以叫haha,只要不重复就行,因为我们的serviceId也是唯一的,所以干脆把serviceId当做路由的id,然后再配置一个映射路径就行了。
测试:访问http://127.0.0.1:10000/user-service/api/v1/user/2
3.2.4、方式四
把zuul配置全部删除。我们再访问一下http://127.0.0.1:10000/user-service/api/v1/user/2
我们惊奇的发现依旧可以路由成功。这是什么原因呢,什么都没有配置为什么可以成功呢,因为形如方法三的配置简直是太常见了,所以我们的zuul完全可以把所有的注册列表拉去出来一一匹配。虽然这样看来前面都是白讲了,但是我们一步步的知道了为什么,知其然,知其所以然。
3.2.5、方式五
方式四存在一个问题,就是zuul把所有的微服务都匹配了一个默认路由,但是如果有一个服务我们不想对外开放呢。我们修改一下配置
zuul:
# routes: #路由的规则
# user-service: /user-service/**
ignored-services: #排除哪些服务,是一个集合
- user-service
- order-service
3.3、Zuul的权限控制
3.3.1、顶级父类ZuulFilter
在ZuulFilter中有四个重要的抽象方法:
public abstract class ZuulFilter implements IZuulFilter, Comparable<ZuulFilter> {
abstract public String filterType();//过滤器类型
abstract public int filterOrder();//过滤器优先级
boolean shouldFilter();// 来自IZuulFilter 要不要过滤
Object run() throws ZuulException;// IZuulFilter 过滤的逻辑
}
filterType
:返回字符串,代表过滤器的类型。包含以下4种:pre
:请求在被路由之前执行routing
:在路由请求时调用post
:在routing和errror过滤器之后调用error
:处理请求时发生错误调用
filterOrder
:通过返回的int值来定义过滤器的执行顺序,数字越小优先级越高。shouldFilter
:返回一个Boolean
值,判断该过滤器是否需要执行。返回true执行,返回false不执行。run
:过滤器的具体业务逻辑。
3.3.2、过滤器执行生命周期
这张是Zuul官网提供的请求生命周期图,清晰的表现了一个请求在各个过滤器的执行顺序。
- 正常流程:
- 请求到达首先会经过前置(Pre)过滤器,而后到达路由(Routing)类型,进行路由,请求就到达真正的服务提供者,执行请求,返回结果后,会到达后置(Post)过滤器。而后返回响应。前置后置是针对路由过滤器来说的。
- 异常流程:
- 整个过程中,pre或者routing过滤器出现异常,都会直接进入error过滤器,再error处理完毕后,会将请求交给POST过滤器,最后返回给用户。
- 如果是error过滤器自己出现异常,最终也会进入POST过滤器,而后返回。
- 如果是POST过滤器出现异常,会跳转到error过滤器,但是与pre和routing不同的时,请求不会再到达POST过滤器了。
所有内置过滤器列表:
3.3.3、使用场景
场景非常多:
- 请求鉴权:一般放在pre类型,如果发现没有访问权限,直接就拦截了
- 异常处理:一般会在error类型和post类型过滤器中结合来处理。
- 服务调用时长统计:pre和post结合使用。
3.3.4、自定义登录过滤器
校验用户请求中有没有access_token,有代表请求有效放行。
package com.bigfly.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.apache.commons.lang.StringUtils;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpUtils;
/**
* 登录拦截过滤器
*/
//注入到Spring容器中
@Component
public class LoginFilter extends ZuulFilter {
/**
* 过滤器类型
* @return
*/
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
/**
* 过滤器优先级
* @return
*/
@Override
public int filterOrder() {
return FilterConstants.PRE_DECORATION_FILTER_ORDER - 1;
}
/**
* 需不需要过滤
* @return
*/
@Override
public boolean shouldFilter() {
//拿到上下文对象
RequestContext ctx = RequestContext.getCurrentContext();
//拿到请求路径
String url = ctx.getRequest().getRequestURI();
//针对订单服务过滤
if(StringUtils.isNotBlank(url)&&url.startsWith("/order-service/")){
return true;
}
return false;
}
/**
*
* @return
* @throws ZuulException
*/
@Override
public Object run() throws ZuulException {
//拿到上下文对象
RequestContext ctx = RequestContext.getCurrentContext();
//拿到请求
HttpServletRequest request = ctx.getRequest();
String token = request.getHeader("token");
if(StringUtils.isBlank(token)){
token = request.getParameter("token");
}
//拦截token为空的请求
if(StringUtils.isBlank(token)){
ctx.setSendZuulResponse(false);//是否放行
ctx.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());//设置返回状态码
}
return null;
}
}
测试:访问http://localhost:10000/order-service/api/v1/order/2
我们加上参数token再测试一下
http://localhost:10000/order-service/api/v1/order/2?token=123asd
注意:在ZuulProperties中配置了
private Set<String> sensitiveHeaders = new LinkedHashSet(Arrays.asList("Cookie", "Set-Cookie", "Authorization"));
为了安全起见,Zuul网关会过滤到请求头中的"Cookie", "Set-Cookie", "Authorization"。如果想不被过滤可加配置
zuul:
routes: #路由的规则
user-service: /user-service/**
#处理http请求为空问题
sensitiveHeaders:
3.4、Zuul的限流
如果服务器只能1秒最多能负担100个请求,如果一次性来了200个请求,这样服务器承担不了。所以我们就会对接口进行评估,评估过后然后对接口进行限流。我们使用谷歌的guava框架,SpringCloud已经集成了guava。
3.4.1、令牌桶算法
每秒放一定量的令牌,然后每次请求会从桶中拿一个令牌,拿到令牌的请求放行,没有拿到令牌的请求就拦下来
3.4.2、代码编写
package com.bigfly.filter;
import com.google.common.util.concurrent.RateLimiter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.apache.commons.lang.StringUtils;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.http.HttpStatus;
/**
* 针对订单接口的限流
*/
public class OrderRateLimiterFilter extends ZuulFilter {
/**
* 创建令牌桶 每一秒创建多少令牌
*/
private static final RateLimiter RATE_LIMITER = RateLimiter.create(100);
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return FilterConstants.SERVLET_DETECTION_FILTER_ORDER -1;
}
@Override
public boolean shouldFilter() {
//拿到请求路径
String url = RequestContext.getCurrentContext().getRequest().getRequestURI();
//针对订单服务过滤
if(StringUtils.isNotBlank(url)&&url.startsWith("/order-service/")){
return true;
}
return false;
}
@Override
public Object run() throws ZuulException {
//用非阻塞的方式拿令牌
boolean flag = RATE_LIMITER.tryAcquire();
if(!false){
RequestContext ctx = RequestContext.getCurrentContext();
ctx.setSendZuulResponse(false);//是否放行
ctx.setResponseStatusCode(HttpStatus.TOO_MANY_REQUESTS.value());//设置返回状态码
}
return null;
}
}
这里就简单说一下,不做测试了。
3.5、Zuul的熔断和降级
Zuul里面以及涵盖了Hystrix和Ribbon,并且和feign不同的是Zuul 默认Hystrix已经开启。所以这里我们只要配一下Hystrix和ribbon的超时时长就行了。
hystrix:
command:
default:
execution.isolation.thread.timeoutInMilliseconds: 3000
ribbon:
ConnectTimeout: 250 # Ribbon的连接超时时间
ReadTimeout: 1000 # Ribbon的数据读取超时时间
OkToRetryOnAllOperations: true # 是否对所有操作都进行重试
MaxAutoRetriesNextServer: 1 # 切换实例的重试次数
MaxAutoRetries: 0 # 对当前实例的重试次数
我们要求Ribbon的超时时长必须小于Hystrix的超时时长,这样就不会导致ribbon还没有完成重试,Hystrix就熔断了。
//AbstractRibbonCommand类
//真正的ribbon的超时时长
ribbonTimeout = (ribbonReadTimeout + ribbonConnectTimeout) * (maxAutoRetries + 1) * (maxAutoRetriesNextServer + 1);
3.6、Zuul的高可用
Zuul的高可用其实就是启动多台Zuul服务器就行了,但是这样Zuul高可用了,用户不知道要访问哪一个Zuul,所以在Zuul外面还要套一层nginx,通过nginx实现反向代理和负载均衡。但是这样Zuul没问题了nginx如何实现高可用呢,一般大型的电商网站都会用ip漂移,一个域名会绑定多个ip,多个nignx,然后网络运营商会根据用户的请求分配到最近的ngixn上,如果只有一个ip,也可以用主从nginx,(nginx+keeplive+LVS)一旦主nginx挂了,从nignx就会顶上。