.NET并发处理超大型JSON文件时程序挂起问题排查
问题描述
我的.NET控制台应用在并发接收并反序列化单个字符数超1200万的超大型JSON文件时出现挂起,最终被Polly的熔断机制或超时强制终止。
核心代码实现
并发处理逻辑
using System.Text.Json; Parallel.For(0, 1000000, async (i, state) => { var strategy = GetPollyResilienceStrategy(); await strategy.ExecuteAsync(async () => { var stream = await httpClient.GetStreamAsync( GetEndPoint(i), cancellationToken); var foo = await JsonSerializer.DeserializeAsync<Foo>( stream, cancellationToken: cancellationToken); // 处理逻辑包含CPU密集和IO操作,可能涉及额外API调用 var processedFoo = Process(foo); queue.Add(processedFoo); }); });
Polly弹性策略
AsyncPolicyWrap GetPollyResilienceStrategy() { var retry = Policy .Handle<Exception>() .WaitAndRetryAsync(Backoff.DecorrelatedJitterBackoffV2( retryCount: 3, medianFirstRetryDelay: TimeSpan.FromMinutes(1))); var timeout = Policy.TimeoutAsync(timeout: TimeSpan.FromMinutes(5)); var circuitBreaker = Policy .Handle<Exception>() .AdvancedCircuitBreakerAsync( failureThreshold: 0.5, samplingDuration: TimeSpan.FromMinutes(10), minimumThroughput: 2, durationOfBreak: TimeSpan.FromMinutes(1)); var strategy = Policy.WrapAsync(retry, timeout, circuitBreaker); return strategy; }
后台队列处理
var queue = new BlockingCollection<Foo>(); var listner = Task.Factory.StartNew(() => { while (true) { Foo foo; try { foo = queue.Take(cancellationToken); } catch (OperationCanceledException) { break; } LongRunningCpuBoundTask(foo); SerializeToCsv(foo); } }, creationOptions: TaskCreationOptions.LongRunning);
运行背景
应用预期稳定运行数日,但数小时后出现挂起。调试发现越来越多线程卡在JSON反序列化环节,当约5个线程卡住时,Parallel.For不再启动新任务,且每次卡住的任务索引i不同。由于JSON文件过大,无法留存所有接收的JSON用于调试。
疑问
- 程序为何挂起?
JsonSerializer是否存在内置的容量上限(如缓冲区大小)导致此问题? - 是否可能因
GetStreamAsync读取了损坏数据,导致JsonSerializer在反序列化损坏JSON时陷入异常分支而卡住?
解答
问题1:程序挂起的原因及JsonSerializer的容量限制
- 挂起核心原因并非
JsonSerializer的内置容量上限,而是大概率和.NET运行时的已知bug相关:System.Text.Json在处理超大JSON时,特定场景下会进入无限循环或极端耗时的处理逻辑,导致线程被永久占用。 - 此外,
Parallel.For依赖.NET线程池调度任务,当多个线程卡在反序列化操作时,线程池可用线程被耗尽,无法启动新任务,最终导致整个应用陷入挂起状态。 - 补充:你的Polly超时策略可能未有效终止卡住的操作——如果
JsonSerializer.DeserializeAsync内部存在同步阻塞逻辑(即使方法带Async后缀),Polly基于任务取消的超时机制无法强制中断卡住的线程,进一步加剧线程池资源耗尽的问题。
问题2:损坏数据是否会导致反序列化卡住
- 可能性极低。损坏的JSON通常会直接抛出
JsonException,触发Polly的重试或熔断逻辑,而非卡住线程。你遇到的线程挂起现象,更符合运行时bug中描述的无限循环场景,而非损坏数据导致的异常分支阻塞。
可行解决方案(除升级.NET版本外)
- 替换序列化库:改用Newtonsoft.Json(Json.NET)处理超大JSON,它在大文件序列化/反序列化场景下的稳定性更成熟,无此类已知无限循环问题。
- 流式反序列化优化:使用
System.Text.Json.Utf8JsonReader进行逐节点流式读取,仅提取业务所需字段,避免一次性加载整个超大JSON到内存,同时降低触发bug的概率。 - 修复Polly超时有效性:使用Polly的
TimeoutStrategy.Pessimistic(悲观超时),它会在超时后强制中断线程(需注意线程安全);或把反序列化逻辑封装到独立Task中,配合CancellationToken的注册逻辑强制终止操作。 - 控制并发度:手动设置
Parallel.For的MaxDegreeOfParallelism(如根据CPU核心数设为8-16),避免线程池瞬间被大量并发任务占满,降低线程耗尽的风险。 - 线程状态监控:在应用中加入线程堆栈监控(如使用
System.Diagnostics),记录卡住线程的调用栈,确认是否为JsonSerializer内部的无限循环逻辑。
内容的提问来源于stack exchange,提问作者Dr. Strangelove
相关产品推荐
相关产品推荐

