如何解决 ASP.NET Core on Linux (Kubernetes) 中的线程饥饿问题?

How to troubleshoot thread starvation in ASP.NET Core on Linux (Kubernetes)?

我是 运行 ASP.NET 核心 API Linux,在 Google 云中的 Kubernetes 上。

这是一个 API 高负载,并且在每次请求时它都在执行一个长时间(1-5 秒)、CPU 密集型操作的库。

我看到的是,在部署后 API 可以正常工作一段时间,但在 10-20 分钟后它变得没有响应,甚至健康检查端点(只是 returns 硬编码200 OK) 停止工作并超时。 (这让 Kubernetes 杀死了 pods。)

有时我还会在日志中看到臭名昭著的 Heartbeat took longer than "00:00:01" 错误消息。

谷歌搜索这些现象让我指向 "Thread starvation",因此启动了太多线程池线程,或者太多线程阻塞等待某事,以至于池中没有剩余线程可以接收 ASP.NET 核心请求(因此即使是健康检查端点也会超时)。

解决此问题的最佳方法是什么?我开始监视 ThreadPool.GetMaxThreadsThreadPool.GetAvailableThreads 返回的数字,但它们保持不变(完成端口始终是 1000,对于最大和可用,工作人员始终是 32767 ).
还有其他 属性 我应该监控的吗?

一般来说,长时间的运行 工作是 Web 应用程序的诅咒。您需要亚秒级的响应时间来获得健康的 Web 应用程序。如果您需要做的工作是同步的或 CPU-bound,则尤其如此。异步至少可以在此过程中释放线程,但是由于 CPU-bound 工作,线程被束缚了。

您应该将您正在做的任何事情卸载到不同的进程,然后监控进度。对于 API,这里的典型方法是将工作安排在不同的进程上,然后立即 return 一个 202 Accepted,在响应正文中有一个端点,客户端可以利用它来监视progress/get 最终完成的结果。您还可以实现一个 webhook,客户端可以注册以接收该过程已完成的通知,而不必经常检查它。

您唯一的选择是投入更多资源解决问题。例如,您可以在负载均衡器后面暂存多个实例,在每个实例之间分配请求以减少每个实例的总体负载。

您的代码中也完全有可能存在一些效率低下或问题,可以通过纠正这些问题来减少进程所花费的时间 and/or 消耗的资源。举一个简单的例子,假设您正在使用 Task.Run 之类的东西,您可能会通过 而不是 这样做来释放大量线程。 Task.Run 几乎不应该在 Web 应用程序的上下文中使用。但是,您还没有发布任何代码,因此无法在那里为您提供准确的指导。

您确定您的 ASP.NET 核心 Web 应用程序 运行 正在耗尽线程吗?它可能只是使所有可用的 pod 资源饱和,导致 Kubernetes 杀死 pod 本身,因此您的网络应用程序。

我在 OpenShift 环境中 ASP.NET Core web API 运行ning 在 Linux RedHat 上确实遇到了非常相似的情况,这也支持 Kubernetes 中的 pod 概念:一次调用大约需要 1 秒才能完成,在大工作负载下,它首先变慢然后无响应,导致 OpenShift 关闭 pod,因此我的网络应用程序也是如此。

可能是您的 ASP.NET 核心 Web 应用程序未 运行 线程不足,尤其是考虑到 ThreadPool 中可用的大量工作线程。 相反,活动线程的数量及其 CPU 需求与它们 运行 所在的 Pod 中可用的实际毫核相比可能太大了:实际上,在创建之后,这些活动线程太多了许多可用 CPU,其中大多数最终被调度程序排队并等待执行,而实际上只有一小部分 运行。 然后调度程序完成它的工作,确保 CPU 在线程之间公平共享,通过频繁切换那些将使用它的线程。 至于你的情况,线程需要大量和长时间的 CPU 绑定操作,随着时间的推移资源会饱和并且网络应用程序变得无响应。

一个缓解措施可能是为您的 pods 提供更多容量,尤其是毫核,或者增加 pods Kubernetes 的数量,可以根据需要部署。 但是,在我的特定情况下,这种方法并没有多大帮助。 相反,通过将一个请求的执行时间从 1 秒减少到 300 毫秒来改进 API 本身,显着提高了 Web 应用程序的整体性能并真正解决了问题。

例如,如果您的库在多个请求中执行相同的计算,您可以考虑在您的数据结构上引入缓存,以略微占用内存(这对我有用)来提高速度,尤其是如果您的操作主要是 CPU 绑定,如果您对您的网络应用程序有这样的请求需求。 如果这对您的 API 的工作量和响应有意义,您也可以考虑启用 cache response in ASP.NET Core。 使用缓存,您可以确保您的 Web 应用程序不会执行相同的任务两次,从而释放 CPU 并降低线程排队的风险。

更快地处理每个请求将使您的 Web 应用程序更不容易填满可用资源的风险 CPU,从而降低太多线程排队等待执行的风险。