一、从一次线上事故说起
假设你是一个小公司的后端开发,你们做了一个电商系统,用户下单先请求网关,网关再去调用商品服务、库存服务、优惠券服务。一切看起来都很正常。直到有一天大促,优惠券服务突然卡死,响应时间从 50 毫秒涨到了 5 秒。你发现网关里的线程全都堵在等待优惠券服务响应上,接着其它服务的请求也进不来了,整个系统像多米诺骨牌一样倒下。这时候你才意识到,光有网关还不够,还得让网关学会“见势不妙就快速放弃”,这就用到了熔断器。
Zuul 是 Spring Cloud 全家桶里的老牌网关,负责把外部请求转发到内部服务。Hystrix 是 Netflix 开源的容错库,它的熔断功能就像家里电闸的保险丝——电流太大就自动跳闸,保护电器不被烧掉。把两者放一起,就能让网关在某个下游服务不健康的时候,快速返回降级结果,而不是傻傻等着超时,把网关和整个系统拖垮。
二、Zuul 网关到底是个啥
2.1 网关的作用
网关是所有外部请求进系统的“总大门”。统一收口,做路由转发、鉴权、限流、日志等等。没有网关的话,前端直接调用几十个服务,运维管理乱,安全控制难,耦合也重。有了 Zuul,就能把公共逻辑下沉到网关这一层,每个服务专心做自己的业务。
2.2 Zuul 的工作机制
Zuul 的核心是一堆过滤器(Filter),它是“管道+过滤器”的模式。请求进来后,会依次经过 pre 过滤器(比如鉴权)、routing 过滤器(真正转发到服务)、post 过滤器(比如加响应头)。如果有异常,还有 error 过滤器。这有点像流水线作业,每个工人只负责一道工序。Zuul 处理一个请求时,调用链如下:
请求进来 -> pre 过滤器 -> routing 过滤器 -> post 过滤器 -> 响应返回
过滤器之间通过请求上下文传递数据。Zuul 内置了路由规则,你只需要在配置文件里写好“哪个路径转发到哪个服务”,它就能把请求发过去。
三、Hystrix 熔断器又是怎么回事
3.1 为什么需要熔断
在微服务架构里,一个请求可能依赖好几个服务。如果其中一个服务挂了,而其它服务还拼命给它发请求,那这个服务的线程会越积越多,最后资源耗尽,整个系统都不可用。熔断器给每个依赖调用设置一个“健康阈值”,当失败次数太多或错误率达到阈值,熔断器“打开”,后续请求直接走降级逻辑,不再真实的去调用那个有问题的服务。过一段时间再放一小部分流量进去试试,如果恢复了,熔断器“闭合”,恢复正常调用。
3.2 Hystrix 的核心概念
Hystrix 一个重要概念是“命令模式”。你把对下游服务的每次调用包装成一个 HystrixCommand 或者 HystrixObservableCommand。执行时,Hystrix 会先检查熔断器是否打开,如果开着就直接执行 fallback(降级),否则才真正去调用。调用过程中,Hystrix 会用独立线程池来跑,并且设置超时时间。如果超时或者线程池满了,都会走降级逻辑。
Hystrix 有五种状态:关闭、打开、半开。关闭的时候正常请求,当失败率达到阈值(默认 50%)且 10 秒内请求数达到至少 20 个,熔断器打开。打开后,等待一个 sleep 窗口(默认 5 秒),然后进入半开状态,放一个请求试水,如果成功就关闭,否则继续打开。
四、Zuul 与 Hystrix 是怎么勾搭上的
4.1 集成原理
Zuul 和 Hystrix 的集成不是靠硬编码,而是 Spring Cloud 在 Zuul 的 routing 过滤器里自动加上了 Hystrix 包装。具体来说,Zuul 内部有一个 RibbonRoutingFilter,这个过滤器会用 RibbonCommandFactory 创建出 HystrixCommand,然后像调用普通命令一样去执行路由转发。也就是说,你在 Zuul 里配置的路由,本质上是被 Hystrix 包裹起来的。
可以简单理解为:Zuul 负责接收请求、决定往哪转发;Hystrix 负责在转发过程中做保护。一旦下游服务超时、连接失败、返回错误,Hystrix 就会介入,这时可以定义一个降级处理器,返回一段友好的提示给调用方。
4.2 线程隔离和超时
Hystrix 默认使用线程池隔离。每个路由(或者每个服务)会对应一个独立的线程池,这样某个服务的线程池满了,不会影响其他服务。Zuul 集成 Hystrix 后,每个路由的线程池大小、超时时间都可以单独配置。如果并发量特别大,还可以改用信号量隔离,避免线程上下文切换开销,但信号量隔离没有“排队”“超时等待”这些功能,选择时得看具体场景。
五、动手写一个例子
这一节我们用一段完整的代码演示,技术栈是 Java + Spring Cloud Netflix (Finchley 版本)。假设我们要做一个最简单的网关,它把 /product 开头的请求转发到商品服务。
5.1 创建一个网关服务
先建一个 Maven 工程,在 pom.xml 里引入依赖:
<!-- 使用Java 8,Spring Boot 2.0.x,Spring Cloud Finchley -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.6.RELEASE</version>
</parent>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Zuul网关启动器,里面会连带引入Hystrix相关依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<!-- 注册中心,用Eureka做服务发现 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
注意,spring-cloud-starter-netflix-zuul 里已经带了 Hystrix,所以你不需要再单独引 Hystrix 的依赖。接着写启动类:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
@SpringBootApplication
@EnableZuulProxy // 开启Zuul代理功能,自动集成Hystrix
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
5.2 配置路由和熔断参数
在 application.yml 里配置路由规则,以及 Hystrix 的超时和线程池参数。因为我们使用了 Eureka,所以可以直接用服务名作为 serviceId 来路由。
server:
port: 8080
spring:
application:
name: api-gateway
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
zuul:
# 路由前缀,所有请求前面加上 /api
prefix: /api
routes:
# 商品服务路由
product:
path: /product/**
serviceId: product-service
# 订单服务路由
order:
path: /order/**
serviceId: order-service
# Hystrix的全局配置
hystrix:
command:
default:
execution:
isolation:
thread:
# 超时时间:3000毫秒,超过就降级
timeoutInMilliseconds: 3000
circuitBreaker:
# 熔断器打开的阈值:10秒内请求数达到10个
requestVolumeThreshold: 10
# 错误率阈值:50%的请求失败就打开熔断器
errorThresholdPercentage: 50
# 熔断打开后睡眠时间:5秒后进入半开状态
sleepWindowInMilliseconds: 5000
上面的配置演示了全局默认值。如果你想针对某个服务单独配置,可以写成这样:
hystrix:
command:
# 这里用 "服务名" 或者 "服务名+命令后缀" 都可以
product-service:
execution:
isolation:
thread:
timeoutInMilliseconds: 1500
5.3 写一个降级处理类
当路由转发失败、超时、或者熔断器打开时,Hystrix 会执行 fallback。在 Zuul 里,你需要实现 FallbackProvider 接口,重写 getRoute 和 fallbackResponse 方法。getRoute 返回你要给哪个路由降级,如果是 * 或 null,则表示所有路由通用。
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;
import java.nio.charset.StandardCharsets;
/**
* 网关降级处理类,对商品服务的路由做降级。
* 如果未来要支持更多服务,可以按 route 分开写。
*/
@Component
public class ProductServiceFallback implements FallbackProvider {
/**
* 指定这个降级类作用于哪个路由。
* 返回 "*" 表示对所有路由生效;
* 返回 "product-service" 表示只对商品服务降级。
*/
@Override
public String getRoute() {
return "product-service";
}
/**
* 降级后返回给调用方的响应体。
* 这里写了一段JSON格式的提示信息。
*/
@Override
public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
// 返回HTTP 200,这样外部客户端的HTTP层不会报错
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 {
// 要返回给客户端的JSON内容
String body = "{\"code\":503,\"message\":\"服务暂时不可用,请稍后再试\"}";
return new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8));
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
};
}
}
如果你希望所有路由都走同一个降级逻辑,可以把 getRoute() 改成返回 "*",同时你还能在 fallbackResponse 里拿到当前路由对应的 Throwable,根据异常类型做不同提示。比如超时返回“慢”,连接失败返回“挂了”:
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;
import java.nio.charset.StandardCharsets;
@Component
public class GlobalFallback implements FallbackProvider {
@Override
public String getRoute() {
// 对所有路由生效
return "*";
}
@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 {
// 根据路由的名字给出不同的提示,方便调试
String message = " 请稍后重试";
if (cause != null && cause.getMessage() != null) {
message = cause.getMessage() + message;
}
String body = "{\"code\":503,\"message\":\"[" + route + "] " + message + "\"}";
return new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8));
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
};
}
}
5.4 测试一下效果
启动 Eureka 服务注册中心,再启动你的商品服务(假设跑在 8081 端口,并且注册到 Eureka),然后启动网关。在浏览器访问:
http://localhost:8080/api/product/info
网关会把这个请求路由到商品服务。现在故意把商品服务停止,再访问同样的地址,网关会在 3 秒钟左右返回降级结果:
{"code":503,"message":"[product-service] 请稍后重试"}
同时 Hystrix 的指标监控页(如果引入了 spring-boot-starter-actuator)可以访问:
http://localhost:8080/hystrix.stream
你会在数据流里看到很多指标,比如请求总数、错误百分比、熔断器状态。连续多失败几次,错误率超过 50%,熔断器就会打开。这个时候你再重启商品服务,但熔断器还是开着的,需要等 5 秒睡眠窗口过去后,进去半开状态试一下,成功后就自动恢复。
六、应用场景和适用边界
这套方案最适合的是“内部微服务之间关系紧密、业务链路相对固定”的场景。比如你有一个 B 端系统,前端请求必须经过网关进入几个核心服务,服务间用 REST 调用,整体并发量中等。这时候用 Zuul + Hystrix 能显著提高系统的鲁棒性。
但如果你的服务数量特别多,或者你用的是异步非阻塞的响应式架构(比如 WebFlux),那 Zuul 1.x 是基于 Servlet 的,同步阻塞模型,在超高并发下吞吐量会有瓶颈。另外,如果团队已经上了云原生的 Service Mesh 或 API 网关(比如 Spring Cloud Gateway、Kong、APISIX),那 Zuul + Hystrix 的这套组合就稍显过时,不过它的思路依然是所有服务保护框架的基石。
七、技术优缺点
7.1 优点
第一,简单直接。Spring Cloud 封装好了,你只需要写配置和降级类,不用关心复杂的底层原理。第二,保护效果明显。超时、线程池隔离、熔断、降级,一套组合拳能够最大限度防止一个下游服务拖垮整个网关。第三,监控方便。Hystrix 提供了实时指标流,可以接入 Turbine、仪表盘等,快速定位问题服务。
7.2 缺点
第一,内存和 CPU 成本高。Hystrix 为每个线程池维护队列和线程,线程切换也带来开销,在对延迟极其敏感的场景里,这可能成为瓶颈。第二,配置复杂,容易踩坑。超时时间、线程池大小、熔断阈值之间约束太多,调参不好容易误触发熔断。第三,维护状态不佳。Netflix 在 2018 年宣布 Hystrix 停止新功能开发,虽然项目能继续用,但遇到新需求可能需要自己二次开发。第四,Zuul 1 的同步模型性能不如异步网关。所以在高并发场景下,需要谨慎评估。
八、开发中的注意事项
第一,超时时间要设得比真实业务慢一点点。比如商品服务一般需要 200 毫秒,你设置成 3000 毫秒;如果设得太紧,稍微一个网络抖动就熔断了,反而影响用户体验。一般建议先压测出 P99 延迟(99% 的请求在多少毫秒内完成),再设置超时时间。第二,线程池大小要给足,但不能太大。太小了容易排队,太大了反而浪费资源。你可以统计网关的平均并发数,预留 1.5 倍到 2 倍。第三,fallback 里不要做太重的操作。降级接口一般是快速返回一个静态结果,不要再调用数据库、MQ 或其它服务,否则降级的过程自己又拖垮了别的系统。第四,熔断器的阈值要结合流量来设置。如果系统本来就是低流量,比如每分钟才 10 个请求,那 requestVolumeThreshold 设成 20 是不合适的,因为很难达到触发条件。你可以把阈值调低一些。第五,一定要注意 Hystrix 的线程池名称是跟路由的 serviceId 关联的,如果你配了多个路由指向同一个服务,线程池可能被共享,某个路由堵住会间接影响另一个路由,这时可以用 key 属性手动指定不同的命令名称。
九、总结
Zuul 网关和 Hystrix 熔断器的集成,本质上是“路由转发”和“容错保护”两种思想的结合。Zuul 帮你把请求分发到合适的地方,Hystrix 帮你盯着每一个转发过程,一旦下游不对劲就立刻阻断、降级、撬开熔断开关。虽然现在有了更新的网关和云原生方案,但这里面蕴含的经验永远不过时:不要让任何一个不可控的依赖拖垮你整个系统,要学会快速失败、优雅降级。这一套技术组合,在中小团队里依然是非常实用且高效的选择。
希望这篇博客能帮你理清 Zuul 和 Hystrix 之间的配合关系。如果你现在正在用 Spring Cloud 做微服务,不妨照着上面的示例动手跑一跑,亲自体验一下熔断器打开的时候那种“过山车”般的感觉。实践出真知,多踩坑才能记得更牢。
评论
围绕“Zuul网关与Hystrix熔断器的集成原理与实践”参与讨论