Skip to content

Zuul 详解 — Netflix API 网关

Zuul 是 Netflix 开源的 API 网关(API Gateway)组件,作为微服务架构的"前门",负责路由转发、鉴权认证、限流、日志、跨域等统一处理。在 Spring Cloud 生态中,Zuul 1.x 曾是默认网关方案,后被 Spring Cloud Gateway 取代。


目录

  1. 为什么需要网关
  2. 核心架构
  3. 过滤器详解
  4. 核心配置
  5. 路由转发
  6. Zuul + Ribbon 负载均衡
  7. Zuul + Hystrix 熔断
  8. Zuul 1.x vs Zuul 2.x vs Gateway
  9. 为什么 Zuul 被 Gateway 取代
  10. 面试要点

1. 为什么需要网关

1.1 微服务架构中的痛点

在没有网关的微服务架构中,每个客户端直接与服务实例通信,会带来以下问题:

                        +--------+
                        | 客户端  |
                        +--------+
                         |  \  |  \
                        /   |  |   \
              +--------+ +--------+ +--------+ +--------+
              | 用户服务 | | 订单服务 | | 商品服务 | | 支付服务 |
              +--------+ +--------+ +--------+ +--------+
痛点说明
认证耦合每个微服务都需要独立实现认证逻辑,代码重复且难以统一管理
跨域问题浏览器端调用不同域名的微服务时,需要每个服务单独配置 CORS
路由混乱客户端需要知道所有微服务的地址,服务迁移时客户端需要同步更新
流量失控缺乏统一的限流和熔断机制,单个服务过载可能拖垮整个系统
日志分散请求日志分散在各服务中,排查问题困难
协议转换不同客户端(Web、App、IoT)可能需要不同的协议和数据格式

1.2 引入网关后的架构

     +--------+     +--------+     +--------+
     | Web 端 |     | App 端 |     | 第三方  |
     +--------+     +--------+     +--------+
          |              |              |
          +--------------+--------------+
                         |
                    +---------+
                    |  Zuul   |  <-- API 网关(统一入口)
                    +---------+
                    /    |    \
          +--------+ +--------+ +--------+ +--------+
          | 用户服务 | | 订单服务 | | 商品服务 | | 支付服务 |
          +--------+ +--------+ +--------+ +--------+

引入网关后,所有客户端请求统一经过网关,由网关负责:

能力说明
统一入口客户端只需知道网关地址,无需关心后端服务拓扑
路由转发根据请求路径将请求转发到对应的微服务
鉴权认证在网关层统一校验 JWT / OAuth2 Token,下游服务无需重复认证
限流保护后端服务,防止流量突增
日志聚合统一记录请求日志,便于链路追踪和监控
跨域处理统一在网关层配置 CORS,下游服务无需关心
协议转换将 HTTP 请求转换为内部协议,或将响应格式统一

2. 核心架构

2.1 Zuul 1.x 架构概述

Zuul 1.x 基于 Servlet 实现,核心是 ZuulServlet,它维护一条 过滤器链(Filter Chain)。每个请求到达后,会依次经过多个过滤器处理。

         HTTP 请求
            |
            v
   +------------------+
   |   ZuulServlet    |  <-- javax.servlet.http.HttpServlet
   +------------------+
            |
            v
   +------------------+      +------------------+
   |  ZuulFilterRunner | --> |  FilterProcessor  |
   +------------------+      +------------------+
            |
            v
   =============================================
   |           Filter Chain (过滤器链)            |
   |                                             |
   |  pre 过滤器  -->  route 过滤器  -->  post 过滤器 |
   |       |                |               |     |
   |       +---- error -----+---- error ----+     |
   |                                             |
   =============================================
            |
            v
      +----------+
      | 微服务实例 |
      +----------+

2.2 四种过滤器类型

Zuul 定义了四种标准过滤器类型,按固定顺序执行:

    请求到达
       |
       v
   +-------+
   |  pre  |  前置过滤器:请求被路由前调用
   +-------+  - 鉴权认证
       |      - 限流控制
       |      - 请求日志
       |      - 参数校验
       v
   +-------+
   | route |  路由过滤器:将请求转发到后端服务
   +-------+  - Ribbon 负载均衡
       |      - Hystrix 熔断
       |      - HTTP 请求转发
       v
   +-------+
   | post  |  后置过滤器:路由完成后调用
   +-------+  - 添加响应头
       |      - 响应统计
       |      - 日志记录
       v
   响应返回客户端

   +-------+
   | error |  错误过滤器:任意阶段发生异常时调用
   +-------+

