Работающий программист

NoSQL-база данных Cassandra, часть 2: программирование и кластеризация

Тэд Ньюард

Тэд НьюардВ статье «NoSQL-база данных Cassandra: приступаем к работе» за август 2012 г. я рассказал об Apache Cassandra. Она описывается как «распределенная, децентрализованная, эластично масштабируемая, высокодоступная, отказоустойчивая база данных с открытым исходным кодом, настраиваемой согласованностью и ориентацией на столбцы, причем проект распределенной структуры основан на Amazon Dynamo, а ее модель данных — на Google Bigtable» (источник: «Cassandra: The Definitive Guide», O’Reilly Media, 2010). Я рассмотрел, как установить Cassandra (которая, являясь базой данных на основе Java, требует получения и запуска на компьютере Java Virtual Machine, если такой виртуальной машины у вас еще нет), как подключиться к ней из командной строки и что представляет собой ее модель данных. Модель данных заслуживает повторного упоминания, так как по своей структуре она весьма значительно отличается от реляционной базы данных, с которой знакомо большинство разработчиков.

Как я говорил в прошлый раз (msdn.microsoft.com/magazine/JJ553519), Cassandra — хранилище данных, ориентированное на столбцы (поля), а значит, вместо хранения идентично структурированных наборов данных, размещаемых согласно фиксированной структуре (схеме таблицы), Cassandra хранит семейства столбцов (column families) в пространствах ключей (keyspaces). Или в более понятных терминах, Cassandra сопоставляет значение ключа с варьируемым количеством пар «имя-значение» (столбцов), в результате чего одна строка (запись) может полностью отличаться от другой.

Например, возьмите пространство ключей Earth, созданное мной в прошлый раз, с семейством столбцов People, в которое я буду записывать строки, и они могут выглядеть так:

RowKey: tedneward
  ColumnName:"FirstName", ColumnValue:"Ted"
  ColumnName:"LastName", ColumnValue:"Neward"
  ColumnName:"Age", ColumnValue:41
  ColumnName:"Title", ColumnValue:"Architect"
RowKey: rickgaribay
  ColumnName:"FirstName", ColumnValue:"Rick"
  ColumnName:"LastName", ColumnValue:"Garibay"
RowKey: theartistformerlyknownasprince
  ColumnName:"Identifier", ColumnValue: <image>
  ColumnName:"Title", ColumnValue:"Rock Star"

Как видите, каждая строка содержит концептуально схожие данные, но не во всех строках находятся одинаковые данные — все зависит от того, что именно нужно хранить разработчику или бизнес-пользователю под конкретным ключом строки. Я не знаю, сколько лет Рику (Rick), поэтому не указываю это значение. В реляционной базе данных, если бы схема определяла поле возраста, которое не может содержать NULL-значения, я не смог бы записать вообще никакой информации и Рике. Но Cassandra говорит: «А почему бы и нет?».

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

О Кассандра, ну почему же ты Кассандра?

Для начала мне нужно подключиться к Cassandra из Microsoft .NET Framework. Для этого применяется один из двух способов: неуправляемый Apache Thrift API или сторонняя оболочка неуправляемого Thrift API. Thrift — это компонент для вызова двоичных удаленных процедур, во многих отношениях похожий на DCOM (бьюсь об заклад, что вы не вспоминали об этой технологии уже несколько лет) или CORBA, или .NET Remoting. Это особенно низкоуровневый подход к взаимодействию с Cassandra, и, хотя Thrift поддерживает C#, подготовить все это да еще и запустить — задача далеко не тривиальная. К альтернативам Thrift относятся FluentCassandra, cassandra-sharp, Cassandraemon и Aquiles (испанское произношение Ахилла) (кстати, благодаря всем этим названиям древнегреческая мифология не забывается и остается актуальной). Все это является проектами с открытым исходным кодом и предлагает некоторые неплохие абстракции Cassandra API. В этой статье я решил воспользоваться FluentCassandra, но любой из них работает вполне прилично, несмотря на периодические «холивары» на форумах в Интернете.

