Общие вопросы обеспечения высокой производительности

Tsql теория > Общие вопросы обеспечения высокой производительности
15.02.2013 18:26:28



Статья:

Общие вопросы обеспечения высокой производительности

Изложению рекомендаций по повышению эффективности работы серверных приложений в целях обеспечения высокой производительности системы посвящена значительная часть настоящей книги. Но следует учитывать, что объем работы, выполняемой системой за единицу времени, зависит не только от того, насколько рационально организовано функционирование серверных компонентов, но и от многих других факторов. В частности, значительное влияние на производительность оказывают применяемые способы создания и поддержки соединений, а также продолжительность соединений. В следующих разделах приведены некоторые общие рекомендации по повышению производительности соединений.

Управление соединениями

Для того чтобы иметь возможность выполнить любой запрос, необходимо иметь в своем распоряжении открытое соединение с базой данных. Если такое соединение отсутствует, то оно должно быть установлено, и на это расходуется значительная часть суммарного времени, требуемого для выполнения запроса. Безусловно, общие затраты времени на установление соединения изменяются в зависимости от многих факторов, но даже при наиболее благоприятном стечении обстоятельств для открытия соединения может потребоваться в несколько раз больше времени, чем на осуществление операций, предусмотренных в большинстве простых запросов.

В связи с этим приходится прибегать к различным способам уменьшения указанных издержек. Ниже приведены простые рекомендации, которые позволяют успешно устранить основные проблемы, связанные с созданием и использованием соединений.

¦ Применение при любой возможности пулов соединений. В большинстве современных методов обеспечения связи предусмотрено использование пулов соединений, причем создание таких пулов предусмотрено по умолчанию. Пулом соединений называется совокупность заранее установленных и постоянно поддерживаемых соединений, которые могут использоваться в клиентских программах. Пулы включают соединения, соответствующие применяемой модели обеспечения взаимодействия с базой данных (ODBC, OLE-DB, SQL Native Client и т.д.). Это означает, что выполнение операторов открытия и закрытия соединений в коде клиентского компонента приложения фактически не приводит к открытию и закрытию соединений в серверном компоненте; вместо этого соединение берется из пула, а затем возвращается в пул для возможного повторного использования. После того как в клиентской программе (возможно, даже в другом процессе одной и той же программы) вызывается на выполнение оператор, требующий установления соединения с теми же свойствами (такими как имя сервера, имя пользователя, пароль, протокол), в этой программе повторно используется существующее открытое соединение, а не создается полностью новое соединение, что позволяет значительно сократить издержки, связанные с созданием и уничтожением соединений. Тем не менее применение пула соединений связано с определенным риском, поэтому данной теме будет посвящено несколько слов ниже в данном приложении.

¦ Обеспечение совместного использования соединений в пределах процесса. Если в приложении не выполняется одновременно несколько запросов, то велика вероятность того, что достаточно будет поддерживать только одно соединение с сервером. В таком случае может быть предусмотрена возможность использовать соединение, глобальное по отношению ко всему приложению или, по крайней мере, по отношению к определенной функциональной области (учитывая то, что чем крупнее приложение, тем сложнее обеспечить совместную эксплуатацию соединений, поскольку, кроме всего прочего, приходится координировать усилия разработчиков, связанные с передачей прав на использование соединения из одной части приложения в другую). При таком подходе соединение открывается только один раз, после того, как в этом возникает необходимость, а затем, при выполнении различных функций, требующих доступа к базе данных, применяется ссылка на это соединение. Благодаря этому исключается необходимость открывать и закрывать соединение в каждой функции, которая обращается к базе данных.

¦ Своевременное закрытие соединений. Любое соединение, независимо от того, создано ли оно как локальное или глобальное, после завершения его использования должно быть явно закрыто. Если в приложении открыто только одно соединение, которое используется глобально, то оператор закрытия этого соединения необходимо включить в состав кода останова. Не следует рассчитывать исключительно на то, что соединение будет закрыто автоматически после выхода объекта соединения из области определения. Безусловно, такое закрытие действительно происходит автоматически, но автор пришел к выводу, что при этом операция закрытия выполняется менее надежно (хотя сервер так или иначе закрывает неиспользуемое соединение по тайм-ауту), кроме того, оперативная память, выделенная на клиентском и серверном компьютерах в целях поддержки соединения, освобождается не столь быстро.

