为什么 (Dapper) 异步 IO 非常慢?

Why is (Dapper) async IO extremely slow?

提问人:Brent Arias 提问时间:7/25/2018 最后编辑:Brent Arias 更新时间:7/25/2018 访问量:3970

问:

我正在使用负载测试来分析 Dapper 访问 SQL Server 的“大致”性能。我的笔记本电脑既是负载生成器,又是测试目标。我的笔记本电脑有 2 个内核、16GB RAM,并且运行的是 Windows 10 专业版 v1709。数据库是在 Docker 容器中运行的 SQL Server 2017(容器的 Hyper-V VM 具有 3GB RAM)。我的负载测试和测试代码使用的是 .net 4.6.1。

模拟 10 个并发客户端 15 秒后的负载测试结果如下:

  • 同步 Dapper 代码:每秒 750+ 个事务。
  • 异步 Dapper 代码:每秒 4 到 8 个事务。哎呀!

我意识到异步有时可能比同步代码慢。我还意识到我的测试设置很弱。但是,我不应该从异步代码中看到如此糟糕的性能。

我已将问题缩小到与 Dapper 和 .我需要帮助才能最终解决这个问题。探查器结果如下。System.Data.SqlClient.SqlConnection

我想出了一种俗气的方法来强制我的异步代码实现每秒 650+ 个事务,我稍后会讨论,但现在首先是时候展示我的代码了,它只是一个控制台应用程序。我有一个测试类:

public class FitTest
{
    private List<ItemRequest> items;
    public FitTest()
    {
        //Parameters used for the Dapper call to the stored procedure.
        items = new List<ItemRequest> {
            new ItemRequest { SKU = "0010015488000060", ReqQty = 2 } ,
            new ItemRequest { SKU = "0010015491000060", ReqQty = 1 }
            };
    }
... //the rest not listed.

同步测试目标

在 FitTest 类中,在负载下,以下测试目标方法每秒实现 750+ 个事务:

public Task LoadDB()
{
    var skus = items.Select(x => x.SKU);
    string procedureName = "GetWebInvBySkuList";
    string userDefinedTable = "[dbo].[StringList]";
    string connectionString = "Data Source=localhost;Initial Catalog=Web_Inventory;Integrated Security=False;User ID=sa;Password=1Secure*Password1;Connect Timeout=30;Encrypt=False;TrustServerCertificate=True;ApplicationIntent=ReadWrite;MultiSubnetFailover=False";

    var dt = new DataTable();
    dt.Columns.Add("Id", typeof(string));
    foreach (var sku in skus)
    {
        dt.Rows.Add(sku);
    }

    using (var conn = new SqlConnection(connectionString))
    {
        var inv = conn.Query<Inventory>(
            procedureName,
            new { skuList = dt.AsTableValuedParameter(userDefinedTable) },
            commandType: CommandType.StoredProcedure);

        return Task.CompletedTask;
    }
}

我没有显式打开或关闭 SqlConnection。我知道 Dapper 为我做到了这一点。此外,上述代码返回 a 的唯一原因是因为我的加载生成代码旨在使用该签名。Task

异步测试目标

我的 FitTest 类中的另一个测试目标方法是这样的:

public async Task<IEnumerable<Inventory>> LoadDBAsync()
{
    var skus = items.Select(x => x.SKU);
    string procedureName = "GetWebInvBySkuList";
    string userDefinedTable = "[dbo].[StringList]";
    string connectionString = "Data Source=localhost;Initial Catalog=Web_Inventory;Integrated Security=False;User ID=sa;Password=1Secure*Password1;Connect Timeout=30;Encrypt=False;TrustServerCertificate=True;ApplicationIntent=ReadWrite;MultiSubnetFailover=False";

    var dt = new DataTable();
    dt.Columns.Add("Id", typeof(string));
    foreach (var sku in skus)
    {
        dt.Rows.Add(sku);
    }

    using (var conn = new SqlConnection(connectionString))
    {
        return await conn.QueryAsync<Inventory>(
            procedureName,
            new { skuList = dt.AsTableValuedParameter(userDefinedTable) },
            commandType: CommandType.StoredProcedure).ConfigureAwait(false);
    }

}

同样,我没有明确打开或关闭连接 - 因为 Dapper 为我做了这件事。我还通过显式打开和关闭来测试此代码;它不会改变性能。针对上述代码 (4 TPS) 执行操作的负载生成器的分析器结果如下:

async with "using" statement

如果我按如下方式更改上述内容,则确实会改变性能:

//using (var conn = new SqlConnection(connectionString))
//{
    var inv = await conn.QueryAsync<Inventory>(
        procedureName,
        new { skuList = dt.AsTableValuedParameter(userDefinedTable) },
        commandType: CommandType.StoredProcedure);
    var foo = inv.ToArray();
    return inv;
//}

在本例中,我已将 转换为类的私有成员,并在构造函数中对其进行初始化。也就是说,每个客户端每个负载测试会话一个 SqlConnection。在负载测试期间,它永远不会被处置。我还更改了连接字符串以包含“MultipleActiveResultSets=True”,因为现在我开始收到这些错误。SqlConnectionFitTest

通过这些更改,我的结果变成了:每秒 640+ 个事务,并抛出 8 个异常。例外情况都是“InvalidOperationException:BeginExecuteReader 需要打开且可用的连接。连接的当前状态为正在连接。探查器在以下情况下会生成:

enter image description here

在我看来,这看起来像是 Dapper 与 SqlConnection 的同步错误。

负载生成器

我的负载生成器,一个名为 的类,被设计为在构造时被赋予一个委托列表。每个委托都有一个唯一的 FitTest 类实例化。如果我提供一个包含 10 个委托的数组,则它被解释为表示 10 个客户端,用于并行生成负载。Generator

为了启动负载测试,我有这个:

//This `testFuncs` array (indirectly) points to either instances
//of the synchronous test-target, or the async test-target, depending
//on what I'm measuring.
private Func<Task>[] testFuncs;
private Dictionary<int, Task> map;
private TaskCompletionSource<bool> completionSource;

public void RunWithMultipleClients()
{
    completionSource = new TaskCompletionSource<bool>();

    //Create a dictionary that has indexes and Task completion status info.
    //The indexes correspond to the testFuncs[] array (viz. the test clients).
    map = testFuncs
        .Select((f, j) => new KeyValuePair<int, Task>(j, Task.CompletedTask))
        .ToDictionary(p => p.Key, v => v.Value);

    //scenario.Duration is usually '15'. In other words, this test
    //will terminate after generating load for 15 seconds.
    Task.Delay(scenario.Duration * 1000).ContinueWith(x => {
        running = false;
        completionSource.SetResult(true);
    });

    RunWithMultipleClientsLoop();
    completionSource.Task.Wait();
}

对于设置来说,实际负载的生成如下:

public void RunWithMultipleClientsLoop()
{
    //while (running)
    //{
        var idleList = map.Where(x => x.Value.IsCompleted).Select(k => k.Key).ToArray();
        foreach (var client in idleList)
        {
            //I've both of the following.  The `Task.Run` version
            //is about 20% faster for the synchronous test target.
            map[client] = Task.Run(testFuncs[client]); 
            //map[client] = testFuncs[client]();
        }
        Task.WhenAny(map.Values.ToArray())
            .ContinueWith(x => { if (running) RunWithMultipleClientsLoop(); });
    //    Task.WaitAny(map.Values.ToArray());
    //}
}

循环 和 ,注释掉,表示一种不同的方法,具有几乎相同的性能;我把它放在身边做实验。whileTask.WaitAny

最后一个细节。我传入的每个“客户端”委托首先都包装在一个指标捕获函数中。指标捕获函数如下所示:

private async Task LoadLogic(Func<Task> testCode)
{
    try
    {
        if (!running)
        {
            slagCount++;
            return;
        }

        //This is where the actual test target method 
        //is called.
        await testCode().ConfigureAwait(false);

        if (running)
        {
            successCount++;
        }
        else
        {
            slagCount++;
        }
    }
    catch (Exception ex)
    {
        if (ex.Message.Contains("Assert"))
        {
            errorCount++;
        }
        else
        {
            exceptionCount++;
        }
    }
}

当我的代码运行时,我没有收到任何错误或异常。

好吧,我做错了什么?在最坏的情况下,我预计异步代码只会比同步代码慢一点。

C# .NET SQL-Server 异步 dapper

评论

0赞 mjwills 7/25/2018
在快速代码中,如果更改为计时会发生什么?CommandType.StoredProcedure);CommandType.StoredProcedure).ToList();
0赞 Brent Arias 7/25/2018
@mjwills 加上,我每秒可以得到 850+ 笔交易。我想我的笔记本电脑今天更快了。但是我刚才又尝试了异步,得到了 3 TPS。:(ToList()
0赞 Joe Phillips 7/25/2018
分析它,看看什么很慢
0赞 Brent Arias 7/25/2018
@JoePhillips 更新了带有分析结果的问题。
0赞 Joe Phillips 7/25/2018
如果我没看错的话,这是您有效运行的 QueryAsync 代码:github.com/StackExchange/Dapper/blob/...

答: 暂无答案