FluentCassandra доступен в виде NuGet-пакета, поэтому самый простой способ начать с ним работу — запустить NuGet Package Manager в проекте Visual Studio Test (чтобы можно было писать исследовательские тесты) и выполнить команду Install-Package FluentCassandra. (Самая свежая версия на момент написания статьи — 1.1.0.) Затем дважды проверяем, что сервер Cassandra все еще работает после того, как я поиграл с ним в своей рубрике за август, и можно писать первый исследовательский тест: подключение к серверу.

FluentCassandra находится в пространстве имен FluentCassandra и двух вложенных пространствах имен (Connections и Types), поэтому я включаю их и пишу тест, чтобы проверить подключение к базе данных:

private static readonly Server Server =
  new Server("localhost");
[TestMethod]
public void CanIConnectToCassandra()
{
  using (var db = new CassandraContext(keyspace:"system", 
     server:Server))
  {
    var version = db.DescribeVersion();
    Assert.IsNotNull(version);
    testContextInstance.WriteLine("Version = {0}", version);
    Assert.AreEqual("19.30.0", version);
  }
}

Заметьте, что к тому времени, когда вы будете читать мою статью, вполне вероятно, что номер версии будет отличаться от используемого мной, поэтому, если второе утверждение (assertion) провалится, проверьте в окне отладочного вывода возвращаемую строку. (Помните, что исследовательские тесты, по сути, во многих отношениях сводятся к проверке вашего понимания API, а значит, запись вывода — вовсе не такая плохая идея, как в автоматизированном модульном тесте.)

Класс CassandraContext имеет пять перегруженных версий для подключения к выполняемому серверу Cassandra — все они имеют дело с информацией подключения в той или иной форме. В данном конкретном случае, поскольку пространство ключей, где я хочу сохранить данные и впоследствии читать их, еще не создано, я подключаюсь к «системному» пространству ключей. Оно используется Cassandra для хранения различных системных деталей во многих отношениях аналогично тому, как большинство реляционных баз данных резервирует один экземпляр для метаданных базы данных, информации, связанной с защитой, и т. д. Но это означает, что я вовсе не хочу записывать данные в это системное пространство ключей; мне нужно создать свое, и эта операция лежит в основе следующего исследовательского теста, как показано на рис. 1.

Рис. 1. Создание системного пространства ключей

[TestMethod]
public void DoesMyKeyspaceExistAndCreateItIfItDoesnt()
{
  using (var db = new CassandraContext(keyspace:"system", 
     server:Server))
  {
    bool foundEarth = false;
    foreach (CassandraKeyspace keyspace in
      db.DescribeKeyspaces())
    {
      Apache.Cassandra.KsDef def = keyspace.GetDescription();
      if (def.Name == "Earth")
        foundEarth = true;
    }
   if (!foundEarth)
    {
      var keyspace = new CassandraKeyspace(
        new CassandraKeyspaceSchema
      {
        Name = "Earth"
      }, db);
      keyspace.TryCreateSelf();
    }
    Assert.IsTrue(db.KeyspaceExists("Earth"));
  }
}

Следует признать, что цикл по всем пространствам ключей в базе данных излишен — я делаю так только для того, чтобы продемонстрировать, что в FluentCassandra API есть такие места, где нижележащий API на основе Thrift пробивается сквозь тучи абстракций, и одно из них — тип Apache.Cassandra.KsDef.

Теперь, когда у меня есть пространство ключей, мне потребуется минимум одно семейство столбцов внутри этого пространства. Самый простой способ создать его, задействовать Cassandra Query Language (CQL), чем-то напоминающий SQL-подобный язык, как показано на рис. 2.

Рис. 2. Создание семейства столбцов с помощью Cassandra Query Language

