Прогноз: облачно

Бескрайняя Microsoft Azure

Джозеф Фулц

 

Joseph FultzЛично мне нравится, как все в этом мире движется по спирали. Мне всегда казалось, что эволюция любого объекта или механизма отражает дуализм — некая концепция из прошлого выводится на новый уровень. Особенно ярко это проявляется в технологиях, так как они часто меняются за короткие промежутки времени.

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

И тут появляется NoSQL, кажущаяся смесь естественной и реляционной моделей. NoSQL — это СУБД, оптимизированная для хранения и извлечения больших объемов данных. Это способ хранения данных в стиле документов, в то же время использующий некоторые механизмы, существующие сейчас в реляционных СУБД.

Один из основных инструментов NoSQL — MongoDB от 10gen Inc., СУБД с открытым исходным кодом, ориентированная на документы, и в этой статье я намерен сосредоточиться на некоторых аспектах использования MongoDB в среде Microsoft Azure. Я исхожу из того, что у вас есть некоторое представление о NoSQL и MongoDB. Если это не так, советую прочитать статьи Джули Лерман (Julie Lerman) «What the Heck Are Document Databases?» за ноябрь 2011 г. (msdn.microsoft.com/magazine/hh547103) и Тэда Ньюарда (Ted Neward) «Going NoSQL with MongoDB» за май 2010 г. (msdn.microsoft.com/magazine/ee310029).

Сначала о главном

Если вы подумываете о том, чтобы опробовать MongoDB, или рассматриваете ее как альтернативу Microsoft Azure SQL или Microsoft Azure Tables, то должны понимать некоторые вопросы проектирования и планирования, часть из которых относится к инфраструктуре, а часть — к разработке.

Архитектура развертывания

В целом, серверная часть данных должна быть доступной и отказоустойчивой. Чтобы добиться этого в MongoDB, вы используете набор репликации (replication set). Наборы репликации обеспечивают как восстановление после сбоев, так и репликацию, используя некоторые зачатки искусственного интеллекта (AI) для разрешения любой связи при выборе основного узла набора. Для ваших ролей Microsoft Azure это означает, что вам потребуются три экземпляра для подготовки минимального набора репликации плюс какое-то хранилище, которое вы сможете сопоставить с диском для каждой из этих ролей. Имейте в виду, что из-за различий в виртуальных машинах (VM) вам скорее всего понадобятся VM по меньшей мере среднего размера для любой более-менее значительной системы. Иначе память или вычислительные ресурсы могут быстро стать узким местом.

На рис. 1 изображена типичная архитектура развертывания минимального MongoDB ReplicaSet, закрытого для публичного доступа. Вы могли бы перестроить ее для поддержки внешнего доступа, но это лучше делать через уровень сервисов. Одна из задач, которые помогают решать встроенные средства MongoDB, — проектирование и развертывание архитектуры распределенных данных. В MongoDB есть полнофункциональный набор для поддержки горизонтального разбиения баз данных на разделы (sharding); скомбинируйте эту поддержку с наборами реплик и Microsoft Azure Compute и вы получите высокомасштабируемое, распределенное и надежное хранилище данных. Чтобы вам было легче приступить к работе, 10gen предоставляет решение-пример, где подготавливается минимальный ReplicaSet. Необходимую информацию вы найдете по ссылке bit.ly/NZROWJ, а файлы можно получить с GitHub по ссылке bit.ly/L6cqMF.

Рис. 1. Развертывание MongoDB в Microsoft Azure

Internal Endpoint tcp:27017 Внутренняя конечная точка tcp:27017
Worker Role:0 Рабочая роль:0
MongoDB Primary Основной узел MongoDB
Worker Role:1 Рабочая роль:1
MongoDB Member 2 Член MongoDB 2
Worker Role:2 Рабочая роль:2
MongoDB Member 3 Член MongoDB 3
Microsoft Azure Storage Microsoft Azure Storage

Схема данных

Эксперт в проектировании схем баз данных может попасть впросак в случае NoSQL. Здесь больше нужны навыки в объектном моделировании и интеграционных проектах для инфраструктур обмена сообщениями. Причин для этого две:

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

Одно из первых действий в переходе от реляционных наборов к документам MongoDB — пересмотр схемы данных. Для некоторых объектов, которые существуют отдельно в реляционной модели, поддерживается отделение и в MongoDB. Например, Products и Orders по-прежнему будут отдельной схемой в MongoDB, и вы сможете использовать внешний ключ для операций поиска между двумя этими таблицами. Упрощенно говоря, трансляция схемы для этих двух объектов, связанных друг с другом, по большей части выполняется напрямую, как показано на рис. 2.

Прямая трансляция схемы
Рис. 2. Прямая трансляция схемы

Однако это может оказаться не так легко, когда вы работаете со схемами, в которых нет такого четкого концептуального разделения, даже если в реляционной модели они разделялись очевидным образом. Например, Customers и CustomerAddresses являются сущностями, которые могли бы быть объединены, чтобы Customer содержал набор связанных адресов (рис. 3).

Преобразование реляционной схемы в схему со вложенными объектами
Рис. 3. Преобразование реляционной схемы в схему со вложенными объектами

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

Взаимодействие с данными

Поведение запросов и кеширования — важные вещи в реляционной системе, но в данном случае наиболее важным остается поведение кеширования. Во многом по аналогии с Microsoft Azure Tables вы можете легко поместить объект в MongoDB. Но в отличие от Microsoft Azure Tables и ближе к базам данных Microsoft Azure SQL индексировать можно любое поле, что обеспечивает более высокую производительность запросов применительно к одиночным объектам. Однако отсутствие операций объединения (joins) (и общей недостаточности выразительности запросов) приводит к тому, что запрос, который раньше возвращал большой объем данных с помощью одного или более объединений, превращается во множество вызовов серверного хранилища данных. Получение набора объектов с последующей выборкой связанного набора для каждого элемента в первом наборе может оказаться довольно устрашающей задачей. Используя свою реляционную базу данных Pubs, я мог бы написать следующий SQL-запрос для получения фамилий всех авторов и всех публикаций по каждому автору:

Select authors.au_lname, authors.au_id,
  titles.title_id, titles.title
From authors inner join titleauthor
  on authors.au_id = titleauthor.au_id
  inner join titles on
  titles.title_id = titleauthor.title_id
Order By authors.au_lname

Чтобы получить те же данные, используя MongoDB и драйвер для C#, требуется примерно такой код, который приведен на рис. 4.

Рис. 4. Присоединение к наборам MongoDB

MongoDatabase mongoPubs = _mongoServer.GetDatabase("Pubs");
MongoCollection<BsonDocument> authorsCollection =
  mongoPubs.GetCollection("Authors");
MongoCursor<BsonDocument> authors = authorsCollection.FindAll();
string auIdQueryString = default(string);           
Dictionary<string,BsonDocument> authorTitles =
  new Dictionary<string,BsonDocument>();
// Формируем строку для сравнения "In", формируем список
// документов авторов, затем добавляем названия публикаций
foreach (BsonDocument bsonAuthor in authors)
{
  auIdQueryString = bsonAuthor["au_id"].ToString() + ",";
  authorTitles.Add(bsonAuthor["au_id"].ToString(), 
    new BsonDocument{{"au_id",
    bsonAuthor["au_id"].ToString()},
   {"au_lname", bsonAuthor["au_lname"]}});
   authorTitles.Add("titles",
   new BsonDocument(new Dictionary<string,object>()));
}
// Подстраиваем последний символ
auIdQueryString = auIdQueryString.Remove(auIdQueryString.Length-1,1);
// Создаем запрос
QueryComplete titleByAu_idQuery = Query.In("au_id", auIdQueryString);
Dictionary<string, BsonDocument> bsonTitlesToAdd =
  new Dictionary<string,BsonDocument>();
// Выполняем запрос, соединяем авторов и названия
foreach (BsonDocument bsonTitle in 
  authorsCollection.Find(titleByAu_idQuery))
{
  Debug.WriteLine(bsonTitle.ToJson());
  // Добавляем в BsonDocument автора
  BsonDocument authorTitlesDoc = 
    authorTitles[bsonTitle["au_id"].ToString()];
  ((IDictionary<string, object>) authorTitlesDoc["titles"]).Add(bsonTitle["title_id"].ToString(), 
      bsonTitle);
}

Есть некоторые способы, позволяющие оптимизировать все это через код и структурирование, но не забывайте тот факт, что, хотя MongoDB хорошо «заточен» под прямые запросы даже применительно к вложенным объектам, более сложные запросы, формирующие наборы из разных сущностей, требуют намного больше ручной работы. Большинство из нас использует LINQ, которая помогает навести мосты между объектным и реляционным мирами. Любопытно, что в MongoDB эти мосты вам понадобятся, но по прямо противоположной причине — из-за отсутствия реляционной функциональности.

Возможно, вам также будет не хватать ссылочных ограничений (referential constraints), особенно ограничений внешнего ключа. Поскольку вы можете добавлять в набор MongoDB буквально все, в каком-то элементе могут быть (а могут и не быть) данные, связывающие его с другими сущностями. Для хардкорных фанатов реляционных СУБД это может показаться провальным недостатком платформы, но на деле это не так. Тут уже сказывается концептуальное расхождение в философии. Для баз данных NoSQL в целом идея заключается в том, что логика выносится из хранилища данных и что оно предназначается только для чтения и записи данных. Таким образом, если вы ощущаете в своей реализации MongoDB необходимость в явном введении таких вещей, как ограничения внешнего ключа, то делаете это через уровень прикладной логики и уровень сервисов, размещаемый поверх хранилища данных.

Миграция

После перепроектирования схем данных и учета поведения запросов и требований пора попробовать получить какие-нибудь данные из хранилища в облаке.

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

Первый шаг — ссылки на библиотеки MongoDB.Bson и Mongo¬DB.Driver и добавление выражений using:

using MongoDB.Bson.IO;
using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Bson.Serialization.Conventions;
using MongoDB.Bson.Serialization.IdGenerators;
using MongoDB.Bson.Serialization.Options;
using MongoDB.Bson.Serialization.Serializers;
using MongoDB.Driver.Builders;
using MongoDB.Driver.GridFS;
using MongoDB.Driver.Wrappers;

После этого в объектах станут видны некоторые новые методы, крайне полезные при переходе от обычных .NET-объектов на Bson-объекты, используемые с MongoDB. Как показано на рис. 5, это становится совершенно очевидным в функции для преобразования выходных строк из выборки в BsonDocument для сохранения в MongoDB.

Рис. 5. Миграция данных с применением LINQ и MongoDB

pubsEntities myPubsEntities = new pubsEntities();
var pubsAuthors = from row in myPubsEntities.authors
  select row;
MongoDatabase mongoPubs = _mongoServer.GetDatabase("Pubs");
mongoPubs.CreateCollection("Authors");
MongoCollection<BsonDocument> authorsCollection =
  mongoPubs.GetCollection("Authors");
BsonDocument bsonAuthor;
foreach (author pubAuthor in pubsAuthors)
{
  bsonAuthor = pubAuthor.ToBsonDocument();
    authorsCollection.Insert(bsonAuthor);
}

В простом примере на рис. 5 данные преобразуются напрямую с использованием методов расширения MongoDB. Но будьте осторожны, особенно с LINQ, выполняя этот тип операций. Так, если я попытаюсь выполнить ту же операцию непосредственно для Titles, глубина объектного графа таблицы Titles в модели сущности приведет к тому, что драйвер MongoDB вызовет переполнение стека. В таком случае преобразование требуется более детально «расписать» в коде, как показано на рис. 6.

Рис. 6. Индивидуальное преобразование значений

pubsEntities myPubsEntities = new pubsEntities();
var pubsTitles = from row in myPubsEntities.titles
  select row;
MongoDatabase mongoPubs = _mongoServer.GetDatabase("Pubs");
MongoCollection<BsonDocument> titlesCollection =
  mongoPubs.GetCollection("Titles");
BsonDocument bsonTitle;
foreach (title pubTitle in pubsTitles)
{
  bsonTitle = new BsonDocument{ {"titleId", pubTitle.title_id},
     {"pub_id", pubTitle.pub_id},
     {"publisher", pubTitle.publisher.pub_name},
     {"price", pubTitle.price.ToString()},
     {"title1", pubTitle.title1}};
  titlesCollection.Insert(bsonTitle);
}

Чтобы преобразование было максимально простым, лучше всего написать SQL-запросы, которые возвращают индивидуальные сущности — их легче добавлять в соответствующий набор MongoDB. Для объектов BsonDocument, имеющих наборы дочерних документов, потребуется создать родительский BsonDocument, добавить дочерние BsonDocument в родительский, а затем включить родительский объект в набор.

При переходе с базы данных Microsoft Azure SQL на реализацию MongoDB нужно преобразовать весь код, находящийся в хранимых процедурах, представлениях и триггерах. Во многих случаях код получится немного проще, так как вы будете иметь дело с одним BsonDocument с дочерними объектами, которые вы сохраняете как единое целое, а не обрабатываете все реляционные ограничения множества таблиц. Более того, вместо написания T-SQL вы сможете использовать свой любимый .NET-язык со всей поддержкой Visual Studio IDE. Однако все, что касается транзакций между документами, вам придется писать самому. С одной стороны, это головная боль из-за того, что нужно перенести все функциональность платформы Microsoft Azure SQL в прикладной код. С другой стороны, как только вы закончите с этим, вы получите чрезвычайно быстрое и масштабируемое серверное хранилище данных, так как все операции с ним сводятся исключительно к передаче данных. Вы также получаете высокомасштабируемый промежуточный уровень, перемещая всю логику, которая ранее находилась в реляционной СУБД, в подходящий промежуточный уровень.

И последнее важное замечание. Из-за природы этого хранилища данных весьма вероятно, что объем данных увеличится. Дело в том, что в каждом документе хранятся и схема, и данные. Хотя для большинства это не имеет особого значения, в некоторых разработках это следует учитывать на стадии проектирования, поскольку стоимость пространства в Microsoft Azure Tables весьма низка.

Заключение

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

Поддержка LINQ в версии 1.4 драйвера для C# (текущая версия — 1.5.0.4566) значительно улучшена, так что код, который вы будете писать, не будет для вас чем-то совершенно непривычным. Поэтому, если ваш проект или решение может выиграть от такого хранилища данных NoSQL, как MongoDB, не пугайтесь нового синтаксиса, потому что различия минимальны. Но учтите некоторые важные отличия отказоустойчивой платформы СУРБД, такой как Microsoft Azure SQL, от MongoDB. Например, мониторинг и контроль работоспособности потребуют больше ручной работы. Вместо мониторинга лишь некоторых экземпляров Microsoft Azure SQL вам придется вести мониторинг рабочих ролей хоста, хоста хранилища Microsoft Azure Blob с файлами базы данных и файлов журналов самой MongoDB.

NoSQL-решения предлагают отличную производительность для некоторых операций с базами данных и ряд полезных и интересных средств, которые могут оказаться ценной находкой для группы разработки решения. Если вы имеете дело с большими объемами данных и ограничены в бюджете, MongoDB на платформе Microsoft Azure может быть отличным дополнением в архитектуре вашего решения.


Джозеф Фулц (Joseph Fultz) — архитектор ПО в Hewlett-Packard Co. (в группе HP.com Global IT). Ранее работал в Microsoft с корпоративными заказчиками и с независимыми разработчиками ПО (ISV), которые проектируют архитектуры и создают программные решения.

Выражаю благодарность за рецензирование статьи эксперту Вен-Мину Йи (Wen-ming Ye).