Zuul

不管是来自于客户端(PC或移动端)的请求,还是服务内部调用。一切对服务的请求都会经过Zuul这个网关,然后再由网关来实现 鉴权、动态路由等等操作。Zuul就是我们服务的统一入口。

入门

新建项目

新建一个新的模块 gateway,导入相关依赖

<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>

编写启动类

@SpringBootApplication
@EnableZuulProxy     // 开启Zuul的网关功能
public class GatewayApplication {

    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class);
    }
}

编写配置

server:
  port: 10010 #服务端口
spring: 
  application:  
    name: api-gateway #指定服务名

编写路由规则

我们需要用Zuul来代理user-service服务,先看一下控制面板中的服务状态:

请输入图片描述

  • ip为:127.0.0.1
  • 端口为:8888

映射规则:

zuul:
  routes:
    user-service: # 这里是路由id,随意写
      path: /user-service/** # 这里是映射路径
      url: http://127.0.0.1:8888 # 映射路径对应的实际url地址

我们将符合path 规则的一切请求,都代理到 url参数指定的地址

本例中,我们将 /user-service/**开头的请求,代理到http://127.0.0.1:8888

启动测试

访问的路径中需要加上配置规则的映射路径,我们访问:http://127.0.0.1:10010/user-service/user/5

请输入图片描述

面向服务的路由

在刚才的路由规则中,我们把路径对应的服务地址写死了!如果同一服务有多个实例的话,这样做显然就不合理了。

我们应该根据服务的名称,去Eureka注册中心查找 服务对应的所有实例列表,然后进行动态路由才对!

添加Eureka客户端依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

修改配置

zuul:
  routes:
    user-service: # 这里是路由id,随意写
      path: /user-service/** # 这里是映射路径
      serviceId: user-service # 指定服务名称
      # url: http://127.0.0.1:8888 # 映射路径对应的实际url地址


eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka

启动测试

再次启动,这次Zuul进行代理时,会利用Ribbon进行负载均衡访问: 请输入图片描述

日志中可以看到使用了负载均衡器: 请输入图片描述

简化路由配置

在刚才的配置中,我们的规则是这样的:

  • zuul.routes.<route>.path=/xxx/**: 来指定映射路径。<route>是自定义的路由名
  • zuul.routes.<route>.serviceId=/user-service:来指定服务名。

而大多数情况下,我们的<route>路由名称往往和 服务名会写成一样的。因此Zuul就提供了一种简化的配置语法:zuul.routes.<serviceId>=<path>

比方说上面我们关于user-service的配置可以简化为一条:

zuul:
  routes:
    user-service: /user-service/** # 这里是映射路径

省去了对服务名称的配置。

默认的路由规则

  • 默认情况下,一切服务的映射路径就是服务名本身。
    • 例如服务名为:user-service,则默认的映射路径就是:/user-service/**

Zuul注册到Eureka后,会默认给所有服务都添加一个默认配置 就比如: 我们之前在配置里并没有配置consumer相关的配置 但是我们访问http://127.0.0.1:10010/consumer_server/consumer/1却可以成功

请输入图片描述

当然我们也可以选择禁用掉默认配置,只允许它服务间调用

zuul:
  ignored-services:
    - consumer_server

也可以一次性禁用掉所有的默认配置

zuul:
  ignored-services:    "*"

这个时候我们再去访问刚才的路径http://127.0.0.1:10010/consumer_server/consumer/1就成功不了了 请输入图片描述

值得注意的是

  • 当你简化了路由配置后,就比如简化成这样user-service: /user/**,这个时候,Zuul的默认配置依旧会生效
  • 意思就是说,你可以使用http://127.0.0.1:10010/user-service/user/1访问,也可以使用http://127.0.0.1:10010/user/user/1访问

请输入图片描述

路由前缀

zuul:
  prefix: /api # 添加路由前缀
  routes:
      user-service: # 这里是路由id,随意写
        path: /user-service/** # 这里是映射路径
        service-id: user-service # 指定服务名称

我们通过zuul.prefix=/api来指定了路由的前缀,这样在发起请求时,路径就要以/api开头。

路径/api/user-service/user/1将会被代理到/user-service/user/1

可以添加前缀必然也可以去除前缀

就比如我们上面说可以使用http://127.0.0.1:10010/user/user/1访问

那我们也可以使用http://127.0.0.1:10010/user/1访问

zuul:
  routes:
    user-service: # 这里是路由id,随意写
      path: /user-service/** # 这里是映射路径
      serviceId: user-service
      strip-prefix: false #去除前缀
      # url: http://127.0.0.1:8888 # 映射路径对应的实际url地址
  ignored-services:
    - consumer_server

重启项目访问成功

请输入图片描述

  • strip-prefix: false 去除前缀默认为true,跟在zuul后面就是全局去除(此时对于路由id所配置的路径就无效),跟路由id就是局部去除
  • 我们虽然使用http://127.0.0.1:10010/user/1访问成功,少写了一个/user
  • 但是yml配置文件里却多写了很多,有得有失,所以具体情况具体对待

过滤器

Zuul作为网关的其中一个重要功能,就是实现请求的鉴权。而这个动作我们往往是通过Zuul提供的过滤器来实现的。

ZuulFilter

ZuulFilter是过滤器的顶级父类。在这里我们看一下其中定义的4个最重要的方法:

public abstract ZuulFilter implements IZuulFilter{

    abstract public String filterType();    //过滤器类型

    abstract public int filterOrder();        //过滤器顺序
    
    boolean shouldFilter();// 来自IZuulFilter  要不要过滤

    Object run() throws ZuulException;// IZuulFilter    过滤逻辑
}
  • shouldFilter:返回一个Boolean值,判断该过滤器是否需要执行。返回true执行,返回false不执行。
  • run:过滤器的具体业务逻辑。
  • filterType:返回字符串,代表过滤器的类型。包含以下4种:
    • pre:请求在被路由之前执行
    • routing:在路由请求时调用
    • post:在routing和errror过滤器之后调用
    • error:处理请求时发生错误调用
  • filterOrder:通过返回的int值来定义过滤器的执行顺序,数字越小优先级越高。

过滤器执行生命周期

这张是Zuul官网提供的请求生命周期图,清晰的表现了一个请求在各个过滤器的执行顺序。

请输入图片描述

  • 正常流程:
    • 请求到达首先会经过pre类型过滤器,而后到达routing类型,进行路由,请求就到达真正的服务提供者,执行请求,返回结果后,会到达post过滤器。而后返回响应。
  • 异常流程:
    • 整个过程中,pre或者routing过滤器出现异常,都会直接进入error过滤器,再error处理完毕后,会将请求交给POST过滤器,最后返回给用户。
    • 如果是error过滤器自己出现异常,最终也会进入POST过滤器,而后返回。
    • 如果是POST过滤器出现异常,会跳转到error过滤器,但是与pre和routing不同的时,请求不会再到达POST过滤器了。

所有内置过滤器列表:

请输入图片描述

使用场景:

  • 请求鉴权:一般放在pre类型,如果发现没有访问权限,直接就拦截了
  • 异常处理:一般会在error类型和post类型过滤器中结合来处理。
  • 服务调用时长统计:pre和post结合使用。

自定义过滤器

定义过滤器类

package cn.itcast.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

/**
 * @Author: 萧一旬
 * @Description:
 * @Date: Create in 14:38 2019/4/13
 */