¦ Отказ от использования необычных параметров настройки соединения. Наибольшие проблемы в связи с вводом в действие не повторяющихся при других обстоятельствах параметров настройки соединения возникают при использовании пулов соединений, но не следует забывать и о том, что в связи с переопределением свойств соединения с помощью опции SET или других аналогичных средств SQL Server повторное применение этого соединения в других операциях доступа к базе данных может стать невозможным. Что же касается пулов соединений, то необходимо учитывать их характерную особенность — отсутствие привязки к определенной системе управления базами данных (соединения с базой данных Oracle, MySQL и др. также могут объединяться в пулы), поэтому диспетчер пула соединений может не распознать, что какое-то соединение после переопределения его свойств стало отличаться от других соединений, не изменившихся со времени их открытия и включения в пул. Итак, достаточно представить себе, что задан, например, уровень изоляции транзакций, отличный от используемого по умолчанию. Это может иметь катастрофические последствия, если то же соединение будет повторно использовано в другом процессе, действующем с учетом предусмотренного по умолчанию уровня изоляции.

¦ По возможности, применение режима работы MARS. Режим работы MARS (Multiple Active Result Sets— несколько активных результирующих наборов) — это новое средство, впервые появившееся в версии SQL Server 2005, которое позволяет выполнять одновременно в одном и том же соединении несколько операций. Для каждой операции, требующей доступа к базе данных, создается активная среда, представляющая собой копию среды, применяемой по умолчанию для соединения с базой данных, после чего такая операция выполняется относительно независимо (но в случае уничтожения соединения прекращается аварийно).

Снижение количества операций обмена данными с сервером

Производительность системы, функционирование которой основано на использовании операций доступа к данным, во многом зависит также от количества таких операций.

В предыдущем разделе было показано, как добиться более качественного управления соединениями. В частности, в нем было отмечено, насколько значительными являются издержки, связанные с созданием соединения. Но следует учитывать, что издержки, хоть и меньшие, возникают и при выполнении каждой операции, связанной с использованием соединения с сервером базы данных. Каждый раз, когда в базу данных передается запрос, происходит формирование сетевых пакетов, содержащих этот запрос, пакеты передаются по сети на тот компьютер, где функционирует сервер базы данных (после согласования всех параметров, связанных с сетевой передачей), и вслед за этим полученный запрос восстанавливается в исходном виде. Перед обработкой на сервере запрос на выполнение любой отдельно взятой операции определенное время находится в очереди.

Продолжительность пребывания единственного запроса в очереди (которое обычно составляет от одной миллисекунды до нескольких десятков миллисекунд), в сравнении с другими временными затратами на выполнение запроса, относительно невелико. Но для осуществления некоторых транзакций, представляющих собой отдельно оформленный функциональный блок кода, приходится выполнять сотни, тысячи или даже миллионы отдельных запросов к серверу.

Разумеется, ни один разработчик не стал бы намеренно применять такую организацию работы приложения, при которой вместо одной операции доступа к базе данных неоправданно используется несколько (и даже очень много) таких операций. Но, к сожалению, автору слишком часто приходилось быть свидетелем того, что разработчики, чья деятельность во всем остальном не заслуживает ни малейшего упрека, создают программы, которые характеризуются именно этим недостатком. Такие люди, специализирующиеся в области создания программного обеспечения, часто не учитывают, сколько раз вызывается код, выполнение которого приводит к увеличению количества операций доступа к базе данных, и по большей части даже не задумываются над этим. Поэтому рекомендуем всегда самым тщательным образом проверять работу приложения, рассматривая при этом все аспекты его функционирования.

Ниже описаны некоторые причины, по которым организация работы приложения может оказаться недостаточно рациональной.

Выборка данных с иерархической структурой

Эта проблема очень часто обнаруживается при создании кода пользовательского интерфейса, хотя может также возникнуть в связи с применением объектной модели, в которой реализована иерархия (например, если в одном объекте представлены коллекции других объектов).