[TestMethod]
public void CreateAColumnFamily()
{
  using (var db = new CassandraContext(keyspace:"Earth", 
    server: Server))
  {
    CassandraColumnFamily cf = db.GetColumnFamily("People");
    if (cf == null)
    {
      db.ExecuteNonQuery(@"CREATE COLUMNFAMILY People (
        KEY ascii PRIMARY KEY,
        FirstName text,
        LastName text,
        Age int,
        Title text
);");
    }
    cf = db.GetColumnFamily("People");
    Assert.IsNotNull(cf);
  }
}

Опасность CQL в том, что его грамматика, умышленно сделанная похожей на SQL, в сочетании с легко приходящим в голову неправильным утверждением: «поскольку в Cassandra есть столбцы, значит, в ней должны быть таблицы, как в реляционной базе данных» может обмануть неопытного разработчика и заставить его мыслить в категориях реляционных баз данных. А это ведет к концептуально неправильным допущениям. Возьмите, к примеру, столбцы на рис. 2. В реляционной базе данных только эти пять полей были бы допустимы в этом семействе столбцов. В Cassandra это просто «примерный план» (чем-то напоминающий кодекс «Пиратов Карибского моря»). Но альтернатива (вообще не использовать CQL) на сегодняшний день еще менее привлекательна: Cassandra предлагает TryCreateColumnFamily API (не показан), но, сколько бы раз я ни пытался разобраться в нем, он все равно остается для меня еще более запутанным вариантом, чем подход с CQL.

«Факты, факты, факты! Я не могу делать кирпичи без глины!»

Как только у нас появилось семейство столбцов, при сохранении объектов в базе данных можно прочувствовать реальную мощь FluentCassandra API (рис. 3).

Рис. 3. Сохранение объектов в базе данных

[TestMethod]
public void StoreSomeData()
{
  using (var db = new CassandraContext(keyspace:"Earth", 
    server: Server))
  {
    var peopleCF = db.GetColumnFamily("People");
    Assert.IsNotNull(peopleCF);
    Assert.IsNull(db.LastError);
    dynamic tedneward = peopleCF.CreateRecord("TedNeward");
    tedneward.FirstName = "Ted";
    tedneward.LastName = "Neward";
    tedneward.Age = 41;
    tedneward.Title = "Architect";
    db.Attach(tedneward);
    db.SaveChanges();
    Assert.IsNull(db.LastError);
  }
}

Обратите внимание на использование ключевого слова dynamic из C# 4.0, подкрепляющего идею о том, что семейство столбцов не является строго типизированным набором пар «имя-значение». Это позволяет коду на C# отразить природу хранилища данных, ориентированного на столбцы. В этом можно убедиться ,если сохранить в пространстве ключей информацию еще о нескольких людях, как показано на рис. 4.

Рис. 4. Сохранение информации о других людях в пространстве ключей

[TestMethod]
public void StoreSomeData()
{
  using (var db = new CassandraContext(keyspace:"Earth", 
    server: Server))
  {
    var peopleCF = db.GetColumnFamily("People");
    Assert.IsNotNull(peopleCF);
    Assert.IsNull(db.LastError);
    dynamic tedneward = peopleCF.CreateRecord("TedNeward");
    tedneward.FirstName = "Ted";
    tedneward.LastName = "Neward";
    tedneward.Age = 41;
    tedneward.Title = "Architect";
    dynamic rickgaribay = peopleCF.CreateRecord("RickGaribay");
    rickgaribay.FirstName = "Rick";
    rickgaribay.LastName = "Garibay";
    rickgaribay.HomeTown = "Phoenix";
    dynamic theArtistFormerlyKnownAsPrince =
    peopleCF.CreateRecord("TAFKAP");
    theArtistFormerlyKnownAsPrince.Title = "Rock Star";
    db.Attach(tedneward);
    db.Attach(rickgaribay);
    db.Attach(theArtistFormerlyKnownAsPrince);
    db.SaveChanges();
    Assert.IsNull(db.LastError);
  }
}

И вновь обратите внимание на то, что для Рика (Rick) имеется поле HomeTown, которое не было определено в предыдущем описании этого семейства столбцов. Это вполне приемлемо и весьма обычная практика.

