七、路由网关SpringCloudZuul

  本章介绍Netflix构建微服务的另一个 组件一一智能路由网关组件ZuuloZuul 作为微服务系统的网关组件,用于构建边界服务( Edge Service),致力于动态路由、过滤、监控、弹性伸缩和安全。
注:工程代码是在前几章的基础上迭代的,可参考前几章的代码。

一、为什么需要Zuul

Zuul作为路由网关组件,在微服务架构中有着非常重要的作用,主要体现在以下 6 个方面。

  • Zuul、 Ribbon以及Eureka相结合,可以实现智能路由和负载均衡的功能, Zuul能够将请求流量按某种策略分发到集群状态的多个服务实例。
  • 网关将所有服务的API接口统一聚合,并统一对外暴露。外界系统调用 API接口时, 都是由网关对外暴露的API接口,外界系统不需要知道微服务系统中各服务相互调用的复杂性。微服务系统也保护了其内部微服务单元的API接口,防止其被外界直接调用,导致服务的敏感信息对外暴露。
  • 网关服务可以做用户身份认证和权限认证,防止非法请求操作 API 接口,对服务器起到保护作用。
  • 网关可以实现监控功能,实时日志输出,对请求进行记录。 网关可以用来实现流量监控,在高流量的情况下,对服务进行降级。
  • API 接口从内部服务分离出来,方便做测试。

二、Zuul的工作原理

  Zuul是通过Servlet来实现的, Zuul通过自定义的ZuulServlet (类似于SpringMVC的DispatcServlet)来对请求进行控制。 Zuul的核心是一系列过滤器,可以在Http请求的发起和响应返回期间执行一系列的过滤器。 Zuul包括以下4种过滤器。

  • PRE: 这种过滤器是在请求路由到具体的服务之前执行的,这种类型的过滤器可以做 安全验证 ,例如身份验证、 参数验证等。
  • ROUTE : 这种过滤器用于将请求路由到具体的微服务实例 。在默认情况下 ,它使用 Http Client进行网络请求。
  • POST: 这种过滤器是在请求己被路由到微服务后执行的 。 一般情况下,用作收集统计信息、指标,以及将响应传输到客户端 。
  • ERROR: 这种过滤器是在其他过滤器发生错误时执行的 。
      Zuul采取了动态读取、编译和运行这些过滤器。 过滤器之间不能直接相互通信,而是通过RequestContext对象来共享数据, 每个请求都会创建一个RequestContext对象。 Zuul过滤器具有以下关键特性。
  • Type (类型): Zuul过滤器的类型,这个类型决定了过滤器在请求的哪个阶段起作用, 例如Pre、 Post阶段等。
  • Execution Order (执行顺序) :规定了过滤器的执行顺序,Order的值越小,越先执行 。
  • Criteria (标准): Filter执行所需的条件 。
  • Action (行动): 如果符合执行条件,则执行Action (即逻辑代码)。
Zuul中默认实现的Filter
类型 顺序 过滤器 功能
pre -3 ServletDetectionFilter 标记处理Servlet的类型
pre -2 Servlet30WrapperFilter 包装HttpServletRequest请求
pre -1 FormBodyWrapperFilter 包装请求体
route 1 DebugFilter 标记调试标志
route 5 PreDecorationFilter 处理请求上下文供后续使用
route 10 RibbonRoutingFilter serviceId请求转发
route 100 SimpleHostRoutingFilter url请求转发
route 500 SendForwardFilter forward请求转发
post 0 SendErrorFilter 处理有错误的请求响应
post 1000 SendResponseFilter 处理正常的请求响应

禁用指定的Filter
可以在application.yml中配置需要禁用的filter,格式如下:

zuul:
      FormBodyWrapperFilter:
        pre:
          disable: true
