Я работаю в проекте веб-API ASP.NET (полная .NET Framework 4.6.1) и использую базу данных SQL Azure, API развертывается в службе приложений Azure. Что касается уровней служб, мы используем S2 в случае базы данных SQL Azure (50 DTU) и B1 в случае AppService, где развернута конечная точка API (1 ядро и 1,75 ГБ ОЗУ). На данный момент мы используем 2 экземпляра (2 ВМ с балансировщиком нагрузки)
Наша команда QA пытается выяснить возможности платформы с точки зрения производительности. Они настроили тест производительности с JMeter, который заключается в запуске 4000 запросов в течение 60 секунд.
После первых запусков тестов производительности процент ошибок HTTP 500 был очень высоким, после просмотра журналов мы обнаружили множество таких исключений:
System.InvalidOperationException: Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.
at System.Data.Common.ADP.ExceptionWithStackTrace(Exception e)
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Data.Entity.SqlServer.DefaultSqlExecutionStrategy.<>c__DisplayClass4.<<ExecuteAsync>b__3>d__6.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Data.Entity.SqlServer.DefaultSqlExecutionStrategy.<ExecuteAsyncImplementation>d__9`1.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Data.Entity.Core.EntityClient.EntityConnection.<OpenAsync>d__8.MoveNext()
Первое, что я подумал, было об утечке соединения: мы просматривали код и отслеживали соединения на SQL Server с помощью команды sp_who2, но соединения удалялись должным образом.
Мы используем контейнер для инъекций, который создает контекст Entity Framework (запросы являются асинхронными) каждый раз, когда должен быть обработан новый запрос, контекст Entity Framework удаляется автоматически, когда запрос заканчивается (зависимости с заданной областью действия).
Мы пришли к выводу, что нам необходимо увеличить размер пула соединений, чтобы уменьшить время ожидания в сценариях с огромной нагрузкой трафика.
Проведя быстрый поиск в Интернете, я обнаружил, что значение максимального размера пула по умолчанию равно 100:
https://www.connectionstrings.com/all-sql-server-connection-string-keywords/
Решил увеличить значение до 400:
Server=tcp:XXXX.database.windows.net,1433;Initial Catalog=XXXX;Persist Security Info=False;User ID=XXXX;Password=XXXXXXXXXXXX;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Max Pool Size=400;
После повторения теста производительности наше удивление заключалось в том, что мы не заметили никаких улучшений, поскольку мы получали такое же соотношение ошибок HTTP 500. Мы пришли к выводу, что максимальный размер пула игнорируется.
Следующее, что мы сделали, - это мониторинг SQL Server во время теста производительности, чтобы узнать, сколько сеансов было открыто из каждого хост-процесса, в данный момент мы используем для этой цели следующее SQL-предложение:
SELECT
COUNT(*) AS sessions,
host_name,
host_process_id,
program_name,
DB_NAME(database_id) AS database_name
FROM
sys.dm_exec_sessions AS s
WHERE
(is_user_process = 1) AND
(program_name = '.Net SqlClient Data Provider')
GROUP BY host_name, host_process_id, program_name, database_id
ORDER BY sessions DESC
После мониторинга открытых сеансов каждым хост-процессом (виртуальными машинами, на которых развернута конечная точка API) мы обнаружили, что с каждой виртуальной машины создавалось только 128 сеансов базы данных.
На данный момент мне приходит в голову несколько вариантов, которые могут объяснить такое странное поведение:
- Принимая во внимание, что пул соединений - это концепция, которая принадлежит клиентской стороне, первое, что я подумал, это то, что за такое поведение отвечает какой-то параметр в пуле приложений IIS.
- Другой вариант - это то, что только 128 сеансов могут быть открыты каждым хост-процессом и входом в базу данных. Я не нашел в Интернете ничего, что указывало бы на это ... но в других базах данных, таких как Oracle, это ограничение можно настроить так, чтобы ограничить количество сеансов, открываемых при каждом входе в систему.
- Последний вариант ... в некоторых блогах и потоках stackoverflow я читал, что полученное нами исключение (время ожидания истекло до получения соединения из пула. Это могло произойти из-за того, что все объединенные соединения использовались и максимальный размер пула был достигнут) может вводить в заблуждение и существует вероятность того, что причиной исключения является другая проблема.
Быстрое решение - отключить пул в строке подключения, но это последнее, что я бы сделал ..
Другим решением было бы масштабировать AppService, чтобы добавить больше экземпляров виртуальных машин, но это дорого с точки зрения денег.
Кто-нибудь знает, существует ли какое-то ограничение в Azure AppServices, которое объясняет, почему при включении пула подключений открываются только 128 сеансов?