2.3 核心类关系

   ZuulFilter (抽象类)
       |
       +-- filterType()  返回 "pre" | "route" | "post" | "error"
       +-- filterOrder() 返回过滤器执行顺序(数字越小越先执行)
       +-- shouldFilter() 返回 true | false,决定是否执行
       +-- run()          过滤器核心逻辑

   FilterProcessor
       |
       +-- runFilters("pre")   执行所有 pre 过滤器
       +-- runFilters("route") 执行所有 route 过滤器
       +-- runFilters("post")  执行所有 post 过滤器
       +-- runFilters("error") 执行所有 error 过滤器

3. 过滤器详解

3.1 四种过滤器类型及执行顺序

3.1.1 pre 前置过滤器

执行时机: 请求到达后、路由转发前。

典型用途:

场景说明
鉴权认证校验 JWT Token、OAuth2 Token,未认证直接拒绝
限流基于 IP / 用户 / 接口的请求频率限制
请求日志记录请求来源、路径、参数、时间戳
参数校验校验必填参数、参数格式
请求转发将请求体中的参数统一处理(如解密、格式转换)

3.1.2 route 路由过滤器

执行时机: pre 过滤器全部通过后、请求转发到后端服务时。

典型用途:

场景说明
服务路由根据 serviceId 查找服务实例(通过 Ribbon)
负载均衡Ribbon 自动选择一个可用实例
熔断保护Hystrix 包裹路由请求,失败时触发降级
URL 路由直接转发到指定的 URL 地址

3.1.3 post 后置过滤器

执行时机: 路由完成后、响应返回给客户端前。

典型用途:

场景说明
添加响应头统一添加 Server、X-Request-Id、Cache-Control 等
响应统计记录响应耗时、状态码、响应大小
响应修改对响应体进行统一的格式转换或脱敏
日志记录记录完整的请求-响应日志

3.1.4 error 错误过滤器

执行时机: 在 pre、route、post 任意阶段抛出异常时。

典型用途:

场景说明
异常统一处理捕获异常并返回统一的错误响应格式
错误日志记录错误堆栈和上下文信息
降级响应返回友好错误提示而非堆栈信息

3.2 完整执行流程图

                    HTTP 请求到达
                         |
                         v
              +---------------------+
              |  pre 过滤器链执行     |
              |                     |
              |  [鉴权Filter]       |  <-- filterOrder: 1
              |  [限流Filter]       |  <-- filterOrder: 2
              |  [日志Filter]       |  <-- filterOrder: 3
              |  ...               |
              +---------------------+
                   |           |
              (正常完成)    (抛出异常)
                   |           |
                   v           v
          +----------------+  +-----------------+
          | route 过滤器链   |  | error 过滤器链   |
          |                 |  |                 |
          | [RibbonFilter]  |  | [异常处理Filter]  |
          | [HystrixFilter]  |  | [错误日志Filter]  |
          | [SimpleHostFilter] |                 |
          +----------------+  +-----------------+
               |           |           |
          (正常完成)    (抛出异常)      |
               |           |           |
               v           v           v
          +----------------+           |
          | post 过滤器链   |           |
          |                 |           |
          | [添加响应头]    |  <--------+
          | [响应统计]      |
          +----------------+
                   |
                   v
              HTTP 响应返回

3.3 自定义过滤器代码示例

java
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.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

/**
 * 自定义前置过滤器:Token 鉴权
 */
@Component
public class AuthFilter extends ZuulFilter {

    private static final Logger log = LoggerFactory.getLogger(AuthFilter.class);

    /**
     * 过滤器类型:pre / route / post / error
     */
    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

    /**
     * 过滤器执行顺序:数字越小越先执行
     * FilterConstants.PRE_DECORATION_FILTER_ORDER = 5
     * 这里设为 1,表示在 pre 阶段最早执行
     */
    @Override
    public int filterOrder() {
        return 1;
    }

    /**
     * 是否执行该过滤器
     * 可以通过配置或请求路径判断是否跳过鉴权
     */
    @Override
    public boolean shouldFilter() {
        RequestContext ctx = RequestContext.getCurrentContext();
        String requestURI = ctx.getRequest().getRequestURI();

        // 登录接口、健康检查等不需要鉴权
        if (requestURI.startsWith("/api/auth/login") ||
            requestURI.startsWith("/api/health")) {
            return false;
        }
        return true;
    }

    /**
     * 过滤器核心逻辑
     */
    @Override
    public Object run() throws ZuulException {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();

        String token = request.getHeader("Authorization");
        log.info("请求路径: {}, Token: {}", request.getRequestURI(), token);

        if (token == null || token.isEmpty()) {
            log.warn("缺少 Token,拒绝请求: {}", request.getRequestURI());
            // 不再向下游路由,直接返回错误
            ctx.setSendZuulResponse(false);
            ctx.setResponseStatusCode(401);
            ctx.setResponseBody("{\"code\":401,\"message\":\"未授权:缺少 Token\"}");
            ctx.getResponse().setContentType("application/json;charset=UTF-8");
            return null;
        }

        // 校验 Token 有效性(伪代码)
        if (!validateToken(token)) {
            log.warn("Token 无效,拒绝请求: {}", request.getRequestURI());
            ctx.setSendZuulResponse(false);
            ctx.setResponseStatusCode(401);
            ctx.setResponseBody("{\"code\":401,\"message\":\"未授权:Token 无效或已过期\"}");
            ctx.getResponse().setContentType("application/json;charset=UTF-8");
            return null;
        }

        // 将解析出的用户信息放入请求头,传递给下游服务
        ctx.addZuulRequestHeader("X-User-Id", extractUserId(token));
        ctx.addZuulRequestHeader("X-User-Name", extractUserName(token));

        log.info("鉴权通过,用户: {}", extractUserName(token));
        return null;
    }

    private boolean validateToken(String token) {
        // 实际项目中:调用认证服务或直接解析 JWT
        return token.startsWith("Bearer ");
    }

    private String extractUserId(String token) {
        // 解析 JWT 获取 userId
        return "12345";
    }

    private String extractUserName(String token) {
        // 解析 JWT 获取 userName
        return "张三";
    }
}

自定义路由后置过滤器:添加响应头与统计

java
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.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;

@Component
public class ResponseStatFilter extends ZuulFilter {

    private static final Logger log = LoggerFactory.getLogger(ResponseStatFilter.class);

    @Override
    public String filterType() {
        return FilterConstants.POST_TYPE;
    }

    @Override
    public int filterOrder() {
        // 在 SendResponseFilter (order=1000) 之前执行
        return 900;
    }

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

    @Override
    public Object run() throws ZuulException {
        RequestContext ctx = RequestContext.getCurrentContext();

        // 添加统一的响应头
        ctx.getResponse().addHeader("X-Request-Id", ctx.getRequest().getHeader("X-Request-Id"));
        ctx.getResponse().addHeader("X-Gateway", "Zuul");

        // 记录请求耗时
        long startTime = (long) ctx.get("startTime");
        long duration = System.currentTimeMillis() - startTime;

        int statusCode = ctx.getResponseStatusCode();
        String requestURI = ctx.getRequest().getRequestURI();

        log.info("请求完成: {} {} | {} | {}ms",
                ctx.getRequest().getMethod(), requestURI, statusCode, duration);

        return null;
    }
}

自定义错误过滤器:统一异常处理

java
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.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;

@Component
public class ErrorFilter extends ZuulFilter {

    private static final Logger log = LoggerFactory.getLogger(ErrorFilter.class);

    @Override
    public String filterType() {
        return FilterConstants.ERROR_TYPE;
    }

    @Override
    public int filterOrder() {
        return -1; // 在 error 阶段最先执行
    }

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

    @Override
    public Object run() throws ZuulException {
        RequestContext ctx = RequestContext.getCurrentContext();
        Throwable throwable = ctx.getThrowable();

        log.error("Zuul 网关异常: {}", throwable != null ? throwable.getMessage() : "未知错误", throwable);

        // 重置响应
        ctx.setResponseStatusCode(500);
        ctx.setResponseBody("{\"code\":500,\"message\":\"网关内部错误\"}");
        ctx.getResponse().setContentType("application/json;charset=UTF-8");

        // 清除错误标记,防止错误继续传播
        ctx.setThrowable(null);
        ctx.set("error.status_code", null);
        ctx.set("error.message", null);

        return null;
    }
}

4. 核心配置

4.1 完整 YAML 配置示例

yaml
spring:
  application:
    name: api-gateway

server:
  port: 8080

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

