You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.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用于调试。

疑问

  1. 程序为何挂起?JsonSerializer是否存在内置的容量上限(如缓冲区大小)导致此问题?
  2. 是否可能因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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.18 06:40:19