asp dotnet core 记一次应用拒绝响应调试 开启线程等待同步用光线程池
我有一个上古的库,我使用这个库用来上报日志,而刚才日志服务挂了。然后我就发现了我的应用拒绝响应了,通过 VisualStudio 断点调试可以发现线程池的线程全部被占用了。因为没有可用线程因此所有对 asp dotnet core 应用的访问全部都不会收到响应,为什么我的另一个应用日志服务挂了会让我的业务应用拒绝响应?为什么我的业务应用会使用线程池所有的线程,为什么线程池的所有线程被占用将会让应用拒绝响应
很好复现这个坑,在开始复现之前,需要聊一下背景
我有一个业务应用和一个日志服务,基本上可以认为日志服务和业务没有任何关联,而且我从上层业务调用可以看到,都是异步使用。而在日志服务全部挂掉的时候,开始业务应用还能使用,但是当请求大概访问了 100 次,就发现后续的访问都没有任何返回。同时在业务应用的本机控制台和日志文件里面都没有任何记录,而控制台也没有收到 50x 等错误,也就是业务应用还在工作,但是没有任何响应
我在本地上可以复现,使用 VisualStudio 开启所有异常,也什么都没收到。在应用配置文件 appsettings.json 文件里面将日志配置设置为 Debug 也没有拿到任何有用的信息
原本每次的请求都会在默认的 asp dotnet core 日志输出至少一条日志,但是此时什么日志都没有输出
而此时的业务应用的 cpu 和内存占用都很少,在没有请求的时候,可以看到 cpu 几乎没有占用
在点击 VisualStudio 暂停的时候,可以看到业务应用创建了大量的线程
其实调试到线程的时候,大概半个下午了,哈哈
其实我不知道如果一个 asp dotnet core 应用对所有的请求都没有返回,也没有报错的时候可以如何调试
在看到有大量的线程被创建的时候,此时可以调试的是打开 调试->窗口->并行堆栈 这个工具可以辅助调查所有线程问题
如果一个应用创建了大量线程,如果这些线程都是通过 Task.Run 创建,那么意味着线程池里面的线程全部都使用了。也就是此时的下一次调用 Task.Run 需要等待线程池重新分配或创建线程。如果线程池没有空闲的可以分配需要等待一段时间才能创建新的线程,于是此时的应用就会卡住没有返回值
而根据 Eleven 老师的 asp dotnet core 源代码分析课程可以了解到,在 asp dotnet core 服务主机里面的线程是主线程固定的,但是调用到对应的控制器需要通过线程池调度。当然更多细节还请小伙伴关注 Eleven 老师的社区
在用光线程池的线程,此时的请求可以被主机处理,因此不会抛出远程服务器拒绝请求。但是主机通过线程池调度到对应控制器,因为线程池没有足够的线程,因此将会进入很长的等待。特别是有后续请求,那么将需要不断排队。这就是为什么我看到的业务应用拒绝服务
进一步的调试可以通过并行堆栈找到最多相同的堆栈,也就是有多少线程都在相同的堆栈里,那么证明这部分逻辑有锅
我在调试中看到如下代码
我的底层库给我的方法是异步的上报日志方法,但是这个日志上报方法的核心是通过 Task.Run 一个线程进行同步调用
其实在 asp dotnet core 的性能优化中,要尽量不使用 Task.Run 方法,在 ASP.NET Core Performance Best Practices 官方文档 和译文 ASP.NET Core 性能优化最佳实践 - Newbe36524 - 博客园 都有提到,原因还请小伙伴看这两篇博客
那么为什么上面的代码将会让线程池的线程都在等待?原因是 GetResponse 是一句同步的代码,同步的代码等待网络的返回,而此时我的日志服务大概写了如下代码
[HttpGet]
[Route("/")]
public async Task<IActionResult> Get()
{
await Task.Task;
return Ok();
}
private static readonly TaskCompletionSource<bool> Task = new TaskCompletionSource<bool>();
这是我写出的最简单的日志服务的代码,这个代码的所有请求都会进入到 await Task.Task;
等待一个不会返回的任务,也就是任何的请求进来只能等待超时
而刚好上面业务应用的等待是没有设置超时的,在同步的调用等待一个不会返回的请求,此时的线程就被占用了
如果业务应用对每次请求都需要进行如上面的从线程池获取线程然后进行同步访问,那么线程池的将会被用光。在线程池的线程都被占用的时候,下次调用 Task.Run 就会先等待一段时间,如果等待一段时间还没有线程可以调度,那么此时才会在线程池新建线程
所以应用如果拒绝响应,首先需要调查应用是否用光了线程池,然后再调查连接数。如果是线程池用光,那么打开并行堆栈,看线程最多的堆栈是什么,然后通过堆栈和源代码可以找到是否存在锁或者调用 IO 同步
如果发现这个的 asp dotnet core 应用的性能不足,因为线程开启过多,那么此时可以全局找 Task.Run 的代码,尽可能干掉这部分逻辑
而本文的坑,可以使用将同步修改为异步的方法解决,换句话说,不需要通过线程池开启线程的方法,通过IO自带的异步方法进行异步IO请求。此时在 IO 的异步里面将会自动出让 CPU 执行,这部分是硬件的支持,因此进入异步的 IO 将不会占用线程,线程可以回到线程池给其他业务调用
一个可选的方法是将一些不重要但是需要慢慢执行的任务放在生产者消费者队列里面,如果这部分任务很小,可以尝试使用我的 AsyncQueue 高性能低资源占用的类,详细请看 dotnet 使用 AsyncQueue 创建高性能内存生产者消费者队列
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- Python保留字总结
- Python进阶 | 五分钟带你弄懂迭代器与生成器,夯实代码能力
- [Go]GO语言实战项目-gin框架上传图片文件
- [Go] Golang练习项目-GO语言实现选择排序
- 设计模式~策略模式
- Java单元测试框架(一)——JUnit4
- Java单元测试框架(二)——JUnit5
- 绘制双坐标轴图
- 用箭头和文字来标记重要的点
- 32.Python字符串方法split
- 程序员过关斩将--解决分布式session问题
- 常见的C编程段错误及对策
- Python 内置函数之——zip()
- js 将数据保存到本地
- MySQL遇见SELECT list is not in GROUP BY clause and contains nonaggre的问题