@[TOC](6.1 使用 Zuul 进行服务路由)


前言

Zuul 是 Netflix 开源的一个 API Gateway 服务器, 本质上是一个 Web servlet 应用;
Zuul 在云平台上提供动态路由,监控,弹性,安全等边缘服务的框架。Zuul 相当于是设备和 Netflix 流应用的 Web 网站后端所有请求的前门;


1. Zuul 基础知识

1.1 Zuul 是什么

  • Zuul 是一种提供动态路由、监视、弹性、安全性等功能的边缘服务;
  • Zuul 是 Netflix 出品的一个基于 JVM 路由和服务端的负载均衡器;

1.2 Zuul 提供的功能

  • 将应用程序中的所有服务的路由映射到一个 URL,即所有客户端调用都经过这个路口;
  • 构建可以对通过网关的请求进行检查和操作的过滤器;
  • 简单来说就是:代理、路由、过滤;

1.3 Zuul 支持的三种类型过滤器

  • 前置过滤器:在请求被路由到目标服务前执行;
    • 用于:统一消息格式、用户验证和授权、打印日志等;
  • 路由过滤器:在请求被路由到目标服务时执行;
    • 用于:确定是否需要进行某些级别的动态路由;(灰度发布等)
  • 后置过滤器:在请求被路由到目标服务后执行;
    • 用于:记录响应信息、给目标服务的响应添加头信息、收集统计数据;

zuul 的三种类型过滤器

2. 构建 Zuul 网关服务

2.1 引入 pom.xml 依赖

<!-- zuul相关依赖jar包 -->
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
<!-- 监控相关,用于查看路由信息相关 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

2.2 修改 bootstrap.yml 配置文件

  • 使用自动映射路由不需要做其他特殊配置;
  • 其他特殊配置参考本篇《2.4 配置路由》;
eureka:
  instance:
    preferIpAddress: true
  client:
    registerWithEureka: true
    fetchRegistry: true
    serviceUrl:
        defaultZone: http://localhost:8761/eureka/

#开启查看路由的端点
management:
  endpoints:
    web:
      exposure:
        include: 'routes'

2.3 在主启动类上添加注解

  • @EnableZuulProxy:表示该服务为一个 Zuul 服务器(常用);
    • 开发人员需要使用 Zuul 与 Eureka 等服务注册中心(如 Consul )进行集成的时使用;
  • @EnableZuulServer:也是表示该服务为一个 Zuul 服务器(不加载任何 Zuul 反向代理过滤器,也不使用 Nettlix Eureka 进行服务发现);
    • 开发人员想要构建自己的路由服务,而不使用任何 Zuul 预置的功能时使用;

2.4 配置路由

2.4.1 自动映射路由

  • 不需要做额外配置;
  • 访问 http://localhost:{zuul-port}/routes
  • 通过 Zuul 注册的服务映射展示在左边,路由映射到实际的 Eureka 服务 ID 展示在右边;
  • Zuul 将基于 Eureka 服务的 ID 来公开服务,如果服务的实例没有在运行 , Zuul 将不会公开该服务的路由;

