【发布时间】:2019-07-10 00:23:15
【问题描述】:
在更改服务器并尝试增加一些数据库密集型任务的工作线程数后,我注意到我的应用程序存在性能问题。
经过一些测试,我发现问题在于从 dataReader 读取数据。在 30 个线程上执行简单查询至少比在单线程上慢 15 倍。使用 PerfView 我发现大部分时间都浪费在了 BLOCKED_TIME。
对于测试,我使用带有 Ryzen Threadripper (32cores/64threads) 的服务器和 SqlServer 的本地实例。在具有相似规格的生产服务器上得到相同的结果。
我已经尝试运行 30 个应用程序实例 - 2-3 个实例和 30 个实例之间的性能几乎没有差异,因此服务器性能足以承载 30 个并行查询。
我尝试了一些连接字符串的更改,例如增加/减少最小/最大池大小、禁用池、将 LCP 更改为 TCP - 没有结果。
class Program
{
static void Main(string[] args)
{
var ids = new List<Guid>() { ... }; //filled by database ids
var stats = new ConcurrentBag<long>();
//warmup
stats.Add(TestMethod());
Console.WriteLine(String.Format("|{0}|{1,5}ms|", "warmup", stats.Average()));
//start 1 to 30 threads (test on server with 32 cores / 64 threads)
for (int i = 1; i <= 30; i++)
{
stats = new ConcurrentBag<long>();
var tasks = Enumerable.Range(0, i).Select(idx =>
{
var id = ids[idx]; // separate ids to be sure we're not reading same records from disk
return Task.Run(() =>
{
for (int j = 0; j < 20; j++)
{
stats.Add(TestMethod(id));
}
});
}).ToArray();
Task.WaitAll(tasks);
Console.WriteLine(String.Format("|{0,2}|{1,5}ms|", i, (int)stats.Average()));
}
Console.WriteLine("End");
Console.ReadLine();
}
private static long TestMethod()
{
var records = new List<object[]>();
var sw = new Stopwatch();
using (var connection = new SqlConnection(ConnectionString))
{
connection.Open();
using (var transaction = connection.BeginTransaction())
using (var command = connection.CreateCommand())
{
command.Transaction = transaction;
command.CommandText = SqlQuery;
command.Parameters.Add(new SqlParameter("id", id));
// measure only dataReader time
sw.Start();
using (var dataReader = command.ExecuteReader())
{
// got ~2000 rows from query
while (dataReader.Read())
{
//read all data from row, test on Guid
var values = new object[6];
dataReader.GetValues(values);
records.Add(values);
}
}
sw.Stop();
}
}
return sw.ElapsedMilliseconds;
}
有什么方法可以提高性能并让我的应用程序可以通过线程数进行扩展?
编辑。 数据库结构和示例查询重现:
/****** Object: Table [dbo].[Table_1] Script Date: 05.07.2019 14:08:15 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[Table_1](
[Id] [uniqueidentifier] NOT NULL,
[Ref1] [uniqueidentifier] NULL,
[Field1] [uniqueidentifier] NULL,
[Field2] [uniqueidentifier] NULL,
CONSTRAINT [PK_Table_1] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
/****** Object: Table [dbo].[Table_2] Script Date: 05.07.2019 14:08:15 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[Table_2](
[Id] [uniqueidentifier] NOT NULL,
[Field1] [uniqueidentifier] NULL,
CONSTRAINT [PK_Table_2] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
/****** Object: Index [IDX_Table_1_Ref1] Script Date: 05.07.2019 14:08:15 ******/
CREATE NONCLUSTERED INDEX [IDX_Table_1_Ref1] ON [dbo].[Table_1]
(
[Ref1] ASC
)
INCLUDE ( [Field1],
[Field2]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = OFF) ON [PRIMARY]
GO
ALTER TABLE [dbo].[Table_1] WITH CHECK ADD CONSTRAINT [FK_Table_1_Table_2] FOREIGN KEY([Ref1])
REFERENCES [dbo].[Table_2] ([Id])
GO
ALTER TABLE [dbo].[Table_1] CHECK CONSTRAINT [FK_Table_1_Table_2]
GO
select
t2.id as Id,
t2.Field1 as Field1,
t1.Id as T1_Id,
t1.Ref1 as T1_T2,
t1.Field1 as T1_Field1,
t1.Field2 as T1_Field2
from dbo.Table_2 t2
join dbo.Table_1 t1 on t1.Ref1 = t2.Id
where t2.id = @id
现在 T1 中有 30 条记录,T1 中有 2000 * 30 条记录,因此每个线程都在具有 30 条记录的同一数据集上工作。用随机 newid() 填充的数据。
编辑2。
我还在案例中比较了这个解决方案 - 30 个单独的进程与 Sql Server 上的 1 个进程和 30 个线程。 30 个单独的进程运行良好 - 它相当于原始执行时间的 150%,而不是 1500%。 大多数差异 - 使用 30 个单独的进程和单线程,我得到约 14 个等待任务和 20k 个批处理请求/秒,使用单进程和 30 个线程,我得到 > 30 个等待任务(主要在网络 I/O 上)和 2k 个批处理请求/秒。
设置
"System.GC.Server": true
解决了我的问题,现在它可以扩展到服务器上的最大可用线程。感谢您的帮助!
【问题讨论】:
-
"增加一些db 密集型 tasks的工作线程数"你很有可能会面对table锁。检查 SSMS 数据库监视器(资源等待)并检查基础设施问题(网络、io)和数据库问题(锁、闩锁)。您还可以启动 SQL Server Profiler 来监控数据库锁,看看是什么阻止了什么。
-
如果你有一个线程瓶颈你的数据库,是什么让你认为多个线程会有所帮助?此外,您可能正在用完线程池线程,因为您正在对它们进行 IO(无论如何这是错误的做法)。最后,在尝试通过创建更多问题来解决问题之前,您可能应该查看 sql、查询计划或索引的效率,或者使用内部插入和更新来解决问题
-
那句老话(我刚编出来的)是“我有一个问题,然后我尝试创建30个线程来解决它们,现在我有更多的问题”
-
并行运行一个错误的查询不会让它运行得更快。它会使它变慢,因为阻塞会增加。即使有一个写得很好的查询,你想并行化什么?服务器的磁盘、RAM、CPU 还是一样的。您的机器的网卡和带宽仍然相同,因此除非您有多个网卡,否则您不会获得任何性能提升
-
TPL 和并行性与查询性能无关。 SQL Server 已经使用并行执行 - 在 Enterprise 中自动执行,在 Standard 中基于查询提示。您发布的代码不相关。 查询是什么样的?它的执行计划是什么?有多少行?
标签: c# .net sql-server task-parallel-library