Nginx vs Tomcat:为什么有了 Nginx 还要用 Tomcat?
一句话结论(30s)
Nginx 和 Tomcat 不互斥而是分工——因为 Nginx(C 语言、事件驱动)负责「接客/转发/挡子弹」,Tomcat(Java、Servlet 容器)负责「真正干活跑业务」,二者解决的是完全不同的层面问题,所以黄金组合是「Nginx:80 → Tomcat:8080」,各司其职不可互相替代。
核心原理(2min)
- Nginx:C 语言,静态资源零拷贝(
sendfile())、反向代理、负载均衡、限流,并发强内存小,但不管业务。 - Tomcat:实现 Java Servlet 规范,把 HTTP 翻译成
HttpServletRequest/HttpServletResponse,承载 Spring/MyBatis/JVM 生态。 - Spring Boot 内嵌 Tomcat:因为 Spring MVC 构建在 Servlet API 之上,注解最终依赖 Servlet 容器。
- 经典架构:动静分离 + HTTPS 终结 + 优雅重启,Tomcat 只监听
127.0.0.1:8080不暴露外网。
底层深入(5-10min)
一个经典的面试题
“Nginx 性能那么强,为什么不用 Nginx 直接跑 Java 业务逻辑?”
这个问题背后的困惑很合理:Nginx 能处理 10 万并发连接、单机 C10K 毫无压力、内存占用仅几 MB——相比之下 Tomcat 跑一个 Spring Boot 随随便便占 500MB 内存。为什么不直接用 Nginx?
答案在于:它们解决的是完全不同层面的问题。
根本差异:语言决定了能力边界
Nginx:C 语言写的”接线员”
Nginx 由 Igor Sysoev 用纯 C 语言写成,从 2004 年发布至今,核心能力始终是:
接收 HTTP 请求 → 解析 URL → 决定把请求转发给谁 → 转发
它的设计哲学是 “我不管业务逻辑,我只管接和转”。Nginx 的四层能力:
| 能力 | 说明 | 例子 |
|---|---|---|
| 静态资源 | 直接返回文件内容 | 图片、CSS、JS、HTML |
| 反向代理 | 把请求转发给后端服务器 | proxy_pass http://backend |
| 负载均衡 | 在多个后端之间分发流量 | upstream 轮询/加权/一致性哈希 |
| 限流/安全 | 频率限制、IP 黑白名单 | limit_req_zone、WAF |
Nginx 在处理静态资源时直接调用 Linux 的 sendfile() 系统调用——零拷贝,数据从磁盘到网卡完全在内核态完成。这就是为什么它单机能轻松应对数万 QPS。
想一想:为什么 C 语言的 Nginx 能扛 10 万并发,Java 的 Tomcat 不能? 因为并发模型不同。Nginx 用事件驱动(epoll)+ 非阻塞 IO,一个 worker 进程就能同时照看几万个连接,内存才几 MB;Tomcat 默认一个请求一个线程,10 万并发就要 10 万个线程,光线程栈就占上百 GB 内存,还伴随海量上下文切换。所以 Nginx 的”强”是”IO 密集接线”场景的强,不代表它跑业务也快。
Tomcat:Java 写的”业务处理器”
Tomcat 实现了 Java Servlet 规范——它提供一个完整的运行时环境,让 Java 代码可以:
- 解析 HTTP 请求 → 封装成
HttpServletRequest - 找到对应的 Servlet → 执行业务逻辑
- 把结果封装成
HttpServletResponse→ 返回 HTTP 响应
@RestController
public class OrderController {
@PostMapping("/order")
public Result placeOrder(@RequestBody OrderDTO dto) {
// 复杂的业务逻辑:校验、查库、扣库存、发消息...
return orderService.place(dto);
}
}
这段代码里有数据库查询、事务管理、远程 RPC 调用、消息队列发送——这些 Spring 生态的能力,都运行在 Tomcat 提供的 Servlet 容器中。Nginx 做不到这些,因为 C 语言里没有 Spring、没有 MyBatis、没有 JVM 生态。
想一想:为什么
sendfile()能加速静态资源? 因为传统”读文件发网络”要经历”磁盘 → 内核缓冲区 → 用户态缓冲区 → 内核 Socket 缓冲区 → 网卡”四次拷贝、两次用户态/内核态切换;sendfile()让数据从磁盘直接进内核、再直接到网卡,全程零用户态拷贝。少两次拷贝和两次切换,静态资源吞吐就翻上去了——这就是”零拷贝”的价值。
Spring Boot 为什么内嵌 Tomcat?
Spring Boot 默认内嵌 Tomcat(也支持 Jetty、Undertow),因为 Spring MVC 本质上是构建在 Servlet API 之上的:
HTTP 请求 → Tomcat(接收+封装) → DispatcherServlet → Controller → Service → DAO
Tomcat 在这里扮演的是 “把 HTTP 协议翻译成 Java 对象” 的角色。没有 Servlet 容器,Spring MVC 的 @RequestMapping、@RequestBody 这些注解就无法工作——因为它们最终都依赖于 HttpServletRequest 和 HttpServletResponse。
想一想:为什么 Spring MVC 必须依赖 Servlet 容器? 因为
@RequestMapping、@RequestBody这些注解最终都要落到HttpServletRequest/HttpServletResponse这两个接口上,而这两个接口是 Servlet 规范定义的、由 Tomcat 实现的。Spring MVC 本质是”在 Servlet API 之上搭的一层 MVC 框架”,没有 Servlet 容器这层”翻译”,注解就找不到可执行的地基。
黄金组合:Nginx:80 → Tomcat:8080
实际生产环境中,这二者从不互斥——它们是一个经典双层架构:
用户浏览器
↓
Nginx(:80 / :443)
├── /static/* → 直接返回(Nginx 处理)
├── /api/* → proxy_pass → Tomcat (:8080)
└── /ws/* → proxy_pass → Tomcat (:8080)
动静分离
server {
listen 80;
server_name www.example.com;
# 静态资源:Nginx 自己搞定
location /static/ {
root /var/www/html;
expires 30d; # 浏览器缓存 30 天
add_header Cache-Control "public, immutable";
}
# 动态请求:转发给 Tomcat
location /api/ {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
这样做的好处:
- 安全:Tomcat 只监听
127.0.0.1:8080,不暴露在外网。攻击者无法直接打到 Tomcat。 - 效率:图片/CSS/JS 这些占流量 80% 的静态资源由 Nginx 零拷贝返回,不消耗 Tomcat 线程。
- HTTPS 终结:Nginx 负责 SSL/TLS 加解密,Tomcat 只处理 HTTP 明文——减轻 Java 进程的 CPU 负担。
- 优雅重启:Tomcat 重启时,Nginx 自动把请求转发到其他健康实例,用户无感知。
想一想:为什么 Tomcat 只监听
127.0.0.1:8080不暴露外网? 因为 Tomcat 承载的是业务逻辑,是最需要保护的核心资产。把它藏在 Nginx 后面、只允许本机回环访问,攻击者就无法直接打到 Tomcat,所有流量必须先过 Nginx 这一道”安检门”(限流、WAF、HTTPS 终结),安全边界更清晰。
一个完整的请求链路
1. 浏览器 GET https://www.example.com/order/list
2. DNS 解析 → Nginx 服务器 IP
3. TLS 握手(Nginx 处理)
4. Nginx 收到 HTTP 请求,判断 /order/list → proxy_pass → 127.0.0.1:8080
5. Tomcat Acceptor 线程接收连接 → 分配给 Worker 线程
6. DispatcherServlet → OrderController.listOrders()
7. Service → MyBatis → MySQL
8. 结果以 JSON 返回 → Tomcat → Nginx → 浏览器
整个链路中,Nginx 的 CPU 时间约 0.1ms,Tomcat 的业务处理约 50ms——分工明确,各司其职。
为什么不用 Nginx + Lua 替代 Tomcat?
OpenResty 给 Nginx 嵌入了 LuaJIT,可以写业务逻辑。但它仍然无法替代 Tomcat,原因:
- 生态缺失:没有 Spring 的依赖注入、事务管理、ORM、消息队列集成。
- 开发效率:Lua 动态类型、无编译检查、IDE 支持弱——复杂业务逻辑的维护成本是 Java 的 3 倍以上。
- 团队技能:Java 开发者是主流,招 Lua 高手难。
OpenResty 适合做 API 网关层面的轻量逻辑(鉴权、限流、路由),不适合做业务系统的核心逻辑。
总结
| 维度 | Nginx | Tomcat |
|---|---|---|
| 语言 | C | Java |
| 定位 | 反向代理 / 静态资源服务器 | Servlet 容器 / 业务逻辑引擎 |
| 并发模型 | 事件驱动(epoll) | 线程池(BIO/NIO) |
| 内存占用 | ~10MB | ~500MB+ |
| 业务逻辑 | 不支持 | 完整 Java 生态 |
| 典型端口 | 80 / 443 | 8080(内网) |
Nginx 负责”接客”和”挡子弹”,Tomcat 负责”真正干活”。 这不是技术缺陷导致的分工——这是架构设计中的”关注点分离”原则。在可预见的未来,这仍然是最优解。
章末提问
Q1:为什么有了 Nginx 还要 Tomcat,直接用 Nginx 跑 Java 业务行不行? 结论先行:不行,因为二者解决不同层面——Nginx 是 C 语言写的反向代理,没有 JVM 生态,跑不了 Spring/MyBatis 业务。 因为 Nginx 的设计哲学是”只接和转、不管业务”,它的强项是事件驱动的海量连接处理;而 Java 业务依赖 Spring、ORM、事务这些 JVM 生态,只有 Tomcat 这样的 Servlet 容器能承载。硬要用 Nginx 跑业务,等于让接线员去写报表,既没有工具也做不好。
Q2:Nginx 的零拷贝 sendfile 是怎么加速静态资源的?
结论先行:它把”磁盘到网卡”的数据搬运用 sendfile() 系统调用全程留在内核态完成,省掉用户态拷贝和上下文切换。
因为传统方式数据要”磁盘 → 内核 → 用户态 → 内核 → 网卡”四次拷贝;sendfile() 让数据从磁盘直接到内核、再到网卡,零用户态参与。少两次拷贝和两次切换,静态资源(图片/CSS/JS,占流量 80%)吞吐大幅提升,这也是 Nginx 单机扛数万 QPS 的关键。
Q3:为什么 Spring Boot 要内嵌 Tomcat?
结论先行:因为 Spring MVC 构建在 Servlet API 之上,必须有一个 Servlet 容器把 HTTP 翻译成 Java 对象。
因为 @RequestMapping、@RequestBody 等注解最终都依赖 HttpServletRequest/HttpServletResponse,这两个接口正是 Servlet 规范定义的。内嵌 Tomcat 让 Spring Boot 应用”打成一个 jar 就能跑”,无需额外部署外部容器,开箱即用。