《深入理解 Spring Cloud 与微服务构建》第十章 路由网关 Spring Cloud Zuul

《深入理解 Spring Cloud 与微服务构建》第十章 路由网关 Spring Cloud Zuul,第1张

《深入理解 Spring Cloud 与微服务构建》第十章 路由网关 Spring Cloud Zuul 《深入理解 Spring Cloud 与微服务构建》第十章 路由网关 Spring Cloud Zuul

文章目录
  • 《深入理解 Spring Cloud 与微服务构建》第十章 路由网关 Spring Cloud Zuul
  • 一、Zuul 简介
  • 二、Zuul 的工作原理
  • 三、案例实战
    • 1.搭建 Zuul 服务
    • 2.在 Zuul 上配置 API 接口的版本号
    • 3.在 Zuul 上配置熔断器
    • 4.在 Zuul 中使用过滤器
    • 5.Zuul 的常见使用方式

一、Zuul 简介

Zuul 作为微服务系统的网管组件,用于构建边界服务(Edge Service),致力于动态路由、过滤、监控、d性伸缩和安全。Zuul 在微服务架构中的重要作用主要体现在以下 6 个方面:

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

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

  • PRE 过滤器:它是在请求路由到具体的服务之前执行的,这种类型的过滤器可以做安全验证,例如身份验证、参数验证等
  • ROUTING 过滤器:它用于将请求路由到具体的微服务实例。在默认情况下,它使用 Http Client 进行网络请求
  • POST 过滤器:它是在请求已被路由到微服务后执行的。一般情况下,用作收集统计信息、指标,以及将响应传输到客户端
  • ERROR 过滤器:它是在其它过滤器发生错误时执行的

Zuul 采取了动态读取、编译和运行这些过滤器。过滤器之间不能直接相互通信,而是通过 RequestContext 对象来共享数据,每个请求都会创建一个 RequestContext 对象。Zuul 过滤器具有以下关键特性:

  • Type(类型):Zuul 过滤器的类型,这个类型决定了过滤器在请求的那个阶段起作用,例如 Pre、Post 阶段等
  • Execution Order(执行顺序):规定了过滤器的执行顺序,Order 的值越小,越先执行
  • Criteria(标准):过滤器执行所需的条件
  • Action(行动):如果服务执行条件,则执行 Action(即逻辑代码)

Zuul 请求的生命周期如图所示:

当一个客户端 Request 请求进入 Zuul 网关服务时,网关先进入 “pre filter”,进行一系列的验证、 *** 作或者判断。然后交给 “routing filter” 进行路由转发,转发到具体的服务实例进行逻辑处理、返回数据。当具体的服务处理完后,最后由 “post filter” 进行处理,该类型的处理器处理完之后,将 Response 信息返回给客户端

ZuulServlet 是 Zuul 的核心 Servlet。ZuulServlet 的作用是初始化 ZuulFilter,并编排这些 ZuulFilter 的执行顺序。该类中有一个 service() 方法,执行了过滤器执行的逻辑

		public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        try {
            this.init((HttpServletRequest)servletRequest, (HttpServletResponse)servletResponse);
            RequestContext context = RequestContext.getCurrentContext();
            context.setZuulEngineRan();

            try {
                this.preRoute();
            } catch (ZuulException var12) {
                this.error(var12);
                this.postRoute();
                return;
            }

            try {
                this.route();
            } catch (ZuulException var13) {
                this.error(var13);
                this.postRoute();
                return;
            }

            try {
                this.postRoute();
            } catch (ZuulException var11) {
                this.error(var11);
            }
        } catch (Throwable var14) {
            this.error(new ZuulException(var14, 500, "UNHANDLED_EXCEPTION_" + var14.getClass().getName()));
        } finally {
            RequestContext.getCurrentContext().unset();
        }
    }

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

三、案例实战 1.搭建 Zuul 服务

本案例基于上一章案例的基础上进行搭建,新建一个 Spring Boot 工程,取名为 eureka-zuul-client,在 pom 文件中引入相关依赖,包括继承了主 Maven 工程的 pom 文件,引入 Eureka Client 的起步依赖 spring-cloud-starter-netflix-eureka-client、Zuul 的起步依赖 spring-cloud-starter-netflix-zuul、Web 功能的起步依赖 spring-boot-starter-web,以及 Spring Boot 测试的起步依赖 spring-boot-starter-test。代码如下:



    
        Eureka
        org.sisyphus
        1.0-SNAPSHOT
    
    4.0.0

    eureka-zuul-client

    
        
            org.springframework.cloud
            spring-cloud-starter-netflix-eureka-client
        
        
            org.springframework.cloud
            spring-cloud-starter-netflix-zuul
        
        
            org.springframework.boot
            spring-boot-starter-web
        
        
            org.springframework.boot
            spring-boot-starter-test
        
    


在程序的启动类 EurekaZuulClientApplication 加上 @EnableEurekaClient 注解,开启 EurekaClient 的功能;加上 @SpringBootApplication 注解,表明自己是一个 Spring Boot 工程;加上 @EnableZuulProxy 注解,开启 Zuul 的功能。代码如下:

package com.sisyphus;

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

@EnableZuulProxy
@EnableEurekaClient
@SpringBootApplication
public class EurekaZuulClientApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaZuulClientApplication.class, args);
    }
}

