ASP.NET Core 2.2 API请求体读取超时问题排查求助
问题背景
生产环境偶发错误:
Reading the request body timed out due to data arriving too slowly. See MinRequestBodyDataRate
生产环境同时存在快速与慢速移动网络请求,测试环境通过Jmeter将请求限速至2048字节/秒(该速率本不应触发Kestrel默认240B/s的MinRequestBodyDataRate限制),以最多6个并发请求的中等负载复现了该错误。
异常抛出在自定义TextInputFormatter类的JArray.Load调用处,代码如下:
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding) { var stream = context.HttpContext.Request.Body; using (var reader = context.ReaderFactory(stream, encoding)) using (var jsonReader = new JsonTextReader(reader)) { // Don't close the request stream because you many need it in other middleware or controllers. jsonReader.CloseInput = false; bool isSuccessful = true; List<BaseCommand> model = null; try { Assembly assembly = typeof(BaseCommand).Assembly; // here we're reading and parsing the json array directly from the stream var objCollection = JArray.Load(jsonReader); Log.Information("Json message:{0}", JsonConvert.SerializeObject(objCollection)); model = objCollection .Select((obj) => { // here some code mapping the incoming data to the internal type BaseCommand // ... return mappedCmd; }).ToList(); } catch (Exception ex) { Log.Error(ex, "Error parsing BaseCommand"); isSuccessful = false; } if (isSuccessful) { if (model == null && !context.TreatEmptyInputAsDefaultValue) return InputFormatterResult.NoValueAsync(); else return InputFormatterResult.SuccessAsync(model); } return InputFormatterResult.FailureAsync(); } }
并发表现
当服务器存在5-6个活跃请求时,错误率约为12%;典型JSON payload大小在1kB至20kB之间。取消Jmeter限速后,服务器活跃请求数维持在1左右,无错误产生。
已尝试的无效方案:
- 将
JArray.Load()替换为异步的await JArray.LoadAsync() - 将线程池最小线程数从1调整为10
服务器资源情况:CPU从未超过15%,内存从未超过10%,API托管在基于Alpine Linux镜像的Docker容器中。
问题疑问
请问是否我的代码存在性能问题,还是应限制入站请求数来避免该错误?
分析与解决建议
核心原因分析
- Kestrel速率限制的计算逻辑:
MinRequestBodyDataRate是基于整个请求体读取周期的平均速率,而非瞬时速率。如果服务器线程被占用导致请求体读取被阻塞,即使客户端发送速率达标,平均速率仍可能低于阈值触发超时。 - 自定义Formatter的同步操作阻塞IO线程:即使将
JArray.Load改为异步,后续的Select.ToList()是同步CPU绑定操作,会占用IO线程,导致Kestrel无法及时处理请求体读取,拖慢整体速率。
具体解决步骤
优化自定义InputFormatter的异步处理
把映射逻辑改为异步,避免在IO线程上执行同步CPU操作,确保IO线程尽快释放回线程池:// 替换原有的Select.ToList()逻辑 var tasks = objCollection.Select(async obj => { // 这里改为异步的映射逻辑 // ... return mappedCmd; }); model = await Task.WhenAll(tasks);调整Kestrel请求体速率限制参数
针对慢速网络场景,适当放宽速率限制或延长宽限期,给慢速连接更多缓冲时间:webBuilder.ConfigureKestrel(options => { // 降低最低速率要求,延长宽限期 options.Limits.MinRequestBodyDataRate = new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(10)); });检查Docker容器资源限制
即使主机资源充足,Docker容器可能被限制了CPU核心数或线程数,导致线程池无法扩容处理并发。检查容器的CPU配额设置,适当增加可用核心数。移除日志中的冗余序列化操作
代码中JsonConvert.SerializeObject(objCollection)会对整个JArray重新序列化,带来额外CPU开销,并发时加剧线程占用。建议改为记录请求长度、元素个数等特征信息,而非完整序列化内容。临时缓解:限制入站请求数
如果以上优化无法立即生效,可通过HAProxy或Kestrel的并发限制功能,将活跃请求数控制在4以内,降低并发压力。但这只是临时方案,核心仍需优化代码与配置。
内容的提问来源于stack exchange,提问作者XouDo