Также заметьте, что FluentCassandra API предоставляет свойство LastError, которое содержит ссылку на последнее исключение, сгенерированное в базе данных. Оно может пригодиться для проверки, когда состояние базы данных уже не известно (например, при возврате из набора вызовов, которые могли «проглотить» сгенерированное исключение, или в том случае, когда база данных сконфигурирована так, чтобы не генерировать исключения).

А теперь еще раз, с чувством

Подключение к базе данных, создание пространства ключей (а потом его удаление), определение семейств столбцов и запись образцов данных — все это я буду делать неоднократно в рамках этих тестов. Эта последовательность кода — отличный кандидат на размещение в методах настройки с предварительной проверкой (pre-test setup) и сброса с повторной проверкой (post-test teardown). Удаляя пространство ключей после каждого теста и воссоздавая его перед следующим тестом, я сохраняю девственность базы данных и поддерживаю ее в известном состоянии перед запуском теста, как показано на рис. 5.

Рис. 5. Выполнение теста

[TestInitialize]
public void Setup()
{
  using (var db = new CassandraContext(keyspace:"Earth", 
    server: Server))
  {
    var keyspace = new CassandraKeyspace(
      new CassandraKeyspaceSchema {
      Name = "Earth",
      }, db);
    keyspace.TryCreateSelf();
    db.ExecuteNonQuery(@"CREATE COLUMNFAMILY People (
    KEY ascii PRIMARY KEY,
    FirstName text,
    LastName text,
    Age int,
    Title text);");
    var peopleCF = db.GetColumnFamily("People");
    dynamic tedneward = peopleCF.CreateRecord("TedNeward");
    tedneward.FirstName = "Ted";
    tedneward.LastName = "Neward";
    tedneward.Age = 41;
    tedneward.Title = "Architect";
    dynamic rickgaribay = peopleCF.CreateRecord("RickGaribay");
    rickgaribay.FirstName = "Rick";
    rickgaribay.LastName = "Garibay";
    rickgaribay.HomeTown = "Phoenix";
    dynamic theArtistFormerlyKnownAsPrince =
    peopleCF.CreateRecord("TAFKAP");
    theArtistFormerlyKnownAsPrince.Title = "Rock Star";
    db.Attach(tedneward);
    db.Attach(rickgaribay);
    db.Attach(theArtistFormerlyKnownAsPrince);
    db.SaveChanges();
  }
}
[TestCleanup]
public void TearDown()
{
  var db = new CassandraContext(keyspace:
    "Earth", server: Server);
  if (db.KeyspaceExists("Earth"))
    db.DropKeyspace("Earth");
}

«Моей державе в мире места мало. Все рушится.»

Чтение данных из Cassandra осуществляется парой способов. Первый заключается в получении данных из семейства столбцов с помощью метода Get объекта CassandraColumnFamily, как показано на рис. 6.

Рис. 6. Получение данных методом Get

[TestMethod]
public void StoreAndFetchSomeData()
{
  using (var db = new CassandraContext(keyspace:"Earth", 
    server: Server))
  {
    var peopleCF = db.GetColumnFamily("People");
    Assert.IsNotNull(peopleCF);
    Assert.IsNull(db.LastError);
    dynamic jessicakerr = peopleCF.CreateRecord("JessicaKerr");
    jessicakerr.FirstName = "Jessica";
    jessicakerr.LastName = "Kerr";
    jessicakerr.Gender = "F";
    db.Attach(jessicakerr);
    db.SaveChanges();
    Assert.IsNull(db.LastError);
    dynamic result = peopleCF.Get("JessicaKerr").FirstOrDefault();
    Assert.AreEqual(jessicakerr.FirstName, result.FirstName);
    Assert.AreEqual(jessicakerr.LastName, result.LastName);
    Assert.AreEqual(jessicakerr.Gender, result.Gender);
  }
}

