我在 Linux 上运行一个 ASP.NET Core API,在 Google Cloud 中的 Kubernetes 上运行。
这是一个高负载的 API,在每个请求上,它都会执行一个库,执行长时间(1-5 秒)的 CPU 密集型操作。
我看到的是,部署后 API 可以正常工作一段时间,但在 10-20 分钟后它变得无响应,甚至运行状况检查端点(仅返回硬编码200 OK
)也停止工作并超时。(这使得 Kubernetes 杀死了 pod。
有时我还会在日志中看到臭名昭著的Heartbeat took longer than "00:00:01"
错误消息。
谷歌搜索这些现象将我指向"线程饥饿",因此启动了太多线程池线程,或者太多线程阻塞了等待某些内容,因此池中没有线程可以拾取 ASP.NET Core请求(因此甚至运行状况检查端点超时)。
解决此问题的最佳方法是什么?我开始监视ThreadPool.GetMaxThreads
和ThreadPool.GetAvailableThreads
返回的数字,但它们保持不变(完成端口始终1000
max 和可用,并且工作线程始终32767
)。
还有其他我应该监控的属性吗?
一般来说,长时间运行的工作对于 Web 应用程序来说是令人厌恶的。你需要正常运行的 Web 应用的亚秒级响应时间。如果您需要执行的工作是同步的或 CPU 密集型的,则尤其如此。异步至少可以在过程中释放线程,但是对于 CPU 密集型工作,线程是
捆绑的。您应该将正在执行的操作卸载到其他流程,然后监视进度。对于 API,这里的典型方法是将工作安排在不同的进程上,然后立即返回 202 Accepted,并在响应正文中有一个终结点,客户端可以利用该终结点来监视进度/获取最终完成的结果。您还可以实现一个 webhook,客户端可以注册该钩子以接收进程已完成的通知,而无需不断检查它。
您唯一的其他选择是投入更多资源来解决问题。例如,您可以在负载均衡器后面暂存多个实例,在每个实例之间分配请求以减少每个实例的总体负载。
也完全有可能代码中存在一些效率低下或问题,可以纠正这些问题,以减少流程花费的时间和/或消耗的资源。举个简单的例子,假设你正在使用类似Task.Run
的东西,如果不这样做,你可能会释放大量的线程。Task.Run
几乎不应该在 Web 应用程序的上下文中使用。但是,您尚未发布任何代码,因此无法在那里为您提供确切的指导。
你确定你的 ASP.NET Core Web 应用线程用完了吗?可能它只是使所有可用的 pod 资源饱和,导致 Kubernetes 只是杀死 pod 本身,因此您的 Web 应用程序。
我确实遇到过一个非常类似的场景,在 OpenShift 环境中的 Linux RedHat 上运行 ASP.NET Core Web API,它也支持 pod 概念,就像在 Kubernetes 中一样:一个调用大约需要 1 秒才能完成,在大工作负载下,它首先变慢,然后没有响应,导致 OpenShift 杀死 pod, 所以我的网络应用程序也是如此。
这可能是您的 ASP.NET 核心 Web 应用程序没有耗尽线程,特别是考虑到 ThreadPool 中可用的大量工作线程。 相反,与运行它们的 Pod 中可用的实际毫核相比,活动线程的数量及其 CPU 需求可能太大了:事实上,在创建后,这些活动线程对于可用的 CPU 来说太多了,以至于它们中的大多数最终被调度程序排队并等待执行, 而只有一堆会真正运行。 然后,调度程序完成其工作,通过频繁切换使用它的线程,确保 CPU 在线程之间公平共享。 至于您的情况,线程需要繁重且长时间的 CPU 密集型操作,随着时间的推移,资源会饱和,Web 应用程序变得无响应。
缓解步骤可能是为您的 Pod 提供更多容量,尤其是毫核,或者增加 Kubernetes 可以根据需要部署的 Pod 数量。 但是,在我的特定情况下,这种方法并没有多大帮助。 相反,通过将一个请求的执行从 1 秒减少到 300 毫秒来改进 API 本身,明智地提高了整体 Web 应用程序性能并实际解决了这个问题。
例如,如果您的库在多个请求中执行相同的计算,您可以考虑在数据结构上引入缓存,以便以轻微的内存成本(这对我有用)来提高速度,特别是如果您的操作主要受 CPU 限制,并且您对 Web 应用程序有这样的请求需求。 您也可以考虑在 ASP.NET Core 中启用缓存响应,如果这对 API 的工作负载和响应有意义。 使用缓存,可以确保 Web 应用不会执行相同的任务两次,从而释放 CPU 并降低排队线程的风险。
更快地处理每个请求将使 Web 应用不易出现填满可用 CPU 的风险,从而降低过多线程排队等待执行的风险。