Spring-Cloud 学习笔记-(7)路由网关Zuul

Spring-Cloud 学习笔记-(7)路由网关Zuul

1、简介

2、Zuul加入后的架构

  1. 所有的服务启动后回想Eureka注册。

  2. Zuul作为请求的入口,先进行一顿筛选,对有权限的请求去Eureka注册中心拉去列表,获取服务实例,然后路由到制定的服务上。

  3. 服务之间调用同样也可以先经过网关,由网关获取对应实例去路由。

    最主要的功能,鉴权和限流

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注册中心。****

  1. 加依赖

    <!-- eureka-client -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    
  2. 加配置

    # 注册中心
    eureka:
      client:
        service-url:
          defaultZone: http://127.0.0.1:8761/eureka
    
  3. 修改规则

    zuul:
     routes: #路由的规则
       hehe: #(规则式key-value形式,key只要不重复就行)
         path: /user-service/**  #用来表示以什么开头的路径
         serviceId: user-service #路由到的serviceId
    
    
  4. 同样我们访问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就会顶上。

posted @ 2018-12-26 15:27  神秘的大飞子  阅读(388)  评论(0编辑  收藏  举报