进程、线程、异步和连接池:服务怎样同时处理请求

十个用户同时请求,不一定要创建十个进程;用了异步,也不代表计算会快十倍。先分清“谁在执行、谁在等待、能同时做多少工作”,才看得懂框架的性能表现。

1. 四个基本概念

进程拥有自己的地址空间和资源。线程是执行流,同进程线程通常共享内存,因此访问共同可变状态要协调。并发表示多个任务在一段时间内交错推进;并行表示多个任务在同一时刻执行。异步则是一种发起操作后不必阻塞当前执行流等待完成的组织方式。

协程在等待点让出执行权,适合大量网络等待;CPU 密集计算如果持续占住事件循环,其他请求仍会被拖住。线程池、进程池、异步 I/O 不是互相替代的口号,要看运行时和工作类型。Python、Java 和 Node 的具体线程与内存模型不同,别把某一个语言的限制推广到所有后端。

2. 连接池解决什么

建立数据库连接有成本,池把连接复用起来,并限制同时访问数据库的数量。借连接、执行查询、归还连接是一整个生命周期;发生异常也要归还。事务通常绑定某条连接,事务未结束就把连接交给别的请求,会混乱事务归属。

池不是越大越好。数据库能稳定处理的并行事务有限,过多连接可能增加锁竞争和上下文切换。设有 10 个应用实例,每个最多 30 个连接,总潜在连接数就有 300;数据库迁移工具和后台任务还要额外占用连接。

Text
总连接预算 = 应用实例数 × 单实例池上限 + 后台任务 + 管理预留 请求总时限 = 排队预算 + 连接/查询预算 + 编码与网络余量

这是预算分配方法,不是硬件容量预测公式。自动扩容时还要重新检查池预算,否则加应用实例反而压垮数据库。

3. 用有界并发保护下游

下面是可运行的 Python 3.11+ 示例,最多同时执行三个模拟 I/O 操作。sleep 代表可让出的等待,不是拿休眠替代真实网络处理。

Python
import asyncio async def main(): limit = asyncio.Semaphore(3) async def fetch_one(number): async with limit: await asyncio.sleep(0.01) return number * 2 results = await asyncio.gather(*(fetch_one(n) for n in range(6))) assert results == [0, 2, 4, 6, 8, 10] print(results) if __name__ == "__main__": asyncio.run(main())

这个例子一次只创建六个任务。面对百万条输入,即使信号量限制为三,提前创建百万个等待任务仍会消耗内存;应进一步使用有界队列和固定数量 Worker,边读取边处理。

4. 背压、取消和过载

当生产速度超过消费速度,系统需要让上游减速或明确拒绝新工作,这叫背压。无界队列只把“处理不完”延后变成内存耗尽。为队列设容量、等待时限和拒绝策略,再提供可观察的指标。

取消也不是撤销历史。客户端断开连接时,数据库写入可能已经提交;停掉等待不等于回滚外部 HTTP 请求。业务写操作仍需要幂等标识和可查询结果。取消未开始的排队任务和中断正在执行的任务,要分别设计。

自检与参考

压测时分别记录响应时间、排队时间、活动连接和池等待人数。若池等待增加而数据库已饱和,加大池是否只会让情况更糟?用有限并发与拒绝策略做一次对比实验。