专栏首页林德熙的博客asp dotnet core 记一次应用拒绝响应调试 开启线程等待同步用光线程池

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 创建高性能内存生产者消费者队列

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • win10 uwp 线程池 为什么需要线程池什么是线程池线程池原理应用等待代码完成定时器

    如果大家有开发 WPF 或以前的程序,大概知道线程池不是 UWP 创造的,实际上在很多技术都用到线程池。 为什么需要线程池,他是什么?如何在 UWP 使用线程池...

    林德熙
  • dotnet 多线程禁止同时调用相同的方法 禁止方法重入调用 双检锁的设计

    大家在使用多线程的时候,是否有关注过线程安全的问题。如果咱的代码在使用多线程时,在相同的时间有多个线程同时执行相同的方法,此时也许就存在数据安全的问题,如多个线...

    林德熙
  • C# dotnet 自己实现一个线程同步上下文

    昨天鹏飞哥问了我一个问题,为什么在控制台程序的主线程等待某个线程执行完成之后回来,是在其他线程执行的。而 WPF 在等待某个线程执行完成之后,可以回到主线程执行...

    林德熙
  • Java多线程学习(二)——Thread类的方法使用

    停止线程意味着在线程处理完任务之前停掉正在做的操作,也就是放弃当前操作。有以下三种方法终止正在运行中的线程:

    小森啦啦啦
  • 从使用到原理学习Java线程池

    在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。在Java中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销...

    哲洛不闹
  • Java 多线程编程(“锁”事碎碎念)

    对于多个线程间的共享数据,悲观锁认为自己在使用数据的时候很有可能会有其它线程也刚好前来修改数据,因为在使用数据前都会加上锁,确保在使用过程中数据不会被其它线程修...

    叶志陈
  • Java 多线程编程(聊聊线程池)

    线程是一种昂贵的系统资源,其“昂贵”不仅在于创建线程所需要的资源开销,还在于使用过程中带来的资源消耗。一个系统能够支持同时运行的线程总数受限于该系统所拥有的处理...

    叶志陈
  • 编程体系结构(05):Java多线程并发

    线程是操作系统能够进行运算调度的最小单位,包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程...

    知了一笑
  • 新手一看就懂的线程池

    线程池是帮助我们管理线程的工具,它维护了多个线程,可以降低资源的消耗,提高系统的性能。

    好好学java

扫码关注云+社区

领取腾讯云代金券