Zuul入门

Zuul网关

网关是什么?

我们在了解Zuul之前,先来了解一下网关是什么,它对于之后的学习有很大的帮助。因为Zuul就是在SpringCloud中起到一个网关作用的组件。

网关是连接两个或者以上不同网络的设备,它可以是一台路由器、交换机等设备。例如在一个机房中,有若干个局域网,它们又通过一个网关连接,网关再与外界的互联网通讯,当你想访问这个机房的设备时,也会先通过网关网关再按需转发到对应的机器,但是如果网关坏了,整个机房的设备就无法正常联网。它既是连接这些局域网的一个中介也是守在整个机房前的大门,对于不友善的请求也可以进行拦截,类似于路由器的MAC过滤、IP地址过滤、端口过滤、限制流量等功能。

什么是API网关

在微服务架构中,通常会有多个提供者。设想一个电商系统,可能会有商品、订单、支付、用户等多个类型的服务,而每个类型的服务数量也会随着整个系统体量的增大也会随之增长和变更。作为UI端,在展示页面时可能要从多个微服务中聚合数据,而服务和划分的位置结构可能会有所改变。网关就可以对外暴露聚合API,屏蔽内部微服务的微小变动,保持整个系统的稳定性。

当然这是是众多网关功能的一部分,它还可以做负载均衡,统一鉴权,协议转换,监控数据等一系列功能。

Zuul简介

Zuul是Spring Cloud全家桶中的微服务API网关。它的取名来自于电影《捉鬼敢死队》中的怪兽Zuul。

所有设备或网站来的请求都会经过Zuul到达后端的Netflix应用程序。作为一个便捷性质的应用程序,Zuul提供了动态路由、监控、弹性负载和安全功能。Zuul底层利用了各种Filter实现如下功能。

  • 认证和安全 识别每个需要认证的资源,拒绝不符合要求的请求。
  • 性能检测 在服务便捷追踪并统计数据,提供精确的生产视图。
  • 动态路由 根据需要将请求动态路由到后端集群。
  • 压力测试 逐渐增加对集群流量以了解其性能。
  • 负载卸载 预先为每种类型的请求分配容量,当请求超过容量时自动丢弃。
  • 静态资源处理 在某些边界返回某些相应。

可以说Zuul就是微服务中的守关大BOSS,它下裆下所有请求,再按需转发。

在没有Zuul之前,用户可以随意的访问我们的所有服务,但这样就出现了安全问题。对于一部分服务我们只希望让服务之间去调用,不希望用户直接拿到接口。加入Zuul之后微服务的架构就如下图所示:

以前用户可以直接请求微服务,而现在用户必须先经过Zuul网关,由Zuul进行证身份、负载均衡、请求转发等功能。

Zuul路由快速入门

第一步:创建一个微服务项目,继承之前创建的SpringCloud Parent项目。

<?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>SpringCloudParent</artifactId>
        <groupId>cn.rayfoo</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>Zuul-Server</artifactId>
    <packaging>jar</packaging>

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

</project>

第二步:创建启动类,加入@SpringBootApplication和@EnableZuulProxy两个注解。

package cn.rayfoo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;

/**
 * @Author: rayfoo@qq.com
 * @Date: 2020/7/5 2:02 下午
 * @Description:
 */
@EnableZuulProxy
@SpringBootApplication
public class ZuulRunner {

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

}

第三步:配置yml文件

server:
  port: 80
