Проблема производительности при использовании нескольких параллельных запросов - SqlClient - PullRequest
2 голосов
/ 05 июля 2019

Я заметил проблему с производительностью моего приложения после смены сервера и попытки увеличить количество рабочих потоков для некоторых задач с интенсивным использованием базы данных.

После некоторых тестов я обнаружил, что проблема заключается в чтении данных из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;
        }

Есть лиЛюбой способ повысить производительность и сделать мое приложение масштабируемым с количеством потоков?


edit.Структура базы данных и пример запроса для воспроизведения:

/****** 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

В настоящее время существует 30 записей в T1 и 2000 * 30 записей в T1, поэтому каждый поток работает с одним набором данных с 30 записями.Данные заполнены случайным образом newid ().


edit2.

Я также сравнил это решение в случаях - 30 отдельных процессов против 1 процесса и 30 потоков на Sql Server.30 отдельных процессов работают нормально - это примерно 150% от исходного времени выполнения, а не 1500%.Большинство различий - с 30 отдельными процессами и одним потоком я получил ~ 14 ожидающих задач и 20 000 пакетных запросов / сек, с одним процессом и 30 потоками я получил> 30 ожидающих задач (в основном по сетевому вводу / выводу) и 2 000 пакетных запросов / сек.


Настройка

"System.GC.Server": true

решила мою проблему, теперь она масштабируется до максимального количества доступных потоков на сервере.Спасибо за помощь!

1 Ответ

1 голос
/ 09 июля 2019

Проверьте настройки GC.

https://www.dotnetcurry.com/csharp/1471/garbage-collection-csharp-dotnet-core

Настройка параметров

ServerGarbageCollection = true
ConcurrentGarbageCollection = false

может помочь. :)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...