Skip to content

阻塞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, ServletWebFlux, 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核数)
并发请求1000010000
实际吞吐~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 性能这么好,以及为什么不能混用阻塞组件了!