zuul:
  routes:
    emp-provider:
      path: /employee/**
      url: http://127.0.0.1:8081

其中emp-provider为路由id,可以是任意名称,只要不重复即可,path为该路由的拦截规则,为将拦截到的请求转发到哪个服务器上。这样的规则可以配置多个,但是要注意缩进。

启动Eureka、Zuul、provider、consumer测试:

![image-20200705142626681](/Users/rayfoo/Library/Application Support/typora-user-images/image-20200705142626681.png)

虽然成功显示了页面,但是发现问题:当服务器出现异常,没有进行服务降级,排查后发现转发的是provider而非consumer,所以要想实现容错还需配置为8082端口,而且要想办法屏蔽8081端口。

而且请求路径多了一级employee,略显臃肿。

优化配置

由于请求路径多了一级,先的过于重复,我们可以将拦截规则称配置为路由id,测试OK

server:
  port: 80
zuul:
  routes:
    emp-provider:
      path: /emp-provider/**
      url: http://127.0.0.1:8081

整合Eureka

上面只是一个请求,如果是所有的请求都配置也是非常繁琐的,并且转发路径是写死的,产生了硬编码,且无法实现负载均衡。所以我们在配置的时候,不应该是面向URL的,而是面向服务的配置。下面我们进行下一步的优化。

第一步:在Zuul-Server中引入Eureka客户端依赖

<?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>SpringCloudParent</artifactId>
        <groupId>cn.rayfoo</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>Zuul-Server</artifactId>
    <packaging>jar</packaging>

    <dependencies>
        <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>
    </dependencies>
</project>

第二步:配置eureka服务器,将url配置改为serviceId配置

server:
  port: 80
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:8761/eureka,http://127.0.0.1:8762/eureka
spring:
  application:
    name: zuul-Server
zuul:
  routes:
    emp-provider: /emp-provider/**

此时的服务调用流程是用户请求先被path拦截到,再通过从eureka上拉去的服务中找到emp-privider,再根据Ribbon提供的负载均衡规则进行负载均衡转发到具体的服务地址。

上述的配置还是非常的繁琐,SpringCloud也考虑到了开发者的需求,实现了简化版本的配置。下面的配置的作用与上述一致。(key为服务的id,value为拦截路径。)只要遵循此路径的都会被转发到其对应key的服务

server:
  port: 80
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:8761/eureka,http://127.0.0.1:8762/eureka
zuul:
  routes:
    emp-provider: /emp-provider/**

Zuul默认配置

此时consumer是没有在routers中配置的,但是我们再浏览器中直接访问,却惊奇的发现,也可以访问【注:这里的consumer已打错了,已更正】

![image-20200705150013298](/Users/rayfoo/Library/Application Support/typora-user-images/image-20200705150013298.png)

这是由于这样的编写太常见了,索性SpringCloud就直接默认对Eureka中所有的服务按照该规则进行了配置。所以Zuul其实不进行配置也可以直接实现的。

zuul忽略配置

但是Zuul的默认配置存在着一些弊端,比如服务名过长,服务不能对外隐藏。所以我们一般不会直接使用默认的配置。

在Zuul中可以配置ignored-services,用来隐藏取消某个服务的默认配置。其value是List类型的,配置方式如下:

server:
  port: 80
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:8761/eureka,http://127.0.0.1:8762/eureka
spring:
  application:
    name: zuul-Server
zuul:
  routes:
    emp-provider: /emp-provider/**
  ignored-services:
    - emp-consumer
    - emp-provider

此时emp-provider服务仍然可以照常使用http://localhost/emp-provider/employee/7566访问(虽然忽略了默认配置,但是手动配置的内容与默认相等),但是emp-consumer已经无法通过zuul的端口进行访问了。

路由前缀

在上一步中,使用http://localhost/emp-provider/employee/7566已经可以进行的路由转发了,但是还需要加上服务名作为前缀,还是过于繁琐,能不能把路有前缀也去掉呢?使其的调用像本地调用一样。

答案是可以的,但是配置略为繁琐,先将path改为/employee/**,再将路由id下配置一个属性:strip-prefix: false ,之后直接访问http://localhost/employee/7566即可。

server:
  port: 80
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:8761/eureka,http://127.0.0.1:8762/eureka
spring:
  application:
    name: zuul-Server
zuul:
  routes:
    emp-provider: #路由id
      path: /employee/** #拦截路径
      serviceId: emp-provider #转发到的服务名称
      strip-prefix: false #是否要去掉前缀
  ignored-services:
    - emp-consumer
    - emp-provider

此时调用微服务项目就像直接调用的是同一台机器一样了,但是用户不知道的是背后可能是一个庞大的集群。

zuul的属性中还存在一个prefix,可以给zuul所拦截的请求增加前缀,例如prefix: /api,就只会拦截api开头的url了,不过这个属性一般不会配置。

Zuul过滤器

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

ZuulFilter是zuul中实现的过滤器的顶级父类,这里我们看一下其中定义的四个最重要的方法。

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。
  • filterType(); 过滤类型,可以使用FilterConstants调用其内的常量,也可以使用下面的字符串常量
    • pre:前置,在路由之前执行
    • routing:在路由请求时调用
    • post:在routing和error过滤器之后调用
    • error:处理请求时发生错误调用
  • filterOrder(); 过滤顺序 通过返回的int值来定义过滤器的执行顺序,数字越小优先级越高。同样可以使用FilterConstants中的常量设置,其中定义了zuul中内置过滤器的顺序,我们可以根据这个顺序来指定自己过滤器的顺序,一般借助这些过滤器的值+1或者-1。
  • run(); 过滤器的具体业务逻辑

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

所有内置过滤器列表:

过滤器使用场景

Zuul过滤器的使用场景

  • 权限鉴定:一般用在pre类型,如果发现没有访问权限就直接拦截。
  • 异常处理:一般会放在error类型,亦或是post类型过滤器中结合使用。
  • 服务调用时长统计:pre结合post使用。
  • 流量限制:在pre和post的配合下可以判断在线人数,可以根据在线人数进行人数限制

自定义过滤器

接下来我们来自定义一个过滤器,模拟一个登录的校验。基本逻辑:如果请求中有access-token参数,则认为请求有效,放行。

第一步:创建一个filter类,继承ZuulFilter,加上@Component注解

package cn.rayfoo.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;

/**
 * @Author: rayfoo@qq.com
 * @Date: 2020/7/5 4:10 下午
 * @Description: 用户校验过滤器
 */