Это прекрасно, если бы я заранее знал ключ, но по большей части это не так. По сути, почти всегда точная запись или записи будут неизвестны. Поэтому другой подход (не показан) заключается в интеграции FluentCassandra и LINQ для написания запросов в стиле LINQ. Однако этот вариант не так гибок, как традиционный LINQ. Поскольку имена полей заранее не известны, писать LINQ-запросы для поиска, например, всех Ньюардов в базе данных (проверяя пару «имя-значение» LastName в семействе столбцов) гораздо труднее.

К счастью, CQL спешит на помощь, как показано на рис. 7.

Рис. 7. Использование интеграции между Cassandra и LINQ для написания запроса в стиле LINQ

[TestMethod]
public void StoreAndFetchSomeDataADifferentWay()
{
  using (var db = new CassandraContext(keyspace:"Earth", 
    server: Server))
  {
    var peopleCF = db.GetColumnFamily("People");
    Assert.IsNotNull(peopleCF);
    Assert.IsNull(db.LastError);
    dynamic charlotte = peopleCF.CreateRecord("CharlotteNeward");
    charlotte.FirstName = "Charlotte";
    charlotte.LastName = "Neward";
    charlotte.Gender = "F";
    charlotte.Title = "Domestic Engineer";
    charlotte.RealTitle = "Superwife";
    db.Attach(charlotte);
    db.SaveChanges();
    Assert.IsNull(db.LastError);
    var newards = db.ExecuteQuery("SELECT * FROM People WHERE LastName='Neward'");
    Assert.IsTrue(newards.Count() > 0);
    foreach (dynamic neward in newards)
    {
      Assert.AreEqual(neward.LastName, "Neward");
    }
  }
}

Но заметьте: если я запускаю этот код как есть, он дает ошибку — Cassandra не позволит мне использовать пару «имя-значение» в семействе столбцов как критерий фильтрации, пока не будет явно определен индекс. Это требует еще одного CQL-выражения:

db.ExecuteNonQuery(@"CREATE INDEX ON People (LastName)");

Обычно мне нужно настраивать это в момент создания семейства столбцов. Также обратите внимание на то, что, поскольку Cassandra не использует схемы, часть "SELECT *" этого запроса несколько обманчива: она вернет все пары «имя-значение» в семействе столбцов, но это не означает, что в каждой записи будет присутствовать каждое поле. То есть запрос с "WHERE Gender='F'" никогда не будет учитывать записи, где нет поля Gender, а значит, из рассмотрения выпадут Rick, Ted и «The Artist Formerly Known as Prince». Это полностью отличается от системы управления реляционными базами данных, где каждая запись в таблице должна иметь значения для каждого поля (хотя лично я частенько обхожу это сохранением в полях значения NULL, что считается некоторыми одним из смертных грехов).

Полностью описать здесь язык CQL просто невозможно, поэтому я отсылаю вас на веб-сайт Cassandra, где вы найдете нужную информацию по ссылке bit.ly/MHcWr6.

Я еще не окончательно разделался с проклятой пророчицей: хотя запись и чтение данных в Cassandra — самая интересная часть для разработчиков (ведь именно этим они ежедневно занимаются), многоузловая конфигурация тоже является большой частью сказания о Cassandra. Создать ее на единственном компьютере под управлением Windows (для целей разработки) — задача весьма нетривиальная. Сделать то же самое на множестве серверов гораздо легче.

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

Даже делая скидку на преувеличение и приукрашивание, всего одна десятая часть этого объема (5 Пб, или более 5000 Тб) — весьма увесистый кусок данных. Если быть честным, то на веб-сайте Cassandra (cassandra.apache.org) заявлено: «Самый крупный известный кластер Cassandra включает более 300 Тб данных, распределенных по более чем 400 компьютерам». А это все равно довольно трудно сделать с использованием реляционной базы данных в одной из предопределенных конфигураций.

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

Краткое напоминание по установке

В первой статье из этой серии (msdn.microsoft.com/magazine/jj553519) я прошел весь путь установки Cassandra (местами весьма мучительный) из ZIP-файла и командной строки: убедитесь, что у вас установлена исполняющая среда Java и она указана в переменной окружения PATH, не забудьте сконфигурировать переменную окружения JAVA_HOME, распакуйте дистрибутив Cassandra в какой-либо каталог, а затем запустите файл cassandra.bat из каталога bin, чтобы подготовить и запустить сервер.

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

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

Чтобы всех отыскать…

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

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

Cassandra 1.1 (последняя версия на момент написания этой статьи) хранит всю свою конфигурационную информацию в каталоге /conf. В этом каталоге есть два файла, которые мне нужно отредактировать: log4j-server.properties и cassandra.yaml. Мне также нужно выяснить, куда будут помещаться данные и журналы узлов, поэтому я забегу немного вперед и просто создам два подкаталога в каталоге установки Cassandra. Предполагая, что вы установили Cassandra по пути C:\Prg\apache-cassandra-1.1.0 (как это сделал я), вы предпочтете создать два новых каталога по этому пути — по одному для каждого узла, который вы собираетесь создать: C:\Prg\apache-cassandra-1.1.0\node1 и \node2.

В этих двух каталогах скопируйте содержимое каталога /conf, что приведет к размещению нужных вам двух файлов. Вам также понадобится скопировать файл cassandra.bat из /bin. Это третий и последний файл, в который потребуется внести изменения, чтобы сообщить Cassandra местоположение конфигурационных файлов, необходимых ей для запуска.

Ну разве не забавно возиться со всеми этими причиндалами Java?

Первый файл, log4j-server.properties, является конфигурационным файлом для проекта протоколирования диагностической информации с открытым исходным кодом — log4j. (Java использует файлы с расширением .properties во многом аналогично тому, как в Windows когда-то применялись файлы с расширением .ini.) Ваш главный интерес здесь — убедиться, что каждый узел Cassandra пишет файл журнала диагностики в место, отличное от тех, которыми пользуются другие узлы. Лично мне нужно, чтобы все данные для каждого узла находились в этих каталогах \node1 и \node2, поэтому я нахожу следующую строку в log4j-server.properties:

log4j.appender.R.File=/var/log/cassandra/system.log

Я хочу изменить ее так, чтобы она была больше похожа на стиль Windows и указывала \node1:

log4j.appender.R.File=C:/Prg/apache-cassandra-1.1.0/node1/
  log/system.log

Каталог log не существует до запуска Cassandra — она создаст его сама, если такого каталога нет. Кстати, убедитесь, что слеши прямые. Java распознает и прямые и обратные слеши, но в синтаксисе файлов .properties обратные слеши используются как escape-последовательности символов — примерно так, как в строках C#.

Далее нужно открыть файл cassandra.yaml, куда мы внесем следующий набор изменений. Синтаксис «.yaml» расшифровывается как «Yet Another Markup Language», и — да, вы правильно догадались — это еще один синтаксис конфигурационных файлов в стиле .ini. В Java никогда не заморачивались стандартизацией подобных вещей, поэтому такое смешение нескольких стилей конфигурационных файлов в одном проекте — вполне обычное дело (как в Cassandra).

Вам нужно изменить несколько параметров здесь; они разбросаны по всему файлу (который, кстати, заполнен просто тонной комментариев, и поэтому все достаточно понятно, если прочитать их полностью):

cluster_name: 'Test Cluster'
data_file_directories:
  - /var/lib/cassandra/data
commitlog_directory: /var/lib/cassandra/commitlog
saved_caches_directory: /var/lib/cassandra/saved_caches
listen_address: localhost
rpc_address: localhost

Параметр cluster_name не обязателен, но все равно неплохо изменить его, возможно, на нечто вроде «MyCluster» или «Big Cluster O Fun». Но остальные параметры вы должны обязательно изменить. Записи directories должны указывать на каталоги \node1 и \node2 соответственно.

