Appearance
阻塞IO和非阻塞式IO的对比
我来详细讲解 Spring Cloud Gateway 的非阻塞式 与 传统 Tomcat 的阻塞式 的核心区别。这是理解现代网关架构的关键知识点。
一、核心概念:阻塞 vs 非阻塞
1.1 生活类比
🏦 传统银行柜台(阻塞式 Tomcat)
├─ 客户A → 窗口1 → 办理业务(30分钟)→ 窗口1一直被占用
├─ 客户B → 等待... 窗口1空出才能办理
├─ 客户C → 等待...
└─ 10个窗口 = 最多同时服务10人,其他人干等着
🏧 现代餐厅叫号系统(非阻塞式 Netty/WebFlux)
├─ 客户A → 取号(留下手机号)→ 坐着等,窗口不占用
├─ 客户B → 取号 → 坐着等
├─ 客户C → 取号 → 坐着等
├─ 客户D → ... 100人都可以先取号
└─ 菜做好了 → 短信通知 → 客户来取
→ 10个窗口可以服务的吞吐量 = 远大于10人1.2 技术本质区别
| 维度 | 阻塞式 Tomcat | 非阻塞式 Netty (Gateway) |
|---|---|---|
| 线程模型 | 1请求 = 1线程独占 | 少量线程处理大量连接 |
| 线程等待 | I/O时线程阻塞等待 | I/O时线程去处理其他请求 |
| 资源消耗 | 线程多,内存大 | 线程少,内存小 |
| 并发能力 | 几千 QPS | 几万 ~ 几十万 QPS |
| 代码风格 | 同步,顺序执行 | 异步,回调/Reactive |
| 代表框架 | Spring MVC, Servlet | WebFlux, Netty, Gateway |
二、深入原理:Tomcat 阻塞式详解
2.1 Tomcat 的请求处理流程
┌─────────────────────────────────────────┐
│ Tomcat 线程池(200线程) │
├─────────────────────────────────────────┤
│ Thread-1 ──→ 接收 Request-1 │
│ │ 读取Body(阻塞!等待网络) │
│ │ ↓ │
│ │ 调用Controller(计算) │
│ │ ↓ │
│ │ 查询MySQL(阻塞!等待DB) │
│ │ ↓ │
│ │ 等待Redis(阻塞!等待缓存) │
│ │ ↓ │
│ │ 调用下游HTTP(阻塞!等待响应) │
│ │ ↓ │
│ └────────→ 返回 Response → 空闲 ⚠️ │
│ │
│ Thread-2 ──→ 接收 Request-2(同时) │
│ ...同样路径,同样阻塞... │
│ │
│ Thread-200 ─→ 接收 Request-200 │
│ ← 线程池耗尽!Request-201 等待或拒绝 │
└─────────────────────────────────────────┘
关键问题:线程大部分时间都在 **等待I/O**,却占着茅坑不拉屎!2.2 代码层面的阻塞表现
java
// ========== 传统 Spring MVC(阻塞式) ==========
@RestController
public class OrderController {
@Autowired
private RestTemplate restTemplate; // 同步阻塞!
@Autowired
private JdbcTemplate jdbcTemplate; // 同步阻塞!
@GetMapping("/order/{id}")
public Order getOrder(@PathVariable Long id) {
// 1. 当前线程 BLOCKED 等待数据库查询
Order order = jdbcTemplate.queryForObject(
"SELECT * FROM t_order WHERE id = ?",
Order.class, id
);
// 2. 当前线程 BLOCKED 等待HTTP响应(可能100ms+)
Inventory inventory = restTemplate.getForObject(
"http://inventory-service/sku/" + order.getSkuId(),
Inventory.class
);
// 3. 当前线程 BLOCKED 等待Redis响应
String cache = stringRedisTemplate.opsForValue().get("key");
// 整个过程中,这个线程啥也干不了,只能等
return order;
}
}线程状态实时监控:
bash
# 查看 Tomcat 线程状态
jstack <pid> | grep "http-nio" | head -20
http-nio-8080-exec-1 WAITING at java.net.SocketInputStream.socketRead0 ← 等网络
http-nio-8080-exec-2 RUNNABLE at ...Controller.getOrder ← 在计算
http-nio-8080-exec-3 WAITING at java.net.SocketInputStream.socketRead0 ← 等MySQL
http-nio-8080-exec-4 BLOCKED at java.net.SocketInputStream.socketRead0 ← 等Redis
...三、深入原理:Gateway 非阻塞式详解
3.1 核心:Reactor 模式 + Netty EventLoop
┌─────────────────────────────────────────────────────────┐
│ Netty EventLoopGroup(少量线程) │
│ 例如:CPU核数 * 2 = 8个线程 │
├─────────────────────────────────────────────────────────┤
│ │
│ EventLoop-1 ──●─────────────────────────────────── │
│ │ │ 注册 Channel-1(客户端连接1) │
│ │ │ 注册 Channel-2(客户端连接2) │
│ │ │ 注册 Channel-3...(可能1万个连接) │
│ │ │ │
│ │ ▼ 有数据可读? │
│ │ ┌─────────┐ │
│ └────→ │ Selector │ ← 多路复用,批量检测所有Channel │
│ └────┬────┘ │
│ │ │
│ ▼ Channel-1 有HTTP请求数据 │
│ ┌───────────┐ │
│ │ 读取请求头 │ ← 非阻塞,立即读完可读的 │
│ │ 解析... │ │
│ │ 发现要调MySQL │ │
│ │ 发起异步I/O │ ← 注册回调,线程不等待! │
│ │ return │ ← 线程立即释放,处理下一个 │
│ └───────────┘ │
│ │
│ 【MySQL响应回来】 │
│ ↓ │
│ EventLoop-1 再次调度:找到 Channel-1 的回调 │
│ → 继续处理:读取MySQL结果 → 组装响应 → 写回客户端 │
│ │
│ 同一时间:EventLoop-1 还在穿插处理 Channel-2/3/4... │
│ 一个线程 = 管理成千上万个连接的"事件调度员" │
│ │
└─────────────────────────────────────────────────────────┘3.2 代码层面的非阻塞表现
java
// ========== Spring Cloud Gateway(非阻塞式) ==========
@Component
public class CustomGatewayFilter implements GatewayFilter {
@Autowired
private WebClient webClient; // 非阻塞HTTP客户端!
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// Mono = 0或1个结果的异步容器(Future的升级版)
// 1. 修改请求头(同步计算,极快)
exchange.getRequest().mutate().header("X-Gateway", "Gateway-01");
// 2. 鉴权校验(发起异步HTTP,不阻塞!)
Mono<AuthResult> authMono = webClient.get()
.uri("http://auth-service/verify?token=" + token)
.retrieve()
.bodyToMono(AuthResult.class);
// ↑ 注意:这里立即返回,不等待HTTP响应!
// 3. 使用 flatMap 链式处理"未来的结果"
return authMono.flatMap(auth -> {
// 这个回调在auth-service响应后才执行
if (!auth.isValid()) {
return denyRequest(exchange); // 拒绝
}
// 继续 downstream
return chain.filter(exchange); // 放行到目标服务
});
}
}3.3 底层 Netty 的 I/O 多路复用
┌─────────────────────────────────────────────────┐
│ Linux epoll / Kqueue / IOCP │
│ (操作系统级多路复用) │
├─────────────────────────────────────────────────┤
│ │
│ 用户空间 内核空间 │
│ │ │ │
│ │ epoll_create() │ │
│ │ ─────────────────> │ 创建epoll实例 │
│ │ │ │
│ │ epoll_ctl(ADD, │ │
│ │ socket_fd_1) │ 注册10000个socket │
│ │ epoll_ctl(ADD, │ 到同一个epoll │
│ │ socket_fd_2) │ │
│ │ ... │ │
│ │ │ │
│ │ epoll_wait() │ ← 只有一个系统调用! │
│ │ ─────────────────> │ 内核告诉哪些socket就绪 │
│ │ <───────────────── │ (可能有1000个同时就绪) │
│ │ │ │
│ │ 遍历就绪的socket │ 用户程序逐个处理 │
│ │ 处理完继续wait │ 没有任何线程阻塞等待! │
│ │
│ 对比:10000个连接 │
│ 阻塞式 = 10000个线程阻塞等待 │
│ 非阻塞 = 8个EventLoop轮询处理 │
│ 效率差:1250倍! │
│ │
└─────────────────────────────────────────────────┘四、Spring Cloud Gateway 的完整架构
4.1 请求在 Gateway 中的流转
┌─────────────────────────────────────────────────────────────┐
│ Client Browser │
└─────────────────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Spring Cloud Gateway (Netty Server) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Netty 接收HTTP请求 │ │
│ │ → 解码为 HttpRequest │ │
│ │ → 包装为 ServerWebExchange(请求/响应的上下文容器) │ │
│ │ → 提交到 EventLoop 的 TaskQueue │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Gateway Handler Mapping │ │
│ │ → 匹配 Route(根据路径、Host、Header等) │ │
│ │ → 确定 Predicate 是否通过 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Filter Chain(责任链,全部是非阻塞的Mono/Flux操作) │ │
│ │ │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │PreFilter│ → │PreFilter│ → │PreFilter│ ← 请求过滤│ │
│ │ │StripPrefix│ │GlobalAuth│ │RateLimit│ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ Netty Routing Filter(核心转发) │ │ │
│ │ │ - 使用 TcpClient 连接下游服务 │ │ │
│ │ │ - 发起异步HTTP请求(connect时注册回调) │ │ │
│ │ │ - 线程立即返回,不等待! │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ (下游响应回来后,回调继续执行) │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │PostFilter│ → │PostFilter│ → │PostFilter│ ← 响应过滤│ │
│ │ │AddHeader│ │LogResponse│ │Metric │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Netty 编码并写出响应 │ │
│ │ ← 整个过程中,EventLoop 线程从未阻塞! │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘4.2 关键源码:Gateway 的 Netty 底层
java
// reactor-netty 的底层连接(Gateway 转发使用的核心)
public class HttpClientOperations {
// 发起请求 - 完全异步
public final Mono<Void> send() {
return Mono.create(sink -> {
// 1. 获取Channel
Channel channel = channel();
// 2. 发送HTTP请求(非阻塞!)
channel.writeAndFlush(request)
.addListener(future -> {
// 3. 发送完成的回调(不需要线程等待)
if (future.isSuccess()) {
sink.success(); // 通知Mono成功
} else {
sink.error(future.cause());
}
});
// 4. 方法立即返回,线程不等待写完成!
});
}
// 接收响应 - 也是回调驱动
void onInboundNext(ByteBuf msg) {
// Netty 收到数据时,EventLoop 调用此方法
// 数据被放入 Flux 流,等待下游订阅者处理
if (msg.get()) {
inbound.onNext(msg); // 推入响应流
}
}
}五、对比实验:直观感受差异
5.1 模拟场景:Gateway 转发到慢服务
java
// ========== 下游服务(故意慢200ms)==========
@GetMapping("/slow")
public String slow() throws InterruptedException {
Thread.sleep(200); // 模拟数据库查询或外部调用
return "ok";
}5.2 压测对比
场景:Gateway → 下游慢接口(延迟200ms)
| 指标 | Tomcat 阻塞式网关 | Gateway (Netty) |
|---|---|---|
| 线程数 | 200 (默认) | 8 (CPU核数) |
| 并发请求 | 10000 | 10000 |
| 实际吞吐 | ~1000 RPS 后开始拒绝 | ~8000 RPS 稳定运行 |
| 内存占用 | 2GB+ | 500MB |
| CPU使用 | 大量线程切换开销 | 极低 |
| 99线延迟 | 飙涨到数秒 | 稳定在220ms |
5.3 可视化对比
Tomcat 阻塞式(200线程)处理 10000 并发:
时间轴(ms) 0 200 400 600 800
Thread-1 [req1] [req201] [req401] ...
Thread-2 [req2] [req202] [req402]
...
Thread-200 [req200] [req400] [req600]
↑
同时只有200个请求在处理
其余9800个排队等待!
→ 第10000个请求要等:10000/200*200ms = 10秒!
Gateway Netty(8线程)处理 10000 并发:
EventLoop 0 50 100 150 200 250 300 ...
-1 [r1][r2][r3]...[r50]→[r1回调]→[r51]...
-2 [r51][r52]...[r100]→[r52回调]→...
...
-8 同样模式
每个EventLoop管理~1250个连接的"就绪事件"
请求A发完后,线程立即处理请求B的发送
请求A的响应回来时,再回调继续
第10000个请求:最多等待一个调度周期 ~220ms六、为什么 Gateway 选择非阻塞?
6.1 网关的特殊性
| 网关特性 | 为什么需要非阻塞 |
|---|---|
| I/O密集型 | 90%时间在转发网络请求,纯等待 |
| 高并发刚需 | 作为流量入口,连接数巨大 |
| 低延迟要求 | 自身不能成为瓶颈 |
| 代理转发为主 | 自身业务逻辑少,不需要复杂计算 |
6.2 架构演进
阶段1:单体应用(Spring MVC + Tomcat)
├── 适合:业务复杂,CRUD多
└── 线程阻塞在:数据库、缓存、外部HTTP
阶段2:服务拆分 + API网关
├── 网关成为瓶颈!
├── Tomcat网关:10000连接需要10000线程 → 内存爆炸
└── Netty网关:10000连接只需几十个线程 → 轻松应对
阶段3:云原生网关
├── Kubernetes Ingress
├── Service Mesh Sidecar
└── 全部基于非阻塞I/O(Envoy/Linkerd也用类似原理)七、常见误区澄清
| 误区 | 真相 |
|---|---|
| "非阻塞就是多线程" | ❌ 非阻塞是少线程处理多连接,线程从不等待 |
| "非阻塞一定更快" | ❌ 单体简单请求,同步可能更快(少调度开销)。非阻塞优势在高并发I/O场景 |
| "用了Gateway就是非阻塞" | ❌ 如果在Filter里用RestTemplate(阻塞),会阻塞EventLoop,灾难! |
| "非阻塞代码更好写" | ❌ 异步代码更难调试,需要理解Mono/Flux、背压等概念 |
错误示范:阻塞EventLoop
java
@Component
public class BadFilter implements GatewayFilter {
@Autowired
private RestTemplate restTemplate; // ❌ 阻塞的!
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// ❌❌❌ 致命错误!在EventLoop线程中阻塞!
String result = restTemplate.getForObject("http://xxx", String.class);
// 这个线程阻塞了,该EventLoop管理的所有连接都卡住!
return chain.filter(exchange);
}
}
// 正确做法:使用 WebClient(非阻塞)
@Component
public class GoodFilter implements GatewayFilter {
@Autowired
private WebClient webClient; // ✅ 非阻塞
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return webClient.get()
.uri("http://xxx")
.retrieve()
.bodyToMono(String.class)
.flatMap(result -> chain.filter(exchange)); // ✅ 链式异步
}
}八、总结
┌─────────────────────────────────────────────────────────┐
│ 核心对比表 │
├─────────────────────────────────────────────────────────┤
│ 维度 阻塞式 Tomcat 非阻塞式 Netty/Gateway │
├─────────────────────────────────────────────────────────┤
│ 线程模型 一请求一线程 少量线程轮询所有连接 │
│ I/O处理 线程阻塞等待 注册回调,立即返回 │
│ 并发能力 数千 数十万 │
│ 内存占用 高(线程栈1M/个) 低(单连接几KB) │
│ 适用场景 计算密集型业务 I/O密集型网关/代理 │
│ 编程复杂度 简单(同步代码) 较高(异步/响应式) │
└─────────────────────────────────────────────────────────┘
Spring Cloud Gateway 的选择:
├── 底层 Netty:非阻塞网络I/O
├── WebFlux:非阻塞Web框架
├── WebClient:非阻塞HTTP客户端
└── Mono/Flux:响应式编程模型
= 专门为高并发网关场景设计的全链路非阻塞架构理解了这些,你就明白为什么 Gateway 性能这么好,以及为什么不能混用阻塞组件了!