编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • 计算机原理

  • 网络协议

    • README
    • 通过看新闻熟悉网络
    • 通过购物熟悉加密
    • 从0到1部书电商网站
    • 请求网络的通用流程
    • 网络编程模型的概念
    • 传输协议TCP和UDP
    • Socket的发展和设计
    • 传输数据的设计思想
    • 网络域名解析的流程
    • HTTP服务设计流程
      • 01.工作案例引入
        • 1.1 一个HTTP服务的性能攻坚之路
        • 1.2 问题背后的HTTP服务知识图谱
      • 02.HTTP服务架构
        • 2.1 Web服务器的角色
        • 2.2 服务端处理模型
        • 2.3 经典服务器架构
      • 03.请求处理流水线
        • 3.1 接收连接
        • 3.2 解析请求
        • 3.3 路由分发
        • 3.4 处理业务逻辑
        • 3.5 构建响应
        • 3.6 发送响应
      • 04.并发模型设计
        • 4.1 单线程阻塞模型
        • 4.2 多进程/多线程模型
        • 4.3 IO多路复用模型
        • 4.4 异步IO模型
        • 4.5 各模型对比
      • 05.HTTP服务优化
        • 5.1 Keep-Alive连接复用
        • 5.2 Pipeline管线化
        • 5.3 压缩传输
        • 5.4 分块传输编码
      • 06.Web服务器深度剖析
        • 6.1 Nginx架构设计
        • 6.2 事件驱动的本质
        • 6.3 Worker进程模型
        • 6.4 Nginx与Apache对比
      • 07.请求解析的细节
        • 7.1 HTTP解析状态机
        • 7.2 请求体处理策略
        • 7.3 慢速攻击与防御
        • 7.4 请求大小限制
      • 08.负载均衡策略
        • 8.1 负载均衡算法
        • 8.2 健康检查机制
        • 8.3 会话保持方案
        • 8.4 灰度发布设计
      • 09.综合案例:HTTP服务性能的四次进化
        • 9.1 案例背景与目标
        • 9.2 第一代:单线程原型——能跑就行
        • 9.3 第二代:多线程+Keep-Alive——略有改进
        • 9.4 第三代:Nginx反向代理——架构分层
        • 9.5 第四代:全链路优化——逼近极限
        • 9.6 四种方案横向对比
        • 9.7 案例升华:现代Web架构的本质
        • 9.8 全文知识图谱回顾
      • 10.思考题与作业
        • 10.1 基础思考题
        • 10.2 进阶思考题
        • 10.3 动手作业
    • HTTP协议设计思想
    • HTTPS协议设计策略
    • HTTP连接和跳转
    • HTTP代理和缓存设计
    • 如何去排查网络故障
    • WebSocket实时通信
    • HTTP3与QUIC协议
  • 操作系统

  • 数据库原理

  • 计算机
  • 网络协议
杨充
2024-10-24
目录

HTTP服务设计流程

# 10.HTTP服务设计流程

# 目录介绍

  • 01.工作案例引入
    • 1.1 一个HTTP服务的性能攻坚之路
    • 1.2 问题背后的HTTP服务知识图谱
  • 02.HTTP服务架构
    • 2.1 Web服务器的角色
    • 2.2 服务端处理模型
    • 2.3 经典服务器架构
  • 03.请求处理流水线
    • 3.1 接收连接
    • 3.2 解析请求
    • 3.3 路由分发
    • 3.4 处理业务逻辑
    • 3.5 构建响应
    • 3.6 发送响应
  • 04.并发模型设计
    • 4.1 单线程阻塞模型
    • 4.2 多进程/多线程模型
    • 4.3 IO多路复用模型
    • 4.4 异步IO模型
    • 4.5 各模型对比
  • 05.HTTP服务优化
    • 5.1 Keep-Alive连接复用
    • 5.2 Pipeline管线化
    • 5.3 压缩传输
    • 5.4 分块传输编码
  • 06.Web服务器深度剖析
    • 6.1 Nginx架构设计
    • 6.2 事件驱动的本质
    • 6.3 Worker进程模型
    • 6.4 Nginx与Apache对比
  • 07.请求解析的细节
    • 7.1 HTTP解析状态机
    • 7.2 请求体处理策略
    • 7.3 慢速攻击与防御
    • 7.4 请求大小限制
  • 08.负载均衡策略
    • 8.1 负载均衡算法
    • 8.2 健康检查机制
    • 8.3 会话保持方案
    • 8.4 灰度发布设计
  • 09.综合案例:HTTP服务性能的四次进化
    • 9.1 案例背景与目标
    • 9.2 第一代:单线程原型(能跑就行)
    • 9.3 第二代:多线程+Keep-Alive(略有改进)
    • 9.4 第三代:Nginx反向代理(架构分层)
    • 9.5 第四代:全链路优化(逼近极限)
    • 9.6 四种方案横向对比
    • 9.7 案例升华:现代Web架构的本质
    • 9.8 全文知识图谱回顾
  • 10.思考题与作业
    • 10.1 基础思考题
    • 10.2 进阶思考题
    • 10.3 动手作业

# 01.工作案例引入

# 1.1 一个HTTP服务的性能攻坚之路

场景:小刘是一名后端工程师,负责公司电商系统的订单查询 API。刚完成一版实现并通过了功能测试,就收到了运维的告警。

问题 ① —— "压测才 50 QPS 就扛不住了":测试团队用 JMeter 压测 GET /api/orders,50 并发用户,QPS 只有 48,平均响应时间 1.2 秒。小刘看代码——业务逻辑就是一条 SELECT,数据库返回只需 3ms。剩下的 1197ms 去哪了?

问题 ② —— "文件上传 10MB 就 OOM 了":产品要求支持商品图片上传,最大 20MB。小刘在 Controller 里用 @RequestBody byte[] 接收,结果上传 10MB 的文件时 JVM 直接 OutOfMemoryError。同样的代码在笔记本上 10MB 没问题,到 2GB 堆内存的服务器上反而崩了。

问题 ③ —— "每来一个新用户,老用户就卡一下":测试反馈:在 20 个并发用户访问时,只要有一个用户发起慢查询(比如导出报表),其他所有用户的请求都跟着变慢——像排队一样。小刘检查了线程池配置:maxThreads=200,理论上不应该排队。

问题 ④ —— "运维说服务器 TCP 连接数爆了":监控显示服务器上有 8000 多个 TCP 连接处于 TIME_WAIT 状态。小刘数了数后端只有 3 台服务器,每台的外网 IP 有限——连接数远远超过了预期的"并发 200 × 3 台 = 600"。

问题 ⑤ —— "灰度发布时用户一会儿看到旧版,一会儿看到新版":测试团队在进行灰度发布测试时发现:同一个用户在短时间内多次访问 GET /api/user/info,有时返回旧版本的字段格式,有时返回新版本。检查发现配置了粘性会话(Sticky Session),但负载均衡器似乎没有按预期工作。