Предположим, что в пользовательском интерфейсе должна быть представлена табличная форма (или другая иерархическая структура), в верхней части которой имеется шапка, а за ней следуют данные, раскрывающие содержание шапки. Иногда шапки состоят не из одного, а из нескольких уровней. Очень часто при создании подобного интерфейса применяется подход, который состоит в том, что для обработки данных на каждом уровне иерархии предусматривается отдельная функция. Кроме того, нередко можно встретить такую организацию приложения, когда применяется определенная форма рекурсивного вызова одной и той же функции для доступа по крайней мере к части иерархии. Такая функция отвечает за создание только относящегося к ней уровня вывода, поэтому получает соответствующий параметр и на его основе формирует запрос на выборку своей части данных. Получив требуемую информацию, функция создает свою часть табличной формы, а затем либо вызывает функцию формирования следующего уровня, либо возвращает управление вызывающей функции, чтобы можно было продолжить процесс построения формы.

Таким образом, при указанной организации работы приложения применяется целый ряд небольших запросов, притом что все требуемые данные можно было бы получить с помощью всего лишь одного или двух запросов. Иначе говоря, намного более эффективным был бы проект, в котором предусмотрено получение всех необходимых данных с помощью одного или нескольких ведущих запросов, а затем передача результирующего набора (наборов) в функцию формирования пользовательского интерфейса. И на этом все взаимодействие с базой данных было бы закончено. Вызов функции создания пользовательского интерфейса мог бы осуществляться столько раз, сколько потребуется, а выборка данных свелась бы к нескольким отдельным запросам, или операциям обмена данными с сервером.

Зависимые данные

В этом сценарии количество операций обмена данными с сервером слишком велико потому, что организация приложения предусматривает использование на определенном этапе запросов, возвращающих только промежуточные данные, т.е. данные, необходимые лишь для определения того, каким должен быть следующий запрос (иными словами, результаты, полученные вначале, фактически не представляют собой конечные данные). Предположим, что в приложении должны быть созданы отчет с данными обо всех заказчиках, которые удовлетворяют определенным критериям, причем отчеты необходимо сформировать отдельно по каждому заказчику7. Безусловно, можно было бы выполнить запрос, с помощью которого создается исходный список, а затем передавать в базу данных отдельные запросы, относящиеся к каждому заказчику в полученном списке.

Но этот вариант нельзя назвать очень эффективным, поэтому можно предложить некоторые другие способы организации работы данного приложения, как описано ниже.

¦ Выполнить единственный запрос, который группирует необходимые данные, а затем контролировать в полученном результирующем наборе условия перехода от одной группе к другой, определяя время начала формирования нового отчета. Этот вариант привлекает автора меньше, поскольку в нем предусматривается применение кода, функционирующего с учетом условий эксплуатации конкретного процесса, а практика показывает, что даже при небольшом нарушении условий протекания процесса работа опирающегося на него кода нарушается. Безусловно, не следует полностью отбрасывать этот вариант как непригодный, но нужно помнить, что он не может служить оптимальным решением в любой ситуации.

¦ Использовать хранимую процедуру или включить в свой запрос полный пакет операторов выборки данных, чтобы в нем выполнялись все необходимые запросы и происходил возврат нескольких наборов данных в одном вызове (после чего остается лишь отдельно обработать наборы данных).

Пропускная способность

Если пропускная способность сети не позволяет своевременно передавать весь объем данных, которыми обмениваются компоненты сетевых приложений, то нормальная эксплуатация приложений становится невозможной. При этом многое зависит от того, как организовано функционирование клиентской части приложения. Если пользовательский интерфейс не формируется локально, и во время того, как пользователь открывает новое окно или развертывает список, по сети каждый раз передаются мегабайты данных, то вряд можно надеяться, что будет достигнута высокая производительность приложения. Все процессы взаимодействия с пользователем будут протекать чрезвычайно медленно. Дела обстоят еще хуже, если на клиентском компьютере вообще не выполняется какая-либо производительная работа и все данные, необходимые для обеспечения функционирования пользовательского интерфейса, поступают только с сервера. Чтобы проще было понять, насколько важной является задача оптимального распределения нагрузки между клиентом и сервером, достаточно напомнить, что в сети обычно эксплуатируется одновременно сразу несколько (а иногда и очень много) экземпляров клиент-серверных приложений. В таком случае значимость любых ошибок при проектировании приложений существенно возрастает.

В частности, кроме двух указанных выше недостатков приложений (чрезмерное количество операций обмена данными с сервером и неправильное распределение нагрузки по созданию пользовательского интерфейса между клиентской и серверной частями приложения), часто отмечается еще один существенный недостаток. Дело в том, что некоторые разработчики, либо в целях минимизации количества операций выборки данных, либо просто потому, что предпочитают работать с данными не на языке SQL, а на языке, который является для них более комфортным, загружают на клиентский компьютер с сервера очень крупные совокупности данных (намного превышающие объем действительно требуемых данных), а затем выполняют поиск по условию и другие операции манипулирования этими данными.