2.4.2 手动映射路由

  • 需要在 application.yml 里进行配置:

    zuul:
    routes:
      xxxservice: /xxx/**
  • 配置前原接口:/xxxservice/**
  • 配置后的接口:/xxx/**
  • 可以配置不存在的服务路由,但当尝试为不存在的服务调用路由时会返回 500 错误;
  • 上述路由将包括自动路由和手动路由两种路由规则,如果要排除自动路由需要添加 ignored-services 属性:

    zuul:
    ignored-services: 'xxxservice'
    routes:
      xxxservice: /xxx/**
    • ignored-services: * 将排除所有自动映射路由;
  • 还可以给给路由添加前缀,用来区分 API 路由与内容路由:
    zuul:
    ignored-services: 'xxxservice'
    prefix: /api 
    routes:
      xxxservice: /xxx/**
  • 上述配置后,路由接口将变为:/api/xxxservice/**

2.4.3 使用静态 URL 手动映射路由

  • 用来路由那些不受 Eureka 管理的服务,可以建立 Zuul 直接路由到一个静态定义的 URL;

    zuul:
    routes:
      yyy:  #Zuul 用于在内部识别服务的关键字
        path: /yyy/**  #许可证服务的静态路由
        url: http://{yyyservice-ip:yyyservice-port}  #已建立许可证服务的实例,它将被直接调用,而不是由 Zuul 通过 Eureka 调用
  • 由于不使用 Eureka,因此需要手动配置 Zuul 来禁用 Ribbon 与 Eureka 集成,列出 Ribbon 将进行负载均衡的各个服务实例。配置如下:

    zuul:
    routes:
      yyy:  
        path: /yyy/**  
        serviceId: yyy  #定义一个服务 ID,该 ID 将用于在 Ribbon 中查找服务
    ribbon:
    eureka: 
      enabled: false  #在 Ribbon 中禁用 Eureka 支持
    yyy:
    ribbon:
      listOfServers: http://{yyyservice-ip-1:yyyservice-port-1}, http://{yyyservice-ip-2:yyyservice-port-2}  #指定请求会路由到服务器列表
    • 为什么要使用 Ribbon:支持 Ribbon 的服务客户端会在本地缓存服务实例的位置,不需要每次解析服务位置时都调用 Eureka;

2.4.4 动态重新加载路由配置

  • 其允许在不回收 Zuul 服务器的情况下更改路由的映射。现有的路由可以被快速修改,以及添加新的路由;
  • 动态配置相关需要结合《2.1 使用 Spring Cloud Config 管理服务配置项》篇里的使用 git 外部化微服务配置;
  • 将配置文件交给 git 后,我们直接修改配置文件即可,不需要做额外配置;

2.4.5 配置服务超时

  • Zuul 使用 Netflix 的 Hystrix 和 Ribbon 库;
    • Hystrix 会对需要超过 1s 的服务请求会返回一个 HTTP 500 错误;
    • Ribbon 则会处理超过 5s 的服务请求;
  • 可以使用如下配置进行覆盖:
    • 修改所有服务的 Hystrix 默认超时为 2.5s:hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 2500
    • 修改特定服务(xxx)的 Hystrix 默认超时为 2.5s:hystrix.command.xxx.execution.isolation.thread.timeoutInMilliseconds: 2500
    • 修改特定服务(xxx)的 Ribbon 默认超时为 7s:xxx.ribbon.ReadTimeout: 7000

3. 使用 Zuul 的三种类型过滤器示例

在 filters 包下;

3.1 前置过滤器 TrackingFilter 示例模板

//所有 Zuul 过滤器必须扩展 ZuulFilter 类,并覆盖四个方法:filterType()、filterOrder()、shouldFilter() 和 run()
@Component
public class TrackingFilter extends ZuulFilter{
    private static final int FILTER_ORDER =  1;
    private static final boolean SHOULD_FILTER=true;
    private static final Logger logger = LoggerFactory.getLogger(TrackingFilter.class);

    //所有过滤器的常用方法都封装进 FilterUtils 类中
    @Autowired
    FilterUtils filterUtils;

    //告诉的是那种类型过滤器
    @Override
    public String filterType() {
        return FilterUtils.PRE_FILTER_TYPE;
    }

    //返回整数值,指示不同类型的过滤器执行顺序
    @Override
    public int filterOrder() {
        return FILTER_ORDER;
    }

    //是否执行过滤器
    @Override
    public boolean shouldFilter() {
        return SHOULD_FILTER;
    }

    //每次服务通过过滤器时执行的方法
    @Override
    public Object run() {
        ...
    }
}
  • 其他两种 Zuul 过滤器与示例类似;

4. 使用 Zuul 的路由过滤器实现灰度发布示例

使用 Zuul 路由过滤器实现灰度发布示例

  • 需要将 50% 的用户请求路由到新服务,50% 的用户请求路由到旧服务。即 A/B 测试;
  • 本例中先需要查询目标服务是否存在,只有存在才进行 A/B 测试;
  • 注意区分目标服务、新服务和旧服务的概念:
    • 这里的目标服务指该次请求既定的归属地,用来判断 Zuul 是否能路由过去;
    • 只有判断有目标服务后,才根据一定策略决定路由到新服务还是旧服务;
    • 路由到新服务这里称转发路由;

4.1 扩展 ZuulFilter 类

  • 与本篇《3.1 前置过滤器 TrackingFilter 示例模板》一样,需要扩展 ZuulFilter 类,并覆盖 4 个方法;
  • 其中重点在 run() 方法;

4.2 自定义 run() 方法

  • 主要的路由逻辑,先查询目标服务是否存在,再根据返回值确定路由到新版本还是旧版本;
@Override
public Object run() {
    RequestContext ctx = RequestContext.getCurrentContext();

    //确定目标服务是否存在
    AbTestingRoute abTestRoute = getAbRoutingInfo( filterUtils.getServiceId() );

    //接受路径权重 abTestRoute,生成随机数,确定是否路由到新服务
    if (abTestRoute!=null && useNewRoute(abTestRoute)) {
        //构建新服务的路由路径
        String route = buildRouteString(ctx.getRequest().getRequestURI(),
                abTestRoute.getEndpoint(),
                ctx.get("serviceId").toString());
        //完成转发到新版本服务的工作
        forwardToNewRoute(route);
    }
    return null;
}

4.3 getAbRoutingInfo() 查找目标服务

  • 该方法的作用是:确定目标服务是否存在;
private AbTestingRoute getAbRoutingInfo(String serviceName){
    ResponseEntity<AbTestingRoute> restExchange = null;
    try {
        //尝试调用目标服务
        restExchange = restTemplate.exchange(
                         "http://newroutesservice/v1/route/abtesting/{serviceName}",
                         HttpMethod.GET,
                         null, AbTestingRoute.class, serviceName);
    }
    catch(HttpClientErrorException ex){
        //如果没有找到目标服务,将报错 HTTP 404,并返回空值
        if (ex.getStatusCode()== HttpStatus.NOT_FOUND) return null;
        throw ex;
    }
    return restExchange.getBody();
}

4.4 useNewRoute() 将决定是否路由到新服务

  • 该方法的作用是:使用随机数确定调用到新版本还是旧版本;
  • 返回 true 说明路由到新服务,反之不转发;
    public boolean useNewRoute(AbTestingRoute testRoute){
    Random random = new Random();
    //检查路由是否为活跃状态
    if (testRoute.getActive().equals("N")) {
        return false;
    }
    //使用随机数确定调用到新版本还是旧版本
    int value = random.nextInt((10 - 1) + 1) + 1;
    if (testRoute.getWeight()<value) {
        return true;
    }
    return false;
    }

4.5 转发路由的逻辑

  • 在目标服务存在,且实施路由到新服务的策略后,最后执行的路由到新服务的逻辑;
//Spring Cloud 提供,用于代理服务请求的辅助方法
private ProxyRequestHelper helper = new ProxyRequestHelper();

private void forwardToNewRoute(String route) {
    RequestContext context = RequestContext.getCurrentContext();
    HttpServletRequest request = context.getRequest();
    //创建发送到服务的所有 HTTP 请求首部的副本
    MultiValueMap<String, String> headers = this.helper.buildZuulRequestHeaders(request);
    //创建所有 HTTP 请求参数的副本
    MultiValueMap<String, String> params = this.helper.buildZuulRequestQueryParams(request);
    String verb = getVerb(request);
    //创建新服务的 HTTP 主体的副本
    InputStream requestEntity = getRequestBody(request);
    if (request.getContentLength() < 0) {
        context.setChunkedRequestBody();
    }

    this.helper.addIgnoredHeaders();
    CloseableHttpClient httpClient = null;
    HttpResponse response = null;

    try {
        httpClient  = HttpClients.createDefault();
        //使用 forward() 方法调用新服务
        response = forward(httpClient, verb, route, request, headers,params, requestEntity);
        //将服务调用的结果保存会 Zuul 服务器
        setResponse(response);
    }
    catch (Exception ex ) {
        ex.printStackTrace();
    }
    finally{
        try {
            httpClient.close();
        }
        catch(IOException ex){}
    }
}

最后

::: hljs-center

新人制作,如有错误,欢迎指出,感激不尽!

:::

::: hljs-center

欢迎关注公众号,会分享一些更日常的东西!

:::

::: hljs-center

如需转载,请标注出处!

:::

::: hljs-center

公众号

:::