问题 ⑥ —— "接口响应时间在高峰期突然翻倍":晚上 8 点促销开始后,POST /api/orders 的 P99 延迟从 50ms 飙升到 800ms,但 CPU 和数据库都显示正常。小刘用 tcpdump 抓包,发现大量 TCP 重传包——原来带宽被打满了,1Gbps 的网卡在高峰期跑到了 980Mbps。

疑惑链条:

  • "数据库只要 3ms,为什么 API 要 1.2 秒?" → 每个请求都要新建 TCP 连接(三次握手)+ 可能的 TLS 握手,这些开销累积起来远超业务处理时间
  • "为什么 2GB 内存上传 10MB 就 OOM?" → Spring 默认把整个请求体加载到内存再交给 Controller,10MB 文件 × 200 并发 = 2GB,刚好打满
  • "线程池有 200 个线程,为什么不并发?" → 如果业务方法上加 synchronized 或数据库连接池只有 10 个连接,200 个线程中最多只有 10 个能真正干活,其余全在排队
  • "8000 个 TIME_WAIT 是哪来的?" → HTTP/1.0 默认每个请求新建连接;HTTP/1.1 默认 Keep-Alive,但如果客户端或服务端没正确配置,连接依然频繁关闭
  • "灰度发布为什么会话保持失灵?" → 粘性会话通常基于 Cookie,如果 Cookie 的 Path/Domain 设置有误,或者负载均衡器配置了错误的会话超时
  • "CPU 和数据库都正常,为什么延迟暴涨?" → 带宽瓶颈——响应体太大(未压缩),1Gbps 网卡在 3000 QPS 时就饱和了。每个请求的响应体如果不压缩,100KB × 3000 = 300MB/s = 2.4Gbps

小刘这一串问题,本质都是在问:HTTP 服务从接收请求到返回响应,每一层到底在做什么?性能瓶颈藏在哪里?如何在"连接管理、并发模型、传输优化"之间找到最优解?——这正是"HTTP 服务设计流程"要回答的。

# 1.2 问题背后的HTTP服务知识图谱

把这次性能攻坚翻译成 HTTP 服务技术语言:

用户请求 POST /api/orders 在服务端的处理路径:

客户端 ──TCP──→ [网卡 → 内核协议栈 → Socket接收缓冲区]
                    │
                    ↓ ① 建立连接
              accept() 获取连接 ← 问题①④的发生地
                    │
                    ↓ ② 读取并解析HTTP请求
              状态机解析请求行/头/体 ← 问题②的发生地
                    │
                    ↓ ③ 路由+业务处理
              线程池执行业务逻辑 ← 问题③的发生地
                    │
                    ↓ ④ 构建HTTP响应
              序列化JSON + 设置响应头 ← 问题⑥的发生地
                    │
                    ↓ ⑤ 发送响应
              write() → TCP发送缓冲区 → 网卡
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

六类问题与后续章节的映射关系:

问题 症状 根因所在的知识点 对应章节
① 50 QPS就扛不住 短连接 → Keep-Alive 05.连接复用
② 10MB文件OOM 请求体全加载 → 流式处理 07.请求体处理
③ 新用户慢查询拖垮老用户 线程池/连接池瓶颈 04.并发模型
④ 8000个TIME_WAIT 短连接 → Keep-Alive + 连接池 05.连接复用
⑤ 灰度发布会话错乱 会话保持 → 无状态JWT 08.灰度发布
⑥ 高峰期延迟翻倍 响应体未压缩 → gzip 05.压缩传输

本章的主线就是沿着这六类问题,一层一层拆解 HTTP 服务从"接收到返回"的完整流水线。读完之后,你不仅能排查这些性能问题,还能理解为什么 Nginx 是反向代理的首选、为什么 HTTP/2 用多路复用替代 Pipeline、为什么现代后端架构都讲究"无状态"。

# 02.HTTP服务架构

# 2.1 Web服务器的角色

疑惑:为什么不直接让应用程序监听80端口处理HTTP请求?为什么要有Nginx、Apache这样的Web服务器?

答疑:Web服务器承担的不仅仅是"接收请求、返回内容",它还需要处理:连接管理(成千上万的并发连接)、SSL/TLS加解密、静态文件服务、反向代理、负载均衡、访问日志、安全防护等。将这些通用能力抽象为独立的Web服务器,应用程序只需专注业务逻辑。

回到问题①的场景:如果小刘的应用直接监听 80 端口,它必须自己处理:TCP 连接管理(Keep-Alive、TIME_WAIT)、慢客户端、请求超时、大请求体的流式读取。而这些都是 Web 服务器(Nginx)已经做好的成熟能力。

# 2.2 服务端处理模型

一个HTTP请求在服务端的处理流程:

客户端请求 → [Web服务器] → [应用服务器] → [数据库/缓存]
               │                │
           Nginx/Apache    Tomcat/Node.js/Go
           静态文件服务      业务逻辑处理
           反向代理          动态内容生成
           负载均衡
           SSL终止
1
2
3
4
5
6
7

# 2.3 经典服务器架构

服务器 语言 并发模型 特点
Apache C 多进程/多线程(prefork/worker) 稳定可靠,模块丰富
Nginx C 事件驱动(epoll/kqueue) 高并发,内存占用低
Node.js JavaScript 单线程事件循环 IO密集型场景优秀
Go net/http Go goroutine-per-connection 天然并发,部署简单
Tomcat Java 线程池(NIO/APR) Java生态标准

# 03.请求处理流水线

# 3.1 接收连接

服务器在指定端口(通常80/443)上调用 listen() 监听连接请求。当客户端发起TCP三次握手后,服务器调用 accept() 获取已建立的连接。

关键问题:

  • 连接队列:内核维护两个队列——SYN队列(半连接)和Accept队列(全连接)
  • SYN Flood攻击:恶意发送大量SYN包但不完成握手,耗尽SYN队列
  • 防御手段:SYN Cookie机制——不分配资源,而是将连接信息编码在Cookie中

# 3.2 解析请求

从TCP字节流中解析出HTTP请求报文:

GET /api/users?page=1 HTTP/1.1\r\n     ← 请求行
Host: api.example.com\r\n               ← 请求头
Content-Type: application/json\r\n
\r\n                                     ← 空行(标记头部结束)
{"name": "yc"}                          ← 请求体(可选)
1
2
3
4
5

解析的难点在于:TCP是字节流协议,没有消息边界。服务器需要:

  1. 逐字节读取,直到遇到 \r\n\r\n(空行),确定头部结束
  2. 从 Content-Length 或 Transfer-Encoding: chunked 确定请求体长度
  3. 读取对应长度的请求体

# 3.3 路由分发

根据请求的方法(GET/POST/PUT/DELETE)和路径,将请求分发给对应的处理器:

GET  /api/users     → listUsers()
GET  /api/users/:id → getUser()
POST /api/users     → createUser()
PUT  /api/users/:id → updateUser()
1
2
3
4

这就是RESTful API设计的核心思想:用HTTP方法表示操作,用URL表示资源。

路由匹配的实现方式:

常见的路由匹配策略:

1. 精确匹配(优先级最高)
   /api/users → 完全匹配