Подобную организацию работы приложения необходимо прежде всего прокомментировать так, что само по себе стремление прежде всего упростить работу разработчика, а не усовершенствовать приложение, плохо сказывается на конечном результате. В частности, задача разработчика часто упрощается, если основная нагрузка по обработке данных возлагается на клиентскую часть приложения. Но такое решение не всегда оправдано, поскольку приводит к увеличению объема данных, передаваемых по сети.

Чтобы правильно выбрать способ распределения нагрузки в приложении, необходимо учесть все факторы. Если в клиентскую часть приложения за один раз перебрасывается огромное количество данных, причем не только тех, что требуются (поскольку предусмотрено проведение фильтрации данных непосредственно на клиентском компьютере), то, безусловно, приходится выполнять гораздо меньше операций обмена данными с сервером, но тем самым работа, для выполнения которой СУБД SQL Server приспособлена лучше всего, передается в другую программу. Язык SQL превосходно справляется с задачами выборки данных по критерию и сортировки, поэтому нагрузку, связанную с выполнением таких операций, лучше всего возложить на серверную часть приложения. Достаточно лишь предусмотреть применение эффективных способов осуществления этих операций.

Примеры обеспечения связи

В этом разделе приведены некоторые чрезвычайно упрощенные (иначе говоря, упрощенные до предела) примеры установления соединений. Для каждого языка приведено несколько примеров, по одному из двух наиболее важных разновидностей операций (выборка простого набора данных и выполнение запроса, не возвращающего наборы данных).

Применение средств установления соединений, предусмотренных в языке C#

В настоящее время трудно себе представить, как можно обойтись без использования языка С#. Когда я писал свою предыдущую книгу, язык C# фактически находился лишь на этапе внедрения. Это — довольно качественный язык, относительно простой в изучении (и в этом отношении во многом подобный языку VB). Еще одним преимуществом языка C# является то, что в нем реализованы многие принципы, лежащие в основе языка С. Кроме того, в синтаксисе языков C# и С есть много общего (и благодаря этому упрощается переход с одного языка на другой).

Возврат набора данных

using System;

using System.Data.SqlClient;