Воедино созвать…

Последние два параметра необходимо изменить по другим причинам. Cassandra, как вы помните, инстинктивно хочет выполнять по одному сервису на каждом компьютере, поэтому она полагает, что достаточно привязать TCP/IP-сокет к localhost. Но, если вы собираетесь выполнять на одном компьютере два и более сервисов, такой вариант не прокатит. Поэтому вы должны сообщить ей о необходимости привязки к адресам, которые в конечном счете будут разрешаться как один и тот же компьютер, хотя у этих адресов могут быть разные значения. К счастью, достаточно явным образом указать 127.0.0.1 для node1, 127.0.0.2 для node2 и т. д.

(Возможно, вы спрашиваете, почему это работает; ответ выходит за рамки статьи, но объяснение этому вы найдете в любом нормальном справочнике по TCP/IP. Если не верите, попробуйте ввести на своем компьютере ping 127.0.0.1 и ping 127.0.0.2. Оба адреса будут нормально разрешены. А если вам не нравится указывать значения, всегда можно присвоить им имена в вашем файле hosts, который находится в каталоге C:\Windows\System32\drivers\etc.)

Причина того, что Cassandra нуждается в формировании этой сетевой конфигурации, отчасти вызвана тем, что она будет распознавать кольцо, сначала подключаясь к «начальному» узлу («seed» node), который потом сообщит этому экземпляру о других узлах в кольце. Все это часть протокола gossip, используемого для распространения важной информации по кольцу. Если бы мы настраивали кольцо на работу на разных машинах, Cassandra понадобилось бы, чтобы конфигурационный параметр для начального узла указывал на некий выполняемый узел, но в данном случае все узлы находятся на одной машине, и адрес по умолчанию — 127.0.0.1 — работает, как надо.

После всех изменений файл cassandra.yaml в \node1 должен выглядеть так:

cluster_name: 'Test Cluster'
data_file_directories:
  - C:/Prg/apache-cassandra-1.1.0/node1/data
commitlog_directory: C:/Prg/apache-cassandra-1.1.0/
  node1/commitlog
saved_caches_directory: C:/Prg/apache-cassandra-1.1.0/
  node1/saved_caches
listen_address: localhost
rpc_address: localhost

А для \node2 этот файл должен выглядеть следующим образом:

cluster_name: 'Test Cluster'
data_file_directories:
  - C:/Prg/apache-cassandra-1.1.0/node2/data
commitlog_directory: C:/Prg/apache-cassandra-1.1.0/
  node2/commitlog
saved_caches_directory: C:/Prg/apache-cassandra-1.1.0/
  node2/saved_caches
listen_address: 127.0.0.2
rpc_address: 127.0.0.2

Наконец, Cassandra нужно сообщить, где искать конфигурационные файлы при запуске, и обычно она проверяет для этого переменную окружения CLASSPATH, добавляемую Java (что смутно напоминает механизм разрешения сборок в .NET Framework, но, если сказать безо всяких обиняков, отстает в своем развитии примерно на пяток лет). Кроме того, ей нужно предоставлять некоторую информацию для управления и мониторинга в JMX (Java-эквивалент PerfMon или Windows Management Instrumentation) через какой-нибудь TCP/IP-порт, а два сервиса не могут использовать один и тот же порт. Таким образом, последние изменения нужны внести в cassandra.bat:

REM Убедитесь, что при запуске не применяются
REM пользовательские переменные CLASSPATH
set CLASSPATH="%CASSANDRA_HOME%\node1"

А для cassandra.bat в \node2:

REM Убедитесь, что при запуске не применяются
REM пользовательские переменные CLASSPATH
set CLASSPATH="%CASSANDRA_HOME%\node2"

Также в файле для \node2 замените значение вместо исходного 7199 на указанное ниже:

-Dcom.sun.management.jmxremote.port=7299^

Ну разве я не говорил, что Java — забавная штуковина?

…И единою черною волей сковать