Filter的生命周期

  当一个客户端Request请求进入Zuul 网关服务时,网关先进入“pre filter飞进行一系列的验证、操作或者判断。 然后交给“ route filter”进行路由转发,转发到具体的服务实例进行逻辑处理、返回数据。当具体的服务处理完后,最后由"post filter"进行处理,该类型的处理器处理完之后,将Response信息返回给客户端。
  ZuulServlet是Zuul的核心Servlet。ZuulServlet的作用是初始化ZuulFilter,并编排这些 ZuulFilter的执行顺序。该类中有一个service()方法,执行了过滤器执行的逻辑。

    @Override
    public void service() throws ServletException, IOException {
        try {
            try {
                preRoute();
            } catch (ZuulException e) {
                error(e);
                postRoute();
                return;
            }
            try {
                route();
            } catch (ZuulException e) {
                error(e);
                postRoute();
                return;
            }
            try {
                postRoute();
            } catch (ZuulException e) {
                error(e);
                return;
            }
        } catch (
                Throwable e) {
            error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));
        } finally {
            RequestContext.getCurrentContext().unset();
        }
    }

从上面的代码可知,首先执行preRoute()方法,这个方法执行的是P阻类型的过滤器的逻辑。 如果执行这个方法时出错了,那么会执行error(e)和postRoute()。然后执行route()方法,该方法是执行ROUTE类型过滤器的逻辑。最后执行postRoute(),该方法执行了POST类型过滤器的逻辑 。


Zuul中Filter的生命周期
自定义Filter

实现自定义Filter,需要继承ZuulFilter的类,并覆盖其中的4个方法。

public class MyFilter extends ZuulFilter {
    @Override
    String filterType() {
        return "pre"; //定义filter的类型,有pre、route、post、error四种
    }

    @Override
    int filterOrder() {
        return 10; //定义filter的顺序,数字越小表示顺序越高,越先执行
    }

    @Override
    boolean shouldFilter() {
        return true; //表示是否需要执行该filter,true表示执行,false表示不执行
    }

    @Override
    Object run() {
        return null; //filter需要执行的具体操作
    }
}

三、案例实战

3.1 搭建 Zuul 服务

本章的案例是在上一章案例的基础上进行讲解的。新建一个SpringBoot工程,取名为eureka-zuul-client,在pom文件中引入相关依赖,包括继承了主Maven工程的pom文件,具体代码如下:

<dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
<dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
</dependency>

  在工程的配置文件 application.yml 中做相关的配置,包括配置服务注册中心的地址为 http://localhost:8671/eureka,端口号为5000,程序名为eureka-zuul-client。最后来重点讲解一下Zuul路由的配置写法,在本案例中,zuul.routes.hiapi.path为 “/hiapi/**”, zuul.routes.hiapi.serviceld为“ eureka-client”,这两个配置就可以将以“hiapi”开头的Uri路由到 eureka-client服务。其中,zuul.routes.hiapi中的“ hiapi”是门己定义的,需要指定它的path和 serviceld,两者配合使用,就可以将指定类型的请求Url路由到指定的Serviceld。同理,满足以“/ribbonapi" 开头的请求Url都会被分发到eureka-ribbon- client,满足以“/feignapi/”开头的请求Url都会被分发到eureka-feign-client服务。如果某服务存在多个实例, Zuul结合Ribbon会做负载均衡,将请求均分的部分路由到不同的服务实例。

spring:
  application:
    name: eureka-zuul-client

server:
  port: 5000

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8671/eureka/

zuul:
  routes:
    hiapi:
      path: "/hiapi/**"
      serviceId: eureka-client
    ribbonapi:
      path: "/ribbonapi/**"
      serviceId: eureka-ribbon-client
    feignapi:
      path: "/feignapi/**"
      serviceId: eureka-feign-client

  依次启动工程eureka-server、eureka-client、eureka-ribbon-client、eureka-feign-client和eureka-zuul-client,其中eureka-client启动两个实例,端口为8672和8673。在浏览器上多次访问http://localhost:5000/hiapi/hi?name=ben,浏览器会交替显示以下内容:


  可见Zuul在路由转发做了负载均衡。同理,多次访问http://localhost:5000/feignapi/hi?name=benhttp://localhost:5000/ribbonapi/hi?name=ben,也可以看到相似的内容。如果不需要用Ribbon做负载均衡,可以指定服务实例的url,用zuul.routes.hiapi.url配置指定,这时就不需要配置zuul.routes.hiapi.serviceld了。一旦指定了url,Zuul就不能做负载均衡了,而是直接访问指定的url,在实际的开发中这种做法是不可取的。修改配置的代码如下:

