摘要
为什么epoll、Reactor、多路复用这些高性能网络名词理解起来门槛很高?Dubbo、Netty、MySQL、Redis、Tomcat、Kafka、RocketMQ、Elasticsearch,这些组件看似形态各不相同,底层共享同一套高性能网络通用骨架:TCP连接建立与生命周期管理、紧凑二进制协议去除冗余Header、对象与二进制的序列化编解码,以及Boss/Worker/业务三层线程隔离。整套体系重点研究连接管控与三层线程隔离;不同中间件仅仅是协议设计、线程池参数存在差异。这也是吃透一个框架,再学习其他中间件能够快速上手的根本原因。这套架构诞生的核心矛盾:硬件资源(内存、CPU、文件句柄)有限,但业务请求近乎无限。本文从资源约束切入,沿着操作系统I/O原语、应用层线程模型、Reactor/Proactor模型、连接生命周期,再到主流中间件实战案例逐层拆解,看懂所有高性能网络设计背后的必然性。读完你会发现,各类复杂架构,本质都是为了用有限硬件承载海量请求。
一、场景与问题起源:海量请求下,服务器的三大硬约束
所有高性能网络架构的复杂设计,从来不是技术人员炫技,而是线上真实业务场景倒逼出来的必然选择。
互联网业务流量天生带有突发性:秒杀瞬间流量洪峰、前端页面大量刷新、接口重试风暴、长连接心跳持续堆积、微服务链式调用引发级联压力……从业务视角看,网络请求、TCP连接几乎可以无限增长。
但站在服务器硬件与操作系统层面,资源永远固定、稀缺,存在硬性上限。无限增长的业务请求,撞上有限的服务器资源,就是高性能网络编程最核心的底层矛盾,也是 epoll、多路复用、Reactor、线程隔离、连接池等所有架构方案的起点。
线上绝大多数服务雪崩、接口超时、连接打满、CPU持续飙高、服务失去响应的故障,根源都逃不开操作系统的这三道硬约束,没有任何捷径可以绕过。
1.1 内存硬约束:每一条TCP连接都会持续占用内存
很多开发者存在误区:TCP连接建立几乎不消耗资源。事实并非如此。每当一条TCP连接创建,操作系统内核就要分配专属资源:读写缓冲区、连接状态结构体、超时管理信息。同时应用框架还要创建Channel对象、读写缓存、编解码上下文。
单条空闲连接占用的内存虽然只有几KB到数MB,但当连接规模达到万级、十万级时,内存占用会快速累积。一台普通服务器,仅仅维护数万条空闲长连接,就可能吃掉数十GB内存,直接触发服务OOM、频繁Full GC,严重时会造成整机卡死。
1.2 CPU硬约束:线程上下文切换是高并发隐形杀手
早期最简单的网络模型是「一连接一线程」,逻辑简单,代码好写,在低并发场景稳定可用,但完全扛不住海量连接。操作系统创建线程需要分配栈内存;当线程阻塞、唤醒时,需要保存和恢复CPU上下文,单次上下文切换耗时在微秒级别。
一旦并发连接达到千级、万级,系统里会存在大量阻塞、唤醒、切换的线程。CPU大量算力会被消耗在线程调度与上下文切换上,留给业务逻辑、网络读写的算力所剩无几。最终现象:CPU 100%打满,但接口响应极慢,吞吐量断崖式下跌,服务失去处理能力。
1.3 文件句柄硬约束:服务器并发连接的物理天花板
Linux系统中,每一个TCP Socket、文件,本质都对应一个文件句柄(FD)。操作系统对单进程、单机的文件句柄数量有严格上限。系统默认单进程句柄上限仅为1024;即便手动调优,硬件与系统的极限也很难突破百万。
一旦并发连接数超过句柄上限,操作系统会直接拒绝新建TCP连接,抛出Too many open files。对外表现就是接口超时、无法建立连接,服务直接不可用。文件句柄,就是绝大多数网络服务的物理并发上限。
1.4 核心矛盾总结:有限资源承载无限请求
综合上面三道硬约束,提炼全文贯穿始终的核心逻辑:
资源有限(内存/CPU/文件句柄) × 请求无限 = 所有高性能网络架构的唯一设计驱动力
传统BIO阻塞模型、简单多线程模型,在低QPS场景足够简单稳定。可面对海量请求、十万级长连接场景,会瞬间击穿资源上限,完全无法支撑业务。
后续所有I/O模型、线程模型、架构设计的迭代演进,核心目标始终不变:突破传统模型的资源桎梏,在硬件资源固定的前提下,最大化压榨服务器性能,承载海量并发请求。而所有应用层优化,都必须依托操作系统提供的I/O原语实现,这也是我们接下来要拆解底层I/O机制的核心原因。
| 资源 | 硬约束 | 超限后果 |
|---|---|---|
| 内存 | 每个连接至少占用几KB~几MB(缓冲区+上下文) | 万级连接 = 几十GB内存,引发 OOM、GC 卡顿 |
| CPU | 线程切换成本 ~1~10 微秒,上下文保存/恢复 | 千级线程频繁切换 = CPU耗尽在调度,业务卡死 |
| 文件句柄 | 系统上限默认1024,调优后也难超百万 | 连接数 > 句柄数 = 建连失败、服务拒绝访问 |
二、操作系统 I/O 通知机制演进
既然并发瓶颈来自内存、CPU、文件句柄的资源限制,我们就需要操作系统提供更高效的I/O事件通知能力,减少无效等待。应用层所有网络框架,本质都是封装操作系统I/O原语。本章梳理I/O机制的完整演进路线,看懂内核如何支撑海量连接监听。
想要解决高并发资源瓶颈,必须从操作系统底层入手。应用层的所有网络框架、线程模型、异步逻辑,都只是对系统I/O能力的封装和复用,操作系统的I/O原语,直接决定了服务的并发上限与性能下限。
从早期阻塞I/O到现代高性能异步I/O,每一次技术迭代,都是为了破解「资源有限、请求无限」的核心矛盾,逐步解决C10K、C1000K海量并发问题。本章将完整梳理操作系统I/O机制的七代演进历程,看懂所有高性能网络架构的底层根基。
2.1 第一阶段:阻塞 I/O(一切的起点)
int fd = accept(listen_fd); // 阻塞,没连接就干等
read(fd, buf, 1024); // 阻塞,没数据就干等
write(fd, buf, 1024); // 阻塞,缓冲区满就干等
生活场景: 餐厅只有一个服务员,盯着一个客人点餐。客人没想好,服务员就一直站着等——其他客人全部排队等待。
2.2 第二阶段:多进程/多线程(伪并发)
父进程 accept → fork 子进程 → 子进程处理连接
问题: 进程/线程太重。10000 连接 = 10000 进程 = 内存爆 + 调度崩。
2.3 第三阶段:select(1983,SysV)
fd_set fds;
FD_ZERO(&fds);
FD_SET(fd, &fds);
select(max_fd+1, &fds, NULL, NULL, NULL); // 阻塞等事件
| 限制 | 说明 |
|---|---|
| fd 上限 1024 | FD_SETSIZE 宏写死 |
| O(n) 遍历 | 每次都要扫全部 fd |
| 内核/用户态拷贝 | 每次调用都拷贝 fd 集合 |
2.4 第四阶段:poll(1980s 中)
去掉 1024 限制,但仍 O(n) 遍历,仍每次拷贝。
2.5 第五阶段:epoll / kqueue(2000~2002)
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event); // 注册一次
epoll_wait(epfd, events, MAX_EVENTS, -1); // 只等就绪的
| select / poll | epoll | |
|---|---|---|
| fd 集合 | 每次拷贝 | 内核红黑树维护 |
| 就绪检测 | 遍历全部 | 只返回就绪链表 |
| 复杂度 | O(n) | O(1) |
epoll 是解决 C10K 问题的关键武器。 Nginx、Redis、Netty 的高并发能力,底层全靠它。
2.6 第六阶段:IOCP(Windows NT 3.5,1993)
Windows 走了一条完全不同的路——“完成通知”(Proactor):
ReadFile(hSocket, buffer, size, NULL, &overlapped); // 提交异步读,立刻返回
GetQueuedCompletionStatus(iocp, &bytes, &key, &overlapped, INFINITE); // 数据已在内核缓冲区
| epoll / kqueue | IOCP | |
|---|---|---|
| 通知类型 | “可以读了”(就绪) | “已经读完了”(完成) |
| 数据拷贝 | 你自己调 read() | 内核帮你拷贝好 |
| 模型 | Reactor | Proactor |
Windows 在异步 I/O 模型上其实领先了整整一个时代。 但服务器市场 90%+ 跑 Linux,所以 IOCP 生态影响力远不如 epoll。
2.7 第七阶段:io_uring(Linux 5.1+,2019)
Linux 终于补上 Proactor 拼图:
struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
io_uring_prep_read(sqe, fd, buffer, size, offset);
io_uring_submit(ring);
io_uring_wait_cqe(ring, &cqe); // 数据已经在 buffer 里
| epoll | io_uring | |
|---|---|---|
| 模型 | 就绪通知(Reactor) | Proactor-like 完成通知 |
| 系统调用 | 每次 I/O 至少 2 次 | 可批量提交 |
| 零拷贝 | 不支持 | 支持 |
2.8 OS 演进全景时间线
1980 前 阻塞 I/O + 多进程/多线程
↓
1983 select(O(n),1024 限制)
↓
1980s 中 poll(去 1024 限制,仍 O(n))
↓
1993 IOCP(Windows,完成通知 Proactor)
↓
2000 kqueue(FreeBSD,事件注册,接近 O(1))
↓
2002 epoll(Linux,就绪通知 Reactor)
↓
2019+ io_uring(Linux,Proactor 化)
三、网络线程模型五阶段演进
操作系统的 epoll、IOCP、io_uring 等I/O原语,解决了内核监听海量连接、推送就绪事件的核心能力。但内核仅负责事件通知,不参与线程调度、连接托管与任务分发。如何基于这套底层内核能力,在应用层合理绑定连接、事件与线程,最大化压榨并发性能,就衍生出迭代递进的网络线程模型。本章将逐层拆解线程模型的五阶段演进历程,理清高性能服务的应用层设计逻辑。
3.1 第一阶段:BIO — 一连接一线程
客户端1 → 线程1(accept 阻塞 → read 阻塞 → 处理 → write 阻塞)
客户端2 → 线程2(同上)
生活场景: 餐厅每来一个客人就雇一个新服务员。500 个客人 = 500 个服务员 = 人力成本无法承受。
| 说明 | |
|---|---|
| 优点 | 编程最简单,调试最容易 |
| 致命伤 | 线程数随连接数线性增长,10000 连接 = 10GB 栈内存 |
| 本质矛盾 | 并发连接数与线程数线性绑定——C10K 问题根源 |
线程池只是“伪异步”: 线程池把线程数压到固定上限,但线程仍阻塞在 read() 上。长连接场景下照样被占死。
3.2 第二阶段:NIO — 单线程管多连接
JDK 1.4 引入 NIO,核心三件套:Channel、Buffer、Selector(多路复用器)。
Selector 注册 10000 个 Channel
↓
内核(epoll)替你监听所有连接
↓
只返回"有事件的 fd 列表" → O(1) 处理
| BIO | NIO | |
|---|---|---|
| 一个线程管多少连接 | 1 个 | 10000 个 |
| 没数据的连接 | 占着线程干等 | 不占任何线程时间 |
3.3 第三阶段:Reactor — 事件驱动的经典模型
核心角色三个:Reactor(分发事件)、Acceptor(处理连接)、Handler(处理读写)。
单 Reactor 单线程
Reactor(select/epoll) → 收到事件 → Acceptor 建连 / Handler 读写
问题: 业务处理慢会卡死所有连接。
单 Reactor 多线程
Reactor → Acceptor → Handler(只管 I/O) → 业务线程池(处理计算)
问题: Reactor 本身仍是单线程,高并发下分发成瓶颈。
主从 Reactor(Netty 标配)
MainReactor(Boss) → 只管 Accept
↓
SubReactor(Worker,多个) → 管读写 + 分发业务线程池
这就是 BossGroup + WorkerGroup 的由来,也是三层线程隔离架构的核心雏形。
3.4 第四阶段:Proactor — 真正异步
Reactor 是“就绪通知,自己读”;Proactor 是“提交任务,内核读完通知你”。
| Reactor | Proactor | |
|---|---|---|
| 通知时机 | 数据可读/可写(就绪) | 数据已读/写完(完成) |
| 读数据谁做 | 应用层调 read() | 内核帮你读好 |
| 典型实现 | epoll + 应用层 | IOCP / io_uring |
Netty 的巧妙: 底层用 epoll(Reactor),但应用层封装出“完成”语义——你写 channel.write(),Netty 保证写完后回调,看起来像 Proactor。
3.5 第五阶段:虚拟线程 + io_uring(未来方向)
Java 21 虚拟线程 + Linux io_uring = “写同步代码,跑异步性能”。 虚拟线程(轻量,百万级) + io_uring(内核异步) = 业务代码像 BIO 一样直白,底层像 Proactor 一样高效。
四、Reactor 与 Proactor:两层关系的终极对比
前文第二章完成了操作系统内核I/O机制的全演进,厘清了epoll、IOCP、io_uring两大类内核通知能力的本质差异;第三章则从应用层视角,梳理了从BIO到主从Reactor的线程模型迭代,落地了内核能力的上层工程实现。但多数技术文档存在概念混淆,常常将操作系统内核的I/O通知机制与应用层的事件处理设计模式混为一谈,导致大家对Reactor、Proactor的定义边界、适配逻辑认知模糊。为此本章单独做全景拆解与终极对比,从内核、应用两层维度剥离概念、梳理交叉组合关系,同时破除行业高频认知误区,彻底讲清两类模型的底层逻辑与工程取舍。
4.1 一句话区分两层
| 层面 | 说的是什么 | 本质问题 |
|---|---|---|
| 操作系统层面 | 内核给你的 I/O 通知机制 | "内核怎么通知你" |
| 应用层面 | 你怎么组织线程和事件处理 | "你怎么用内核给的能力" |
4.2 OS 层:内核的两种通知方式
这是最底层的能力,操作系统提供,应用程序无法改变底层机制。
| Reactor(就绪通知) | Proactor(完成通知) | |
|---|---|---|
| 内核说 | "fd 3 可以读了" | "fd 3 的数据已经帮你读到缓冲区了,共 1024 字节" |
| 谁拷贝数据 | 你自己调 read() | 内核已经拷好了 |
| 代表机制 | epoll、kqueue | IOCP、io_uring |
4.3 应用层:Douglas Schmidt 提出的两种模式
源自 1990 年代 ACE 框架的正式归纳,属于软件设计模式,是开发者基于操作系统能力做的上层抽象。
| Reactor 模式 | Proactor 模式 | |
|---|---|---|
| 应用层说 | "有事件来了我来处理" | "操作完成了我拿结果" |
| 典型写法 | 事件循环 + 回调(Netty ChannelHandler) | 异步提交 + 完成回调(AIO CompletionHandler) |
| 代表框架 | Netty、Redis、Nginx | Java AIO、Windows IOCP 程序 |
4.4 关键关系:两层可以交叉组合
OS 层的原语 ≠ 应用层必须用的模式。中间可以有一层"翻译",框架可以做封装转换,实现跨层组合。
| 组合 | 怎么做到的 | 举例 |
|---|---|---|
| OS Reactor + 应用层 Reactor | epoll + 事件回调 | Netty(Linux 默认)、Redis |
| OS Proactor + 应用层 Proactor | IOCP + 完成回调 | Windows 原生 AIO |
| OS Reactor + 应用层 Proactor ✅ | epoll 只告诉你"可以读了",框架帮你 read 完再回调 | Netty 在 Linux 上就是这个组合 |
| OS Proactor + 应用层 Reactor | io_uring 完成通知,封装成事件循环 | 新兴框架在探索中 |
OS 通知机制
├── OS Reactor(epoll)→ 应用层 Reactor(事件回调)
├── OS Reactor(epoll)→ 应用层 Proactor(框架帮你 read)
├── OS Proactor(IOCP)→ 应用层 Proactor(完成回调)
└── OS Proactor(io_uring)→ 应用层 Reactor(封装成事件循环)
Netty 的精妙之处:
它在 Linux 上底层是 epoll(OS Reactor),但上层 API 设计成"数据到了你直接处理"(应用层 Proactor 体验)。你写代码感觉是 Proactor,实际跑起来是 Reactor——Netty 在中间做了翻译。
4.5 为什么这个区分重要(常见误区澄清)
| ❌ 常见误区 | ✅ 准确说法 |
|---|---|
| "epoll 就是 Reactor" | epoll 是 OS 层就绪通知;Reactor 是应用层事件处理模式 |
| "AIO 就是 Proactor" | Windows AIO 是 OS+应用双层 Proactor;Linux Java AIO 底层是 epoll 模拟 |
| "Netty 是 Reactor" | Netty 底层用 epoll(OS Reactor),但包装成 Proactor 编程体验 |
两层概念同名不是巧合——应用层的 Reactor/Proactor 模式,就是根据 OS 提供的不同通知机制抽象出来的。但两层可以交叉组合,这也是为什么同一个 Netty 能在 Linux 和 Windows 上提供一致的编程体验。
五、连接与请求的生命周期
理解了事件驱动模型之后,我们再下沉到TCP连接本身。连接不是一次性的瞬时对象,它拥有完整生命周期:建立、数据读写、空闲保活、销毁。高性能网络架构,本质就是精细化管控连接生命周期,把连接、通道、请求三者解耦。长短连接、多路复用、连接池、空闲连接剔除这些常见技术手段,都是针对不同业务的连接生命周期特征做的适配。无状态服务倾向于尽可能复用连接;有状态服务则需要保障连接稳定持久。最终目标都是在硬件资源上限之内,提升连接利用率,规避连接泄露、连接数打满、资源持续占用等线上故障。不同中间件的差异,仅仅体现在生命周期的超时策略、协议编解码、线程调度的细节,这套生命周期骨架本身完全通用。本章拆解连接的生命周期,以及连接池、线程池的池化设计思想和背压。
5.1 三层分离
| 层级 | 概念 | 说明 |
|---|---|---|
| 连接(Connection) | TCP 连接 | 长生命周期,有状态 |
| 通道(Channel) | 逻辑通道 | Netty 抽象,封装连接 |
| 流(Stream) | 请求/响应流 | HTTP/2 多路复用,一条连接多流 |
高速公路三段式: 连接(公路) → 通道(车道) → 流(车)
5.2 五种连接模式演进
| 模式 | 说明 | 代表 |
|---|---|---|
| 短连接 | 一请求一连接 | 早期 HTTP/1.0 |
| 长连接 | 多请求复用连接 | HTTP/1.1 Keep-Alive |
| 多路复用 | 一连接多流并发 | HTTP/2、Dubbo |
| 多连接 | 客户端开多条连接 | 浏览器并发请求 |
| 连接池 | 预建连接,复用 | 数据库、RPC |
5.3 通用骨架(基于连接全生命周期)
所有网络服务,都遵循这条统一的连接生命周期链路:
Accept(建立连接) → Read(监听可读事件) → Decode(解析请求数据)
→ Process(执行业务逻辑) → Encode(封装响应数据) → Write(回写客户端)
↑ ↓
└────────── 连接复用 / 闲置超时 / 连接销毁 ──────────┘
在传统阻塞I/O模型里,连接生命周期和业务线程是强绑定关系:一条连接独占一个线程,连接不释放,线程就无法回收。一旦并发连接上涨,会迅速耗尽内存、CPU调度资源,这也是海量长连接场景下阻塞模型无法支撑高并发的根本原因。高性能网络架构的核心优化思路,就是把连接生命周期与业务线程彻底解耦。依托多路复用能力,框架统一托管全部存活连接,事件就绪时才触发处理,实现线程资源按需复用。
基于这条生命周期链路,衍生出经典的三层线程分工:Boss线程专职处理连接创建,管控连接准入;Worker线程负责I/O事件监听与数据读写,托管所有长连接;独立业务线程池专门执行请求业务逻辑,防止耗时DB、RPC阻塞I/O通道。
5.4 无状态 vs 有状态
| 无状态 | 有状态 | |
|---|---|---|
| 特点 | 请求独立,可任意调度 | 请求依赖,需串行/绑定 |
| 连接策略 | 多路复用,连接池 | 长连接,单连接串行 |
| 代表 | Dubbo、HTTP 无状态服务 | MySQL、etcd、Redis |
5.5 池化兜底
池化的本质:用数学上限兜住无限请求。
连接池:限制“最多同时开多少条连接”
线程池:限制“最多同时跑多少个任务”
两者都是把“请求无限”压到“资源有限”的硬约束内。
| 服务 | 连接策略 | 说明 |
|---|---|---|
| Dubbo | 一条连接多路复用 | 少量连接(默认 1 条/实例) |
| MySQL | 多条连接,每条串行 | 大量连接 + 严格上限 |
| HTTP 客户端 | Keep-Alive 复用 | 连接池 + 空闲超时 |
Dubbo 一条连接顶 MySQL 几十条,因为多路复用。
5.6 背压 Backpressure
池化是硬上限:直接限制并发连接、线程的最大数量,超出直接拒绝排队。而背压(Backpressure)是软限流,解决「下游处理速度跟不上上游发送速度」的流速不匹配问题。
当接收端处理不过来时,通过协议反馈、窗口调整,通知上游降低发送速率,避免接收端队列无限堆积、内存溢出。
TCP本身自带滑动窗口做基础背压;Dubbo、RocketMQ、WebFlux等框架,会在应用层实现背压机制。
一句话区分:池化是“最多允许多少个请求进来”;背压是“上游慢点发,我处理不过来了”,二者搭配,共同兜底无限请求带来的压力。
六、业界全景实战:13 个知名服务三大流派
前面章节搭建了完整理论骨架:资源约束→内核I/O原语→线程事件模型→连接生命周期。理论最终要落地,我们可以用这套统一框架去剖析市面上主流中间件、Web服务。本章将13款知名服务归类为三大流派,用统一视角看懂它们的选型思路。
流派一:OS 线程 + epoll(Reactor 系)
| 服务 | 层数 | 核心思路 |
|---|---|---|
| Nginx | 1 | 多进程单 epoll,零锁 |
| Netty | 2 | BossGroup + WorkerGroup |
| Tomcat | 3 | Acceptor → Poller → Executor |
| Kafka | 3 | Acceptor → Processor → RequestHandler + 零拷贝 |
| RocketMQ | 4 | Netty + 8 个业务线程池 |
| Elasticsearch | 多层 | Netty + 十几个专用线程池 |
| ZooKeeper | 3 | 自研 Java NIO Reactor |
| gRPC | 各语言不同 | 跨语言 Reactor 规范 |
流派二:用户态轻量线程(M:N 调度系)
| 服务 | 模型 | 核心思路 |
|---|---|---|
| RabbitMQ | Erlang Actor | 轻量进程 + 队列串行 |
| etcd | Go goroutine | netpoll + Raft 单 goroutine |
| Consul | Go 混合 | 多协议 + Raft + Gossip |
流派三:单线程 / 极简模型
| 服务 | 模型 | 核心思路 |
|---|---|---|
| Redis | 单线程 epoll | 内存操作微秒级,无锁 |
| MySQL | 线程池 | 磁盘 I/O 有状态,限并发 |
选型三问
- 业务处理快不快? 快 → 单层(Redis);慢 → 多层隔离(Kafka、RocketMQ)
- 有没有状态? 无状态 → 多路复用(Dubbo);有状态 → 串行/限并发(MySQL、etcd)
- 用什么语言? Java → Netty;Go → goroutine;Erlang → Actor;C → 裸 epoll
终极因果链
资源有限(内存/CPU/句柄)× 请求无限
↓
操作系统 I/O 原语演进(第二章):
阻塞 I/O → select → poll → kqueue/epoll → IOCP → io_uring
↓
应用层网络线程模型五阶段演进(第三章):
BIO → NIO → Reactor 三代 → AIO → 虚拟线程 + io_uring
└── 三层隔离(Boss / Worker / 业务)
↓
Reactor 与 Proactor 两层关系终极对比(第四章):
OS层(就绪通知 vs 完成通知)
+ 应用层(事件驱动 vs 异步回调)
+ 交叉组合(Netty 的翻译) + 常见误区澄清
↓
连接与请求的生命周期(第五章):
Connection/Channel/Stream 三层分离 → 高速公路三段式
→ 五种连接模式演进 → 通用骨架 → 无状态/有状态 → 池化兜底 → 背压
↓
业界全景实战(第六章):
13 个服务三大流派,统一骨架的不同实现
每一个“高大上”的架构名词——epoll、Reactor、Proactor、多路复用、连接池、线程池——拆开来看,全是“资源有限 × 请求无限”这个核心矛盾在不同层级给出的工程解法。不是设计者刻意把架构做复杂,是硬件资源的客观约束倒逼出来的设计取舍。