🔢 并发数量深度解析
并发到底取决于什么?如何确定最优并发数?
并发(Concurrency)不是越大越好,也不是越小越好。并发数量的最优值取决于一系列因素的综合权衡。
一、并发 vs 并行 — 先搞清概念
| 概念 | 定义 | 举例 |
|---|---|---|
| 并发 Concurrency | 多个任务在同一个时间段内交替执行(逻辑上同时) | 一个人同时煮水、切菜、炒菜(来回切换) |
| 并行 Parallelism | 多个任务在同一个时刻真正同时执行(物理上同时) | 三个人各做一道菜,同时进行 |
关键区别:并发是"看起来同时",并行是"真正同时"。单核 CPU 可以有并发但不可能有并行。
现实关系:并行是并发的子集——你设计的并发任务,最终通过多核 CPU 实现部分并行。
二、并发数量的决定因素
因素①:任务类型(最重要)
| 任务类型 | 特点 | CPU 占用 | 最优并发 | 例子 |
|---|---|---|---|---|
| CPU 密集型 | 大量计算,很少等待 | 90~100% | ≈ CPU 核数 | 视频编码、模型推理 |
| IO 密集型 | 大量等待,很少计算 | 10~30% | 几十~几千 | Web 服务、API 调用 |
| 混合型 | 有计算有等待 | 30~70% | 介于两者之间 | 数据处理+写入 |
怎么判断?跑一个任务,看 CPU 占用率:
- CPU 飙到 90%+ → CPU 密集型 — 并发别超过核数
- CPU 只有 10~30% → IO 密集型 — 并发可以很大
因素②:硬件资源
CPU 密集型:最大合理并发 ≈ CPU 核心数 × (1 + 超线程系数)
例子:4核8线程 → 合理并发 4~8,再多只会导致上下文切换开销
IO 密集型:最大并发取决于其他资源瓶颈
- 内存:每个线程/协程需要空间
- 文件句柄:Linux 默认 ulimit -n = 1024
- 连接数:数据库连接池、Redis 连接数
- 网络带宽:带宽打满也会限制有效并发
典型资源限制表:
| 资源 | 默认上限 | 可调大 | 超出后果 |
|---|---|---|---|
| OS 线程数 | 取决于内存 | ❌ 每个线程 ~8MB 栈 | OOM 崩溃 |
| 文件句柄 | 1024 | ulimit -n 65535 | Too many open files |
| MySQL 连接 | 151 | 可配置 | 连接被拒绝 |
| PostgreSQL 连接 | 100 | 可配置 | 连接超时 |
| Redis 连接 | 10000 | 可配置 | 连接被拒绝 |
| Nginx 反向代理 | worker_connections 768 | 可配置 | 502 Bad Gateway |
因素③:编程语言与并发模型
| 语言 | 并发模型 | CPU 密集有效并发 | IO 密集有效并发 | 原因 |
|---|---|---|---|---|
| Python | 多线程 / asyncio / 多进程 | 1(多线程无效,GIL) ≈核数(多进程) | 几百~几千(asyncio) 几十(多线程,GIL 切换) | GIL 限制 CPU 并行 |
| Go | goroutine(协程) | ≈核数(自动调度) | 10万+(轻量) | goroutine ~4KB栈 |
| Node.js | 事件循环 + Worker Threads | ≈核数(Worker Threads) 1(主线程) | 几千(事件循环高效) | 单线程不能做 CPU 密集 |
| Java | 线程池 + 虚拟线程 (Loom) | ≈核数(传统线程) | 几千(虚拟线程) 几百(传统线程池) | 虚拟线程类似 goroutine |
关于 Python GIL 的真相:
- GIL 导致 Python 多线程不能利用多核做 CPU 并行
- 但 IO 密集型多线程仍然有效——因为线程在等待 IO 时会释放 GIL
- 正确做法:CPU 密集用
multiprocessing,IO 密集用asyncio或concurrent.futures.ThreadPoolExecutor
因素④:锁竞争与共享资源
并发不是越多越快——锁竞争会导致严重的性能下降: 想象一个卫生间(共享资源): - 1 个人用:不用排队,0 等待 - 10 个人用:稍微排队,还能接受 - 100 个人用:门口排长队,大部分时间在等待 - 1000 个人用:所有人都在排队,实际"使用"时间接近 0 同样的道理适用于: - 数据库行锁(同一行数据被并发更新) - 文件写锁(多个进程写同一个文件) - 分布式锁(Redis Redlock / ZooKeeper) - 信号量(Semaphore) - 连接池(连接不够用) 经验法则:如果并发增加后吞吐量不再线性增长,甚至下降 → 锁竞争太严重了。
因素⑤:外部依赖限制
| 外部依赖 | 常见限制 | 怎么破 |
|---|---|---|
| 数据库 | 连接池上限、磁盘 IOPS、锁竞争 | 连接池调优、读写分离、分库分表 |
| 第三方 API | Rate Limit(如 OpenAI 每分钟 1000 次) | 本地缓存、请求队列、限流 |
| 消息队列 | 消费速度跟不上生产速度 | 增加消费者、优化消费逻辑 |
| 网络带宽 | 带宽打满导致延迟飙升 | CDN、压缩、升级带宽 |
| 文件系统 | 磁盘 IOPS 上限、inode 用完 | SSD、分布式存储 |
三、如何找到最优并发数?
方法:压测 + 观测
# 用 wrk 做简单的 HTTP 压测(Linux) wrk -t4 -c100 -d30s http://localhost:5000/api # -t4: 4 个线程 # -c100: 100 个并发连接 # -d30s: 压测 30 秒 # 返回结果示例: # Thread Stats Avg Stdev Max +/- Stdev # Latency 45.23ms 12.34ms 567ms 83.17% # Req/Sec 2,215 301 3,892 75.33% # 665,432 requests in 30s, 890MB read # Requests/sec: 22,181 # 逐步增加并发 -c200, -c500, -c1000 # 观察:吞吐量(Requests/sec)是否还在增长 # 如果不再增长甚至下降 → 这就是你系统的最优并发点
| 并发数 | 延迟 P50 | 延迟 P99 | 吞吐量 QPS | CPU |
|---|---|---|---|---|
| 10 | 5ms | 12ms | 2,000 | 15% |
| 50 | 25ms | 80ms | 10,000 | 40% |
| 200 | 100ms | 350ms | 22,000 | 80% |
| 500 | 400ms | 1.2s | 21,000 | 95% |
| 1000 | 900ms | 3s | 18,000 | 98% |
从上面数据可以看到:最优并发点是 200。再往上加并发,吞吐量不再增长,延迟反而快速恶化。
四、实战中的经验公式
IO 密集型任务的最优并发 ≈ 预期延迟 × 目标吞吐量 例子:API 响应时间平均 50ms,目标每秒处理 1000 请求 最优并发 = 0.05s × 1000 = 50(这是理论最小值,实际要放大 2~5x) CPU 密集型任务的最优并发 ≈ CPU 核数(或 +1) 例子:8 核服务器跑计算任务 最优并发 = 8(不加超线程)或 16(加超线程) 实际取值建议: - Web 服务(Python Flask + Gunicorn):workers = 2~4 × CPU 核数 - Web 服务(Go):默认 GOMAXPROCS = CPU 核数,goroutine 池 100~10000 - 数据库连接池:Spring Boot 默认 10, Django 默认 10, 调大要谨慎 - 消息队列消费者:通常 1~10 个消费者进程
五、一句话总结
并发数量取决于你的"最短板":
CPU 密集 → 看核数
IO 密集 → 看 IO 延迟 + 外部资源限制
有锁竞争 → 锁释放速度
最优并发不是算出来的,是压测测出来的。
逐步加压,观察吞吐量拐点——拐点处的并发数就是你的最优值。
登录后才能评论哦 ~ 立即登录