Bre*_*ias 6 .net c# sql-server asynchronous dapper
我正在使用负载测试来分析 Dapper 访问 SQL Server 的“棒球场”性能。我的笔记本电脑同时是负载发生器和测试目标。我的笔记本电脑有 2 个内核、16GB 内存,运行的是 Windows 10 Pro, v1709。数据库是在 Docker 容器中运行的 SQL Server 2017(容器的 Hyper-V VM 有 3GB RAM)。我的负载测试和测试代码使用 .net 4.6.1。
我在模拟 10 个并发客户端 15 秒后的负载测试结果如下:
我意识到异步有时比同步代码慢。我也意识到我的测试设置很弱。但是,我不应该从异步代码中看到如此糟糕的性能。
我已将问题缩小到与 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.
Run Code Online (Sandbox Code Playgroud)
在 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;
}
}
Run Code Online (Sandbox Code Playgroud)
我没有明确打开或关闭 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);
}
}
Run Code Online (Sandbox Code Playgroud)
同样,我没有明确打开或关闭连接 - 因为 Dapper 为我做了这些。我还通过显式打开和关闭测试了这段代码;它不会改变性能。针对上述代码(4 TPS)的负载生成器的分析器结果如下:
如果我按如下方式更改上述内容,会改变性能的是什么:
//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;
//}
Run Code Online (Sandbox Code Playgroud)
在这种情况下,我已将 转换SqlConnection
为类的私有成员FitTest
并在构造函数中对其进行初始化。也就是说,每个负载测试会话每个客户端一个 SqlConnection。它永远不会在负载测试期间处理。我还更改了连接字符串以包含“MultipleActiveResultSets=True”,因为现在我开始收到这些错误。
通过这些更改,我的结果变为:每秒 640 多个事务,并抛出 8 个异常。例外都是“InvalidOperationException:BeginExecuteReader 需要一个打开且可用的连接。连接的当前状态是连接。” 在这种情况下,探查器的结果是:
在我看来,这就像 Dapper 中使用 SqlConnection 的同步错误。
我的负载生成器,一个名为 的类Generator
,设计为在构造时给出一个委托列表。每个委托都有一个 FitTest 类的唯一实例。如果我提供一个包含 10 个委托的数组,它被解释为代表 10 个用于并行生成负载的客户端。
为了开始负载测试,我有这个:
//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();
}
Run Code Online (Sandbox Code Playgroud)
设置这么多,实际负载生成如下:
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());
//}
}
Run Code Online (Sandbox Code Playgroud)
注释掉的while
循环 andTask.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++;
}
}
}
Run Code Online (Sandbox Code Playgroud)
当我的代码运行时,我没有收到任何错误或异常。
好的,我做错了什么?在最坏的情况下,我希望异步代码只比同步代码慢一点。
归档时间: |
|
查看次数: |
2313 次 |
最近记录: |