AKS中ASP.NET Web API端点挂起但CPU内存未过载的原因排查
问题背景与故障现象
我有一个ASP.NET Web API解决方案,包含用于检索和过滤记录的简单端点。为卸载数据处理工作,添加了BackgroundService在后台持续处理记录,本地运行正常,但部署到AKS后存活/就绪检查失败、服务端点挂起。计划单独部署数据处理服务,但想搞清楚故障原因:
- 猜测是线程过多,但K9S显示CPU、内存均未过载,是否存在线程过多但资源未过载的情况?
- 或是其他潜在问题?
测试场景:创建50个Service Bus客户端及处理器对应50个队列,通过测试程序发送大量消息压测时出现问题。
后台服务核心代码
// 初始化处理器部分 { _services = services; _auditWriter = auditWriter; foreach (var queueName in QueueConfiguration.QueueNames) { var serviceBusClient = new ServiceBusClient("<connectionString>"); _clients.Add(serviceBusClient); var processorOptions = new ServiceBusProcessorOptions { MaxConcurrentCalls = 20, PrefetchCount = 20, ReceiveMode = ServiceBusReceiveMode.ReceiveAndDelete, }; var processor = serviceBusClient.CreateProcessor(queueName, processorOptions); processor.ProcessMessageAsync += MessageHandler; processor.ProcessErrorAsync += ErrorHandler; processor.StartProcessingAsync(); _queueProcessors.Add(processor); } } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { // Through trial and error you need to await in the ExecuteAsync await Task.Delay(1, stoppingToken); stoppingToken.Register(async () => { foreach (var processor in _queueProcessors) { await processor.DisposeAsync(); } }); try { await Task.Delay(Timeout.Infinite, stoppingToken); } catch (OperationCanceledException) { // Expected; no action needed } return; } private static async Task MessageHandler(ProcessMessageEventArgs args) { try { var data = buildData(); // business logic try { using (var sqlConnection = new SqlConnection(_sqlConnectionString)) //removed connection string for post { _auditWriter.WriteAuditData(data, sqlConnection); } } catch (Exception e) { Console.WriteLine("There was an exception while saving the data to the database"); } using (var scope = _services.CreateScope()) { // use scoped variable to write files to data store } } catch (Exception e) { Console.WriteLine($"Issue processing message for queue {args.EntityPath} | {e}"); } }
Pod资源使用情况
就绪检查失败后,Pod的CPU使用率极低,内存仅占用34%:
故障原因分析与解决方案
1. 线程池耗尽(线程过多但资源未过载的可能)
这种情况确实存在:
- 每个ServiceBusProcessor配置
MaxConcurrentCalls=20,50个队列对应1000个并发消息处理任务。 - 如果
buildData()、数据库操作或文件存储是同步阻塞调用(比如WriteAuditData是同步方法,或SqlConnection操作未用异步API),会占用线程池线程且无法释放。线程池被占满后,ASP.NET Core的请求处理(包括存活/就绪检查)因拿不到线程挂起,此时CPU因大量线程处于阻塞等待状态(如等数据库响应、IO)而使用率不高。
2. ServiceBusClient实例滥用
代码中为每个队列创建独立的ServiceBusClient是反模式:
- ServiceBusClient是线程安全、可复用的重量级对象,推荐全局单例使用。创建50个实例会导致过多TCP连接、资源句柄占用,触发操作系统句柄限制,使进程无法创建新资源(如HTTP请求Socket),进而导致就绪检查失败。
3. 资源泄漏与优雅终止问题
stoppingToken.Register中使用异步委托错误:Register不等待异步操作完成,处理器的DisposeAsync可能在进程终止时未完成,导致资源泄漏。MessageHandler中若CreateScope后的操作存在未捕获异常或同步阻塞,也会持续占用线程池线程。
修复建议
- 复用ServiceBusClient:全局仅创建一个ServiceBusClient实例,所有处理器共享。
- 全异步化IO操作:检查
buildData()、WriteAuditData、文件存储操作,全部使用异步API并await,避免阻塞线程池线程。 - 降低并发数:将
MaxConcurrentCalls调低(比如从20降到5),减少总并发线程数,避免线程池耗尽。 - 修复优雅终止逻辑:在
ExecuteAsync中等待处理器停止,替代Register:protected override async Task ExecuteAsync(CancellationToken stoppingToken) { try { await Task.Delay(Timeout.Infinite, stoppingToken); } catch (OperationCanceledException) { // 等待所有处理器停止 var stopTasks = _queueProcessors.Select(p => p.StopProcessingAsync()); await Task.WhenAll(stopTasks); // 释放资源 foreach (var processor in _queueProcessors) { await processor.DisposeAsync(); } foreach (var client in _clients) { await client.DisposeAsync(); } } } - 检查数据库连接池:确保SqlConnection使用正确异步API,且连接池配置合理(如最大连接数),避免数据库连接耗尽导致线程阻塞。
内容的提问来源于stack exchange,提问作者Jordan Dantas
相关产品推荐
相关产品推荐

