Статьи

В масштабируемых приложениях RavenDB

RavenDB — это база данных документов 2- го поколения для платформы .NET. Он используется в производстве с 2010 года и, вероятно, является самой приятной базой данных, которую вы можете встретить, даже если об этом говорит ее автор.

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

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

Поскольку он является техническим директором Amazon, я уверен, что у него есть хотя бы некоторое представление о масштабируемости.

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

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

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

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

Давайте рассмотрим пример из реальной жизни. Следующая модель взята из нашей собственной внутренней системы обработки заказов:

Как видите, в заказе есть коллекция платежей. В RavenDB это хранится как один документ, например так (orders / 1):

{
   "Type":"Yearly",
   "Quantity":10,
   "ProductId":"products/NHProf",
   "PhoneNumber":"052 52 548 6969",
   "Email":"[email protected]",
   "Address":{
      "Address1":"Hapikus 34",
      "Address2":null,
      "City":"Sede Izhak",
      "State":null,
      "ZipCode":"38840",
      "Country":"Israel"
   },
   "DeliveryDetails":{
      "Name":"Ayende Rahien",
      "Company":"Hiberanting Rhinos"
   },
   "EndsAt":"9999-12-31T23:59:59.9999999",
   "LicenseFor":"Hibernating Rhinos",
   "IpAddress":"89.138.170.144",
   "Payments":[
      {
         "PaymentIdentifier":"E1561235",
         "Total":{
            "Currency":"EUR",
            "Amount":1300.0
         },
         "VAT":{
            "Currency":"EUR",
            "Amount":325.0
         },
         "At":"2010-01-01T22:55:35.0000000",
         "Link":"https://euro.swreg.org/cgi-bin/r.cgi?o=E1561235"
      },
      {
         "PaymentIdentifier":"E1561234",
         "Total":{
            "Currency":"EUR",
            "Amount":1300.0
         },
         "VAT":{
            "Currency":"EUR",
            "Amount":325.0
         },
         "At":"2009-01-01T22:55:35.0000000",
         "Link":"https://euro.swreg.org/cgi-bin/r.cgi?o=E1561234"
      }
   ]
}

 

Вы можете видеть, что у нас есть несколько интересных вещей в этой модели. Во-первых, мы можем структурировать сложные типы, такие как Address или DeliveryDetails. Во-вторых, мы можем хранить прямо внутри документа коллекцию.

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

Это очень важное различие, потому что оно лежит в основе RavenDB; понятие документов как сложных структур, а не просто список предопределенных столбцов, к которым вы присоединяетесь.

Но как это каким-либо образом связано с масштабируемостью?

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

Мало того, мы можем также полагаться на ACID-природу RavenDB при работе с кэшированием. Фактически, RavenDB по умолчанию будет обрабатывать все кэширование для вас, связываясь только с сервером, чтобы проверить, изменилось ли что-то с тех пор, как мы последний раз видели документ.

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

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

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

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

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

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

И как это опять связано с масштабируемостью? Что ж, давайте посмотрим на цифры. Используя обычную частоту обновления, у нас обычно есть 15 — 20 миллисекунд между обновлением документа и RavenDB, завершающим этот процесс индексации документа. При очень большой нагрузке (наш тестовый сценарий включает запись 3 миллионов документов в RavenDB), средняя задержка между записью документа и его отображением в индексах (и, следовательно, видимым для запросов) выросла до секунды с половиной.

Однако это было единственное влияние на систему. Авторы и читатели не были затронуты процессом индексации, и мы смогли продолжить обработку запросов. Сравните это с общими блокировками строк и таблиц, с которыми вам, возможно, придется столкнуться при тяжелых сценариях записи с использованием реляционной базы данных.

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

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

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

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

Первоначально они были разработаны специально для сценариев с горячим резервированием / переключением при отказе, но, как оказалось, они также имеют дополнительное использование. MSNBC.COM использует кластеры высокой доступности RavenDB для создания ведомых для чтения. Они запускают несколько узлов RavenDB в нескольких центрах обработки данных, все они реплицируются друг с другом в режиме мастер / мастер. Мастер записи назначается для каждого центра обработки данных, и все записи поступают в него, в то время как операции чтения распределяются между всеми узлами в центре обработки данных. Эта настройка предполагает, что все данные могут находиться на одном сервере RavenDB. Ограничение размера на сервере RavenDB составляет 16 терабайт, что вполне подходит для многих сценариев. Но как насчет того, когда мы хотим использовать реальный шардинг, разбивая данные на несколько узлов, а не располагая данные во всех узлах?

RavenDB поставляется со встроенной поддержкой шардинга. Давайте посмотрим, как мы можем реализовать разделение по регионам на примере нашего интернет-магазина.

У нас есть клиенты, заказы, счета и продукты. Мы хотим отсеять Клиентов, Заказы и Счета, но не заинтересованы в расфасовке Продуктов. Фактически, для Продуктов у нас есть один мастер записи и несколько рабов чтения. Причиной этого является то, что у нас есть небольшое количество Продуктов, и нет реальной причины их осколков.

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

var shards = new Dictionary<string, Dictionary >
    {
     {"Asia", new Dictionary {Url = "http://ravendb-asia"}},
     {"America", new Dictionary {Url = "http://ravendb-america"}},
     {"Europe", new Dictionary {Url = "http://ravendb-europe"}},
    };

var shardStrategy = new ShardingStrategy(shards)
	.ShardingOn<Customer>(x => x.Region)
	.ShardingOn<Order>(x => x.CustomerId)
	.ShardingOn<Invoice>(x => x.OrderId);

var documentStore = new ShardedDocumentStore(shardStrategy);

documentStore.Initialize();

 

Здесь происходит несколько вещей. Сначала мы определяем наши осколки и даем каждому из них осмысленное имя. Далее мы определим стратегию шардинга. Стратегия шардинга сообщает RavenDB, как распределять данные между шардами. В этом случае клиенты, принадлежащие к региону Европы, перейдут в осколок ravendb-eurupe.

But more interesting, because we are sharding Orders and Invoices based on their parent id, an Order belong to a European customer will actually end up on the European shard. The same for Invoices belonging to an Order belong to a European Customer.

RavenDB is smart enough to figure this out and put related information on the same shard. This means that you have strong locality of information. All of the related data about a Customer is in a single shard, and you can use single node options such as Includes (the ability to fetch related information in a single query), Live Projection (running transformation during the query, including fetching addition data from other documents), and more.

What about querying that information? RavenDB will optimize itself so if you are querying for the recent Orders of a Customer from Asia, only the Asian shard will be hit. It is smart enough to recognize that there is absolutely no need to query the other shards.

But what about when you are querying something that is inherently cross-shard? For example, what if I wanted to query for the 50 most recent Customers, regardless of their region? RavenDB will be able to process this query against all shards, merge the results and give them back to you as if you were working against a single node.

The same holds true for map/reduce operations. RavenDB can handle distributed map/reduce, by querying all the shards and re-reducing the results.

For the most part, however, you should structure your application so even though you are using shards, most of your requests only touch a single shard. This helps for the overall system performance and stability.

Finally, you also have the option of mixing things up, you can have sharding and replication, so if one of your shards goes down, we have a hot backup available to respond to all of the clients.

In conclusion

It is hard to do justice to such a complex topic in a short article, but I do hope that I was able to provide you with a hint of RavenDB capabilities in this regard. We have spent a lot of time an effort trying to make sure that everything just works, and that things are as simple as they possible can be. But not too simple, RavenDB doesn’t believe in being a straightjacket. You have all the knobs required to customize the behavior specifically for your own needs.

Give it a try, you wouldn’t be disappointed…