当前位置:Java -> 揭秘虚拟线程的性能: 揭开噪音之外的真相
在之前的文章中,你了解了 Java 21 中虚拟线程的 历史、优势和缺陷。另外,你可能已经受到了 Quarkus 的启发,它可以帮助你避免缺陷,同时也了解了 Quarkus 如何不断将虚拟线程集成到尽可能多的 Java 库中。
在这篇文章中,你将学习虚拟线程如何以响应时间、吞吐量和驻留状态大小(RSS)来处理并发应用,对比传统阻塞服务和响应式编程。大多数开发人员,包括你和 IT 运维团队,也想知道虚拟线程是否值得替换现有业务应用程序在高并发工作负载的生产环境中。
我使用 Quarkus 实现了 Todo 应用程序的基准测试,包括命令(阻塞)、响应式(非阻塞)和虚拟线程这三种服务类型。Todo 应用程序通过暴露 REST API 实现了对关系型数据库(如 PostgreSQL)的 CRUD 功能。
看看下面的代码片段,了解每种服务的代码以及 Quarkus 如何使开发人员能够实现 getAll()
方法,以从数据库中的 Todo 实体(表)中检索所有数据。在这个代码仓库中找到解决方案代码。
在 Quarkus 应用程序中,你可以使用 @Blocking
注解或非流返回类型(如 String
、List
)的方法和类。
@GET
public List<Todo> getAll() {
return Todo.listAll(Sort.by("order"));
}
将阻塞应用程序转换为虚拟线程应用程序非常简单。如下面的代码片段所示,你只需要在阻塞服务的 getAll()
方法中添加 @RunOnVirtualThread
注解。
@GET
@RunOnVirtualThread
public List<Todo> getAll() {
return Todo.listAll(Sort.by("order"));
}
对于 Java 开发人员来说,编写响应式应用程序应该是一个巨大的挑战,因为他们需要了解响应式编程模型和继续和事件流处理程序的实现。Quarkus 允许开发人员在同一个类中实现非响应式和响应式应用程序,因为 Quarkus 是建立在 Netty 和 Vert.x 等响应式引擎之上的。要在 Quarkus 中创建异步响应式应用程序,你可以使用 @NonBlocking
注解或在 getAll()
方法中设置返回类型为 Uni 或 Mutiny 中的 Multi。
@GET
public Uni<List<Todo>> getAll() {
return Panache.withTransaction(() -> Todo.findAll(Sort.by("order")).list());
}
基准测试场景
为了使测试结果更加高效和公正,我们遵循了 Techempower 的准则,例如进行多种场景的测试,运行在裸金属上,和在 Kubernetes 上 容器上。
这是三种应用程序(阻塞、响应式和虚拟线程)相同的测试场景,如图 1 所示。
图 1:性能测试架构
在性能测试期间,我们将并发级别从 1200 增加到 4400 请求每秒。如预期的那样,虚拟线程在响应时间和吞吐量方面比传统阻塞服务(工作线程)表现更好。更重要的是,它并不总是表现比响应式服务更优。当并发级别达到 3500 请求每秒时,虚拟线程的速度明显变慢,并且低于工作线程。
图 2:响应时间和吞吐量
当你设计一个并发应用程序,不管是在云部署中还是在其他环境中,你或者你的IT运维团队需要估算资源利用率和容量,以及高可伸缩性。CPU和RSS(驻留集大小)的使用是衡量资源利用率的关键指标。在这种情况下,当并发级别达到每秒2000个请求时,CPU和内存的使用率,虚拟线程迅速比工作线程更多。
图3:资源使用(CPU和RSS)
容器运行时(例如Kubernetes)是在云中运行具有高扩展性、弹性和韧性的并发应用程序不可避免的选择。在受限的容器环境中,虚拟线程的内存使用低于工作线程。
图4:内存使用 - 容器
你了解了虚拟线程在多种环境中的响应时间、吞吐量、资源使用和容器运行时方面的表现。虚拟线程似乎在所有时间内都比工作线程上的阻塞服务更好。但是当你仔细查看性能指标时,你会发现在某些并发级别下,测得的性能低于阻塞服务。另一方面,基于事件循环的响应式服务始终比虚拟和工作线程具有更高的性能。
因此,根据你的并发目标,虚拟线程可以提供足够高的性能和资源效率。当然,相比反应式编程,虚拟线程仍然相对简单,不需要 ste多的学习曲线,可以开发并发应用程序。
推荐阅读: 29.三个线程T1、T2、T3,如何让他们按顺序执行?
本文链接: 揭秘虚拟线程的性能: 揭开噪音之外的真相