class Program {

static void Main()

{

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

// Встроенные средства защиты; они применяются, только если следующая // строка не закомментирована

string strConnect = "Data Source=(local);Initial

Catalog=master; Integrated Security=SSPI11 ;

// Средства защиты SQL Server; они применяются, только если следующая // строка не закомментирована

//string strConnect = "Data Source=(local);Initial Catalog=master;

User Id=sa;Password=MyPass";

string strCommand = "SELECT Name, database_id as ID FROM sys.databases";

SqlDataReader rsMyRS = null;

SqlConnection cnMyConn = new SqlConnection(strConnect);

try

{

// Открыть соединение (при этом фактически впервые осуществляется // контакт с сервером базы данных) cnMyConn.Open();

// Создать объект команды

SqlCommand sqlMyCommand = new SqlCommand(strCommand, cnMyConn);

// Создать результирующий набор rsMyRS = sqlMyCommand.ExecuteReader();

//Вывести полученные данные while (rsMyRS.Read())

{

// Вывести данные первого столбца (по порядковому номеру). Можно // также ссылаться на столбец по имени Console.WriteLine(rsMyRS["Name"]);

}

Console.WriteLine();

Console.WriteLine("Press any key to continue...");

Console.ReadKey();

}

finally

{

// Выполнить завершающие действия if (rsMyRS != null)

{

rsMyRS.Close();

}

if (cnMyConn != null)

{

cnMyConn.Close();

}

Выполнение команд без возврата набора данных

using System;

using System.Data.SqlClient;

class Program {

static void Main()

{

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

// Встроенные средства защиты; они применяются, только если следующая

// строка не закомментирована

string strConnect = "Data Source=(local);

Initial Catalog=master; Integrated Security=SSPI11 ; // Средства защиты SQL Server; они применяются, только если следующая // строка не закомментирована

//string strConnect = "Data Source=(local);Initial Catalog=master;

User Id=sa;Password=MyPass";

string strCommand = "CREATE TABLE Foo(Columnl    INT    NOT    NULL    PRIMARY    KEY)";

string strCommand2 = "DROP TABLE Foo";

SqlConnection cnMyConn = new SqlConnection(strConnect);

try

{

// Открыть соединение (при этом фактически    впервые    осуществляется

// контакт с сервером базы данных) cnMyConn.Open();

// Создать объект команды

SqlCommand sqlMyCommand = new SqlCommand(strCommand, cnMyConn);

// Выполнить команду sqlMyCommand.ExecuteNonQuery();

Console.WriteLine("Table Created");

Console.WriteLine("Press enter to continue (you can go check to

make sure that it's there first) ");

Console.ReadLine();

// Внести изменения в текст команды sqlMyCommand.CommandText = strCommand2;

sqlMyCommand.ExecuteNonQuery();

Console.WriteLine("It's gone");

Console.WriteLine();

Console.WriteLine("Press any key to continue...");

Console.ReadKey();

}

finally

{

// Выполнить завершающие действия if (cnMyConn != null)

{

cnMyConn.Close();

}

}

}

}

Применение средств установления соединений, предусмотренных в языке VB.NET

В последнее время язык VB.NET все шире применяется вместо языка Visual Basic. Сам автор в основном предпочитает использовать в собственных разработках язык С#, но очень многие разработчики считают для себя более удобным язык VB.NET.

Возврат набора данных

Imports System

Imports System.Data

Imports System.Data.SqlClient

Module Program

Sub MainO

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

' Встроенные средства защиты; они применяются, только если следующие две 1 строки не закомментированы Dim strConnect As String = _

"Data Source=(local);Initial Catalog=master;Integrated Security=SSPI"

' Средства защиты SQL Server; они применяются, только если следующие две 1 строки не закомментированы 'Dim strConnect As String = _

'    "Data    Source=(local);Initial Catalog=master;User Id=sa;Password=MyPass"

Dim strCommand As String = _

"SELECT Name, database_id as ID FROM sys.databases"

Dim rsMyRS As SqlClient.SqlDataReader

Dim cnMyConn As New SqlClient.SqlConnection(strConnect)

' Открыть соединение (при этом фактически впервые осуществляется контакт 1 с сервером базы данных) cnMyConn.Open()

' Создать объект команды

Dim sqlMyCommand As New SqlClient.SqlCommand(strCommand, cnMyConn)

' Создать результирующий набор rsMyRS = sqlMyCommand.ExecuteReader()

'Вывести полученные данные Do While rsMyRS.Read

' Вывести данные первого столбца (по порядковому номеру). Можно также 1 ссылаться на столбец по имени Console.WriteLine(rsMyRS("Name"))

Loop

Console.WriteLine()

Console.WriteLine("Press any key to continue...")

Console.ReadKey()

' Выполнить завершающие действия rsMyRS.Close() cnMyConn.Close()

End Sub

End Module

Выполнение команд без возврата набора данных

Imports System

Imports System.Data

Imports System.Data.SqlClient

Module Program

Sub Main()

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

' Встроенные средства защиты; они применяются, только если следующие 1 две строки не закомментированы Dim strConnect As String = _

"Data Source=(local);Initial Catalog=master;Integrated Security=SSPI"

' Средства защиты SQL Server; они применяются, только если следующие две ' строки не закомментированы 'Dim strConnect As String = _

'    "Data    Source=(local);Initial Catalog=master;User Id=sa;Password=MyPass"

Dim strCommand As String = "CREATE TABLE Foo (Columnl INT NOT NULL PRIMARY KEY) " Dim strCommand2 As String = "DROP TABLE Foo"

Dim cnMyConn As New SqlClient.SqlConnection(strConnect)

' Открыть соединение (при этом фактически впервые осуществляется ' контакт с сервером базы данных) cnMyConn.Open()

' Создать объект команды

Dim sqlMyCommand As New SqlClient.SqlCommand(strCommand, cnMyConn)

' Выполнить команду sqlMyCommand.ExecuteNonQuery()

Console.WriteLine("Table Created")

Console.WriteLine("Press enter to continue (you can go check to make sure that it's there first)")

Console.ReadLine()

1 Внести изменения в текст команды sqlMyCommand.CommandText = strCommand2

sqlMyCommand.ExecuteNonQuery()

Console.WriteLine("It2s gone")

Console.WriteLine()

Console.WriteLine("Press any key to continue...") Console.ReadKey()