2. 前缀匹配
   /api/ → 匹配所有以 /api/ 开头的路径

3. 正则匹配
   /api/users/[0-9]+ → 匹配数字ID

4. 参数化路由
   /api/users/:id → :id 是动态参数
   
5. 通配符匹配
   /static/* → 匹配所有静态资源路径
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

高效路由匹配算法——前缀树(Trie):

路由前缀树示例:

         /api
        /    \
    /users   /posts
    /    \       \
  /:id  /new    /:id
  
匹配 /api/users/123:
  / → api → users → :id(命中,id=123)
  
时间复杂度:O(路径长度),与路由数量无关
1
2
3
4
5
6
7
8
9
10
11
12

# 3.4 处理业务逻辑

这是应用程序的核心部分,可能涉及:数据库查询、缓存读取、调用第三方服务、计算处理等。

一个典型的请求处理流程:

接收到 GET /api/users/123 请求

1. 身份认证
   解析请求头中的 Authorization(JWT Token)
   验证Token有效性,提取用户身份信息

2. 权限检查
   检查当前用户是否有访问 /api/users/123 的权限

3. 参数校验
   检查路径参数 id=123 是否合法(正整数)

4. 业务处理
   先查缓存(Redis)→ 未命中 → 查数据库(MySQL)
   
5. 结果处理
   将数据库结果转换为API响应格式(JSON)
   设置缓存(供后续请求使用)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 3.5 构建响应

将处理结果封装为HTTP响应报文:状态行 + 响应头 + 响应体。

HTTP响应报文结构:

HTTP/1.1 200 OK\r\n                        ← 状态行
Content-Type: application/json\r\n         ← 响应头
Content-Length: 45\r\n
Cache-Control: private, max-age=60\r\n
X-Request-Id: abc-123-def\r\n
\r\n                                       ← 空行
{"id":123,"name":"yc","role":"admin"}      ← 响应体

关键响应头:
├── Content-Type:告诉客户端响应体的格式
├── Content-Length:响应体的字节长度
├── Cache-Control:缓存策略
├── Set-Cookie:设置Cookie
├── X-Request-Id:请求追踪ID(便于调试)
└── X-Response-Time:处理耗时(性能监控)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

回到问题⑥的场景:小刘的订单 API 每次返回 100KB 的 JSON 响应(含大量未压缩的订单详情字段),3000 QPS 时带宽需求 = 3000 × 100KB = 300MB/s = 2.4Gbps,远超 1Gbps 网卡上限。开启 gzip 压缩后,100KB JSON → 约 20KB,带宽需求降到 480Mbps。

# 3.6 发送响应

将构建好的响应通过TCP连接发送给客户端。如果是 Keep-Alive 连接,发送完成后不关闭连接,继续等待下一个请求。

发送响应的优化技巧:

1. 分块发送(Chunked)
   对于动态生成的大响应,不等全部生成完就开始发送
   减少TTFB(首字节时间)

2. 压缩后发送
   对文本类响应(HTML/CSS/JS/JSON)进行gzip或br压缩
   典型压缩率60~80%

3. 零拷贝发送静态文件
   使用sendfile系统调用,数据不经过用户空间
   
4. 写缓冲
   小的响应数据先放入TCP发送缓冲区
   内核会自动合并小包发送(Nagle算法)
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 04.并发模型设计

# 4.1 单线程阻塞模型

最简单的模型:一次只能处理一个连接。

while True:
    conn = server.accept()   # 阻塞等待连接
    data = conn.recv()       # 阻塞读取数据
    response = process(data) # 处理请求
    conn.send(response)      # 发送响应
    conn.close()             # 关闭连接
1
2
3
4
5
6

问题:在处理一个请求时,其他所有请求都在排队等待。只适合学习和调试。

但它有一个教学价值:让你清楚地看到服务器处理一个HTTP请求的完整生命周期。所有复杂模型都是在此基础上的优化。

# 4.2 多进程/多线程模型

每个连接分配一个进程或线程处理(Apache prefork/worker模式)。

多进程模型(Apache prefork):

  Master进程
    │
    ├── Worker进程1 → 处理连接A(独立地址空间)
    ├── Worker进程2 → 处理连接B(独立地址空间)
    └── Worker进程3 → 处理连接C(独立地址空间)

多线程模型(Apache worker):

  进程1
    ├── 线程1 → 处理连接A(共享内存)
    ├── 线程2 → 处理连接B(共享内存)
    └── 线程3 → 处理连接C(共享内存)
1
2
3
4
5
6
7
8
9
10
11
12
13
14

优点:编程简单,每个连接的处理逻辑是线性的。 缺点:进程/线程的创建和切换开销大。在C10K场景(1万并发连接)下,系统资源会被耗尽。

回到问题③的场景:小刘的线程池虽然 maxThreads=200,但数据库连接池只有 10 个。这意味着 200 个线程中最多 10 个能同时执行 SQL,其余 190 个都在 getConnection() 上阻塞等待。所谓"一个慢查询拖垮所有用户",是因为那个慢查询长时间占着一个数据库连接不放。

C10K问题的本质:

假设每个线程占用2MB栈空间:
  1万并发 × 2MB = 20GB 内存(仅线程栈就超过了大多数服务器的内存)
  
加上线程上下文切换的开销:
  1万个线程 × 每次切换约5~10μs = 单次全部切换约50~100ms
  
结论:多线程模型在1万并发时就已经捉襟见肘
1
2
3
4
5
6
7

# 4.3 IO多路复用模型

一个线程同时监控多个连接的事件(Nginx的核心模型)。

事件循环:
  1. 调用 epoll_wait(),阻塞等待任意连接上的事件
  2. 返回有事件的连接列表
  3. 逐个处理这些事件(读取数据、发送响应等)
  4. 回到步骤1
1
2
3
4
5
IO多路复用技术 系统 时间复杂度 最大连接数
select 跨平台 O(n) 1024(FD_SETSIZE)
poll Linux O(n) 无限制
epoll Linux O(1) 无限制
kqueue BSD/macOS O(1) 无限制

epoll为什么快:select/poll每次调用都要遍历所有连接,而epoll通过内核中的红黑树+回调机制,只返回有事件发生的连接,时间复杂度从O(n)降到O(1)。

# 4.4 异步IO模型

真正的异步IO(如Linux的io_uring):发起IO操作后立即返回,IO完成后内核通知应用程序。与IO多路复用的区别在于,多路复用在数据到达后仍需同步读取,而异步IO连读取都是异步的。

# 4.5 各模型对比

模型 编程复杂度 并发能力 代表实现
单线程阻塞 低 极低 学习用
多进程 中 中 Apache prefork
多线程 中 中 Apache worker
IO多路复用 高 高 Nginx, Redis
协程 中 高 Go net/http
异步IO 高 极高 io_uring

# 05.HTTP服务优化

# 5.1 Keep-Alive连接复用

HTTP/1.0默认每个请求都创建新的TCP连接。HTTP/1.1默认开启 Keep-Alive,多个请求复用同一个TCP连接。

无Keep-Alive:         有Keep-Alive:
TCP握手 → 请求1 → TCP挥手    TCP握手 → 请求1 → 请求2 → 请求3 → TCP挥手
TCP握手 → 请求2 → TCP挥手    (省去了中间的握手和挥手开销)
TCP握手 → 请求3 → TCP挥手
1
2
3
4

回到问题①和④:问题①的 1197ms 延迟中,TCP 握手(约 30ms)+ TLS 握手(约 60ms)+ 慢启动(约 50ms)占了约 140ms,每次请求都重复一遍。问题④的 8000 个 TIME_WAIT 正是因为没有保持长连接,每个请求都走的"新建连接→关闭连接"流程,主动关闭方在 TIME_WAIT 中滞留 60 秒。

Keep-Alive 的正确配置:

# Nginx 配置
keepalive_timeout 65;        # 空闲连接保持时间
keepalive_requests 1000;     # 一个连接上最多处理1000个请求

# 后端应用(Tomcat)
maxKeepAliveRequests 100;    # 限制单连接请求数,防止连接不均衡
1
2
3
4
5
6

# 5.2 Pipeline管线化

在Keep-Alive的基础上更进一步:不必等上一个请求的响应返回就可以发送下一个请求。但由于"队头阻塞"问题(前一个响应延迟会阻塞后续所有响应),实际应用中很少使用。HTTP/2通过多路复用彻底解决了这个问题。

# 5.3 压缩传输

通过 Content-Encoding 头部指定压缩算法:

算法 压缩率 速度 使用场景
gzip 高 中 最通用,兼容性最好
br (Brotli) 更高 较慢 HTTPS场景,现代浏览器
deflate 中 快 较少使用

回到问题⑥的解决方案:开启 gzip 压缩后,100KB JSON → 约 20KB,3000 QPS 下带宽从 2.4Gbps 降到 480Mbps,1Gbps 网卡轻松应对。而且 CPU 开销可接受(gzip 压缩 100KB 约 0.5ms)。

# 5.4 分块传输编码

当服务器无法预先知道响应体大小时,使用 Transfer-Encoding: chunked 分块发送:

HTTP/1.1 200 OK
Transfer-Encoding: chunked

7\r\n          ← 第一块,7个字节
Mozilla\r\n
9\r\n          ← 第二块,9个字节
Developer\r\n
0\r\n          ← 最后一块,0字节表示结束
\r\n
1
2
3
4
5
6
7
8
9

这种设计使得服务器可以"边生成边发送",大幅减少了首字节时间(TTFB)。

# 06.Web服务器深度剖析

# 6.1 Nginx架构设计

Nginx是目前最流行的高性能Web服务器,它的架构设计是理解现代服务器设计的经典案例。

Nginx进程模型:

  Master进程(管理进程)
    │
    ├── 读取和验证配置文件
    ├── 创建、绑定和关闭套接字
    ├── 启动和管理Worker进程
    ├── 处理信号(reload, stop, upgrade)
    └── 不处理任何客户端请求
    
    │
    ├── Worker进程 1(处理请求)
    ├── Worker进程 2(处理请求)
    ├── Worker进程 3(处理请求)
    └── Worker进程 N(通常等于CPU核心数)
         │
         └── 每个Worker进程内:
             事件循环(epoll/kqueue)
             同时处理数千个连接
             完全独立,无需加锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

疑惑:为什么Nginx用多进程而不是多线程?

答疑:

  1. 稳定性:一个Worker进程崩溃不会影响其他Worker
  2. 无需锁:每个Worker独立处理自己的连接,没有共享数据的竞争问题
  3. 利用多核:N个Worker进程自然地使用N个CPU核心
  4. 平滑升级:可以逐个替换Worker进程,实现零停机升级

# 6.2 事件驱动的本质

Nginx的高性能根源在于**事件驱动(Event-Driven)**模型。理解它需要对比传统模型:

传统模型(Apache prefork):
  
  进程1 → 处理连接A → [等待数据...阻塞...] → 处理数据 → [等待...] →
  进程2 → 处理连接B → [等待数据...阻塞...] → 处理数据 → [等待...] →
  进程3 → 处理连接C → [等待数据...阻塞...] → 处理数据 → [等待...] →
  
  每个连接独占一个进程,大部分时间在等待IO(空耗资源)

事件驱动模型(Nginx):

  事件循环 ──→ epoll_wait() ──→ 哪些连接有数据?
                                    │
                              ┌─────┼─────┐
                              ↓     ↓     ↓
                          连接A   连接B   连接C
                          读数据  发响应  接受连接
                              │     │     │
                              └─────┼─────┘
                                    ↓
                              处理完毕,回到epoll_wait()
  
  一个Worker进程处理所有连接,绝不在IO上等待
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

核心理念:在IO密集型场景中,CPU大部分时间在等待网络数据。事件驱动模型消除了这种等待——"有事我就干,没事我就等通知"。

# 6.3 Worker进程模型

每个Worker进程如何处理多个连接的细节:

Worker进程内部结构:

  ┌──────────────────────────────────────────┐
  │              Worker进程                   │
  │                                          │
  │  ┌─────────────────────────────┐         │
  │  │       事件循环              │         │
  │  │                             │         │
  │  │  1. epoll_wait()等待事件    │         │
  │  │  2. 遍历就绪事件列表        │         │
  │  │  3. 调用对应的回调函数      │         │
  │  │  4. 回到步骤1               │         │
  │  └─────────────────────────────┘         │
  │                                          │
  │  ┌────────────────────────────┐          │
  │  │    连接池(connection pool)│          │
  │  │    预分配的连接结构体       │          │
  │  │    worker_connections=1024  │          │
  │  └────────────────────────────┘          │
  │                                          │
  │  ┌────────────────────────────┐          │
  │  │    定时器(红黑树)         │          │
  │  │    管理超时事件             │          │
  │  └────────────────────────────┘          │
  └──────────────────────────────────────────┘
  
  Nginx最大并发连接数 = worker_processes × worker_connections
  默认:4 × 1024 = 4096个并发连接
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

# 6.4 Nginx与Apache对比

对比项 Nginx Apache
架构 事件驱动(异步非阻塞) 多进程/多线程(同步阻塞)
并发能力 轻松处理数万并发 数百~数千并发
内存使用 低(一个连接约几KB) 高(一个连接约几MB)
静态文件 极快(sendfile零拷贝) 较快
动态内容 需要反向代理到后端 可内嵌模块(mod_php)
配置灵活性 配置文件格式简洁 .htaccess支持目录级配置
模块加载 编译时确定(动态模块需1.9.11+) 运行时动态加载
适用场景 高并发、反向代理、静态服务 需要丰富模块生态的场景

结论:在现代架构中,Nginx常作为最前端的反向代理和静态资源服务器,将动态请求代理给后端的应用服务器(如Tomcat、Node.js、Go服务等)。这种分工让每个组件都发挥其最大优势。

# 07.请求解析的细节

# 7.1 HTTP解析状态机

HTTP请求的解析通常使用**有限状态机(Finite State Machine)**实现:

HTTP请求解析状态机:

  [开始] → 读取方法 → 读取空格 → 读取URI → 读取空格 → 读取版本 → \r\n
                                                                    ↓
  [头部结束] ← \r\n ← 读取头部值 ← : ← 读取头部名 ← \r\n ←──────────
       ↓
  [读取请求体](根据Content-Length或chunked编码)
       ↓
  [解析完成]
1
2
3
4
5
6
7
8
9

为什么用状态机而不是一次性读取整个请求再解析?

  1. TCP是字节流协议,数据可能分多次到达
  2. 状态机可以处理部分数据——收到多少解析多少,下次从断点继续
  3. 内存效率高——不需要缓存完整的请求才开始处理

# 7.2 请求体处理策略

对于大请求体(如文件上传),服务器不可能全部加载到内存:

请求体处理策略:

1. 缓存到内存(小请求体)
   适用:几KB的JSON请求
   配置:client_body_buffer_size 16k(Nginx默认)

2. 缓存到临时文件(大请求体)
   适用:文件上传
   配置:client_body_temp_path /var/nginx/body_temp

3. 流式处理(边接收边处理)
   适用:实时流、大文件
   原理:读取一个缓冲区大小的数据就立即处理,不等待完整数据

4. 直接传给后端(代理模式)
   配置:proxy_request_buffering off
   原理:Nginx不缓存请求体,直接转发给后端
   适用:大文件上传且后端能处理流式数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

回到问题②的解决方案:小刘把 @RequestBody byte[] 改成 MultipartFile 并配置 spring.servlet.multipart.max-file-size=20MB,关键配置是 spring.servlet.multipart.file-size-threshold=1MB——小于 1MB 的文件存内存,大于 1MB 的文件自动写入临时目录,彻底解决了大文件 OOM。

# 7.3 慢速攻击与防御

Slowloris攻击是一种利用HTTP协议特性的低带宽DDoS攻击:

Slowloris攻击原理:

  攻击者发送不完整的HTTP请求:
  GET / HTTP/1.1\r\n
  Host: target.com\r\n
  X-Header: partial...          ← 不发送\r\n\r\n,请求永远不完整
  
  每隔几秒发送一个额外的头部行,保持连接不超时
  X-Another: value\r\n          ← 续命,防止服务器关闭连接
  
  重复这个过程,打开成千上万个连接
  最终耗尽服务器的连接池,正常用户无法连接
1
2
3
4
5
6
7
8
9
10
11
12

防御手段:

防御措施 原理 Nginx配置
设置头部超时 限制读取请求头的最大时间 client_header_timeout 10s
限制头部大小 超大头部直接拒绝 large_client_header_buffers 4 8k
限制连接数 限制单IP的并发连接数 limit_conn_zone + limit_conn
限制请求速率 限制单IP的请求频率 limit_req_zone + limit_req
设置请求体超时 限制读取请求体的最大时间 client_body_timeout 10s

# 7.4 请求大小限制

服务器需要对请求各部分的大小进行限制,防止恶意请求:

Nginx的各种大小限制:

# 请求行长度(含URI)
large_client_header_buffers 4 8k;
# URI超过8k返回414 URI Too Long

# 请求头部总大小
large_client_header_buffers 4 8k;
# 单个头部超过8k返回400 Bad Request

# 请求体大小
client_max_body_size 10m;
# 超过10MB返回413 Request Entity Too Large

# 分块传输的最大大小
chunked_transfer_encoding on;
# 配合client_max_body_size限制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

疑惑:为什么有些URL长度有限制(如2048字符)?

答疑:HTTP规范并没有限制URL长度,但实际中各组件都有限制:

  • Internet Explorer:2083字符
  • Chrome/Firefox:约65535字符
  • Nginx:默认8KB(可配置)
  • Apache:默认8190字节

最佳实践:将URL长度控制在2048字符以内,以确保所有客户端和服务器的兼容性。如果需要传输大量参数,应使用POST请求体而非GET查询字符串。

# 08.负载均衡策略

# 8.1 负载均衡算法

HTTP服务的负载均衡是将请求分发到多台后端服务器的核心机制:

算法 原理 优缺点 适用场景
轮询(Round Robin) 依次分配给每台服务器 简单公平,不考虑服务器性能差异 服务器配置相同
加权轮询(Weighted RR) 按权重比例分配 可以适应不同配置的服务器 服务器配置不同
最少连接(Least Conn) 分配给当前连接数最少的 更均匀的负载分布 请求处理时间差异大
IP哈希(IP Hash) 同一IP总是分到同一服务器 天然的会话保持 有状态的应用
一致性哈希 基于请求特征的哈希 节点增减时影响最小 缓存场景
随机(Random) 随机选择 简单,大量请求时趋于均匀 通用场景
Nginx负载均衡配置:

upstream backend {
    # 加权轮询
    server 10.0.0.1:8080 weight=5;
    server 10.0.0.2:8080 weight=3;
    server 10.0.0.3:8080 weight=2;
    
    # 或使用最少连接
    # least_conn;
    
    # 或使用IP哈希
    # ip_hash;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 8.2 健康检查机制

负载均衡器需要知道后端服务器是否健康,以避免将请求发送到已故障的服务器:

健康检查分类:

1. 被动健康检查(Nginx默认)
   当请求转发失败时标记服务器为不健康
   配置:
   server 10.0.0.1:8080 max_fails=3 fail_timeout=30s;
   → 30秒内失败3次,标记为不健康,30秒后重试

2. 主动健康检查(Nginx Plus / OpenResty)
   定期向后端发送探测请求
   配置:
   health_check interval=5s fails=3 passes=2 uri=/health;
   → 每5秒检查一次,连续3次失败标记为不健康
   → 连续2次成功恢复为健康

3. 多层健康检查
   ├── TCP层:能否建立TCP连接
   ├── HTTP层:返回状态码是否为200
   └── 业务层:返回内容是否正确(如JSON中的status字段)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 8.3 会话保持方案

HTTP是无状态协议,但很多应用需要识别用户身份(如购物车、登录状态)。在负载均衡场景下,同一用户的请求可能被分发到不同的服务器,需要解决会话保持问题:

方案对比:

1. 粘性会话(Sticky Session)
   原理:同一用户的请求总是发到同一台服务器
   实现:IP Hash 或 Cookie粘性
   问题:服务器故障时会话丢失(问题⑤的潜在根因)

2. 会话共享(Session Sharing)
   原理:所有服务器共享会话存储
   实现:Redis/Memcached集中存储Session
   优点:任何服务器都能处理任何请求
   
3. 无状态设计(Stateless)
   原理:服务器不存储会话,状态由客户端携带
   实现:JWT Token(在请求头中携带认证信息)
   优点:天然支持负载均衡,无需会话保持
   缺点:Token大小、无法即时吊销
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

推荐方案:现代架构推荐无状态设计(JWT)+ Redis缓存热数据。这样既不依赖粘性会话,又能支持状态快速查询。这就是问题⑤的终极解法——去掉了粘性会话,灰度发布只需 Nginx 层按 Header 或 Cookie 中的版本标记路由即可。

# 8.4 灰度发布设计

灰度发布(金丝雀发布)是通过负载均衡器将少量流量导向新版本,验证无误后逐步扩大:

灰度发布流程:

阶段1:1%流量 → 新版本
       99%流量 → 旧版本
       
阶段2:10%流量 → 新版本(1%验证通过后)
       90%流量 → 旧版本
       
阶段3:50%流量 → 新版本
       50%流量 → 旧版本
       
阶段4:100%流量 → 新版本(全量发布)

流量分配策略:
├── 按用户ID取模
├── 按IP地址
├── 按Cookie中的标记
├── 按请求头中的特定字段
└── 按地理位置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

灰度发布的核心价值:用最小的风险验证变更。如果新版本有问题,只影响1%的用户,可以快速回滚。这是互联网公司发布大型变更时的标准做法。

# 09.综合案例:HTTP服务性能的四次进化

前面我们分别讲了 HTTP 服务的处理流水线、并发模型、连接复用、压缩优化、负载均衡等知识。但这些知识点如果是孤立地看,很难形成"优化 HTTP 服务"的系统能力。

本章用一个贯穿全文的实战案例——从一个"能跑就行"的 HTTP 服务原型开始,经历四次进化,最终成为一个生产级的高性能服务。每一代版本都有代码和实测数据。读完这一节,你应该能形成"看到一个 HTTP 服务就能分析出它的性能瓶颈和优化路径"的能力。

# 9.1 案例背景与目标

假设我们要部署一个商品查询 API:GET /api/products?page=1&size=20,返回 JSON 格式的商品列表,单条响应约 50KB。目标是支撑 5000 QPS、P99 延迟 < 100ms。

我们将实现四个版本:

版本 核心方案 瓶颈 对应问题
V1 Python Flask 单进程 QPS < 10,一个请求卡住所有 ①③
V2 Java Spring Boot + Tomcat 线程池 QPS ~200,TIME_WAIT 爆炸 ②④
V3 Nginx 反向代理 + 应用集群 QPS ~1500,带宽瓶颈 ③⑥
V4 Nginx + gzip + Keep-Alive + 缓存 QPS ~6000,达到目标 全部解决

# 9.2 第一代:单线程原型——能跑就行

最朴素的方式——Flask 开发服务器直接跑:

# V1: Flask 单进程版本
from flask import Flask, request, jsonify
import time

app = Flask(__name__)

@app.route('/api/products')
def get_products():
    page = int(request.args.get('page', 1))
    size = int(request.args.get('size', 20))
    
    # 模拟数据库查询(约3ms)
    time.sleep(0.003)
    
    # 模拟构建响应
    products = [{"id": i, "name": f"商品{i}", "price": 99.9} 
                for i in range((page-1)*size, page*size)]
    
    return jsonify({"code": 200, "data": products})

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

问题诊断:

V1 压测(10并发,持续30秒):
  QPS:      8
  P99延迟:  3.2秒
  CPU:      5%

瓶颈分析(对应问题①③):
  1. Flask开发服务器是单线程的:一次只处理一个请求
  2. 10个并发意味着9个在排队
  3. 每个请求耗时3ms业务+排队时间
  4. 没有Keep-Alive:每个请求新建TCP连接
     
  单次请求的时间分布(冷启动,无缓存):
    TCP握手:      ~30ms(问题①的元凶之一)
    请求排队:      ~3200ms(问题③——单线程串行处理10个请求)
    业务处理:      ~3ms
    响应传输:      ~5ms
    TCP挥手:      ~10ms
    总计:          ~3248ms
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 9.3 第二代:多线程+Keep-Alive——略有改进

改用 Spring Boot + Tomcat,天然的多线程 + Keep-Alive:

// V2: Spring Boot 版本
@RestController
public class ProductController {
    
    @GetMapping("/api/products")
    public ResponseEntity<Map<String, Object>> getProducts(
            @RequestParam(defaultValue = "1") int page,
            @RequestParam(defaultValue = "20") int size) {
        
        // 模拟数据库查询
        List<Product> products = productService.list(page, size);
        
        Map<String, Object> result = new HashMap<>();
        result.put("code", 200);
        result.put("data", products);
        result.put("total", 10000);
        result.put("page", page);
        result.put("size", size);
        
        return ResponseEntity.ok(result);
    }
}

// application.yml - Tomcat 配置
server:
  tomcat:
    threads:
      max: 200        # 最大线程数
      min-spare: 10   # 最小空闲线程
    accept-count: 100 # 等待队列长度
    max-connections: 10000 # 最大连接数
    keep-alive-timeout: 60000 # Keep-Alive超时
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

性能测试:

V2 压测(200并发,持续60秒):
  QPS:      185
  P99延迟:  450ms
  CPU:      40%

改进:
  ✅ QPS从8提升到185(23倍提升,多线程效果)
  ✅ P99延迟从3.2秒降到450ms
  
核心问题:
  ❌ 每个请求的TCP+HTTP开销仍然显著(即使有Keep-Alive)
  ❌ 业务线程和IO处理耦合在同一个JVM中
  ❌ 没有压缩:50KB响应 × 185QPS ≈ 74Mbps,尚可接受
  ❌ 问题②(大文件OOM):@RequestBody默认全量加载
  ❌ 问题④(TIME_WAIT):Keep-Alive配置不当仍会短连接
  
TIME_WAIT分析(对应问题④):
  如果客户端没有正确使用连接池,每次请求都新建TCP
  200并发 × 每秒5次请求 = 1000个连接/秒
  每个连接关闭后TIME_WAIT 60秒
  稳态TIME_WAIT = 1000 × 60 = 60000个!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 9.4 第三代:Nginx反向代理——架构分层

在 Tomcat 前面加 Nginx,实现"关注点分离":

# V3: Nginx 反向代理配置
upstream product_backend {
    least_conn;  # 最少连接算法
    server 10.0.0.1:8080 weight=3 max_fails=3 fail_timeout=30s;
    server 10.0.0.2:8080 weight=3 max_fails=3 fail_timeout=30s;
    server 10.0.0.3:8080 weight=2 max_fails=3 fail_timeout=30s;
    keepalive 64;  # 保持到后端的空闲连接
}

server {
    listen 80;
    server_name api.example.com;
    
    # 静态资源直接由Nginx处理
    location /static/ {
        root /var/www;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }
    
    # 动态请求代理到后端
    location /api/ {
        proxy_pass http://product_backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        
        # 超时配置
        proxy_connect_timeout 3s;
        proxy_read_timeout 10s;
        proxy_send_timeout 10s;
        
        # 缓冲配置
        proxy_buffering on;
        proxy_buffer_size 4k;
        proxy_buffers 8 16k;
    }
    
    # 安全限制(对应第7.3/7.4节)
    client_max_body_size 20m;
    client_header_timeout 10s;
    client_body_timeout 30s;
    limit_conn_zone $binary_remote_addr zone=addr:10m;
    limit_conn addr 10;
}
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
44
45
46

架构变化:

V2 架构(问题③的根因):
  客户端 → Tomcat(IO + 业务逻辑混在一起)
          线程同时做"读网络数据"和"查数据库"
          200个线程 = 200个连接上限

V3 架构:
  客户端 → Nginx(纯IO,高性能)→ Tomcat(纯业务)
          ↓                            ↓
      epoll处理数万连接            线程池处理业务
      非阻塞IO                    个个都在干活,不浪费
      Keep-Alive管理              连接池管理
      gzip压缩
      SSL终止
      Slowloris防御
1
2
3
4
5
6
7
8
9
10
11
12
13
14

性能测试:

V3 压测(1000并发,持续60秒):
  QPS:      1520
  P99延迟:  95ms
  CPU(Nginx): 15%
  CPU(Tomcat×3): 平均45%
  TIME_WAIT:  &lt; 100(Nginx Keep-Alive生效)

改进:
  ✅ QPS从185提升到1520(8倍提升,架构分层+Gzip+多实例)
  ✅ TIME_WAIT从60000降到&lt;100(Nginx管理连接池)
  ✅ 可以处理1000+并发(epoll非阻塞)

瓶颈:
  ❌ 50KB响应 × 1520QPS = 76MB/s ≈ 608Mbps(问题⑥)
     离1Gbps上限还有距离,但如果再翻倍就危险了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 9.5 第四代:全链路优化——逼近极限

在 V3 基础上,把剩余优化点全部加上:

# V4: Nginx 全优化配置
upstream product_backend {
    least_conn;
    server 10.0.0.1:8080 weight=3 max_fails=3 fail_timeout=10s;
    server 10.0.0.2:8080 weight=3 max_fails=3 fail_timeout=10s;
    server 10.0.0.3:8080 weight=2 max_fails=3 fail_timeout=10s;
    server 10.0.0.4:8080 weight=2 max_fails=3 fail_timeout=10s; # 加一台
    keepalive 128;
}

server {
    listen 80;
    server_name api.example.com;

    # ✅ 压缩(解决核心问题⑥)
    gzip on;
    gzip_types application/json text/plain text/css application/javascript;
    gzip_min_length 1024;     # 小于1KB不压缩
    gzip_comp_level 3;        # 压缩级别:1(最快)~9(最小),3是甜点
    gzip_vary on;             # 告诉缓存服务器区分压缩/非压缩版本

    # ✅ 静态资源缓存
    location /static/ {
        root /var/www;
        expires 7d;
        add_header Cache-Control "public, max-age=604800, immutable";
    }

    # ✅ API 缓存(对商品列表这种变化不频繁的接口)
    location /api/products {
        proxy_pass http://product_backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        
        # 缓存配置
        proxy_cache product_cache;
        proxy_cache_key "$scheme$request_method$host$request_uri";
        proxy_cache_valid 200 10s;   # 成功响应缓存10秒
        proxy_cache_valid 404 1m;    # 404缓存1分钟
        proxy_cache_lock on;         # 防止缓存击穿
        
        # 超时
        proxy_connect_timeout 2s;
        proxy_read_timeout 5s;
        proxy_send_timeout 5s;
        
        # 缓冲
        proxy_buffering on;
        proxy_buffer_size 4k;
        proxy_buffers 16 32k;
        proxy_busy_buffers_size 64k;
    }
    
    # ✅ 安全限制
    client_max_body_size 20m;
    client_header_timeout 5s;
    client_body_timeout 20s;
    
    # ✅ 限流
    limit_req_zone $binary_remote_addr zone=api_limit:10m rate=100r/s;
    limit_req zone=api_limit burst=20 nodelay;
    limit_conn_zone $binary_remote_addr zone=addr:10m;
    limit_conn addr 20;
    
    # ✅ Keep-Alive
    keepalive_timeout 65;
    keepalive_requests 1000;
}
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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70

Java 端优化:

// V4: Spring Boot 优化配置
@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addResourceHandlers(ResourceHandlerRegistry registry) {
                // 静态资源带版本号,可以永久缓存
                registry.addResourceHandler("/static/**")
                        .addResourceLocations("classpath:/static/")
                        .setCachePeriod(31556926); // 一年
            }
        };
    }
}

// application.yml
server:
  tomcat:
    threads:
      max: 100
      min-spare: 10
    max-connections: 10000
    keep-alive-timeout: 30000
  compression:
    enabled: true              # 应用层也压缩(Nginx已做,双重保障或分流场景)
    mime-types: application/json,application/xml,text/html
    min-response-size: 2048

spring:
  # 数据库连接池优化
  datasource:
    hikari:
      maximum-pool-size: 50    # 每个Tomcat实例50个DB连接
      minimum-idle: 10
      connection-timeout: 3000
      idle-timeout: 600000
      max-lifetime: 1800000
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

性能测试:

V4 压测(2000并发,持续120秒):
  QPS:      6200
  P99延迟:  48ms
  CPU(Nginx): 12%
  CPU(Tomcat×4): 平均35%
  带宽:     85Mbps(gzip压缩后50KB→约10KB)
  TIME_WAIT: &lt; 50

V4 压测(5000并发):
  QPS:      6800
  P99延迟:  72ms
  CPU(Nginx): 18%
  CPU(Tomcat×4): 平均55%
  
  瓶颈已从应用层转移到网络带宽和数据库
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 9.6 四种方案横向对比

维度 V1 Flask单线程 V2 Tomcat多线程 V3 Nginx反向代理 V4 全链路优化
QPS上限 8 185 1520 6200
5000QPS P99延迟 — — — 72ms
并发连接能力 1 200 10000 20000+
单响应大小 50KB 50KB 50KB→15KB(gzip) 50KB→10KB(gzip)
带宽消耗(5000QPS) — — 2Gbps(溢出) 400Mbps
TIME_WAIT(稳态) — 60000 <100 <50
Keep-Alive ❌ ○ ✅ ✅
Gzip压缩 ❌ ❌ ✅ ✅
响应缓存 ❌ ❌ ❌ ✅(10秒)
慢速攻击防御 ❌ ❌ ○ ✅
实现复杂度 极低 低 中 中高
对应问题 ①③ ②④ ③⑥ 全部解决
对应章节 4.1 4.2/5.1 6.1/6.2 5.3/7.2/8.1
性能进化曲线:

    QPS
    │
    │                              V4 ─── 6200
    │                          /
    │                      /
    │                  / V3 ─── 1520
    │              /
    │          /
    │      / V2 ─── 185
    │  /
    │/ V1 ─ 8
    └──────────────────────────────→ 优化阶段
         Flask   Tomcat   +Nginx   +全优化

瓶颈演变:
  V1: 单线程(一次只能处理一个请求)
  V2: 线程数+连接管理(200线程上限,TIME_WAIT爆炸)
  V3: 应用处理能力(4台Tomcat打满约1800QPS)
  V4: 带宽+数据库(1Gbps网卡,MySQL连接池成为新瓶颈)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 9.7 案例升华:现代Web架构的本质

经历了四次进化,回头看现代 Web 架构的设计模式就豁然开朗了:

现代高性能Web架构的分层设计:

        客户端
          │
    ┌─────▼──────┐
    │   CDN      │  ← 就近缓存,减轻源站压力
    │ (静态资源)  │
    └─────┬──────┘
          │
    ┌─────▼──────┐
    │   Nginx    │  ← 反向代理 + SSL终止 + gzip + 限流
    │  (接入层)   │     epoll处理万级连接
    └─────┬──────┘
          │
    ┌─────▼──────┐
    │  Tomcat/Go │  ← 应用服务器集群(纯业务逻辑)
    │  (应用层)   │     每台实例几百线程,够用就好
    └─────┬──────┘
          │
    ┌─────▼──────┐
    │   Redis    │  ← 缓存层(热点数据不走DB)
    │  (缓存层)   │
    └─────┬──────┘
          │
    ┌─────▼──────┐
    │   MySQL    │  ← 数据层(只处理缓存未命中的请求)
    │  (数据层)   │
    └────────────┘
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

每一层解决什么问题:

层 技术 解决的问题 对应小刘的哪个问题
CDN 边缘缓存 静态资源加速、回源保护 ⑥(减少带宽压力)
Nginx epoll + gzip + Keep-Alive 高并发连接、压缩、连接复用 ①④⑥
应用层 无状态 + JWT 会话简化为Token ⑤
缓存层 Redis 热点数据缓存、分布式锁 —
数据层 MySQL 主从 + 连接池 读写分离、连接复用 ③

# 9.8 全文知识图谱回顾

走到这里,我们用"HTTP 服务性能的四次进化"把全文核心串完了:

                    小刘的六类问题
                    │
    ┌───────┬───────┼───────┬───────┬───────┐
    │       │       │       │       │       │
   ①慢     ②OOM    ③排队   ④TIME   ⑤会话   ⑥带宽
    │       │       │       │       │       │
    ▼       ▼       ▼       ▼       ▼       ▼
 Keep-   流式    并发模型  连接复用  无状态   Gzip
 Alive   处理    线程池   Keep-Alive JWT    压缩
 [5.1]   [7.2]   [4.2]   [5.1]   [8.3]   [5.3]
    │       │       │       │       │       │
    └───────┴───────┼───────┴───────┴───────┘
                    │
        ┌───────────┴───────────┐
        │                       │
  请求处理流水线 [3章]      Nginx事件驱动 [6章]
  accept→parse→route           epoll非阻塞本质
        │                       │
        └───────────┬───────────┘
                    │
      V1→V2→V3→V4  HTTP服务性能的四次进化
          [第9章]     从8QPS到6200QPS
                    │
                    ▼
          CDN + Nginx + 应用 + 缓存 + 数据库
          现代Web分层架构 [9.7节]
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

最终的方法论沉淀——优化一个 HTTP 服务时,都应该问自己三个问题:

  1. 连接怎么管?(短连接 vs Keep-Alive → 时间和空间哪个更贵)
  2. 数据和业务怎么分?(接入层 vs 应用层 → IO密集交给Nginx,CPU密集交给Tomcat)
  3. 响应用了什么?(压缩了吗?缓存了吗?分块了吗?→ 每一字节的带宽和每一次的CPU都是成本)

把这三个问题问到位,你就从"能写接口"进化到了"能设计高性能 HTTP 服务"。

# 10.思考题与作业

# 10.1 基础思考题

  1. 请求处理流水线:以 POST /api/orders 为例,从 TCP 连接建立到响应发送完成,画出完整的时间线,标注每一步的典型耗时。哪些步骤能做并行优化?

  2. Keep-Alive 的收益计算:假设 TCP 三次握手耗时 30ms,TLS 握手耗时 60ms。一个页面需要加载 30 个资源,分别计算:① 全部短连接 ② 全部 Keep-Alive 的总连接时间。Keep-Alive 省了多少时间?

  3. 线程池 vs 事件驱动:为什么 Tomcat 用线程池模型(一个请求一个线程),而 Nginx 用事件驱动模型(一个线程处理所有请求)?列出至少 3 个原因。

  4. Gzip 的决策:对于一张 500KB 的 JPEG 图片,需要开启 gzip 吗?对于 500KB 的 JSON 响应呢?请从"压缩率"和"CPU 开销"两个角度分析。

# 10.2 进阶思考题

  1. 问题③的深度分析:小刘的线程池 maxThreads=200,数据库连接池 maxPool=10。在什么场景下 200 线程是浪费?在什么场景下 10 个数据库连接是瓶颈?如何计算"合适的线程池大小"和"合适的连接池大小"?

  2. TIME_WAIT 的精确计算:一台服务器每秒处理 5000 个 HTTP 请求,每个请求都是短连接(建立→处理→关闭)。服务端主动关闭连接。60 秒后,服务端有多少个连接处于 TIME_WAIT 状态?这个数量是正常的还是需要优化的?如果服务端的内存限制在 4GB,TIME_WAIT 会先达到上限还是内存先爆?

  3. Nginx 缓存 vs 应用缓存:Nginx 的 proxy_cache 和 Redis 的应用缓存有什么区别?在什么场景下你应该用 Nginx 缓存而不是 Redis?Nginx 缓存有什么致命缺陷?

  4. 灰度发布的会话陷阱:如果用 Cookie 粘性会话做灰度发布("带 cookie 的用户走新版"),当用户清除了 Cookie 后会发生什么?如果用 Header 标记(X-Canary: true)呢?对比这两种方案的优缺点。

# 10.3 动手作业

作业一(必做):对比 Gzip 的实际效果。

  • 用 curl 分别请求一个 JSON API 接口,带和不带 Accept-Encoding: gzip 头。
  • 记录响应头中 Content-Length 和 Content-Encoding 字段。
  • 计算压缩率和响应字节数对比,填入下表:
接口 未压缩大小 gzip压缩后大小 压缩率 大小节省
/api/products
/api/users

作业二(选做):复现第 9 节的 HTTP 服务进化。

  • 搭建一个简单的 HTTP 服务(返回 50KB JSON),分别实现 V1(Flask单线程)、V2(Spring Boot/Tomcat)、V4(Nginx + gzip)。用 wrk 或 ab 压测。
  • 记录每个版本的 QPS 上限、P99 延迟、带宽消耗。
  • 分析瓶颈在哪一步,和文中数据对比。
版本 QPS上限 P99延迟(500并发) 带宽消耗 瓶颈
V1
V2
V4

作业三(架构思考):分析一个你熟悉的 Web 应用的 HTTP 层设计。

  • 任选一个你参与过的项目,画出它的 HTTP 请求链路图(从客户端到数据中心)。
  • 标注:用了几层代理?有没有 Gzip 压缩?Keep-Alive 配置多久?连接池多大?
  • 找出一个可以优化的点,给出具体的优化方案和预期收益。
上次更新: 2026/06/09, 15:47:57
网络域名解析的流程
HTTP协议设计思想

← 网络域名解析的流程 HTTP协议设计思想→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式