# =============================================
#              Zuul 核心配置
# =============================================
zuul:

  # -------------------------------------------
  # 路由规则配置
  # -------------------------------------------
  routes:
    # 方式一:使用 serviceId(推荐,自动通过 Eureka 发现服务)
    user-service:
      path: /api/user/**
      serviceId: user-service
      stripPrefix: true         # 转发时去掉 /api/user 前缀(默认 true)

    order-service:
      path: /api/order/**
      serviceId: order-service
      stripPrefix: false        # 转发时保留完整路径

    product-service:
      path: /api/product/**
      serviceId: product-service

    # 方式二:使用 URL(直接指定地址,不经过 Eureka)
    legacy-service:
      path: /api/legacy/**
      url: http://192.168.1.100:8081

    # 方式三:URL 转发(可配置多个上游地址)
    external-api:
      path: /api/external/**
      url: http://external.example.com

    # 方式四:忽略前缀的透明路由
    payment-service:
      path: /payment/**
      serviceId: payment-service

  # -------------------------------------------
  # 忽略的服务:不会自动路由
  # -------------------------------------------
  ignored-services:
    - admin-service        # 不暴露到网关
    - internal-service     # 内部服务

  # 忽略所有自动路由(只保留手动配置的路由)
  # ignored-services: '*'

  # 忽略指定路径模式
  # ignored-patterns:
  #   - /api/admin/**
  #   - /api/internal/**

  # -------------------------------------------
  # 敏感请求头过滤
  # 默认会过滤掉 Cookie、Set-Cookie、Authorization
  # 设置为空列表表示不过滤任何头
  # -------------------------------------------
  sensitive-headers:
    - Cookie
    - Set-Cookie
    - Authorization

  # 全局忽略敏感头(对所有路由生效)
  # sensitive-headers: Cookie,Set-Cookie,Authorization

  # -------------------------------------------
  # 主机连接超时配置
  # -------------------------------------------
  host:
    max-total-connections: 200        # 最大连接数
    max-per-route-connections: 20     # 每个路由的最大连接数
    socket-timeout-millis: 10000      # 读取超时(毫秒)
    connect-timeout-millis: 5000      # 连接超时(毫秒)
    time-to-live: 60000               # 连接存活时间(毫秒)
    time-unit: MILLISECONDS           # 时间单位

  # -------------------------------------------
  # 全局重试配置
  # -------------------------------------------
  retryable: true                     # 全局开启重试

  # -------------------------------------------
  # 添加代理头(如 X-Forwarded-For)
  # -------------------------------------------
  add-proxy-headers: true             # 添加 X-Forwarded-* 头

  # -------------------------------------------
  # 强制原始编码
  # -------------------------------------------
  force-original-query-string-encoding: true

  # -------------------------------------------
  # 忽略 Security 头
  # -------------------------------------------
  ignore-security-headers: false

  # -------------------------------------------
  # 设置请求体的最大大小(字节)
  # -------------------------------------------
  # set-content-length: true

  # -------------------------------------------
  # 前缀剥离
  # -------------------------------------------
  # prefix: /api                       # 全局前缀
  # strip-prefix: true                 # 是否剥离前缀

# =============================================
#          Ribbon 负载均衡配置
# =============================================
ribbon:
  # 连接超时
  ConnectTimeout: 3000
  # 读取超时
  ReadTimeout: 10000
  # 是否对所有操作重试
  OkToRetryOnAllOperations: false
  # 同一实例的最大重试次数
  MaxAutoRetries: 0
  # 其他实例的最大重试次数
  MaxAutoRetriesNextServer: 1

# 针对特定服务的 Ribbon 配置
user-service:
  ribbon:
    ConnectTimeout: 2000
    ReadTimeout: 5000
    MaxAutoRetries: 1
    MaxAutoRetriesNextServer: 2
    # 负载均衡策略
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule

# =============================================
#          Hystrix 熔断配置
# =============================================
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 10000  # 熔断超时
        timeout:
          enabled: true
      circuitBreaker:
        requestVolumeThreshold: 20        # 触发熔断的最小请求数
        errorThresholdPercentage: 50      # 错误率阈值
        sleepWindowInMilliseconds: 5000   # 熔断后休眠时间

  # 针对特定路由的 Hystrix 配置
  # threadpool:
  #   user-service:
  #     coreSize: 10
  #     maxQueueSize: 50

# =============================================
#      路由级别的 Zuul 配置(Ribbon + Hystrix)
# =============================================
zuul:
  routes:
    user-service:
      path: /api/user/**
      serviceId: user-service
      # 路由级别的重试配置
      retryable: true
      # 路由级别的敏感头配置
      sensitive-headers: Cookie,Set-Cookie
      # 路由级别的 URL 配置
      # url: http://user-service/api

# 自定义 Ribbon 和 Hystrix 配置(路由级别)
# 注意:配置键格式为 <serviceId>.<namespace>.<property>
user-service:
  ribbon:
    ConnectTimeout: 2000
    ReadTimeout: 5000
    MaxAutoRetries: 1
    MaxAutoRetriesNextServer: 1
    OkToRetryOnAllOperations: true
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.WeightedResponseTimeRule

hystrix:
  command:
    user-service:  # 对应 serviceId
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 8000

4.2 常见配置场景

场景一:简单路由(指定 URL)

yaml
zuul:
  routes:
    static-service:
      path: /static/**
      url: http://static-cdn.example.com

场景二:服务发现路由(Eureka)

yaml
zuul:
  routes:
    user-service:
      path: /api/user/**
      serviceId: user-service

场景三:忽略自动路由,仅保留手动配置

yaml
zuul:
  ignored-services: '*'
  routes:
    user-service: /api/user/**
    order-service: /api/order/**

场景四:多实例 URL 路由(配合 Ribbon)

yaml
zuul:
  routes:
    legacy-api:
      path: /api/legacy/**
      serviceId: legacy-api

ribbon:
  eureka:
    enabled: false  # 禁用 Eureka,手动指定服务列表

legacy-api:
  ribbon:
    listOfServers: http://192.168.1.100:8081,http://192.168.1.101:8081
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule

5. 路由转发

5.1 两种路由方式

路由方式配置字段特点适用场景
服务路由serviceId通过 Eureka 等注册中心发现服务实例,自动负载均衡微服务架构(推荐)
URL 路由url直接指定目标地址,不走注册中心转发到外部系统或遗留系统

5.2 路径匹配规则

Zuul 使用 Ant 风格的路径匹配模式:

模式说明示例匹配
/**匹配任意层级路径/api/user/123/profile
/*匹配单层级路径/api/user,不匹配 /api/user/123
?匹配单个字符/api/user/? 匹配 /api/user/1
{param}路径参数(占位符)/api/user/{id} 匹配 /api/user/123

5.3 路由优先级

当多个路由规则匹配同一个请求路径时,Zuul 的优先级如下:

1. 精确匹配的 URL 路由          > 最高优先级
2. 精确匹配的 serviceId 路由
3. 通配符匹配的 URL 路由
4. 通配符匹配的 serviceId 路由  > 最低优先级

示例:

yaml
zuul:
  routes:
    # 优先级 1(精确匹配 URL)
    user-static:
      path: /api/user/profile
      url: http://static.example.com

    # 优先级 2(精确匹配 serviceId)
    user-detail:
      path: /api/user/detail
      serviceId: user-service

    # 优先级 3(通配符 URL)
    user-legacy:
      path: /api/user/**
      url: http://legacy.example.com

    # 优先级 4(通配符 serviceId)
    user-service:
      path: /api/user/**
      serviceId: user-service

5.4 stripPrefix 详解

请求路径: /api/user/123/orders
路由配置: path: /api/user/**, serviceId: user-service

stripPrefix: true  (默认)
  转发路径: /123/orders                    # 去掉 /api/user

stripPrefix: false
  转发路径: /api/user/123/orders           # 保留完整路径

5.5 路由配置的多种写法

yaml
zuul:
  routes:
    # 写法一:完整写法(推荐,可读性好)
    user-service:
      path: /api/user/**
      serviceId: user-service

    # 写法二:简洁写法(path 和 serviceId 相同时可省略)
    user-service: /api/user/**

    # 写法三:URL 路由
    legacy-api:
      path: /api/legacy/**
      url: http://192.168.1.100:8080

    # 写法四:forward 本地转发
    local-forward:
      path: /api/local/**
      url: forward:/local

6. Zuul + Ribbon 负载均衡

6.1 默认集成

Zuul 默认集成了 Ribbon 作为负载均衡器。当使用 serviceId 配置路由时,Zuul 会自动:

  1. 通过 Ribbon 从 Eureka 获取服务实例列表
  2. 选择一个可用实例
  3. 将请求转发到该实例
    +---------+
    |  Zuul   |
    +---------+
         |
         v
    +---------+
    | Ribbon  |  <-- 负载均衡器
    +---------+
      /  |  \
     v   v   v
  +---+ +---+ +---+
  |实例1| |实例2| |实例3|  <-- user-service 的三个实例
  +---+ +---+ +---+

6.2 负载均衡策略配置

Ribbon 提供了多种内置负载均衡策略:

策略类策略名称说明
RoundRobinRule轮询按顺序轮流分配请求(默认策略)
RandomRule随机随机选择一个实例
WeightedResponseTimeRule权重响应时间根据响应时间分配权重,响应快的实例分配更多请求
BestAvailableRule最佳可用选择并发请求数最少的实例
RetryRule重试在轮询基础上增加重试机制
AvailabilityFilteringRule可用性过滤过滤掉故障或高并发的实例
ZoneAvoidanceRule区域感知优先选择同区域的实例(默认策略)

配置示例:

yaml
# 全局策略
ribbon:
  NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule

# 针对特定服务的策略
user-service:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.WeightedResponseTimeRule

order-service:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.BestAvailableRule

6.3 自定义负载均衡策略

java
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;

import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 自定义负载均衡策略:按 IP Hash 分配
 */
