编辑
2026-05-10
记录知识
0

目录

前言
PPC:Process Per Connection
fork 代价高昂
进程间通信复杂
并发连接数受限
prefork 优化
TPC:Thread Per Connection
线程创建并非零开销
线程安全陷阱
异常传播问题
调度依然繁重
prethread 改进
PPC vs TPC:如何选择
Python 示例:模拟 TPC 并发
总结
参考资料

前言

一行不恰当的 debug 日志,可能让 TPS 从 30000 骤降到 8000;一个 tcp_nodelay 参数的疏忽,可能让响应时间从 2 毫秒蜕变到 40 毫秒。高性能,是一场精雕细琢的持久战——从磁盘到操作系统,从CPU到内存,从网络到架构,每个环节都可能成为性能的瓶胫或突破口。

站在架构师的高度,高性能设计聚焦两个核心:将单服务器性能发挥到极致,以及必要时设计服务器集群方案。而单服务器达到高性能的关键,在于并发模型的选择。

并发模型有两个根本问题:服务器如何管理连接?服务器如何处理请求?这两个问题与操作系统的 I/O 模型和进程模型紧密交织。今天,我们先走近两种传统的单服务器高性能模式——PPC 与 TPC

PPC:Process Per Connection

PPC,意为"每连接一进程"。每当一个新的客户端连接到来,服务器就 fork 一个新进程,专门伺候这个连接。父进程 accept 连接后 fork 子进程,子进程处理读写和业务逻辑,关闭连接后子进程退出。

这曾是 UNIX 世界标准的网络服务器模型,第一个 Web 服务器 CERN httpd 就采用了这种方式。实现简单直接,在互联网早期,并发量不过几十几百的场景下运转良好。

但随着互联网爆发式增长,PPC 的缺陷开始暴露:

fork 代价高昂

创建进程并非轻而易举——需要分配内核资源,需要复制内存映像(即使有 Copy-on-Write 技术,开销依然可观)。想象每秒面对上千新建连接,操作系统忙于 fork 而疲于奔命。

进程间通信复杂

父子进程诞生时,文件描述符可以通过内存复制传递;但此后若要通信——子进程要汇报处理了多少请求,父进程要汇总统计——就需要 IPC(进程间通信)机制了。管道、消息队列、共享内存……复杂度陡然上升。

并发连接数受限

每个连接占用一个进程,进程数量不断攀升,操作系统调度和上下文切换的开销成为不能承受之重。PPC 能稳定支撑的并发连接数,一般不过几百

prefork 优化

prefork 的思想朴素而有效:提前创建进程池,服务时无需 fork。系统启动时预先创建好一批进程,它们都阻塞在 accept 上,当新连接到达时只有一个进程能抢到(内核保证),但其他进程会被唤醒——这就是著名的"惊群"问题。Linux 2.6 后内核已解决惊群,但进程调度的开销依然存在。

TPC:Thread Per Connection

TPC,每连接一线程。与进程相比,线程更轻量级——创建线程的开销远小于进程,且多线程共享进程内存空间,通信也更为简单。

TPC 的流程与 PPC 类似:accept → 创建线程 → 子线程处理读写 → 关闭连接。不同的是,主进程无需 close 连接文件描述符,因为线程共享进程空间,只需一次 close 即可。

TPC 弱化了 PPC 的两个痛点:fork 代价和进程通信。但它也带来了新的复杂性:

线程创建并非零开销

高并发场景(每秒上万连接)下,线程创建仍是一笔不小开销。

线程安全陷阱

多线程共享地址空间带来了数据竞争和死锁的风险。互斥锁使用不当,轻则性能下降,重则死锁崩溃。

异常传播问题

某个线程的内存越界等异常,可能导致整个进程崩溃——线程之间没有隔离保护。

调度依然繁重

线程切换虽比进程轻量,但在高并发下仍然是负担。

因此,在并发几百连接的场景下,TPC 相比 PPC 的优势并不明显,PPC 反而因无死锁风险、进程隔离更稳定而更受青睐。

prethread 改进

与 prefork 对应,prethread 预先创建线程池。常见的实现方式有两种:

  1. 主进程 accept 连接,分发给某个线程处理
  2. 所有线程都尝试 accept,只有一个成功

Apache 的 MPM worker 模式更进一步:多进程 + 每个进程多线程。既保持了稳定性(某线程崩溃不会波及全进程),又提升了并发能力。默认配置可支撑 16 × 25 = 400 个并发处理线程。