spring:
  application:
    name: eureka-zuul-client
server:
  port: 5000
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8671/eureka/
zuul:
  routes:
    hiapi:
      path: "/hiapi/**"
      url: http://localhost:8672

重新启动 eureka-zuul-service服务, 请求 http://localhost:5000/hiapi/hi?name=ben,浏览器 只会显示以下内容 :

  如果想要指定Url,并且想做负载均衡,那么就需要自己维护负载均衡的服务注册列表。 首先,将ribbon.eureka.enabled改为false,即Ribbon负载均衡客户端不向 EurekaClient获取服务注册列表信息。然后需要自己维护一份注册列表,该注册列表对应的服务名为hiapi-self (这个名字可自定义),通过配置hiapi-self.ribbon.listOfServers来配置多个负载均衡的Url。 代码如下 :

zuul:
  routes:
    hiapi:
      path: "/hiapi/**"
      serviceId: hiapi-self

ribbon:
  eureka:
    enabled: false

hiapi-self:
  ribbon:
    listOfServers: http://localhost:8672,http://localhost:8673

重新启动eureka-zuul-client服务, 在浏览器上访问http://localhost:5000/hiapi/hi?name=ben,浏览器会显示如下内容 :

hi ben, i am from port:8672
hi ben, i am from port:8673
3.2 在 Zuul 上配置 API 接口的版本号

如果想给每一个服务的 API接口加前缀,例如http://localhost:5000/v1/hiapi/hi?name=ben/, 即在所有的API接口上加一个vl作为版本号。 这时需要用到zuul.prefix的配置,配置代码如下:

zuul:
  routes:
    hiapi:
      path: "/hiapi/**"
      serviceId: hiapi-self
    ribbonapi:
      path: "/ribbonapi/**"
      serviceId: eureka-ribbon-client
    feignapi:
      path: "/feignapi/**"
      serviceId: eureka-feign-client
  FormBodyWrapperFilter:
    pre:
      disable: true
  prefix: /vl

重新启动eureka-zuul-client服务,在浏览器上访问http://localhost:5000/v1/hiapi/hi?name=ben/
浏览器会显示如下内容 :

hi ben, i am from port:8672
hi ben, i am from port:8673
3.3 在Zuul上配置熔断器

  Zuul作为Netflix组件,可以与Ribbon、Eureka和Hystrix等组件相结合,实现负载均衡、 熔断器的功能。在默认情况下, Zuul和Ribbon相结合, 实现了负载均衡的功能。下面来讲解如何在Zuul上实现熔断功能。在Zuul中实现熔断功能需要实现ZuulFallbackProvider 的接口。实现该接口有两个方法,一个是getRouteO方法,用于指定熔断功能应用于哪些路由的服务; 另一个方法fallbackResponseO为进入熔断功能时执行的逻辑 。 ZuulFallbackProvider 的源码如下 :

public interface ZuulFallbackProvider {

    public String getRoute();
    public ClientHttpResponse fallbackResponse();
}

实现一个针对eureka-client服务的熔断器,当eureka-client 的服务出现故障时,进入熔断逻辑,向浏览器输入一句错误提示,代码如下:

@Component
public class MyFallbackProvider implements ZuulFallbackProvider {

    @Override
    public String getRoute() {
        return "eureka-client";
    }

    @Override
    public ClientHttpResponse fallbackResponse() {
        return new ClientHttpResponse() {
            @Override
            public HttpStatus getStatusCode() throws IOException {
                return HttpStatus.OK;
            }

            @Override
            public int getRawStatusCode() throws IOException {
                return 200;
            }

            @Override
            public String getStatusText() throws IOException {
                return "OK";
            }

            @Override
            public void close() {
            }

            @Override
            public InputStream getBody() throws IOException {
                return new ByteArrayInputStream("called error, i am fallback.".getBytes());
            }

            @Override
            public HttpHeaders getHeaders() {
                HttpHeaders httpHeaders  = new HttpHeaders();
                httpHeaders.setContentType(MediaType.APPLICATION_JSON);
                return httpHeaders;
            }
        };
    }
}