在工程的配置文件 application.yml 中做相关的配置,包括配置服务注册中心的地址为 http://localhost:8761/eureka,程序的端口号为 5000,程序名为 service-zuul

在本案例中,zuul.routes.hiapi.path 为 “/hiapi/**”,zuul.routes.hiapi.serviceId 为 “eureka-client”,这两个配置就可以将以 “/hiapi” 开头的 Url 路由到 eureka-client 服务。其中,zuul.routes.hiapi 中的 “hiapi” 是自己定义的,需要指定它的 path 和 serviceId,两者配合使用,就可以将指定类型的请求 Url 路由到指定的 ServiceId。同理,满足以 “/ribbonapi” 开头的请求 Url 都会被分发到 eureka-ribbon-client,满足以 “/feignapi” 开头的请求 Url 都会被分发到 eureka-feign-client 服务。如果某服务存在多个实例,Zuul 结合 Ribbon 会做负载均衡,将请求均分的部分路由到不同的服务实例

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
      
server:
  port: 5000
  
spring:
  application:
    name: service-zuul
    
zuul:
  routes:
    hiapi:
      path: /hiapi/**
      serviceId: eureka-ribbon-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 启动两个实例,端口为 8762 和 8763。在浏览器上多次访问 http://localhost:5000/hiapi/hi?name=sisyphus,浏览器会交替显示以下内容:

hi sisyphus,i am from port:8762
hi sisyphus,i am from port:8763

可见 Zuul 在路由转发做了负载均衡。同理,多次访问 http://localhost:5000/feignapi/hi?name=sisyphus 和 http://localhost:5000/ribbonapi/hi?name=sisyphus,也可以看到相似的内容

如果不需要用 Ribbon 做负载均衡,可以指定服务实例的 Url,用 zuul.routes.hiapi.url 配置指定,这时就不需要配置 zuul.routes.hiapi.serviceId 了。一旦指定了 Url,Zuul 就不能做负载均衡了,而是直接访问指定的 Url,在实际的开发中这种做法是不可取的。修改配置的代码如下:

zuul:
  routes:
    hiapi:
      path: /hiapi/**
      url: http://localhost:8762

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

hi sisyphus,i am from port:8762

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

zuul:
  routes:
    hiapi:
      path: /hiapi/**
      serviceId: hiapi-v1

ribbon:
  eureka:
    enabled: false

hiapi-v1:
  ribbon:
    listOfServers: http://localhost:8762,http://localhost:8763

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

hi sisyphus,i am from port:8762
hi sisyphus,i am from port:8763
2.在 Zuul 上配置 API 接口的版本号

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

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

重新启动 eureka-zuul-service 服务,在浏览器上访问 http://localhost:5000/v1/hiapi/hi?name=sisyphus,浏览器会交替显示:

hi sisyphus,i am from port:8762
hi sisyphus,i am from port:8763
3.在 Zuul 上配置熔断器

Zuul 作为 Netflix 组件,可以与 Ribbon、Eureka 和 Hystrix 等组件相结合,实现负载均衡、熔断器的功能。在默认情况下,Zuul 和 Ribbon 相结合,实现了负载均衡的功能。下面来讲解如何在 Zuul 上实现熔断功能

在 Zuul 中实现熔断功能需要实现 FallbackProvider 的接口。实现该接口有两个方法,一个是 getRoute() 方法,用于指定熔断功能应用于哪些路由的服务;另一个方法 fallbackResponse() 为进入熔断功能时执行的逻辑。ZuulFallbackProvider 的源码如下:

public interface FallbackProvider {
    String getRoute();

    ClientHttpResponse fallbackResponse(String route, Throwable cause);
}

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

package com.fallbackProvider;

import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;

@Component
public class MyFallbackProvider implements FallbackProvider {
    @Override
    public String getRoute() {
        return "eureka-client";
    }

    @Override
    public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
        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("oooops!error!i am the fallback.".getBytes());
            }

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

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

oooops!error!i am the fallback.

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

@Override
    public String getRoute() {
        return "*";
    }
4.在 Zuul 中使用过滤器

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

package com.sisyphus.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE;

@Component
public class MyFilter extends ZuulFilter {
    private static Logger log = LoggerFactory.getLogger(MyFilter.class);
    
    @Override
    public String filterType() {
        return PRE_TYPE;
    }

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

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

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

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

token is empty

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

hi sisyphus,i am from port:8762

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

5.Zuul 的常见使用方式

Zuul 是采用了类似于 Spring MVC 的 DispatchServlet 来实现的,采用的是异步阻塞模型,所以性能比 Nginx 差。由于 Zuul 和其它 Netflix 组件可以相互配合、无缝集成,Zuul 很容易就能实现负载均衡、智能路由和熔断器等功能。在大多数情况下,Zuul 都是以集群的形式存在的。由于 Zuul 的横向扩展能力非常好,所以当负载过高时,可以通过添加实例来解决性能瓶颈

一种常见的使用方式是对不同的渠道使用不同的 Zuul 来进行路由,例如移动端用一个 Zuul 网关实例,WEB 端用一个 Zuul 网关实例,其它的客户端用一个 Zuul 实例进行路由

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

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5564069.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-14
下一篇 2022-12-14

发表评论

登录后才能评论

评论列表(0条)

保存