我们的 dotnet-core (3.1) 应用程序遇到高负载问题。
超过一定数量的连接(虚拟用户),我们遇到了瓶颈,服务器被饿死,我们得到请求超时,但进程没有崩溃(没有 kestrel 日志)。我们正在使用 K6对我们的应用程序进行基准测试。目前,负载测试仅在登录页面上执行 GET 请求,这会在一个小数据集(无连接等)上触发一个基本的 SQL 请求。
我们使用 Visual Studio 2019 Perfomance Profiler 工具和 perfview 来调查这个问题,但这些工具都没有帮助我们识别导致此瓶颈的代码部分。
我找到了这篇关于线程池饥饿的文章:https://learn.microsoft.com/fr-fr/archive/blogs/vancem/diagnosing-net-core-threadpool-starvation-with-perfview-why-my-service-is-not-saturating-all-cores-or-seems-to-stall 当我们使用任意值调整最小 ThreadPool 时,如后例所示,我们在性能上有了巨大的改进(不在图表上)。这似乎是一个权宜之计,使用它有多糟糕?
System.Threading.ThreadPool.SetMinThreads(200, 200);
解释:2C_2G/100.csv => 2 核,2Go RAM,100 个虚拟用户
环境:
- nginx 作为反向代理
- K6作为基准工具
- dotnet-core 3.1(带有 EntityFramework)
- 操作系统:Ubuntu 20.04
- mariadb 作为数据库
最佳答案
您正在线程池上执行长时间运行的代码。
下面是使用 Task.Run
执行此操作的方法:
public async Task<byte> CalculateChecksumAsync(Stream stream) => await Task.Run(() =>
{
int i;
byte checksum = 0;
while ((i = stream.ReadByte()) >= 0)
{
checksum += (byte)i;
}
return checksum;
});
对于看起来完全异步代码的不经意的观察者来说,因为有
async/await 和 Task
无处不在。
但事实上,只要它需要,它就会占用一个线程池线程 读取流(这不仅取决于通过的数据量,还取决于 流的带宽)。
当线程池被饿死时,会延迟一秒
线程池将产生一个新线程。这意味着随后调用
Task.Run
会让他们的工作延迟那么久
即使您的 CPU 闲置。
备选方案:
- 尽可能使用异步方法而不是同步方法(例如
Stream.ReadAsync
),尤其是在线程池上时 - 为长时间运行的代码生成长时间运行的任务:
public async Task<byte> CalculateChecksumAsync(Stream stream) => await Task.Factory.StartNew(() => { int i; byte checksum = 0; while ((i = stream.ReadByte()) >= 0) { checksum += (byte)i; } return checksum; }, TaskCreationOptions.LongRunning);
TaskCreationOptions.LongRunning
标志告诉 C# 您需要一个新线程
立即为您的工作生成。
关于c# - 解决重负载下的线程池饥饿问题,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/73637676/