关于JMeter & Async & Await的注意事项
关于异步编程好处的基准测试示例。
引言
本笔记是关于 异步编程 的益处的一个基准示例。
背景
这是一篇关于 JMeter & Async & Await 的笔记。 Async & Await 已经存在一段时间了。Async & Await 的思想如下:
- 对于 I/O 密集型操作,最好不要为并发请求创建太多线程。最好在一定程度上共享同一个线程来处理不同的请求。我们可以为 I/O 操作的完成事件注册一个回调函数,并在等待 I/O 完成时将线程让出来服务其他请求。
- 微软引入了语法糖,称为使用 Async & Await 关键字的 异步编程,而不是显式注册回调函数。
除了语法糖之外,Async & Await 还可以将 异步操作中的异常 冒泡到调用代码。 Async & Await 的思想并非微软独有。最好的例子是 Node。尽管 Node 可以利用计算机上的 多核,但它以在单个线程上服务并发请求而闻名。 JAVA 也有类似的机制,如 NIO 和 CompletableFuture。尽管 Async & Await 被广泛采用,但证明其优势的基准示例却很少。本笔记旨在创建一个简单的示例,并使用 JMeter 来验证以下内容:
- 异步编程 是否提供任何可见的性能优势?
- 异步编程在 CPU 密集型操作上的效率如何?
- 异步编程在 I/O 密集型操作上的效率如何?
- 如果我们有混合 CPU 密集型和 I/O 密集型操作,是否应该使用异步编程?
为了使本笔记保持简单,我将使用 Thread.Sleep() & Task.Delay() 来模拟 CPU 密集型和 I/O 密集型操作。尽管对模拟的有效性存在一些争论,但结果应该为我们决定是否使用 异步编程 提供有说服力的参考价值。
下载 & 安装 JMeter
Apache JMeter 有一个 不错的教程。下载和安装 JMeter 很简单。
- JMeter 是一个 JAVA 应用程序。我们需要下载 JAVA 并将其添加到 PATH 中。根据 JMeter 的说法,如果你想在调试模式下运行 JMeter,你需要一个 JDK;
- 我们可以从 此链接 下载 JMeter。下载压缩文件后,我们可以将其解压到任何地方。
由于以 GUI 模式运行 JMeter 会消耗大量系统资源,因此 JMeter 教程 建议以非 GUI 模式启动 JMeter。但我还是直接在 GUI 模式下启动了 JMeter,并在 GUI 模式下进行了所有测试。
ASP.NET MVC 应用程序
为了测试目的,我在 Visual Studio 2019 中创建了一个 ASP.NET Core 应用程序。它是一个面向 .NET Framework 4.7.2 的控制台应用程序。
Thread.Sleep VS. Task.Delay
在这个例子中,我使用 `Thread.Sleep` 来模拟 CPU 密集型操作,并使用 `Task.Delay` 来模拟 I/O 密集型操作。
- Thread.Sleep 类似于 CPU 密集型操作。它会占用线程一段时间,而该线程无法服务其他请求。
- Task.Delay 类似于 I/O 密集型操作。通过 `await` 语法,线程被释放出来服务其他请求,因此同一个线程可以服务多个请求。
尽管有相似之处,但以下论点仍然成立:
- 尽管两者都会阻塞线程,但在真正的 CPU 密集型操作中 CPU 是忙碌的。但是,当执行 `Thread.Sleep` 时,线程不使用 CPU,操作系统可以调度另一个线程来运行。
- 尽管 `Task.Delay` 和 I/O 密集型操作都不会阻塞线程,但 I/O 密集型操作仍然会使用其他资源,例如网络和硬盘。`Task.Delay` 除了计时器几乎不使用任何资源。
如果你愿意,可以创建自己更全面的示例。我觉得我的简单模拟得出的结果具有说服力。至少,你可以将结果解释为阻塞线程和非阻塞线程之间的区别。
REST API
该控制台应用程序通过 `5050` 端口暴露 4 个 REST 端点。所有 REST API 的功能都相同。每个 API 在 3 秒延迟后都会响应一个 JSON 对象。
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Console;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace kestrel_mvc
{
class Program
{
public static void Main(string[] args)
{
WebHost.CreateDefaultBuilder(args)
.ConfigureLogging(lb =>
lb.AddFilter<ConsoleLoggerProvider>(ll => ll == LogLevel.None))
.UseStartup<Startup>().UseUrls("http://*:5050").Build().Run();
}
}
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.Use(async (ctx, next) =>
{
ctx.Response.Headers["Cache-Control"]
= "no-cache, no-store, must-revalidate";
ctx.Response.Headers["Pragma"] = "no-cache";
ctx.Response.Headers["Expires"] = "-1";
await next();
});
app.UseMvcWithDefaultRoute();
}
}
public class HomeController : Controller
{
private int delayTime = 3 * 1000;
[HttpGet]
public IActionResult Time()
{
Thread.Sleep(delayTime);
return Json(new { Time = DateTime.Now });
}
[HttpGet]
public async Task<IActionResult> AsyncSleepTime()
{
return await Task.Run(() => {
Thread.Sleep(delayTime);
return Json(new { Time = DateTime.Now });
});
}
[HttpGet]
public async Task<IActionResult> AsyncDelayTime()
{
await Task.Delay(delayTime);
return Json(new { Time = DateTime.Now });
}
[HttpGet]
public async Task<IActionResult> AsyncMixedTime()
{
int sleepTime = 1000;
Thread.Sleep(sleepTime);
await Task.Delay(delayTime - sleepTime);
return Json(new { Time = DateTime.Now });
}
}
}
尽管这四个端点在功能上是相同的,但它们实现 3 秒延迟的方式却不同。
- Home/Time - 这是一个同步端点。 3 秒延迟是通过 `Thread.Sleep()` 实现的。
- Home/AsyncSleepTime - 这是一个异步端点,但它仍然使用 `Thread.Sleep()` 来实现延迟。至少有一个线程池中的线程被阻塞。
- Home/AsyncDelayTime - 这是一个真正的异步端点。使用 `Task.Delay()` 来实现延迟,因此线程不会被阻塞。在等待计时器回调时,该线程可用于服务其他请求。
- Home/AsyncMixedTime - 这是一个异步端点。使用 `Thread.Sleep()` 实现 1 秒延迟,使用 `Task.Delay()` 实现 2 秒延迟。
本笔记的目的是使用 `JMeter` 对这些端点进行负载测试。我们将获得一些具体的数据来了解 异步编程 和非阻塞线程的好处。由于我们正在进行性能测试,我在非调试模式下运行了应用程序。当我们启动控制台应用程序时,我们可以在 POSTMAN 中向其中一个端点发出 GET 请求,以验证其功能正常。
https://:5050/Home/AsyncMixedTime
JMeter 测试计划
为了测试 REST 端点的性能,我创建了以下 JMeter 测试计划。
- 我们有来自 `JMeter` 的 300 个线程/用户。所有 300 个用户将在 4 秒内启动。
- 收到响应后,线程/用户将再次发出相同的请求以增加服务器负载。为了不使计算机过载,在每个请求之间会添加一个小的延迟。
- 添加了一个响应断言,以检查每个请求是否收到 HTTP 200 OK。
- 测试持续时间为 1 分钟。
由于我们有四个端点,因此需要手动更改 URL 以测试这 4 个端点中的每一个。
性能测试
在 REST 服务预热后,我们可以使用 `JMeter` 对每个端点进行负载测试。
Home/Time
- 响应时间 - 最小值:4905ms,最大值:26120ms,平均值:14753ms
- 吞吐量 - 18.6/秒
Home/AsyncSleepTime
- 响应时间 - 最小值:5154ms,最大值:18693ms,平均值:10583ms
- 吞吐量 - 26.0/秒
Home/AsyncDelayTime
- 响应时间 - 最小值:3000ms,最大值:3079ms,平均值:3013ms
- 吞吐量 - 93.6/秒
Home/AsyncMixedTime
- 响应时间 - 最小值:3002ms,最大值:12620ms,平均值:4031ms
- 吞吐量 - 70.4/秒
以上结果具有令人信服的一致性。如果您有兴趣,可以进行自己的测试,看看是否能得到类似的比较。您还可以添加并发线程/用户来查看端点何时过载并开始响应错误消息。
结论
根据测试结果,我们可以得出以下结论:
- 异步编程 是否提供任何可见的性能优势?
- 是的,性能优势非常明显。我们可以看到响应时间和吞吐量都有显著的提高。
- 异步编程在 CPU 密集型操作上的效率如何?
- 无效。由于线程被阻塞,异步编程在 CPU 密集型操作上效率不高。
- 异步编程在 I/O 密集型操作上的效率如何?
- 非常有效。由于线程未被阻塞且可由不同请求共享,异步编程在 I/O 密集型操作上非常有效。
- 如果我们有混合 CPU 密集型和 I/O 密集型操作,是否应该使用异步编程?
- 是的,我们应该这样做。尽管某些操作可能会阻塞线程,但其他操作仍然可以利用 异步编程 的优势。
我承认 `Thread.Sleep()` 不是 CPU 密集型操作的精确匹配,`Task.Delay()` 也不是 I/O 密集型操作的精确匹配。但至少,我们可以将结果解释为阻塞线程和非阻塞线程之间的区别。有了以上知识,线程池中的线程数量就变得很重要。以下链接可以帮助我们回答一些更深入的问题:
关注点
- 这是一篇关于 JMeter & Async & Await 的笔记。
- 每台计算机都不同。您在自己的计算机上进行相同测试时可能会看到不同的结果。但我相信您应该看到异步编程优势的相同模式。
- 希望您喜欢我的帖子,这篇笔记能在某种程度上帮助到您。
历史
- 2019年11月6日:首次修订