После того как мы разделались с конфигурациями, начинается самое интересное. Запустите окно командной строки (то, в котором переменные окружения JAVA_HOME и CASSANDRA_HOME указывают на корневой каталог JDK и каталог установки Cassandra) и смените каталог на \node1. Введите cassandra –f в командной строке и наблюдайте за выводом диагностической информации. Это первый экземпляр, и, если все конфигурационные параметры правильны (нет опечаток), вы должны увидеть пролетающие строки текста с заключительной фразой «Listening for thrift clients…».

Теперь во втором окне командной строки перейдите в \node2 и сделайте то же самое. На этот раз при запуске операции через некоторое время вы заметите активность и в первом окне с \node1. После того как экземпляр \node2 подготавливается к работе и запускается, он соединяется с экземпляром \node1 (с «зародышем»), а затем они в основном настраивают друг друга, чтобы начать совместно работать в кольце. В частности, ищите две строки «JOINING: waiting for ring and schema information» и «Node /127.0.0.1 is now part of the cluster» в окне \node2, а в окне \node1 — «Node /127.0.0.2 is now part of the cluster» и «InetAddress /127.0.0.2 is now UP».

Но, если вы не увидели эти сообщения, Cassandra припасла для вас еще один сюрприз. В третьем окне командной строки перейдите в исходный каталог \bin для Cassandra и введите команду nodetool ring –h 127.0.0.1. После этого вы должны увидеть нечто вроде того, что показано на рис. 8.

Рис. 8. Два экземпляра Cassandra, каждый из которых владеет 50% данных

Это по-настоящему захватывающе, потому что, как видно в столбце Owns, два экземпляра Cassandra уже выяснили, что каждый из них должен владеть 50% данных, причем без вашего участия. Великолепно!

Самое лучшая часть в том, что, если вы запустите код из первой половины этой статьи, данные будут распределены по кластеру без дополнительных изменений.

Это дополнение, а не замена

Как и некоторые другие исследованные в этой рубрике СУБД (MongoDB и SQLite), Cassandra не следует рассматривать как полную замену реляционной базе данных, а скорее как дополняющую технологию, которую можно применять там, где функционал реляционной базы данных не слишком хорошо справляется с работой (например, кеширование или хранение сильно неструктурированных наборов данных) или где нужна гибридная система в комбинации с реляционной базой данных. Скажем, некая компания могла бы хранить «фиксированный» набор элементов данных в реляционной базе данных и включать в одно из ее полей ключ Cassandra, чтобы иметь возможность извлекать остальные, неструктурированные данные. Тогда реляционная база данных могла бы оставаться структурированной и реляционной (подчиняясь большинству или всем правилам такой СУБД), но система в целом обеспечивала бы гибкость в хранении дополнительных, заранее непредсказуемых элементов данных, которые пользователям, похоже, всегда хочется добавить в систему по мере ее взросления.

В качестве другого примера возьмите статистические данные по заходам на веб-страницу, где можно было бы легко отслеживать миллионы и миллиарды элементов данных. Создать здесь сервис сокращения URL (вроде bit.ly) было бы тривиальной задачей, так как минимизированный URL-путь (элемент «foobar» в) был бы ключом, а статистические данные, равно как необязательное описание и даже, возможно, периодические снимки перенаправляемого URL, были бы информацией для хранения в Cassandra. Ну и так далее.

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

Удачи в кодировании!


Тэд Ньюард (Ted Neward)консультант по архитектуре в компании Neudesic LLC. Автор и соавтор многочисленных книг, в том числе «Professional F# 2.0» (Wrox, 2010), более сотни статей, часто выступает на многих конференциях по всему миру; кроме того, имеет звание Microsoft MVP в области C#. С ним можно связаться по адресу ted@tedneward.com, если вы заинтересованы в сотрудничестве с его группой. Также читайте его блог blogs.tedneward.com и заметки на Twitter.com/tedneward.

Выражаю благодарность за рецензирование статьи эксперту Келли Соммерсу (Kelly Sommers).