Appearance
Zuul 详解 — Netflix API 网关
Zuul 是 Netflix 开源的 API 网关(API Gateway)组件,作为微服务架构的"前门",负责路由转发、鉴权认证、限流、日志、跨域等统一处理。在 Spring Cloud 生态中,Zuul 1.x 曾是默认网关方案,后被 Spring Cloud Gateway 取代。
目录
- 为什么需要网关
- 核心架构
- 过滤器详解
- 核心配置
- 路由转发
- Zuul + Ribbon 负载均衡
- Zuul + Hystrix 熔断
- Zuul 1.x vs Zuul 2.x vs Gateway
- 为什么 Zuul 被 Gateway 取代
- 面试要点
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: 80004.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.RoundRobinRule5. 路由转发
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-service5.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:/local6. Zuul + Ribbon 负载均衡
6.1 默认集成
Zuul 默认集成了 Ribbon 作为负载均衡器。当使用 serviceId 配置路由时,Zuul 会自动:
- 通过 Ribbon 从 Eureka 获取服务实例列表
- 选择一个可用实例
- 将请求转发到该实例
+---------+
| 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.BestAvailableRule6.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.x | Zuul 2.x | Spring 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 / error | inbound / 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,择机迁移
|
+-- 否 --> 迁移到 Gateway10. 面试要点
高频面试题
问 1:Zuul 的过滤器有哪几种类型?执行顺序是什么?
答: Zuul 定义了四种过滤器类型,执行顺序为 pre -> route -> post,error 过滤器在任意阶段发生异常时触发:
| 类型 | 执行时机 | 典型用途 |
|---|---|---|
| pre | 请求被路由前调用 | 鉴权、限流、日志记录、参数校验 |
| route | 路由请求到微服务时 | 服务路由、负载均衡(Ribbon)、熔断(Hystrix) |
| post | 路由完成后调用 | 添加响应头、响应统计、日志记录 |
| error | 任意阶段发生错误时 | 异常统一处理、降级响应 |
关键点: 即使在 post 阶段发生异常,error 过滤器依然会被调用。
问 2:Zuul 和 Spring Cloud Gateway 的区别是什么?
答:
| 对比维度 | Zuul 1.x | Spring Cloud Gateway |
|---|---|---|
| 底层技术 | Servlet 2.5 | Netty + WebFlux |
| IO 模型 | 阻塞式 BIO | 非阻塞 NIO |
| 性能 | 较低 | 高(3-5 倍于 Zuul) |
| Spring 支持 | 维护模式 | 官方推荐 |
| 过滤器模型 | pre/route/post/error | GatewayFilter + GlobalFilter |
| 动态路由 | 不支持 | 支持 |
| 限流 | 需自行实现 | 内置 RequestRateLimiter |
关键点: 核心区别在于 IO 模型的不同——阻塞 vs 非阻塞,这决定了高并发场景下的性能表现。
问 3:网关在微服务架构中的作用是什么?
答: 网关是微服务架构的"前门",核心作用包括:
- 统一入口:客户端只需知道网关地址,无需关心后端服务拓扑
- 路由转发:根据请求路径、Header 等条件将请求转发到对应的微服务
- 鉴权认证:在网关层统一完成身份认证,下游服务无需重复验证
- 限流熔断:保护后端服务,防止流量突增或服务雪崩
- 日志聚合:统一记录请求日志,便于链路追踪(配合 Sleuth + Zipkin)
- 跨域处理:统一配置 CORS,避免每个服务单独处理
- 协议转换:将外部请求转换为内部协议,或统一响应格式
问 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参考链接:
