Доступ к данным

Применение Entity Framework для уменьшения задержек сети при работе с SQL Azure

Джули Лерман

На первый взгляд переключение с локально управляемой базы данных SQL Server на базу данных SQL Azure в облаке Microsoft кажется весьма трудным. Но на практике это плевое дело: меняете строку подключения, и все! Как любят говорить разработчики в таких ситуациях, это «просто работает».

Однако после такого переключения появляются сетевые задержки, которые могут существенно повлиять на общую производительность приложения. К счастью, хорошее понимание последствий сетевых задержек дает возможность эффективно использовать Entity Framework для уменьшения их влияния в контексте SQL Azure.

Профилирование кода доступа к данным

Для сравнения различных операций доступа к данным в базе данных AdventureWorksDW, размещенной как в моей локальной сети, так и под моей учетной записью в SQL Azure, я использую средства профилирования из Visual Studio 2010 (msdn.microsoft.com/library/ms182372). С их помощью я анализирую вызовы для загрузки информации по некоторым клиентам из этой базы данных с применением Entity Framework. В одном из тестов запрашиваются только клиенты, а затем извлекается относящаяся к ним информация по продажам; при этом используется функция отложенной загрузки в Entity Framework 4.0. В последующем тесте интенсивно загружается информация по продажам наряду с клиентами, причем применяется метод Include. Нарис. 1показано консольное приложение, которое я использовала для выполнения этих запросов и перечисления результатов.

Рис. 1. Запросы, спроектированные для исследования производительности

{    using (var context = new AdventureWorksDWEntities())    {      var warmupquery = context.Accounts.FirstOrDefault();    }    DoTheRealQuery();  }    private static void DoTheRealQuery()  {    using ( var context=new  AdventureWorksDWEntities())    {      var query =   context.Customers.Where(c => c.InternetSales.Any()).Take(100);      var customers = query.ToList();      EnumerateCustomers(customers);    }  }    private static void EnumerateCustomers(List<Customer> customers)  {    foreach (var c in customers)    {      WriteCustomers (c);          }  }    private static void WriteCustomer(Customer c)  {    Console.WriteLine   ("CustomerName: {0} First Purchase: {1}  # Orders: {2}",      c.FirstName.Trim() + "" + c.LastName,  c.DateFirstPurchase, c.InternetSales.Count);  }

Я начала с «разогревающего» запроса, чтобы пройти издержки загрузки метаданных Entity Data Model (EDM) в память, предкомпиляции представлений и других разовых операций. Затем метод DoTheRealQuery запрашивает подмножество сущностей Customers, получает их в виде списка и перечисляет результаты. В процессе перечисления происходит обращение к информации о продажах для каждого клиента, что в данном случае вызывает отложенную загрузку всей этой информации из базы данных.

Анализ производительности в локальной сети

При выполнении этой программы в моей локальной сети применительно к внутренней базе данных SQL Server первый вызов, выполняющий запрос, занял 233 мс.Это вызвано тем, что я лишь извлекаю информацию о клиентах. На перечисление, которое вызывает отложенную загрузку, уходит 195 мс.

Теперь я изменю запрос так, чтобы он интенсивно загружал InternetSales наряду с Customers:

context.Customers.Include("InternetSales").Where(c => c.InternetSales.Any()).Take(100);

Теперь базой данных будут возвращаться эти 100 клиентов наряду со всеми записями по относящимся к ним продажам. Это гораздо больший объем данных.

И вызов query.ToList занимает примерно 350 мс, почти на 33% больше простого возврата 100 клиентов.

Есть и другой эффект от этого изменения. Когда код перебирает клиентов, информация о продажах уже находится в памяти. То есть Entity Framework не нужны дополнительные 100 обменов данными с базой. Это перечисление наряду с записью подробностей занимает около 70% времени, которое уходит при отложенной загрузке.

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

Нарис. 2показано сравнение интенсивной и отложенной загрузки в среде локальной сети. Столбцы ToList указывают время выполнения запроса — в коде это строкаvar customers = query.ToList();. Столбцы «Перечисление» дают представление о скорости работы метода EnumerateCustomers. Наконец, столбцы «Запрос и перечисление» отражают время выполнения метода DoTheRealQuery, которое включает само выполнение запроса, перечисление, создание экземпляра контекста и объявление запроса.

Рис. 2.Сравнение интенсивной и отложенной загрузки из локальной базы данных

Переключение на базу данных SQL Azure

Теперь я меняю строку подключения, чтобы использовать свою базу данных SQL Azure в облаке. Не стоит удивляться, что в этом случае вмешивается фактор задержек в сети между моим компьютером и базой данных в облаке, что замедляет выполнение запросов по сравнению с локальной базой данных. Избежать задержек в этом варианте нельзя. Однако, и на это стоит обратить внимание, увеличение времени не одинаково для разных задач. Для запросов некоторых типов задержки гораздо более выраженные, чем для других. Взгляните нарис. 3.

Рис. 3.Сравнение интенсивной и отложенной загрузки из SQL Azure

На диаграмме видно, что интенсивная загрузка по-прежнему медленнее, чем заблаговременная загрузка информации о клиентах. Но если в локальной среде разница была 30%, то при обращении к SQL Azure она увеличилась примерно до трех раз.

Однако, как видно изрис. 3, самое большое влияние сетевые задержки оказали на отложенную загрузку. После того как данные InternetSales оказываются в памяти благодаря интенсивной загрузке, их перебор осуществляется так же быстро, как и в первом наборе тестов. Но отложенная загрузка вызывает 100 дополнительных обменов с базой данных в облаке. Из-за сетевых задержек каждый такой цикл требует больше времени, и в итоге время получения результатов становится заметно невооруженным глазом.

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

Смысл всех этих изысканий не в том, чтобы обвинить SQL Azure, который на самом деле является высокопроизводительной системой, а в том, что ваш выбор механизма запросов Entity Framework может оказать негативное влияние на общую производительность из-за сетевых задержек.

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

В зависимости от формы возвращаемых вами данных (возможно, вы используете более сложный запрос с большим количеством операторов Include, например) в некоторых случаях более дорогостоящей может оказаться интенсивная загрузка, а в других — отложенная. Возможны и такие ситуации, когда имеет смысл распределять загрузку: одни данные загружать интенсивно, а другие — «лениво»; для этого понадобиться анализ с помощью средств профилирования производительности. И во многих случаях конечной целью является перенос в облако не только базы данных, но и вашего приложения. Windows Azure и SQL Azure рассчитаны на совместную работу. Перенеся свое приложение на платформу Windows Azure и сделав так, чтобы оно получало свои данные от SQL Azure, вы добьетесь максимальной производительности.

Применение проекций для тонкой настройки результатов запроса

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

Например, запрос в следующей модифицированной версии метода TheRealQuery возвращает только подмножество сущностей InternetSales (значение которых больше или равно 1000):

private static void TheRealQuery()      {        using ( var context=new  AdventureWorksDWEntities())        {          Decimal salesMinimum = 1000;          var query =              from customer in context.Customers.Where(c =>               c.InternetSales.Any()).Take(100)              select new { customer, Sales = customer.InternetSales };          IEnumerable customers = query.ToList();          context.ContextOptions.LazyLoadingEnabled = false;          EnumerateCustomers(customers);        }      }

Этот запрос возвращает ту же сотню клиентов, что и предыдущий, но со 155 сопутствующими записями InternetSales вместо 661, которые возвращались без фильтра SalesAmount.

Всегда помните следующую важную вещь, касающуюся проекций и отложенной загрузки. При создании проекций данных контекст не распознает связанные данные как загруженные. Это происходит, только когда они загружаются через метод Include, метод Load или механизм отложенной загрузки. Следовательно, очень важно отключить отложенную загрузку до перечисления, как это сделала я в методе TheRealQuery. Иначе контекст запустит отложенную загрузку данных InternetSales — даже несмотря на то, что они уже находятся в памяти; это приведет к тому, что перечисление будет выполняться гораздо дольше необходимого.

Это учтено в модифицированном методе перечисления:

private static void EnumerateCustomers(IEnumerable customers)  {    foreach (var item in customers)    {      dynamic dynamicItem=item;      WriteCustomer((Customer)dynamicItem.customer);    }  }

Этот метод также использует преимущества динамического типа (ключевое слово dynamic) динамического типа (ключевое слово dynamic) в C# 4 для выполнения позднего связывания (late binding).

Рис. 4демонстрирует значительный выигрыш в производительности за счет более тонко настроенной проекции.

Рис. 4Сравнение интенсивной загрузки с фильтруемой проекцией при использовании SQL Azure

Очевидно, что фильтруемый запрос будет всегда быстрее, чем запрос с интенсивной загрузкой, возвращающий больше данных. Но более интересен тот факт, что база данных SQL Azure обрабатывает фильтруемую проекцию примерно на 70% быстрее, тогда как в случае локальной базы данных выигрыш составляет всего около 15%. Я подозреваю, что при интенсивной загрузке набора InternetSales перечисление в памяти проходит быстрее из-за того, как Entity Framework на внутреннем уровне обращается к нему в отличие от спроецированного набора. Но, поскольку перечисление в этом случае происходит полностью в памяти, фактор сетевых задержек в этом случае отпадает. В целом, улучшение, наблюдаемое при использовании проекции, перевешивает небольшие издержки перечисления проекции.

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

Заключение

Сценарии, которые я обсуждала здесь, крутятся вокруг локально размещенных приложений или сервисов, использующих SQL Azure. Вместо этого вы можете разместить свое приложение или сервис в облаке Windows Azure. Например, конечные пользователи могли бы с помощью Silverlight обращаться к веб-роли Windows Azure, выполняющей Windows Communication Foundation, который в свою очередь обращается к данным в SQL Azure. Тогда вы избавились бы от сетевых задержек между сервисом (который размещен в облаке) и SQL Azure.

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

Джули Лерман ((Julie Lerman) — Microsoft MVP, преподаватель и консультант по .NET, живет в Вермонте. Часто выступает на конференциях по всему миру и в группах пользователей по тематике, связанной с доступом к данным и другими технологиями Microsoft .NET. Ведет блогthedatafarm.com/blogи является автором очень популярной книги «Programming Entity Framework» (O’Reilly Media, 2009). Вы также можете встретить ее на Twitter.com/julielerman.

Выражаю благодарность за рецензирование статьи экспертамУэйну Берри (Wayne Berry), Крейгу Брокшмидту (Kraig Brockschmidt), Тиму Лэверти (Tim Laverty)иСтиву Вайи (Steve Yi)