这篇笔记围绕典型 Java Web / 微服务部署链路展开,目标是把一次请求从域名解析、入口流量接入、网关转发、服务调用到 Tomcat 与 Spring MVC 处理的完整路径串起来,重点说明每一层到底负责什么。
文中主要聚焦传统 Web 集群与 Spring Cloud 微服务体系下的分层关系、请求主路径和常见性能参数,不展开 Kubernetes Ingress、Service Mesh、容器编排等更高一层的平台侧能力。
参考资料:
官方文档:Nginx Reverse Proxy 、 Spring Cloud Gateway Reference 、 Spring Cloud Commons LoadBalancer
组件文档:Apache Tomcat HTTP Connector 、 Spring Framework DispatcherServlet 、 Nacos Spring Cloud
[TOC]
graph TD
A[用户] --> B[DNS轮询/全局负载均衡]
B --> C[负载均衡器集群<br>云LB/F5/HAProxy]
C --> D[Nginx 集群<br>10.0.1.10 ~ 10.0.1.12]
D --> E[Gateway 集群<br>192.168.1.10 ~ 192.168.1.12]
subgraph F [服务治理层]
G[服务注册中心集群<br>Nacos/Eureka]
end
E -. 服务发现 .-> G
subgraph H [业务微服务集群]
I[用户服务集群<br>3节点]
J[订单服务集群<br>5节点]
K[商品服务集群<br>4节点]
L[支付服务集群<br>3节点]
end
E --> I
E --> J
E --> K
E --> L
subgraph M [分布式数据层]
N[MySQL集群<br>主从复制]
O[Redis集群<br>分片]
P[Elasticsearch集群<br>分布式]
end
I --> N
J --> N
J --> O
K --> O
K --> P
这里最容易混淆的点,是“请求经过哪些层”和“哪些组件只是提供治理信息”:
- Nginx、Gateway、业务服务实例通常处在主请求链路上,真实流量会穿过它们。
- 注册中心负责服务注册、服务发现和健康状态同步,通常不承载每个业务请求的数据转发。
- Tomcat 是业务服务内部的 Web 容器,请求到达某个服务实例之后,才会进入 Tomcat 和 Spring MVC。
常见角色分工
| 概念 | 典型实现 | 主要职责 | 与其他层的边界 |
|---|---|---|---|
| 硬件服务器 | Dell PowerEdge、HP ProLiant | 提供 CPU、内存、磁盘、网卡等物理资源 | 属于基础设施,通常不直接承载业务语义 |
| 云服务器 | ECS、EC2、虚拟机 | 在 IaaS 层提供可弹性分配的计算实例 | 本质上仍是资源载体,不等于 Web 服务器或应用服务器 |
| Web 服务器 | Nginx、Apache | 处理 HTTP/HTTPS 接入、反向代理、静态资源分发、SSL 终止 | 更偏流量入口层,不承载主要业务逻辑 |
| 网关 | Spring Cloud Gateway、Kong | 鉴权、限流、路由、灰度、统一入口治理 | 位于应用层入口,负责把请求转到正确的后端服务 |
| 应用服务器 | Tomcat、Jetty、Undertow | 运行 Java Web 应用,遵循 Servlet 规范 | 处在业务服务内部,负责接住已经路由到本实例的请求 |
| 服务注册中心 | Nacos、Eureka | 维护服务实例元数据、健康状态与发现信息 | 提供治理信息,不直接执行业务请求 |
请求到响应
flowchart TD
A[用户浏览器] --> B[1. 点击按钮/提交表单]
B --> C[2. DNS 解析域名]
C --> D[3. 请求到达云 LB / Nginx]
D --> E[4. Gateway<br>认证/路由/限流]
E --> F[5. 匹配路由规则与服务名]
F --> G[6. LoadBalancer 选择实例]
subgraph H [Spring Cloud 微服务集群]
G --> I[7. 路由到具体服务实例<br>Service A]
G --> J[Service B]
G --> K[Service C]
end
I --> L[8. Tomcat 接收请求]
L --> M[9. Spring MVC 调度]
M --> N[10. Controller / Service 执行业务]
N --> O[11. 返回 JSON 或页面]
O --> P[12. 响应沿原路返回]
P --> A
subgraph Q [服务治理]
R[注册中心<br>Nacos/Eureka]
end
E -. 周期拉取或订阅实例信息 .-> R
这条链路里有一个容易误解的地方:Gateway 或服务消费者不一定会在每次请求时实时访问注册中心。更常见的实现方式,是通过 DiscoveryClient 周期刷新或订阅服务列表,再由 LoadBalancer 基于本地缓存的实例信息做负载均衡决策。
Nginx 工作
graph TD
A[外部请求<br>120.79.100.201:443] --> B[服务器网络接口]
B --> C[操作系统内核<br>TCP/IP协议栈]
C --> D[端口分发器]
D --> E{Nginx Worker进程<br>正在监听 0.0.0.0:443}
E --> F[接受连接]
F --> G[处理HTTPS请求]
G --> H[SSL解密]
H --> I[根据nginx.conf路由]
I --> J[反向代理到localhost:8080]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# /etc/nginx/conf.d/api.shop.com.conf
# HTTP服务 - 监听80端口
server {
# 关键配置:监听所有IP的80端口
listen 80;
# 指定这个server块处理哪个域名的请求
server_name api.shop.com;
# 将HTTP请求重定向到HTTPS
return 301 https://$server_name$request_uri;
}
# HTTPS服务 - 监听443端口
server {
# 关键配置:监听所有IP的443端口,并启用SSL
listen 443 ssl;
server_name api.shop.com;
# SSL证书配置
ssl_certificate /etc/ssl/certs/api.shop.com.crt;
ssl_certificate_key /etc/ssl/private/api.shop.com.key;
# SSL协议配置
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:...;
# 业务路由配置
location /api/ {
# 反向代理到 Spring Cloud Gateway
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# 静态文件服务
location /static/ {
root /var/www/html;
expires 30d;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 1. 客户端发起HTTPS请求
curl https://api.shop.com/api/users/123
# 2. 服务器网络层面
请求到达 120.79.100.201:443
↓
操作系统TCP栈接收数据包
↓
检查哪个进程在监听443端口 → 找到Nginx Worker进程
↓
将连接交给Nginx
# 3. Nginx处理阶段
Nginx接受SSL连接
↓
SSL握手、证书验证、密钥交换
↓
解密HTTPS数据,得到原始HTTP请求
↓
根据nginx.conf路由到对应的location块
↓
反向代理到 http://localhost:8080/api/users/123
# 4. 内部转发
建立到localhost:8080的新HTTP连接
↓
Spring Cloud Gateway 接收请求
↓
后续微服务处理...
proxy_pass http://localhost:8080; 这里只是示意 Nginx 可以把入口流量反向代理到本机 Gateway。在多节点部署中,更常见的写法是把上游配置成 upstream gateway_cluster,由 Nginx 在多个 Gateway 节点之间继续做反向代理或健康检查。
负载均衡分层
graph LR
A[客户端] --> B[第一层: 全局负载均衡<br>DNS/云LB]
B --> C[第二层: 反向代理负载均衡<br>Nginx/HAProxy]
C --> D[第三层: 网关动态路由<br>Spring Cloud Gateway]
D --> E[第四层: 微服务客户端负载均衡<br>Ribbon/LoadBalancer]
E --> F[具体服务实例]
| 层级 | 技术 | 作用 | 特点 |
|---|---|---|---|
| 第一层 | DNS轮询、云LB | 流量分发到不同机房/区域 | 基于地理位置的负载均衡 |
| 第二层 | Nginx、HAProxy | 反向代理,分发到网关集群 | 7层负载,支持SSL终止、静态缓存 |
| 第三层 | Spring Cloud Gateway | 微服务路由、认证、限流 | 基于服务名的动态路由 |
| 第四层 | Ribbon、LoadBalancer | 服务实例级别的负载均衡 | 客户端负载均衡,更灵活 |
Nginx 和 Gateway 的边界
| 组件 | 主要关注点 | 常见能力 | 典型边界 |
|---|---|---|---|
| Nginx | 网络入口与 HTTP 接入 | SSL 终止、反向代理、静态资源、连接复用、限速 | 更靠近流量入口,通常不理解具体微服务业务 |
| Gateway | 应用层入口与服务路由 | 鉴权、路由、限流、熔断、灰度、统一过滤链 | 更靠近业务服务发现,理解服务名、路由规则和应用级策略 |
如果只有单体应用,Nginx 后面可以直接转发到 Tomcat;如果是微服务体系,Nginx 与 Gateway 经常同时存在,前者解决入口接入问题,后者解决服务治理问题。
Gateway 路由与客户端负载均衡
sequenceDiagram
participant C as 客户端
participant G as Gateway<br/>(路由决策)
participant L as LoadBalancer<br/>(实例选择)
participant D as DiscoveryClient 缓存
participant E as 服务注册中心
participant U1 as user-service<br/>实例A
participant U2 as user-service<br/>实例B
Note over U1,U2: 服务实例已注册到注册中心
U1->>E: 注册 user-service
U2->>E: 注册 user-service
Note over G,D: Gateway 本地维护路由规则与服务发现缓存
C->>G: GET /api/users/1
Note over G: 1. 路由决策<br/>Path=/api/users/** → user-service
G->>L: 2. 选择 user-service 实例
L->>D: 3. 读取 user-service 实例列表
opt 首次加载或缓存刷新
D->>E: 订阅/拉取最新实例列表
E-->>D: 返回 [实例A, 实例B]
end
D-->>L: 4. 返回 [实例A, 实例B]
Note over L: 5. 负载均衡<br/>选择实例A
L-->>G: 返回实例A地址
G->>U1: 6. 转发请求到实例A
U1-->>G: 返回响应
G-->>C: 返回响应
Ribbon 属于较早期的 Netflix 技术栈,在较新的 Spring Cloud 体系中更常见的是 Spring Cloud LoadBalancer。如果项目仍在使用 Ribbon,可以把这一层理解成“客户端侧的实例选择器”;如果项目已升级,只是具体实现从 Ribbon 切换到了新的 LoadBalancer。
Tomcat 和 Spring MVC
sequenceDiagram
participant C as 客户端 (Browser)
participant T as Tomcat
participant D as DispatcherServlet (Spring MVC 核心)
participant H as HandlerMapping
participant Co as Controller
participant S as Service
participant V as ViewResolver
participant J as JSP/HTML
Note over T, D: 1. Tomcat 接收并解析请求
C->>T: HTTP Request (GET /users/1)
T->>D: 生成 HttpServletRequest/Response<br>并传递给 DispatcherServlet
Note over D, H: 2. Spring MVC 处理请求
D->>H: 查询处理请求的 Controller 方法
H-->>D: 返回 @GetMapping("/users/{id}") 方法
Note over D, Co: 3. 执行业务逻辑
D->>Co: 调用 Controller 方法,并传递参数
Co->>S: 调用 Service 层业务逻辑
S-->>Co: 返回业务数据 (User对象)
Co-->>D: 返回视图名或对象
alt 页面渲染场景
Note over D, V: 4A. 渲染视图
D->>V: 解析视图名 "userDetail"
V-->>D: 返回对应的 JSP/HTML 页面
D->>J: 将模型数据填充到视图
J-->>D: 生成最终的 HTML
D-->>T: 返回 HTML 内容
else REST API 场景
Note over D: 4B. HttpMessageConverter 序列化对象
D-->>T: 返回 JSON / XML 响应体
end
Note over T, C: 5. 返回响应
T-->>C: HTTP Response
连接桥梁 - DispatcherServlet
这是连接 Tomcat 和 Spring MVC 的关键组件
Tomcat 是一个容器,属于基础设施,认Servlet规范
DispatcherServlet 就是 Spring MVC 按照Servlet标准工作的代表
对于现在更常见的 Spring Boot REST 服务,这条链路通常不会经过 ViewResolver,而是直接由 HttpMessageConverter 把对象序列化成 JSON 返回。只有在 JSP、Thymeleaf 这类页面渲染场景下,视图解析器才会成为主路径上的核心环节。
Tomcat 参数
graph TD
A[客户端请求] --> B{连接数检查}
B -->|当前连接 < max-connections| C[建立TCP连接]
B -->|当前连接 >= max-connections| D[拒绝连接或等待]
C --> E{线程池检查}
E -->|有空闲线程| F[分配线程处理请求]
E -->|无空闲线程| G[请求在连接中等待]
F --> H[业务处理]
H --> I[返回响应]
I --> J[释放线程]
J --> K[保持或关闭连接]
| 参数名 | 默认值 | 推荐值 | 说明 |
|---|---|---|---|
max-threads |
200 | 根据 CPU 核数与请求类型评估,常见范围 200 到 800 | Tomcat 工作线程池的最大线程数。值越大并不一定越好,线程过多会带来上下文切换与内存开销 |
min-spare-threads |
10 | 常见可设为 50 到 100 | 最小空闲线程数,用于应对突发流量,减少临时创建线程带来的抖动 |
max-connections |
8192 | 常见可设为 10000+ | Tomcat 可同时维护的连接数上限,连接可以处于空闲 keep-alive 状态,不等于同时执行业务的线程数 |
accept-count |
100 | 常见可设为 500 到 1000 | 当工作线程已满、请求暂时来不及处理时,连接在操作系统与 Connector 之间等待接入处理的队列长度。超过该值的新连接可能被拒绝 |
connection-timeout |
20000 ms | 常见保持在 20000 ms 左右 | 连接建立后等待请求数据的超时时间。过短会误伤慢连接,过长会占用连接资源 |
常见配置示例:
1
2
3
4
5
6
7
8
server:
tomcat:
threads:
max: 800
min-spare: 100
max-connections: 10000
accept-count: 1000
connection-timeout: 20000
max-connections 与 max-threads 的关系
核心结论:不是 1 个线程对应 1 个连接,两者是解耦的。
| 概念 | 作用 | 层面 |
|---|---|---|
| max-connections (8192) | TCP 连接数上限 | 网络层(操作系统维护) |
| max-threads (200) | 工作线程数上限 | 应用层(线程池) |
为什么连接数可以远大于线程数?
-
HTTP Keep-Alive(长连接):一个 TCP 连接上可以发送多个请求,大部分时间连接是空闲的(等待下一个请求),此时不需要占用线程。
-
NIO 模型(Tomcat 8+ 默认):Tomcat 使用 NIO(非阻塞 I/O)模型,少量的 Acceptor 线程和 Poller 线程负责监听所有连接上的事件。只有当某个连接上真正有数据需要处理时,才从线程池里分配一个工作线程。处理完毕后线程归还线程池,连接继续保持。
1
2
3
4
5
6
7
8192个连接(大部分空闲)
↓ 有数据到达
Poller 检测到就绪事件
↓
从200个线程的线程池中取一个线程处理
↓ 处理完毕
线程归还线程池,连接继续保持
- 类比理解:
- max-connections = 餐厅的座位数(8192个座位)
- max-threads = 餐厅的服务员数(200个服务员)
- 不是每个坐着的客人每时每刻都需要服务员,只有在点菜、上菜时才需要
- BIO 模式下的例外:老版本 Tomcat 使用 BIO(阻塞 I/O)模型时,确实是一个连接占用一个线程(线程在 read 时阻塞等待数据),这也是 BIO 效率低的原因——大量线程在空等而不干活。
总结:NIO 模式下,少量线程可以服务大量连接,因为线程只在真正需要执行业务逻辑时才会被占用。所以
max-connections可以设到 8192 甚至 10000+,而max-threads往往只需要按 CPU 与请求耗时做更克制的配置。
常见故障与定位线索
| 现象 | 优先怀疑的层 | 常见原因 |
|---|---|---|
| 域名解析慢、偶发无法访问 | DNS / 全局负载均衡 | DNS 记录异常、权威解析延迟、跨地域网络抖动 |
| HTTPS 握手失败、证书报错 | Nginx / 入口 LB | 证书过期、域名不匹配、TLS 协议或密码套件不兼容 |
入口返回 502 / 504 |
Nginx / Gateway / 上游服务 | 上游实例不可达、Gateway 超时、后端响应过慢 |
| Gateway 正常但单个接口特别慢 | 业务服务 / 数据层 | SQL 慢查询、缓存未命中、下游依赖阻塞 |
| 连接数很多但线程数不高 | Tomcat NIO 正常现象 | 大量 keep-alive 连接空闲,并不代表线程池已经耗尽 |
| 线程池打满、吞吐上不去 | Tomcat / 业务代码 | 业务逻辑阻塞、线程数设置过小、连接池或远程调用耗尽 |
定位时可以按“DNS -> 入口 LB / Nginx -> Gateway -> 服务实例 -> 数据层”这条主链路逐层排查,先确认请求到底卡在接入层、路由层还是业务执行层,再决定是调网络参数、扩实例还是优化代码与 SQL。