public class IpHashRule extends AbstractLoadBalancerRule {

    @Override
    public void initWithNiwsConfig(IClientConfig clientConfig) {
        // 从配置中读取参数(可选)
    }

    @Override
    public Server choose(Object key) {
        ILoadBalancer lb = getLoadBalancer();
        if (lb == null) {
            return null;
        }

        List<Server> reachableServers = lb.getReachableServers();
        if (reachableServers.isEmpty()) {
            return null;
        }

        // 获取客户端 IP(需要从请求上下文中获取,这里简化处理)
        String clientIp = getClientIp();
        int hash = Math.abs(clientIp.hashCode());
        int index = hash % reachableServers.size();

        return reachableServers.get(index);
    }

    private String getClientIp() {
        // 实际项目中从 RequestContext 中获取客户端 IP
        return "127.0.0.1";
    }
}

6.4 Ribbon 重试配置

yaml
user-service:
  ribbon:
    ConnectTimeout: 3000
    ReadTimeout: 5000
    MaxAutoRetries: 1              # 同一实例重试次数
    MaxAutoRetriesNextServer: 2    # 切换到下一个实例的重试次数
    OkToRetryOnAllOperations: false # 是否对所有请求重试(false 表示只重试 GET)

7. Zuul + Hystrix 熔断

