异步模型真的比正确配置的同步模型更有利于吞吐量吗?

20
每个人都知道异步可以提高“吞吐量”、“可伸缩性”,并且在资源消耗方面更加高效。在进行以下实验之前,我也是这样想的(过于简单化)。基本上,它表明,如果我们考虑异步代码的所有开销并将其与正确配置的同步代码进行比较,则几乎没有性能/吞吐量/资源消耗优势。
问题:与正确配置的线程池的同步代码相比,异步代码是否真的执行得更好?也许我的性能测试有某种重大缺陷?
测试设置:两个 ASP.NET Web API 方法和 JMeter 尝试使用 200 个线程线程组调用它们(30 秒的 ramp up 时间)。
[HttpGet]
[Route("async")]
public async Task<string> AsyncTest()
{
    await Task.Delay(_delayMs);

    return "ok";
}

[HttpGet]
[Route("sync")]
public string SyncTest()
{
    Thread.Sleep(_delayMs);

    return "ok";
}

这是响应时间(对数刻度)。注意,当线程池注入足够的线程时,同步代码变得更快。如果我们事先设置了线程池(通过SetMinThreads),它将从一开始就胜过async

response time

你可能会问资源消耗方面的问题。"线程在 CPU 时间调度、上下文切换和 RAM 占用方面成本高昂。" 不要那么快下结论。线程调度和上下文切换是有效的。就栈使用情况而言,线程不会立即占用 RAM,而是只保留虚拟地址空间,并仅提交实际需要的微小部分

让我们看看数据说了什么。即使有更多的线程,同步版本的内存占用也较小(工作集映射到物理内存中)。

stats-1

stats-2

更新。我想发布后续实验结果,这些结果应该更具代表性,因为避免了第一次实验的一些偏差。

首先,第一次实验的结果是使用IIS Express(基本上是开发时间服务器)进行的,所以我需要摆脱它。另外,考虑到反馈,我已经将负载生成机器从服务器中隔离出来(两个位于同一网络中的Azure VM)。我还发现一些IIS线程限制从严格到不可能 被违反,并最终切换到ASP.NET WebAPI自托管来消除IIS变量。请注意,此测试的内存占用/CPU时间与其他测试运行时截然不同,请勿跨不同测试运行比较数字(主机、硬件、机器设置完全不同)。此外,当我转移到另一台机器和另一种托管解决方案时,线程池策略发生了变化(它是动态的),注入率有所增加。

设置:延迟100ms,200个JMeter“用户”,30秒的递增时间。

response-time-2

stats-2-1

stats-2-2

我想用以下内容总结这些实验:是的,在某些特定(更像实验室)的情况下,同步和异步可以获得可比较的结果,但在现实世界中,工作负载无法100%预测且不均衡的情况下,我们不可避免地会遇到某种线程限制:无论是服务器端限制还是线程池增长限制(请记住,线程池管理是具有不总是易于预测属性的自动机制)。此外,同步版本确实具有更大的内存占用(工作集和虚拟内存大小都更大)。就CPU消耗而言,异步也胜出(每个请求的CPU时间指标)。
在IIS上,默认设置下情况更加严峻:由于线程数的相当紧密的限制每个CPU20个线程,同步版本的速度慢了几个数量级(吞吐量更小)。
附:对于IO,请使用异步管道![...松了一口气...]

我并不是说这是一个“生产”场景,它只是一个人工测试,但我认为它仍然是有效的(测试同步与异步的本质),我对结果感到非常困惑,希望能得到一些解释... - Eugene D. Gubenkov
看一下任务定义 public class Task : IThreadPoolWorkItem, IAsyncResult, IDisposable,因此一个任务“简单地”说就是一个后台线程。这可能指向你的观点,即一个正确配置的同步方法(我会选择同步后台方法)将具有与任务类似的特性。但是,一旦考虑到这种配置的复杂性,大多数情况下,任务会更加优越。 - peeyush singh
在线程池中运行多个独立单元是异步性的标志。从技术上讲,你的两个例子都不是同步的 :) - NPras
1
我认为负载太重了。异步是对C10K问题(或当今现实中的C10M问题)的响应。 - Lesiak
4
我准备仅出于测试目的为这个问题点赞和收藏,即使方法论被证明存在缺陷也会提供证据。很少有问题像这样得到如此充分的支持。 - Tom W
显示剩余5条评论
2个回答

13
每个人都知道异步操作可以带来更好的吞吐量、可扩展性,并且在资源消耗方面更加高效。
可扩展性没错,但吞吐量就不一定了。每个异步请求都比同步请求慢,所以只有在需要处理比线程数量更多的请求时(即请求数量超过线程数量),才能看到吞吐量的优势。
异步代码与正确配置的线程池下的同步代码相比,实际上表现得更好吗?
嗯,问题在于“正确配置的线程池”。你假设你可以1)预测负载,2)拥有足够大的服务器来使用一个线程处理一个请求。对于许多(大多数?)真实的生产场景,这两个条件都不成立。
从我的关于异步ASP.NET的文章中可以看出:
为什么不仅仅增加线程池的大小[而不使用异步]?答案是双重的:异步代码比阻塞线程池线程更快地扩展。
首先,异步代码比同步代码更具扩展性。通过更现实的示例代码,ASP.NET服务器的总体可扩展性(经过压力测试)显示出了乘法增长。换句话说,异步服务器可以处理几倍于同步服务器的连续请求数量(对于该硬件,两个线程池的最大值都已达到)。然而,这些实验(不是我做的)是在平均ASP.NET应用程序的预期“现实基线”上进行的。我不知道相同的结果是否适用于noop字符串返回。
其次,异步代码比同步代码更易扩展。这很明显;同步代码在线程池线程数达到上限后就无法比线程注入速率更快地扩展。因此,在突然的重载情况下会出现响应非常缓慢的情况,就像你响应时间图表中开头所示的那样。
我认为你所做的工作很有趣;我特别惊讶于内存使用差异(或者说,缺乏差异)。我很想看到你把这个工作写成博客文章。建议:
- 使用ASP.NET Core进行测试。旧版ASP.NET只有部分异步管道;为了更纯粹地比较同步和异步,需要使用ASP.NET Core。 - 不要在本地测试;这样做时有很多注意事项。我建议选择一个VM大小(或单实例Docker容器或其他)并在云中进行可重复性测试。 - 此外,还要尝试压力测试,除了负载测试。不断增加负载,直到服务器完全不堪重负,并查看异步和同步服务器如何响应。
最后提醒一句(也来自我的文章):
异步代码不能替代线程池。这不是线程池异步代码,而是线程池异步代码。异步代码使您的应用程序最大限度地利用线程池。它将现有的线程池提升到11级。

1
感谢您的详细回答,Stephen!老实说,我一直在等待您在这个您已经做出很大贡献的话题上的权威回答。我将尝试进行一些后续实验,特别是尝试将其推向极限并强制使用更多线程(通过减少延迟)。 - Eugene D. Gubenkov
2
不要只从单一来源进行测试。有时候是测试人员而不是被测试的程序出现了压力。 - Paulo Morgado

1

真正的异步代码(I/O)更具可扩展性,因为它释放了线程池线程进行其他工作,而不是阻塞它们。因此,在相同数量的线程的情况下,它可以处理更多的请求。

但这样做的代价是更多的控制数据结构和更多的工作量。因此,除了节省线程池线程之外,它消耗更多的资源(内存、CPU)。

这一切都是关于可用性,而不是性能。


我在问题中试图展示的是同步版本并不真正表现出自己不够可扩展...顺便说一下,Task.Delay是“真正异步”的。 - Eugene D. Gubenkov
1
@EugeneD.Gubenkov 实际上它的可扩展性要低得多。阻塞从自旋等待开始,这意味着一个核心会被占用一段时间。阻塞大量请求会导致高CPU使用率,进而导致服务器或应用程序池的冻结。当发生这种情况时,额外的流量可能会导致农场中的其他服务器冻结,从而导致连锁反应。 - Panagiotis Kanavos
1
@EugeneD.Gubenkov 这不是猜测,那是一个“问我怎么知道”的事实,“我告诉你们停止阻塞”的结果。 - Panagiotis Kanavos

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接