@Component
public class LoginFilter 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;
    }
}

第二步:配置过滤器

  • type使用FilterConstants进行设置。

  • order同样参考其他过滤器的FilterConstants进行设置。

  • shouldFilter设置为true。

  • run可以选中ZuulFilter按下ctrl+H找到FormBodyWrapperFilter的源码,参考其代码进行配置

package cn.rayfoo.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.http.HttpStatus;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

/**
 * @Author: rayfoo@qq.com
 * @Date: 2020/7/5 4:10 下午
 * @Description: 用户校验过滤器
 */
@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() {
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        //上下文
        RequestContext ctx = RequestContext.getCurrentContext();
        //获取request对象
        HttpServletRequest request = ctx.getRequest();
        //获取请求参数
        String token = request.getParameter("access-token");
        //判断token是否存在
        if(StringUtils.isBlank(token)){
            //设置true放行,设置为false拦截
            ctx.setSendZuulResponse(false);
            //返回一个状态码,从HttpStatus中获取
            ctx.setResponseStatusCode(HttpStatus.FORBIDDEN.value());
        }
        return null;
    }
}

此时测试不加参数的请求结果:

加上参数后:

负载均衡和熔断(整合Hystrix、Ribbon)

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

server:
  port: 80
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:8761/eureka,http://127.0.0.1:8762/eureka
spring:
  application:
    name: zuul-Server
zuul:
  routes:
    emp-provider:
      path: /employee/**
      serviceId: emp-provider
      strip-prefix: false
  ignored-services:
    - emp-consumer
    - emp-provider
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 3000 # 服务降级超时时长:3000ms
ribbon:
  ConnectTimeout: 250 # 连接超时时间(ms)
  ReadTimeout: 2000 # 通信超时时间(ms)
  OkToRetryOnAllOperations: true # 是否对所有操作重试
  MaxAutoRetriesNextServer: 2 # 同一服务不同实例的重试次数
  MaxAutoRetries: 1 # 同一实例的重试次数

其中Ribbon和Hystrix的配置大都是没有提示的,Ribbon的配置的key可以在RibbonCommandFactoryConfiguration类和AbstractRibbonCommand类中找到。

Zuul高可用

Zuul可以注册到Eureka中,它的高可用只需要启动多台,都注册到Eureka上即可。但是启动多台Zuul的时候,用户却不知道要访问哪一台zuul了,此时可以在这多台zuul之上加上一台nginx服务器。其使用反向代理来完成负载均衡和反向代理。Nginx还可以实现主从模式来进行集群。对于超大型项目来说,又通过域名解析来进行更上一层的负载均衡。例如百度,访问后运营商会根据访问位置费配到最近的服务器入口。

SpringCloud中其他重要的组件

SpringCloudConfig

它是一套统一配置中心,可以自动从git上拉取最新的配置,缓存。使用git的Webhook钩子,去通知配置中心,说配置发生了变化,配置中心会通过消息总线去通知所有微服务,进行配置。

SpringCloudBus

消息总线

SpringCloudStream

消息通信

SpringCloudHystrixDashboard

容错统计,形成图形化界面

SpringCloudSleuth

链路跟踪 结合Zipkin

至此SpringCloud的学习暂时告一段落了,我们学习了Eureka注册中心、Ribbon负载均衡、Hystrix熔断、Feign伪装请求、Zuul路由和过滤

参考博客

posted @ 2020-06-25 17:42  张瑞丰  阅读(428)  评论(0编辑  收藏  举报