Web服务基础理论

📑 目录

文章目录

  • [1 Web服务整体架构与流程](#1 Web服务整体架构与流程)
    • [1.1 完整请求流程](#1.1 完整请求流程)
    • [1.2 核心性能瓶颈](#1.2 核心性能瓶颈)
  • [2 IO模型](#2 IO模型)
    • [2.1 什么是IO](#2.1 什么是IO)
    • [2.2 IO的两个阶段(理解所有模型的关键)](#2.2 IO的两个阶段(理解所有模型的关键))
    • [2.3 五种网络IO模型详解](#2.3 五种网络IO模型详解)
      • [2.3.1 阻塞型IO(Blocking IO)](#2.3.1 阻塞型IO(Blocking IO))
      • [2.3.2 非阻塞型IO(Non-blocking IO)](#2.3.2 非阻塞型IO(Non-blocking IO))
      • [2.3.3 信号驱动式IO(Signal-driven IO)](#2.3.3 信号驱动式IO(Signal-driven IO))
      • [2.3.4 异步IO(Asynchronous IO)](#2.3.4 异步IO(Asynchronous IO))
      • [==2.3.5 **IO多路复用** (IO Multiplexing)==](#==2.3.5 IO多路复用 (IO Multiplexing)==)
    • [2.4 五种IO模型对比总结](#2.4 五种IO模型对比总结)
    • [2.5 C10K问题的解决](#2.5 C10K问题的解决)
  • [3 零拷贝技术](#3 零拷贝技术)
    • [3.1 传统IO的问题](#3.1 传统IO的问题)
    • [3.2 零拷贝技术演进](#3.2 零拷贝技术演进)
    • [3.3 MMAP (内存映射)](#3.3 MMAP (内存映射))
    • [3.4 Sendfile](#3.4 Sendfile)
    • [==3.5 DMA辅助的Sendfile (Gather Copy)==](#==3.5 DMA辅助的Sendfile (Gather Copy)==)
    • [3.6 四种技术对比](#3.6 四种技术对比)
    • [3.8 零拷贝的边界](#3.8 零拷贝的边界)
  • [4 主流Web服务器](#4 主流Web服务器)
    • [4.1 Apache--经典的Web服务器](#4.1 Apache--经典的Web服务器)
      • [4.1.2 Prefork模式--Apache的"远古时代"](#4.1.2 Prefork模式--Apache的"远古时代")
      • [4.1.2 Worker模式--进程+线程的折中方案](#4.1.2 Worker模式--进程+线程的折中方案)
      • [4.1.3 Event模式--Apache的"现代化改造"](#4.1.3 Event模式--Apache的"现代化改造")
      • [4.1.4 三种工作模式对比](#4.1.4 三种工作模式对比)
    • [4.2 Nginx--高性能Web服务器](#4.2 Nginx--高性能Web服务器)

1 Web服务整体架构与流程

1.1 完整请求流程

浏览器 → DNS解析 → 建立TCP连接 → 发送HTTP请求 → Web服务器(如Nginx/Apache) → 应用服务器(如uWSGI/PHP-FPM) → 业务处理 → 返回HTTP响应 → 关闭/复用连接

1.2 核心性能瓶颈

瓶颈类型 具体问题 优化方向
网络IO 海量并发连接处理(C10K问题) IO多路复用
磁盘IO 静态文件高效传输 零拷贝技术
计算资源 用户态/内核态切换、数据拷贝开销 减少上下文切换

2 IO模型

陌生的话去复习一下操作系统知识。

用于解决网络并发问题

2.1 什么是IO

在Linux系统中,一切皆文件,IO操作本质上是对**文件描述符(File Descriptor, fd)**的读写。

IO类型 操作对象 典型场景
网络IO Socket套接字 HTTP请求响应、数据库连接
磁盘IO 文件系统 读取静态文件、日志写入

2.2 IO的两个阶段(理解所有模型的关键)

阶段 操作 耗时 优化重点
阶段①:等待数据准备 内核等待数据从磁盘/网卡到达内核缓冲区 (毫秒~秒级) 主要优化点
阶段②:数据拷贝 CPU将数据从内核缓冲区复制到用户进程内存 短(微秒级) 次要优化点

重要:阶段①的耗时通常是阶段②的1000倍以上

2.3 五种网络IO模型详解

2.3.1 阻塞型IO(Blocking IO)

特征:两个阶段都阻塞,进程挂起等待

bash 复制代码
用户进程:[发起read] → [挂起等待...] → [数据就绪] → [拷贝中...] → [返回]
                              ↑ 阻塞!              ↑ 阻塞!
  • 缺点:一个连接一个线程,并发量受限
  • 类比:去餐厅吃饭,点完菜一直站在柜台等

2.3.2 非阻塞型IO(Non-blocking IO)

特征:阶段①不阻塞(轮询检查),阶段②阻塞

bash 复制代码
用户进程:[read]→[EAGAIN]→[read]→[EAGAIN]→[read]→[拷贝...]→[返回]
          立即返回   轮询!    立即返回   轮询!     有数据!
  • 致命缺陷:CPU空转,大量系统调用开销
  • 实际应用:很少单独使用,配合IO多路复用

2.3.3 信号驱动式IO(Signal-driven IO)

特征:阶段①不阻塞(信号通知),阶段②阻塞

  • 优点:等待阶段不阻塞
  • 缺点:Linux信号机制复杂,实际应用少

2.3.4 异步IO(Asynchronous IO)

特征:两个阶段都不阻塞,内核完成所有操作后通知

bash 复制代码
用户进程:[发起aio_read] → [立即返回,继续执行...] → [收到完成通知] → [直接使用数据]
                              ↑                              ↑
                         内核完成数据准备+拷贝                 数据已在用户缓冲区
  • 优点:最理想模型
  • 现状:Windows IOCP成熟,Linux原生AIO不完善(网络IO支持差)

2.3.5 IO多路复用 (IO Multiplexing)

特征:阶段①阻塞在select/poll/epoll(但一个线程监视多个fd),阶段②阻塞

核心思想一个线程监视多个连接,哪个就绪处理哪个

三种实现对比

特性 select poll epoll
数据结构 fd_set (位图) 链表 红黑树 + 就绪链表
最大fd限制 1024 无限制 无限制
检查方式 遍历所有fd O(n) 遍历所有fd O(n) 回调通知 O(1)
就绪通知 返回数量,需遍历查找 返回数量,需遍历查找 直接返回就绪fd列表
数据拷贝 每次调用拷贝fd_set 每次调用拷贝链表 mmap共享内存
时间复杂度 O(n) O(n) O(1)
适用场景 旧系统兼容 跨平台 Linux高并发首选

epoll高效原理

  • fd状态变更时通过回调自动加入就绪链表,无需遍历
  • 只返回就绪的fd,避免无效检查

2.4 五种IO模型对比总结

模型 阶段①(等待数据) 阶段②(数据拷贝) 线程模型 适用场景
阻塞I/O 阻塞 阻塞 一个连接一个线程 低并发、简单应用
非阻塞I/O 轮询检查 阻塞 需配合多路复用 很少单独使用
I/O多路复用 阻塞在epoll/select 阻塞 一个线程管理多个连接 高并发Web服务器
信号驱动I/O 不阻塞,信号通知 阻塞 信号回调 实际应用少
异步I/O 不阻塞 不阻塞 完全异步回调 理想但支持不完善

2.5 C10K问题的解决

方案 模型 能否解决C10K 原因
阻塞IO + 多线程 阻塞IO 不能 1万线程,内存耗尽
非阻塞IO + 轮询 非阻塞IO 不能 CPU 100%空转
IO多路复用 epoll 单线程管理,内存<100MB

Nginx解决C10K的核心:epoll IO多路复用 + 非阻塞socket

3 零拷贝技术

解决文件传输效率问题

3.1 传统IO的问题

发送文件的4次拷贝,4次切换

图片源于Linux-零拷贝技术

操作 次数 问题
上下文切换 4次 用户态↔内核态切换开销
CPU拷贝 2次 主要优化目标
DMA拷贝 2次 必要,硬件完成,无法避免

关键发现:用户进程只是"中转站",并不处理数据,两次CPU拷贝是多余的

3.2 零拷贝技术演进

bash 复制代码
传统IO (4拷贝4切换) 
    ↓  减少1次CPU拷贝
MMAP (3拷贝4切换)
    ↓  减少2次切换,不经过用户空间
Sendfile (3拷贝2切换)
    ↓  真正的零拷贝,CPU完全不参与
DMA Sendfile (2拷贝2切换)

3.3 MMAP (内存映射)

原理:用户空间直接映射内核缓冲区,跳过"内核→用户"的CPU拷贝

bash 复制代码
磁盘 → 内核缓冲区 ←──MMAP映射──→ 用户空间(直接访问)
  ↑                    ↓
  └────DMA拷贝────┘   拷贝到Socket缓冲区 → 网卡
  • 优化:减少1次CPU拷贝
  • 仍有问题:4次上下文切换,write时可能仍有CPU拷贝

3.4 Sendfile

原理:数据完全不经过用户空间,内核直接在内核空间传递

bash 复制代码
用户进程:sendfile(socket_fd, file_fd, offset, count)
              ↓
内核:磁盘 ──DMA──→ 内核缓冲区 ──CPU拷贝──→ Socket缓冲区 ──DMA──→ 网卡
  • 优化:2次上下文切换,1次CPU拷贝
  • 局限:无法修改数据(加密、压缩),仍有一次CPU拷贝

3.5 DMA辅助的Sendfile (Gather Copy)

原理 :利用DMA的Gather操作,网卡直接从内核缓冲区取数据,CPU完全不参与

bash 复制代码
磁盘 ──DMA──→ 内核缓冲区 ──描述符──→ DMA Gather ──→ 网卡
                ↑                      ↑
              PageCache            网卡直接读取
                                   0次CPU拷贝!
                                   不占用Socket缓冲区!

技术细节

  • 内核构建文件描述符(物理地址+长度+偏移)
  • DMA控制器根据描述符列表,从分散的物理页收集数据,直接发送到网卡
  • CPU只传递描述符,不触碰数据

3.6 四种技术对比

指标 传统IO MMAP Sendfile DMA Sendfile
上下文切换 4次 4次 2次 2次
数据拷贝总次数 4次 3次 3次 2次
CPU拷贝次数 2次 1次 1次 0次
数据经过用户空间 经过 经过 不经过 不经过
CPU参与数据搬运 全程 部分 部分 完全不参与
能否处理数据 可以 可以 不可以 不可以
适用场景 小数据处理 大文件随机访问 静态文件服务器 高性能静态文件

3.8 零拷贝的边界

场景 原因 解决方案
需要修改数据 SSL加密、Gzip压缩 使用普通IO,或硬件卸载
文件不在PageCache 需磁盘IO 预热文件,或使用直接IO
网卡不支持Gather 老网卡/虚拟网卡 升级网卡,接受普通Sendfile
小文件传输 系统调用开销 > 拷贝开销 小文件用普通IO,或合并发送

4 主流Web服务器

4.1 Apache--经典的Web服务器

🔭Apache从进程模型到事件驱动:

Apache是Web服务器的"老祖宗",1995年诞生,经历了从进程模型到事件驱动的演进。理解Apache的三代MPM(Multi-Processing Module,多处理模块),就是理解Web服务器架构的演进史。

  • Apache Event模式是"向Nginx学习"的产物,但受限于历史架构,无法完全达到Nginx的简洁高效
  • Nginx的成功证明:对于IO密集型服务,事件驱动 + 非阻塞IO + 零拷贝是终极答案
  • 现代高性能服务器(Redis、Node.js、Netty、Envoy)都遵循这一模式

4.1.2 Prefork模式--Apache的"远古时代"

核心机制:预派生子进程,每个请求独占一个进程

bash 复制代码
# Prefork 进程模型
# 每个进程:fork() → 处理一个请求 → 等待下一个或退出 
主进程 (Master)
	│
	├─► 预派生子进程1  ──► 接收请求A ──► 处理 ──► 释放连接
	│
	├─► 预派生子进程2  ──► 接收请求B ──► 处理 ──► 释放连接
	│
	├─► 预派生子进程3  ──► 接收请求C ──► 处理 ──► 释放连接
	│
	└─► ...最多256个(默认)
# 关键问题:
1. 进程创建/销毁开销大(fork()成本)
2. 内存占用高(每个进程独立地址空间,5-10MB+)
3. 进程数上限 = 并发上限(默认MaxRequestWorkers 256)
4. KeepAlive长连接会占用进程,降低并发能力

性能数据

  • 每个进程内存占用:5-15MB(取决于加载模块)
  • 最大并发:250(默认)~ 1024(极限)
  • 进程切换开销:上下文切换 + TLB刷新

适用场景

  • 需要稳定兼容旧模块(如某些PHP扩展)
  • 低访问量、求稳定的内部系统
  • 需要进程隔离(一个请求崩溃不影响其他)

4.1.2 Worker模式--进程+线程的折中方案

核心机制:多进程 + 多线程混合,进程稳定、线程轻量

bash 复制代码
# Worker 进程-线程模型
主进程 (Master)
	│
	├─► 子进程1───┬─► 线程1 ──► 处理请求A
	│				├─► 线程2 ──► 处理请求B
	│				├─► 线程3 ──► 处理请求C
	│				└─► ..................(每个进程25个线程)
	│
	├─► 子进程2 ──┬─► 线程1 ──► 处理请求D 
	│				└─► ...
	│
	└─► ...多个子进程(默认3个,可调)
# 改进点:
1. 线程创建开销 < 进程创建(线程共享地址空间)
2. 内存占用降低(线程栈通常1MB vs 进程5MB+)
3. 并发能力提升(进程数 × 线程数)
#新问题:
1. KeepAlive长连接仍会阻塞线程,一个线程被KeepAlive占用,就无法处理其他请求
2. 线程安全问题(模块需要线程安全)
3. 进程崩溃会导致该进程内所有线程的请求丢失

Worker模式的KeepAlive陷阱

bash 复制代码
# 场景:100个线程,100个长连接(KeepAlive)
线程1  ──► 连接A(空闲KeepAlive)
线程2  ──► 连接B(空闲KeepAlive)
..................
线程100 ──► 连接Z(空闲KeepAlive)
# 结果:所有线程被占用,新请求无法处理,虽然连接空闲,但线程被绑定,并发能力归零。
# 临时解决方案:
KeepAliveTimeout 5  # 缩短超时时间
MaxKeepAliveRequests 100  # 限制请求数
但这违背了HTTP长连接的设计初衷

4.1.3 Event模式--Apache的"现代化改造"

Apache Event模式是"向Nginx学习"的产物,但受限于历史架构,无法完全达到Nginx的简洁高效

核心机制:事件驱动(epoll) + 专门线程管理KeepAlive

bash 复制代码
# Event 事件驱动模型
主进程 (Master)
	│
	├─► 子进程1───┬─► 监听线程(epoll)
	│				│		├─► 连接A:新请求 ──► 交给工作线程
	│				│		├─► 连接B:KeepAlive ──► 挂起
	│				│		├─► 连接C:新请求 ──► 交给工作线程
	│				│		└─► 连接D:数据到达 ──► 唤醒处理
	│				├─► 工作线程池(处理实际请求)
	│				│		├─► 线程1:处理请求A
	│				│		├─► 线程2:处理请求C
	│				│		└─► ..................
	│				└─► KeepAlive管理(轻量级线程)
							├─► 挂起空闲连接,不占用工作线程
							└─► 新请求到达时,唤醒并分配给工作线程
# 革命性改进:
1. 使用epoll管理连接(一个线程监视成千上万个)
2. 工作线程只处理"活跃请求",不被KeepAlive阻塞
3. KeepAlive连接由专门线程管理,资源占用极低
本质:从"一个连接一个线程" → "一个线程管理多个连接"
这正是Nginx的核心思想

传统Worker模式:
连接 → 占用线程 → K e e p A l i v e 空闲 → 线程被占用 → 并发下降 \begin{aligned} 连接\rightarrow 占用线程 \rightarrow KeepAlive空闲\rightarrow 线程被占用\rightarrow 并发下降 \end{aligned} 连接→占用线程→KeepAlive空闲→线程被占用→并发下降

Event模式:
连接 → e p o l l 监视 → K e e p A l i v e 空闲 → 挂起 ( 不占用线程 ) → 并发保持 ↓ 新请求到达 → e p o l l 通知 → 分配工作线程 → 处理完成 → 释放线程 \begin{aligned} 连接\rightarrow &epoll监视\rightarrow KeepAlive空闲\rightarrow 挂起(不占用线程)\rightarrow 并发保持\\ & \downarrow\\ &新请求到达\rightarrow epoll 通知\rightarrow 分配工作线程\rightarrow 处理完成\rightarrow 释放线程 \end{aligned} 连接→epoll监视→KeepAlive空闲→挂起(不占用线程)→并发保持↓新请求到达→epoll通知→分配工作线程→处理完成→释放线程

Event模式详解

  • 主线程使用epoll管理所有连接
  • 专门线程处理KeepAlive长连接,避免阻塞工作线程
  • 解决了Worker模式下KeepAlive阻塞线程的问题

4.1.4 三种工作模式对比

维度 Prefork Worker Event
进程模型 一个请求一个进程 多进程+多线程 多进程+epoll+线程池
IO模型 阻塞IO 阻塞IO IO多路复用(epoll)
KeepAlive处理 进程被占用 线程被占用 专门线程管理,不阻塞
内存占用 极高(每进程5-15MB) 中等(每线程1MB) 低(单线程管理万级连接)
并发能力 低(~250) 中等(~1000) 高(~10000+)
稳定性 最高(进程隔离) 中(线程崩溃影响进程)
兼容性 最好(所有模块) 一般(需线程安全) 一般
适用场景 低并发、求稳定 中等并发 高并发(推荐)
与Nginx对比 慢10倍+ 慢5倍+ 差距缩小,但仍慢2-3倍

演进逻辑:Prefork → Worker → Event,逐渐从进程模型转向事件驱动。

4.2 Nginx--高性能Web服务器

Nginx高性能Web服务器

相关推荐
打码人的日常分享1 小时前
双碳智慧园区建设方案(PPT)
大数据·运维·网络·云计算·制造
笨蛋不要掉眼泪2 小时前
Spring Cloud Gateway 核心篇:深入解析过滤器(Filter)机制与实战
java·服务器·网络·后端·微服务·gateway
许愿OvO2 小时前
Tomcat部署与Nginx整合实战
运维·nginx·tomcat
柳鲲鹏2 小时前
LINUX下载编译libosmscout
linux·运维·服务器
茶杯梦轩2 小时前
从零起步学习并发编程 || 第七章:ThreadLocal深层解析及常见问题解决方案
服务器·后端·面试
czxyvX2 小时前
018-Linux-Socket编程-UDP
linux·udp
十五年专注C++开发2 小时前
tiny-process-library:一个用 C++ 编写的轻量级、跨平台(支持 Windows、Linux、macOS)的进程管理库
linux·c++·windows·进程管理
学不完的2 小时前
Nginx
linux·运维·nginx·运维开发