7.1 默认集成

Zuul 默认集成了 Hystrix,每个路由都会被 Hystrix 的 HystrixCommand 包裹。当后端服务不可用或响应超时时,Hystrix 会自动打开熔断器,快速失败。

    +---------+
    |  Zuul   |
    +---------+
         |
         v
    +-------------------+
    | HystrixCommand     |
    |  +-----------+    |
    |  | 断路器     |    |  <-- 熔断器状态:CLOSED / OPEN / HALF_OPEN
    |  +-----------+    |
    |       |           |
    |       v           |
    |  +-----------+    |
    |  | Fallback   |    |  <-- 降级逻辑
    |  +-----------+    |
    +-------------------+
         |
         v
    +----------+
    | 微服务实例 |
    +----------+

7.2 熔断参数配置

yaml
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 10000  # 执行超时时间
      circuitBreaker:
        enabled: true
        requestVolumeThreshold: 20        # 10秒内至少20个请求才可能触发熔断
        errorThresholdPercentage: 50      # 错误率超过50%触发熔断
        sleepWindowInMilliseconds: 5000   # 熔断后5秒尝试恢复
      metrics:
        rollingStats:
          timeInMilliseconds: 10000       # 统计窗口10秒

7.3 降级处理(FallbackProvider)

当路由请求失败或熔断时,可以通过 FallbackProvider 返回降级响应:

java
import com.netflix.hystrix.exception.HystrixTimeoutException;
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 UserServiceFallbackProvider implements FallbackProvider {

    /**
     * 指定降级处理的路由(serviceId)
     * 返回 "*" 或 null 表示对所有路由生效
     */
    @Override
    public String getRoute() {
        return "user-service";
    }

    /**
     * 降级响应逻辑
     */
    @Override
    public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
        // 根据异常类型返回不同的降级信息
        if (cause instanceof HystrixTimeoutException) {
            return buildResponse(HttpStatus.GATEWAY_TIMEOUT,
                    "{\"code\":504,\"message\":\"用户服务响应超时,请稍后重试\"}");
        }

        return buildResponse(HttpStatus.SERVICE_UNAVAILABLE,
                "{\"code\":503,\"message\":\"用户服务暂时不可用,请稍后重试\"}");
    }

    private ClientHttpResponse buildResponse(HttpStatus status, String body) {
        return new ClientHttpResponse() {
            @Override
            public HttpStatus getStatusCode() throws IOException {
                return status;
            }

            @Override
            public int getRawStatusCode() throws IOException {
                return status.value();
            }

            @Override
            public String getStatusText() throws IOException {
                return status.getReasonPhrase();
            }

            @Override
            public void close() {
                // 无需操作
            }

            @Override
            public InputStream getBody() throws IOException {
                return new ByteArrayInputStream(body.getBytes("UTF-8"));
            }

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

全局降级处理(对所有路由生效):

java
@Component
public class GlobalFallbackProvider implements FallbackProvider {

    @Override
    public String getRoute() {
        return "*";  // 对所有路由生效
    }

    @Override
    public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
        // 针对不同路由返回不同的降级信息
        String message;
        switch (route) {
            case "user-service":
                message = "用户服务不可用";
                break;
            case "order-service":
                message = "订单服务不可用";
                break;
            default:
                message = "服务 " + route + " 暂时不可用";
        }

        // ... 返回降级响应
        return buildResponse(HttpStatus.SERVICE_UNAVAILABLE,
                "{\"code\":503,\"message\":\"" + message + "\"}");
    }

    private ClientHttpResponse buildResponse(HttpStatus status, String body) {
        // 实现同上
        return null;
    }
}

8. Zuul 1.x vs Zuul 2.x vs Gateway

对比维度Zuul 1.xZuul 2.xSpring Cloud Gateway
底层技术Servlet 2.5(Tomcat)Netty(异步非阻塞)Netty + WebFlux(响应式)
IO 模型阻塞式 IO(BIO)异步非阻塞 IO(NIO)异步非阻塞 IO(NIO)
线程模型每个请求一个线程Event Loop(少量线程)Event Loop(少量线程)
性能较低(线程数受限)高(支撑高并发)高(支撑高并发)
内存占用较高(大量线程栈)低(少量线程)低(少量线程)
Spring Cloud 集成完美集成(spring-cloud-netflix)未集成(仅 Netflix 内部使用)Spring 官方支持(spring-cloud-gateway)
过滤器模型pre / route / post / errorinbound / endpoint / outbound全局过滤器 + 路由过滤器
编程模型同步阻塞(简单易理解)异步回调(复杂度高)响应式编程(Reactor)
路由配置YAML + Java 代码YAML + Java 代码YAML + Java DSL + 动态路由
维护状态进入维护模式Netflix 内部使用Spring 官方活跃维护
社区活跃度极低(不开源)
Spring Boot 兼容2.6.x 为止不适用3.x 全面支持
学习成本
适用场景遗留系统、低并发Netflix 内部新项目、高并发

架构对比

Zuul 1.x(阻塞式):

  请求1 --> [线程1] --阻塞等待--> 后端服务
  请求2 --> [线程2] --阻塞等待--> 后端服务
  请求3 --> [线程3] --阻塞等待--> 后端服务
  ...
  请求N --> [线程N] --阻塞等待--> 后端服务

  问题:线程数随请求数线性增长,高并发下线程上下文切换开销大

Zuul 2.x / Gateway(非阻塞式):

  请求1 --+
  请求2 --+---> [Event Loop 线程池] --非阻塞--> 后端服务
  请求3 --+       (少量线程)
  ...    --+
  请求N --+

  优势:少量线程即可处理大量并发请求,无线程阻塞

9. 为什么 Zuul 被 Gateway 取代

9.1 核心原因

原因详细说明
阻塞式 IO 性能瓶颈Zuul 1.x 基于 Servlet,每个请求占用一个线程。在微服务架构中,网关作为所有流量的入口,高并发场景下线程数爆炸,导致频繁的上下文切换和内存开销,成为系统瓶颈
Gateway 响应式架构Spring Cloud Gateway 基于 WebFlux 和 Reactor,采用非阻塞 IO 和 Event Loop 模型,用少量线程即可处理大量并发请求,性能远超 Zuul 1.x
Zuul 2.x 跳票Netflix 早在 2016 年就宣布要开源 Zuul 2.x,但直到 2018 年才发布,且 Spring Cloud 团队明确表示不会集成 Zuul 2.x,因为其架构过于复杂且与 Spring 生态不兼容
Spring 官方推动Spring 官方将 Gateway 作为 Zuul 的替代品,投入大量资源开发维护,并提供更丰富的功能(动态路由、限流、断言工厂等)
Spring Boot 版本升级Zuul 1.x 在 Spring Boot 2.4.x 之后已经进入维护模式,不再支持 Spring Boot 3.x,而 Gateway 全面支持 Spring Boot 3.x
功能差距Gateway 支持更灵活的路由断言(Predicate)、更丰富的过滤器(GatewayFilter)、动态路由(基于配置中心)、请求限流(基于 Redis)等高级特性

9.2 时间线

2016 年    Netflix 宣布开发 Zuul 2.x(基于 Netty 异步非阻塞)
2017 年    Spring 宣布开发 Spring Cloud Gateway
2018 年    Netflix 开源 Zuul 2.x,但 Spring Cloud 宣布不集成
2018 年    Spring Cloud Gateway 发布 GA 版本
2019 年    Spring Cloud 官方推荐使用 Gateway 替代 Zuul
2020 年    Spring Cloud Hoxton 将 Zuul 标记为维护模式
2021 年+   Spring Cloud 2021.x 起,Gateway 成为唯一推荐的网关方案

9.3 迁移建议

          不推荐新项目使用
    Zuul 1.x ---------------> Spring Cloud Gateway
       |
       | 已有项目且低并发?
       |
       +-- 是 --> 继续使用 Zuul 1.x,择机迁移
       |
       +-- 否 --> 迁移到 Gateway

10. 面试要点

高频面试题

问 1:Zuul 的过滤器有哪几种类型?执行顺序是什么?

答: Zuul 定义了四种过滤器类型,执行顺序为 pre -> route -> post,error 过滤器在任意阶段发生异常时触发:

类型执行时机典型用途
pre请求被路由前调用鉴权、限流、日志记录、参数校验
route路由请求到微服务时服务路由、负载均衡(Ribbon)、熔断(Hystrix)
post路由完成后调用添加响应头、响应统计、日志记录
error任意阶段发生错误时异常统一处理、降级响应

关键点: 即使在 post 阶段发生异常,error 过滤器依然会被调用。


问 2:Zuul 和 Spring Cloud Gateway 的区别是什么?

答:

对比维度Zuul 1.xSpring Cloud Gateway
底层技术Servlet 2.5Netty + WebFlux
IO 模型阻塞式 BIO非阻塞 NIO
性能较低高(3-5 倍于 Zuul)
Spring 支持维护模式官方推荐
过滤器模型pre/route/post/errorGatewayFilter + GlobalFilter
动态路由不支持支持
限流需自行实现内置 RequestRateLimiter

关键点: 核心区别在于 IO 模型的不同——阻塞 vs 非阻塞,这决定了高并发场景下的性能表现。


问 3:网关在微服务架构中的作用是什么?

答: 网关是微服务架构的"前门",核心作用包括:

  1. 统一入口:客户端只需知道网关地址,无需关心后端服务拓扑
  2. 路由转发:根据请求路径、Header 等条件将请求转发到对应的微服务
  3. 鉴权认证:在网关层统一完成身份认证,下游服务无需重复验证
  4. 限流熔断:保护后端服务,防止流量突增或服务雪崩
  5. 日志聚合:统一记录请求日志,便于链路追踪(配合 Sleuth + Zipkin)
  6. 跨域处理:统一配置 CORS,避免每个服务单独处理
  7. 协议转换:将外部请求转换为内部协议,或统一响应格式

问 4:如何自定义 Zuul 过滤器?

答: 继承 ZuulFilter 抽象类,实现四个核心方法:

java
@Component
public class MyZuulFilter extends ZuulFilter {
    @Override public String filterType()   { return "pre"; }      // 过滤器类型
    @Override public int filterOrder()     { return 1; }          // 执行顺序
    @Override public boolean shouldFilter(){ return true; }       // 是否执行
    @Override public Object run()          { /* 核心逻辑 */ }      // 过滤逻辑
}

关键点:

  • filterType() 返回 "pre" / "route" / "post" / "error" 或使用 FilterConstants 常量
  • filterOrder() 数字越小越先执行,建议参照 FilterConstants 中的预定义顺序
  • shouldFilter() 可按需判断是否执行(如跳过登录接口的鉴权)
  • run() 中通过 RequestContext.getCurrentContext() 操作请求/响应

问 5:Zuul 如何实现负载均衡和熔断?

答:

负载均衡: Zuul 默认集成 Ribbon。当路由配置使用 serviceId 时,Zuul 自动通过 Ribbon 从 Eureka 获取服务实例列表,并按照配置的负载均衡策略(默认轮询)选择实例。可通过 <serviceId>.ribbon.NFLoadBalancerRuleClassName 自定义策略。

熔断: Zuul 默认集成 Hystrix。每个路由都由 HystrixCommand 包裹,当后端服务响应超时或错误率达到阈值时,自动打开熔断器快速失败。可通过实现 FallbackProvider 接口提供降级响应。

yaml
# 负载均衡策略
user-service.ribbon.NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule

# 熔断超时
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 10000

面试加分点

  • 理解 Zuul 1.x 的阻塞式 IO 瓶颈:每个请求一个线程,高并发下线程上下文切换开销大
  • 了解 Zuul 2.x 被 Spring Cloud 放弃的原因:架构复杂、与 Spring 生态不兼容
  • 知道 Gateway 的响应式编程模型:基于 WebFlux + Reactor,非阻塞 IO
  • 能说出 Zuul 和 Gateway 的迁移路径:Gateway 是 Spring 官方推荐的替代方案
  • 了解 网关的常见坑:超时配置(Ribbon 超时 + Hystrix 超时的关系)、敏感头过滤导致 Cookie 丢失、路由优先级冲突

附录:常用命令速查

bash
# 启动 Zuul 网关(Spring Boot)
mvn spring-boot:run

# 查看所有路由
curl http://localhost:8080/actuator/routes

# 查看路由详情
curl http://localhost:8080/actuator/routes/details

# 查看过滤器列表
curl http://localhost:8080/actuator/filters

# 手动刷新路由(配合 Spring Cloud Config)
curl -X POST http://localhost:8080/actuator/refresh

参考链接: