为什么并发越高,程序反而越慢

举报
yd_232225224 发表于 2026/09/12 23:44:17 2026/09/12
【摘要】 在进行性能测试时,我们可能会遇到一种反直觉现象:并发数从10提高到50,系统吞吐量明显增加;继续提高到200,吞吐量增长开始变慢;当并发数达到1000时,系统不但没有处理更多请求,响应时间反而急剧上升,甚至出现大量超时。这说明并发并不是越高越好。并发只能让系统更加充分地使用资源,却不能凭空创造计算能力。当请求数量超过系统的处理能力后,新增请求只会进入队列、争抢资源并增加调度开销。理解这一点,...

在进行性能测试时,我们可能会遇到一种反直觉现象:并发数从10提高到50,系统吞吐量明显增加;继续提高到200,吞吐量增长开始变慢;当并发数达到1000时,系统不但没有处理更多请求,响应时间反而急剧上升,甚至出现大量超时。

这说明并发并不是越高越好。并发只能让系统更加充分地使用资源,却不能凭空创造计算能力。当请求数量超过系统的处理能力后,新增请求只会进入队列、争抢资源并增加调度开销。

理解这一点,是正确配置线程池、连接池和服务限流策略的基础。

一、并发和并行并不是一回事

并发表示多个任务在同一段时间内推进,并不要求它们在同一时刻真正执行。

并行则表示多个任务在同一时刻分别由不同的计算资源执行。

假设一颗CPU只有一个核心,同时有三个任务需要运行。操作系统可以让它们轮流使用CPU:

任务A运行一段时间
任务B运行一段时间
任务C运行一段时间
再次运行任务A

从较长的时间尺度看,三个任务都在推进,因此它们是并发执行;但任意时刻只有一个任务真正占用CPU,所以并没有实现并行。

如果CPU拥有多个核心,不同任务才可能在同一时刻分别运行。

因此,创建1000个线程并不意味着有1000个任务可以同时计算。对于8核心CPU,在不考虑其他因素时,同一时刻通常仍然只有少量线程能够真正运行。

二、并发为什么能够提高性能

虽然CPU核心数量有限,但程序并不总是在使用CPU。

一个请求可能经历以下过程:

读取请求
→ 查询数据库
→ 等待数据库响应
→ 执行业务计算
→ 读取文件
→ 等待磁盘
→ 返回结果

当一个线程等待数据库、磁盘或其他外部资源时,CPU可能处于空闲状态。此时让另一个线程继续执行,就能提高资源利用率。

因此,并发对于输入输出密集型任务通常非常有效。它可以把某个任务的等待时间用于处理其他任务。

假设一个任务需要:

  • 10毫秒进行CPU计算;
  • 90毫秒等待数据库。

如果完全串行执行,一个任务需要100毫秒,一秒大约只能处理10个任务。

如果允许多个任务并发,当第一个任务等待数据库时,CPU可以处理其他任务。在资源允许的情况下,系统吞吐量可能获得明显提升。

这也是服务器通常需要并发处理请求的主要原因。

三、为什么继续增加并发会变慢

并发带来的收益存在上限。当关键资源已经接近饱和后,继续增加并发会产生额外成本。

1. CPU上下文切换

当可运行线程数量超过CPU核心数量时,操作系统需要频繁暂停一个线程,再恢复另一个线程。

切换过程中需要保存和恢复:

  • CPU寄存器;
  • 程序执行位置;
  • 栈指针;
  • 调度状态;
  • 部分缓存相关信息。

上下文切换本身不处理业务,却会消耗CPU时间。

线程数量过多时,CPU可能把大量时间花在线程调度上,而不是执行真正的计算任务。

2. CPU缓存命中率下降

现代CPU与内存之间存在多级缓存。线程运行时,会把经常访问的数据加载到CPU缓存中。

当线程频繁切换,不同线程的数据会相互挤占缓存。原线程再次运行时,可能需要重新从速度较慢的内存读取数据。

因此,上下文切换的代价不只是保存和恢复寄存器,还可能破坏数据局部性,降低缓存命中率。

3. 锁竞争加剧

多个线程访问共享数据时,通常需要使用锁保证一致性:

synchronized (lock) {
    updateSharedState();
}

如果只有少量线程,锁竞争可能并不明显;如果大量线程同时请求同一把锁,大部分线程只能等待。

此时增加线程并不会提高吞吐量,因为临界区在同一时刻仍然只能由一个线程执行。更多线程只会形成更长的等待队列。

严重时还可能出现“锁竞争风暴”:线程不断被唤醒、尝试获取锁、失败后再次休眠,大量计算资源消耗在无效竞争上。

4. 内存压力增加

每个线程都需要维护自己的栈和运行状态。线程越多,额外内存占用越大。

如果一个线程分配1MB栈空间,那么1000个线程可能需要预留接近1GB的虚拟地址空间。实际占用方式会因语言、系统和配置不同而变化,但线程绝不是没有成本的。

大量并发请求还可能同时创建:

  • 请求对象;
  • 临时缓冲区;
  • 数据库查询结果;
  • 序列化对象;
  • 日志信息;
  • 待发送的响应数据。

这些对象会增加垃圾回收压力。垃圾回收越频繁,业务线程能够使用的CPU时间就越少。

5. 下游资源已经达到上限

应用线程可以增加,但数据库连接数、磁盘吞吐量和外部接口容量不会自动增加。

假设应用创建了500个工作线程,但数据库连接池只有30个连接,那么最多只有30个线程能够同时访问数据库,其余线程仍然需要等待。

如果盲目扩大数据库连接池,数据库也可能因为并发查询过多而发生:

  • CPU利用率过高;
  • 磁盘输入输出饱和;
  • 锁等待增加;
  • 缓存命中率下降;
  • 查询延迟上升。

性能问题不会因为扩大连接池而消失,它往往只是从应用层转移到了数据库层。

四、吞吐量和响应时间的关系

吞吐量表示系统单位时间内能够完成多少任务,响应时间表示一个任务从进入系统到处理完成需要多久。

在低负载阶段,增加并发通常可以提高吞吐量,而响应时间变化不大。

随着负载接近系统上限:

  • CPU利用率逐渐升高;
  • 数据库连接逐渐耗尽;
  • 请求开始进入队列;
  • 响应时间开始增加。

超过系统容量后,吞吐量可能不再增长,但响应时间会快速恶化。

例如,一个服务每秒最多稳定处理500个请求。如果每秒到达800个请求,那么每秒都会多出300个请求进入队列。只要这种情况持续,队列就会越来越长。

即使每个请求实际只需要几十毫秒处理,用户也可能需要等待数秒,因为大部分时间都消耗在排队上。

五、排队为什么会迅速放大延迟

系统中的资源利用率越接近百分之百,排队延迟通常越不稳定。

当CPU利用率为百分之五十时,偶尔到来的新任务通常能较快获得执行机会;当CPU长期接近百分之百时,任何额外请求都需要等待其他任务完成。

如果任务执行时间完全一致,队列还比较容易预测。但真实系统中的任务通常长短不同:

  • 有些查询只需几毫秒;
  • 有些查询需要扫描大量数据;
  • 有些请求命中缓存;
  • 有些请求需要访问慢速存储;
  • 有些请求触发垃圾回收或重试。

一个特别慢的任务可能占用关键资源,使其后的大量短任务一起等待,这就是队头阻塞。

因此,系统平均响应时间看似正常时,少量请求仍可能出现很高的尾部延迟。

六、为什么平均响应时间可能骗人

假设有100个请求:

  • 95个请求耗时100毫秒;
  • 5个请求耗时5秒。

平均响应时间约为345毫秒。单看平均值,问题似乎不算严重;但实际上,每20个用户中就可能有一个需要等待5秒。

性能分析中通常还要关注分位数:

  • P50:一半请求的响应时间不超过该值;
  • P90:百分之九十的请求不超过该值;
  • P95:百分之九十五的请求不超过该值;
  • P99:百分之九十九的请求不超过该值。

高并发下,P99通常比平均值更早暴露排队、锁竞争和慢查询问题。

平均值适合描述整体水平,P95和P99则更接近最容易影响用户体验的慢请求。

七、线程池不是越大越好

线程池的目的不是创建尽可能多的线程,而是控制同时执行任务的数量。

线程池通常包含两个重要参数:

  • 工作线程数;
  • 任务队列容量。

如果线程数过小,CPU和外部资源可能无法被充分利用;如果线程数过大,则可能出现上下文切换、锁竞争和内存压力。

CPU密集型任务

CPU密集型任务的大部分时间都在进行计算,例如:

  • 图像处理;
  • 视频编码;
  • 数据压缩;
  • 加密计算;
  • 模型推理;
  • 大规模排序。

这类任务的线程数通常应接近CPU核心数。线程过多不会增加真正的并行能力,反而会增加调度成本。

输入输出密集型任务

输入输出密集型任务的大部分时间用于等待,例如:

  • 数据库查询;
  • 文件读写;
  • 远程调用;
  • 消息收发。

这类任务可以配置比CPU核心数更多的线程,因为部分线程等待时,其他线程仍可使用CPU。

可以用下面的关系作为初步估算:

线程数 ≈ CPU核心数 ×(1 + 等待时间 ÷ 计算时间)

例如,一个任务平均计算10毫秒、等待40毫秒,在8核心CPU上可以初步估算:

线程数 ≈ 8 ×(1 + 40 ÷ 10)= 40

这个公式只能提供起始值,最终参数仍然需要通过压力测试确定。

八、无界队列为什么危险

有些线程池使用没有容量限制的任务队列。当请求速度超过处理速度时,新任务会不断进入队列。

短时间看,系统似乎没有拒绝请求;实际上,问题只是被隐藏了。

随着队列增长:

  • 请求等待时间越来越长;
  • 队列对象占用越来越多内存;
  • 用户可能早已放弃请求;
  • 服务器仍在处理已经失去价值的任务;
  • 最终可能发生内存不足。

假设任务处理能力是每秒500个,而每秒进入800个。即使队列能够容纳数百万个任务,也只是让系统晚一点崩溃,并没有解决容量不足的问题。

相比无界队列,一个有容量限制的队列通常更加安全。队列满后,系统应当采取明确策略:

  • 拒绝新请求;
  • 返回系统繁忙;
  • 降低非核心功能质量;
  • 延迟生产任务;
  • 将任务转移到其他实例;
  • 对不同优先级任务进行区分。

拒绝部分请求看起来不够友好,但它可以保护系统继续为其他用户提供服务。

九、超时和重试可能形成雪崩

当系统变慢时,客户端通常会触发超时并重新请求。如果重试策略设计不当,原本已经过载的系统会收到更多请求。

例如,正常流量为每秒500个请求。系统发生短暂抖动后,部分请求超时,每个客户端立即重试两次,那么实际到达量可能迅速增加到每秒1500个。

更多请求导致队列更长,队列更长又导致更多超时,最终形成恶性循环:

系统变慢
→ 请求超时
→ 客户端重试
→ 流量增加
→ 系统进一步变慢

合理的重试机制通常需要:

  • 设置最大重试次数;
  • 使用指数退避;
  • 加入随机抖动;
  • 只重试可安全重复的操作;
  • 设置整体截止时间;
  • 避免所有客户端同时重试。

服务端也应保证关键接口具有幂等性,防止重试造成重复扣款、重复创建订单等副作用。

十、协程和异步能解决所有问题吗

协程和异步输入输出可以减少大量线程带来的调度与内存开销,因此非常适合高并发、长等待的任务。

但它们并不能突破硬件和下游资源上限。

如果任务本身需要大量CPU计算,那么将线程改成协程并不会增加CPU核心数量。如果数据库每秒只能完成一定数量的查询,异步提交更多查询也只会让数据库队列更长。

异步技术主要解决的是“如何用更少的线程管理大量等待中的任务”,而不是“如何无限提高系统吞吐量”。

即使使用协程,仍然需要:

  • 并发数量限制;
  • 有界队列;
  • 超时控制;
  • 背压机制;
  • 下游容量保护;
  • 过载拒绝策略。

十一、什么是背压

背压是指下游处理不过来时,能够通知或限制上游继续发送任务。

假设生产者每秒产生1000个任务,而消费者每秒只能处理600个。如果没有背压,剩余任务只能不断堆积。

加入背压后,系统可以在队列接近上限时:

  • 降低生产速度;
  • 暂停读取新任务;
  • 拒绝低优先级任务;
  • 只保留最新数据;
  • 对任务进行采样;
  • 通知调用方稍后重试。

背压承认系统容量是有限的,并将这种限制明确传递给上游。它通常比依赖无限队列更加可靠。

十二、如何找到合适的并发数

合适的并发数不是根据经验直接猜出来的,而应通过逐步压力测试确定。

可以按照以下方式测试:

  1. 固定请求类型和数据规模;
  2. 从较低并发开始;
  3. 每轮逐步提高并发;
  4. 记录吞吐量、平均延迟、P95和P99;
  5. 同时记录CPU、内存、垃圾回收和队列长度;
  6. 观察数据库连接、磁盘和下游服务;
  7. 找到吞吐量不再明显增长的位置;
  8. 在拐点之前保留一定安全余量。

如果并发从100增加到150时,吞吐量只提高百分之二,但P99延迟增加了两倍,那么继续增加并发通常已经没有意义。

生产环境也不应长期运行在理论最大值附近。系统需要为流量波动、慢查询、垃圾回收和机器故障保留余量。

十三、性能优化应该优化什么

面对并发性能问题,单纯扩大线程池通常不是最佳方案。更有效的方向包括:

减少单个任务的处理时间

  • 优化算法;
  • 减少不必要的计算;
  • 合并重复请求;
  • 使用批处理;
  • 优化数据库查询;
  • 避免频繁创建大对象。

缩短关键资源占用时间

  • 缩小锁的范围;
  • 减少事务持续时间;
  • 尽早释放数据库连接;
  • 避免在持锁状态下进行慢速操作。

降低共享状态

  • 使用不可变对象;
  • 对数据进行分片;
  • 使用线程本地数据;
  • 将全局锁拆分为多把细粒度锁。

控制进入系统的工作量

  • 限流;
  • 有界队列;
  • 请求优先级;
  • 超时与取消;
  • 熔断和降级;
  • 背压。

扩展实际处理能力

  • 增加CPU核心;
  • 增加服务实例;
  • 拆分热点数据;
  • 扩展数据库或存储系统;
  • 将任务分配到不同节点。

优化的关键不是让系统同时接收更多任务,而是让单位时间内完成更多有效任务。

结语

并发的价值在于隐藏等待时间并提高资源利用率,但它不能突破系统的实际容量。

当并发较低时,增加并发可以减少资源空闲,提高吞吐量;当关键资源接近饱和后,更多并发只会带来排队、上下文切换、锁竞争和内存压力。

因此,一个稳定的高并发系统并不是“来多少请求都接收”,而是能够清楚地知道自己的处理上限,并在接近上限时主动进行限流、排队、降级和拒绝。

并发优化真正要寻找的,不是最大的线程数,而是吞吐量、响应时间和系统稳定性之间最合适的平衡点。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。