PPC vs TPC:如何选择

指标PPCTPC
创建开销高(进程)中(线程,轻量但非零)
通信复杂度高(需IPC)低(共享内存)
资源隔离好(进程隔离)差(线程共享)
稳定性高(单进程异常不传染)中(线程可能拖垮进程)
并发能力~256(受进程数限制)~400(MPM worker)

适用场景

  • 常量连接、海量请求:如数据库、Redis、Kafka 等中间件
  • 常量连接、常量请求:如企业内部管理系统
  • 互联网Web服务:海量连接场景,PPC/TPC 都无法支撑,需要 Reactor 模式

Python 示例:模拟 TPC 并发

python
import socket import threading import time from concurrent.futures import ThreadPoolExecutor def handle_client(conn_fd, addr): """模拟处理客户端请求""" print(f"[线程 {threading.current_thread().name}] 处理来自 {addr} 的连接") try: while True: data = conn_fd.recv(1024) if not data: break # 模拟业务处理 time.sleep(0.01) conn_fd.sendall(b"OK") except Exception as e: print(f"[错误] {e}") finally: conn_fd.close() print(f"[线程 {threading.current_thread().name}] 连接 {addr} 已关闭") def simple_tpc_server(port=9999, max_threads=10): """ 简化版 TPC 服务器模型: - 主线程 accept 连接 - 线程池处理请求 """ server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server_sock.bind(('0.0.0.0', port)) server_sock.listen(128) print(f"TPC 服务器启动,监听端口 {port},最大线程数 {max_threads}") with ThreadPoolExecutor(max_threads) as executor: while True: conn_fd, addr = server_sock.accept() print(f"收到来自 {addr} 的连接,提交到线程池") executor.submit(handle_client, conn_fd, addr) def simulate_concurrent_clients(num_clients=5): """模拟并发客户端连接""" def client_request(client_id): try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect(('127.0.0.1', 9999)) sock.sendall(b"request") response = sock.recv(1024) print(f"[客户端 {client_id}] 收到响应: {response.decode()}") sock.close() except Exception as e: print(f"[客户端 {client_id}] 错误: {e}") print(f"\n=== 模拟 {num_clients} 个并发客户端 ===") threads = [] for i in range(num_clients): t = threading.Thread(target=client_request, args=(i+1,)) threads.append(t) t.start() time.sleep(0.01) # 稍微间隔开,便于观察 for t in threads: t.join() print("=== 所有客户端完成 ===\n") if __name__ == "__main__": # 注意:实际运行需要先启动服务器,再运行客户端模拟 # 这里仅展示 TPC 模型的核心逻辑 print("=== TPC 模型核心逻辑演示 ===\n") # 模拟场景 print("场景:5个并发连接请求") print("模型:每连接一线程,线程池上限10") for i in range(5): print(f" 连接 {i+1}: 分配线程 Thread-{i+1} 处理") print("\n特点:") print("- 主线程只负责 accept,不参与业务处理") print("- 线程池复用,避免频繁创建/销毁线程") print("- 每个连接的生命周期内,有专属线程服务")

输出预期

=== TPC 模型核心逻辑演示 === 场景:5个并发连接请求 模型:每连接一线程,线程池上限10 连接 1: 分配线程 Thread-1 处理 连接 2: 分配线程 Thread-2 处理 连接 3: 分配线程 Thread-3 处理 连接 4: 分配线程 Thread-4 处理 连接 5: 分配线程 Thread-5 处理 特点: - 主线程只负责 accept,不参与业务处理 - 线程池复用,避免频繁创建/销毁线程 - 每个连接的生命周期内,有专属线程服务

总结

  • 单服务器高性能的关键在于选择合适的并发模型
  • PPC(每连接一进程)实现简单,但 fork 代价高、进程通信复杂、并发能力有限(~256)
  • TPC(每连接一线程)相比 PPC 更轻量,但存在线程安全、死锁、异常传播等问题
  • prefork 和 prethread 分别是 PPC 和 TPC 的优化版本,通过预创建减少创建开销
  • Apache MPM worker 采用多进程多线程混合模式,兼顾稳定性与并发能力
  • PPC/TPC 适合常量连接海量请求场景(如数据库、中间件),海量连接场景需要更先进的 Reactor 模式

参考资料

  • 《软件架构基础》