重新启动eureka-zuul-client,并且关闭eureka-client的所有实例,在浏览器上访问 http://localhost:5000/hiapi/hi?name=ben,浏览器显示如下图 :

如果需要所有的路由服务都加熔断功能,只需要在 getRoute()方法上返回“*”的匹配符, 代码如下:

public String getRoute() {
        return "*";
    }
3.4 在 Zuul 中使用过滤器

  在前面的内容讲述了过滤器的作用和种类 ,下面来介绍如何实现一个自定义的过滤器。 实现过滤器很简单,只需要继承ZuulFilter,并实现ZuulFilter中的抽象方法,包括filterType() 和filterOrder(),以及IZuulFilter的shouldFilter()和run()的两个方法。其中,filterType() 即过滤器的类型,在前面已经介绍了,它有4种类型,分别是“pre'’“post”“route”和 “error”。 filterOrder()是过滤顺序,它为一个Int类型的值,值越小,越早执行该过滤器。shouldFilter()表示该过滤器是否过滤逻辑,如果为true,则执行run()方法:如果为false,则不执行run()方法。 run()方法写具体的过滤的逻辑。在本例中,检查请求的参数中是否传了token 这个参数 ,如果没有传,则请求不被路由到具体的服 务实例,直接返回响应,状态码为 401。代码如下:

public class MyFilter extends ZuulFilter {

    private static Logger logger = LoggerFactory.getLogger(MyFilter.class);

    @Override
    public String filterType() {
        return "pre"; 
    }

    @Override
    public int filterOrder() {
        return 0; 
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        Object accessToken = request.getParameter("token");
        if (accessToken == null) {
            logger.warn("token is empty");
            ctx.setSendZuulResponse(false);
            ctx.setResponseStatusCode(401);
            try {
                ctx.getResponse().getWriter().write("token is empty");
            } catch (Exception e) {
                return null;
            }
        }
        logger.info("ok");
        return null;
    }
}

重新启动服务,打开浏览器,访问 http://localhost:5000/hiapi/hi?name=ben,浏览器显示如下图:

再次在浏览器上输入 http://localhost:5000/hiapi/hi?name=ben&token=permit,即加上了token这个请求参数,浏览器显示如下图:

可见,MyFilter这个Bean注入IoC容器之后,对请求进行了过滤,并在请求路由转发之前进行了逻辑判断。在实际开发中,可以用此过滤器进行安全验证。本例的架构图如下图所示。


3.5 Zuul 的常见使用方式

  Zuul是采用了类似于SpringMVC的DispatchServlet来实现的,采用的是异步阻塞模型, 所以性能比Ngnix差。由于Zuul和其他Netflix组件可以相互配合、无缝集成,Zuul很容易就能实现负载均衡、智能路由和熔断器等功能。在大多数情况下, Zuul都是以集群的形式存在的。由于 Zuul的横向扩展能力非常好,所以当负载过高时,可以通过添加实例来解决性能瓶颈。一种常见的使用方式是对不同的渠道使用不同的Zuul来进行路由,例如移动端共用一个Zuul网关实例,Web端用另一个Zuul网关实例,其他的客户端用另外一个Zuul实例进行路由。 这种不同的渠边用不同Zuul实例的架构如下图所示。


zuul通过不同的渠道来集群

另外一种常见的集群是通过Ngnix和Zuul相互结合来做负载均衡。暴露在最外面的是Ngnix主从双热备进行Keepalive, Ngnix经过某种路由策略,将请求路由转发到 Zuul 集群上, Zuul 最终将请求分发到具体的服务上。架构图如下图所示。


Nginx和Zuul结合进行负载均衡

总结:这一章节主要学习了zuul的原理,过滤器的类型,zuul结合服务发现和治理进行动态路由,以及zuul的应用场景等内容。
github源码地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342