Skip to content
Go back

Nginx vs Tomcat——为什么有了Nginx还要用Tomcat?

Nginx vs Tomcat:为什么有了 Nginx 还要用 Tomcat?

一句话结论(30s)

Nginx 和 Tomcat 不互斥而是分工——因为 Nginx(C 语言、事件驱动)负责「接客/转发/挡子弹」,Tomcat(Java、Servlet 容器)负责「真正干活跑业务」,二者解决的是完全不同的层面问题,所以黄金组合是「Nginx:80 → Tomcat:8080」,各司其职不可互相替代。

核心原理(2min)

底层深入(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 代码可以:

  1. 解析 HTTP 请求 → 封装成 HttpServletRequest
  2. 找到对应的 Servlet → 执行业务逻辑
  3. 把结果封装成 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 这些注解就无法工作——因为它们最终都依赖于 HttpServletRequestHttpServletResponse

想一想:为什么 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;
    }
}

这样做的好处:

  1. 安全:Tomcat 只监听 127.0.0.1:8080,不暴露在外网。攻击者无法直接打到 Tomcat。
  2. 效率:图片/CSS/JS 这些占流量 80% 的静态资源由 Nginx 零拷贝返回,不消耗 Tomcat 线程。
  3. HTTPS 终结:Nginx 负责 SSL/TLS 加解密,Tomcat 只处理 HTTP 明文——减轻 Java 进程的 CPU 负担。
  4. 优雅重启: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,原因:

  1. 生态缺失:没有 Spring 的依赖注入、事务管理、ORM、消息队列集成。
  2. 开发效率:Lua 动态类型、无编译检查、IDE 支持弱——复杂业务逻辑的维护成本是 Java 的 3 倍以上。
  3. 团队技能:Java 开发者是主流,招 Lua 高手难。

OpenResty 适合做 API 网关层面的轻量逻辑(鉴权、限流、路由),不适合做业务系统的核心逻辑。

总结

维度NginxTomcat
语言CJava
定位反向代理 / 静态资源服务器Servlet 容器 / 业务逻辑引擎
并发模型事件驱动(epoll)线程池(BIO/NIO)
内存占用~10MB~500MB+
业务逻辑不支持完整 Java 生态
典型端口80 / 4438080(内网)

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 就能跑”,无需额外部署外部容器,开箱即用。


Share this post on:

Previous Post
RestTemplate连接池优化——每次new都要三次握手?
Next Post
Java AIO与io_uring——为什么AIO在Linux上是"伪异步"?