@Component
public class LoginFilter 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() {
        // 返回true,代表过滤器生效。
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        //获取请求上下文
        RequestContext context = RequestContext.getCurrentContext();

        //获取request
        HttpServletRequest request = context.getRequest();

        //获取请求参数
        String token = request.getParameter("access-token");

        //判断是否存在
        if (StringUtils.isBlank(token)) {
            //不存在,未登录 拦截
            context.setSendZuulResponse(false);
            //返回403
            context.setResponseStatusCode(HttpStatus.FORBIDDEN.value());
        }
        return null;
    }
}

测试

运行项目 不携带Token访问 请输入图片描述

携带Token访问

请输入图片描述

熔断和负载均衡

Zuul中默认就已经集成了Ribbon负载均衡和Hystix熔断机制。但是所有的超时策略都是走的默认值,比如熔断超时时间只有1S,很容易就触发了。因此建议我们手动进行配置:

zuul:
  retryable: true
ribbon:
  ConnectTimeout: 250 # 连接超时时间(ms)
  ReadTimeout: 2000 # 通信超时时间(ms)
  OkToRetryOnAllOperations: true # 是否对所有操作重试
  MaxAutoRetriesNextServer: 2 # 同一服务不同实例的重试次数
  MaxAutoRetries: 1 # 同一实例的重试次数
hystrix:
  command:
  	default:
        execution:
          isolation:
            thread:
              timeoutInMillisecond: 6000 # 熔断超时时长:6000ms

Ribbon的超时时长,真实值是(read + connect) * 2 ,必须小于Hystrix的时长,否则会报警告,虽然不会影响运行

请输入图片描述

计算公式

ribbonTimeout = (ribbonReadTimeout + ribbonConnectTimeout) * (maxAutoRetries + 1) * (maxAutoRetriesNextServer + 1);
  • 不知是何原因,或许是我设置问题,无论我在ymlribbon.ConnectTimeout值为多少,ribbonConnectTimeout恒等于1000

一些常见的BUG

com.netflix.zuul.exception.ZuulException: Hystrix Readed time out

请输入图片描述

这个错误是应为zuul的默认超时时间比较小,我们配置下zuul的超时时间,因zuul启用了ribbon的负载均衡,还需要设置ribbon的超时时间,注意ribbon的超时时间要小于zuul超时时间 。

zuul:
  host:
    connect-timeout-millis: 15000 #HTTP连接超时要比Hystrix的大
    socket-timeout-millis: 60000   #socket超时
ribbon:
  ReadTimeout: 10000
  ConnectTimeout: 10000

springcloud 加入spring session通过zuul请求session不一致问题

对应的添加一条配置即可

zuul:
  prefix: /api # 添加路由前缀
  routes:
      user-service: # 这里是路由id,随意写
        path: /user-service/** # 这里是映射路径
        service-id: user-service # 指定服务名称

        # 加入以下配置即可
        sensitiveHeaders: "*"

项目代码地址:https://github.com/AHWH980802/cloud-demo