.net BackgroundService等待任务,无限循环中的延迟会严重泄漏SqlConnection对象

wz3gfoph  于 2022-12-27  发布在  .NET
关注(0)|答案(1)|浏览(435)

我在多个BackgroundServices**[. NET 7]**中运行了类似的代码。在运行几天后 (显然,循环之间的延迟是几分钟(到几小时)),成千上万的悬空SqlConnection句柄 (可能所有使用过的句柄仍然被引用,即使正确地与DB断开连接) 导致大量内存泄漏。

MRE

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Diagnostics;

using Microsoft.Data.SqlClient;

namespace Memleak;

static class Program
{
    const string connectionString = "Server=lpc:localhost;"
        + "Integrated Security=True;Encrypt=False;"
        + "MultipleActiveResultSets=False;Pooling=False;";

    static async Task Main(params string[] args)
    {
        using CancellationTokenSource cts = new();
        // not to forget it running
        cts.CancelAfter(TimeSpan.FromMinutes(15));
        CancellationToken ct = cts.Token;

        using Process process = Process.GetCurrentProcess();
        long loop = 1;

        while (true)
        {
            await ConnectionAsync(ct);

            // this seems to be the issue (delay duration is irrelevant)
            await Task.Delay(TimeSpan.FromMilliseconds(1), ct);

            process.Refresh();
            long workingSet = process.WorkingSet64;
            Console.WriteLine("PID:{0} RUN:{1:N0} RAM:{2:N0}",
                process.Id, loop, workingSet);

            ++loop;
        }
    }

    private static async Task ConnectionAsync(CancellationToken ct = default)
    {
        using SqlConnection connection = new(connectionString);
        await connection.OpenAsync(ct);

        using SqlCommand command = connection.CreateCommand();
        command.CommandText = "select cast(1 as bit);";

        using SqlDataReader reader = await command.ExecuteReaderAsync(ct);
        if (await reader.ReadAsync(ct))
        {
            _ = reader.GetBoolean(0);
        }
    }
}

泄漏

以下命令提示符命令显示泄漏:

// dotnet tool install --global dotnet-dump
dotnet-dump collect -p pid
dotnet-dump analyze dump_name.dmp
dumpheap -type Microsoft.Data.SqlClient.SqlConnection -stat
dumpheap -mt mtid
dumpobj objid
gcroot objid

最后一个命令显示了SqlConnection对象的System.Threading.CancellationTokenSource+CallbackNode的巨大列表。

问题

这是一个bug还是按预期工作 (如果是,为什么)?除了去掉所有async代码并只使用Threads之外,是否有任何简单的变通方法?我不能使用Timers,因为延迟随某些因素而变化 (当工作可用时,延迟较短;当工作不进行时,延迟时间更长)

[更新]非异步版本不会泄漏
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Diagnostics;

using Microsoft.Data.SqlClient;

namespace NotMemleak;

static class Program
{
    const string connectionString = "Server=lpc:localhost;" +
        "Integrated Security=True;Encrypt=False;" +
        "MultipleActiveResultSets=False;Pooling=False;";

    static void Main(params string[] args)
    {
        using CancellationTokenSource cts = new();
        // not to forget it running
        cts.CancelAfter(TimeSpan.FromMinutes(15));
        CancellationToken ct = cts.Token;

        using Process process = Process.GetCurrentProcess();

        long loop = 1;
        while (loop < 1000)
        {
            Connection();

            // this seems to be the issue (delay duration is irrelevant)
            ct.WaitHandle.WaitOne(TimeSpan.FromMilliseconds(1));
            // Thread.Sleep();

            process.Refresh();
            long workingSet = process.WorkingSet64;
            Console.WriteLine("PID:{0} RUN:{1:N0} RAM:{2:N0}"
                , process.Id, loop, workingSet);

            ++loop;
        }

        Console.WriteLine();
        Console.WriteLine("(press any key to exit)");
        Console.ReadKey(true);
    }

    private static void Connection()
    {
        using SqlConnection connection = new(connectionString);
        connection.Open();

        using SqlCommand command = connection.CreateCommand();
        command.CommandText = "select cast(1 as bit);";

        using SqlDataReader reader = command.ExecuteReader();
        if (reader.Read())
        {
            _ = reader.GetBoolean(0);
        }
    }
}
pkmbmrz7

pkmbmrz71#

我相信这是github中的related issue。据我所知-这是SqlClient 5.0.1中引入的一个回归错误。基本上,在这一行:

await reader.ReadAsync(ct)

传递标记,reader将在取消此标记时在内部注册回调函数。但是,此注册未在所有代码路径上正确注销。这反过来会导致您的SqlConnection示例可通过该回调注册从CancellationTokenSource访问(该回调注册引用数据读取器、引用命令、引用连接)。
这个问题在一个预览版本中得到了修复,所以你可以尝试安装5.1.0-preview 2,看看这个问题是否消失了。目前还没有包含这个修复的“生产”版本。

相关问题