Pull to refresh

WTF is a SuperColumn? Введение в модель данных Cassandra

Reading time17 min
Views11K
Original author: Arin Sarkissian
Это перевод статьи, датированной 1м сентября 2009 года, следует это учесть при прочтении. — прим. пер.

В последний месяц или два команда инженеров Digg потратила совсем немного времени на изучение, тестирование и окончательное внедрение Cassandra в продакшен. Это был очень веcёлый проект, но до того, как веселье началось, нам пришлось потратить какое-то время на выяснение того, что же представляет собой модель данных Cassandra… фраза «WTF is a «super column»» («что за фигня этот суперстолбец?») была произнесена не один раз.

Если вы работали ранее с РСУБД (это касается почти всех), вы вероятно будете немного обескуражены некоторыми названиями при изучении модели данных Cassandra. Мне и моей команде в Digg потребовалось несколько дней обсуждений, прежде чем мы «врубились». Пару недель назад в списке рассылки разработчиков шёл процесс bikeshed-а на тему полностью новой схемы именования для разрешения неразберихи. На всём протяжении дискуссии я думал: «может, если будет несколько нормальных примеров, люди не будут так смущены названиями». Так, это моя попытка объяснения модели данных Cassandra; она предназначена для того, чтобы вы ознакомились, но не уходили в дебри, и, надеюсь, это поможет прояснить некоторые вещи.


Кусочки


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

Столбец

Столбец (Column) — это минимальный элемент данных. Это триплет, содержащий имя, значение и метку времени. Столбец, представленный в обозначениях JSON:
{
  name: "emailAddress", // имя
  value: "arin@example.com", // значение
  timestamp: 123456789 // метка времени
}

Это всё. Для простоты можно опустить метку времени. И воспринимать столбец как пару имя/значение. Также, стоит отметить, что и имя, и значение бинарны (технически byte[]) и могут быть любой длины.

СуперСтолбец

Суперстолбец (SuperColumn) — это совокупность бинарного имени и значения, которое по сути является таблицей, содержащей неограниченное число столбцов, с ключом — именем столбца. Снова представим это в виде JSON:
{
  name: "homeAddress",
  // неограниченное число столбцов:
  value: {
    // ключ - это имя столбца
    street: {name: "street", value: "1234 x street", timestamp: 123456789},
    city: {name: "city", value: "san francisco", timestamp: 123456789},
    zip: {name: "zip", value: "94107", timestamp: 123456789},
  }
}

Столбец против СуперСтолбца

И столбцы, и суперстолбцы — это пары имени и значения. Ключевое различие в том, что значение обычного столбца — это «строка», а значение суперстолбца — это таблица столбцов. Это главное различие. Их значения содержат разные типы данных. Другое незначительное различие в том, что суперстолбец не содержит метку времени.

Перед тем, как начнём совмещать

Прежде чем идти дальше, я хочу упростить наши обозначения двумя вещами: 1) распрощаться с метками времени в столбцах и 2) вытащить имена столбцов и суперстолбцов наружу, так что это будет выглядеть как пара ключ/значение. Таким образом, мы перейдём от:
{
  name: "homeAddress",
  value: {
    street: {name: "street", value: "1234 x street", timestamp: 123456789},
    city: {name: "city", value: "san francisco", timestamp: 123456789},
    zip: {name: "zip", value: "94107", timestamp: 123456789},
  }
}
к
homeAddress: {
  street: "1234 x street",
  city: "san francisco",
  zip: "94107",
}


Группируем их


Есть структура, используемая для группировки и столбцов, и суперстолбцов…эта структура называется семейством столбцов (ColumnFamily) и существует соответственно в двух вариациях — обычная и супер.

Семейство столбцов

Семейство столбцов — это структура, содержащая неограниченное число строк. Ого, ты сказал строк? Да — строк :) Чтобы это проще уложилось в голове, просто думайте о них, как о строках таблицы в РСУБД.

Итак, каждая строка имеет установленный клиентом (вами) ключ и содержит набор столбцов. Повторюсь, ключи в наборе — это имена столбцов и значения — это сами столбцы:
UserProfile = {
 phatduckk: { // это ключ строки внутри семейства столбцов
  // у нас есть неограниченное число столбцов в этой строке
  username: "phatduckk",
  email: "phatduckk@example.com",
  phone: "(900) 976-6666"
 }, // конец строки
 ieure: { // это ключ другой строки внутри семейства столбцов
  // у нас есть неограниченное число столбцов и в этой строке тоже
  username: "ieure",
  email: "ieure@example.com",
  phone: "(888) 555-1212"
  age: "66",
  gender: "undecided"
 },
}

Помните: для простоты мы показываем только значение столбца, но на самом деле значения в наборе — это целый столбец.

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

Хочу заострить ваше внимание на том, что на этом уровне нет никакой обязательной схемы. У строк нет предопределённого списка столбцов, которые они содержат. В нашем примере выше вы видите, что строка с ключом «ieure» содержит столбцы с именами «age» и «gender», тогда как строка, идентифицируемая ключом «phatduckk» не содержит. Это стопроцентная гибкость: одна строка может содержать 1989 столбцов, тогда как в другой будет всего лишь 2. Одна строка может содержать столбец с именем «foo», тогда как все остальные не будут. Вот она — перспектива отсутствия схемы в Cassandra.

Семейство столбцов тоже может быть супер

Итак, семейство столбцов может быть типа Standard или Super.

То, что мы рассмотрели выше — это был пример типа Standard. Стандартным он является потому, что все его строки содержат таблицу обычных (не супер) столбцов… там нет суперстолбцов.

Если же семейство столбцов имеет тип Super, то напротив: каждая строка содержит набор суперстолбцов. Набор, где ключами являются имена суперстолбцов, а значениями — сами суперстолбцы. И, просто для ясности: семейство суперстолбцов не содержит обычных столбцов. Вот пример:
AddressBook = {
 phatduckk: { // это ключ строки внутри семейства суперстолбцов
  // ключ - имя владельца адресной книги

  // у нас есть неограниченное число суперстолбцов в этой строке
  // ключи внутри строки - это имена суперстолбцов
  // каждый из этих суперстолбцов - это запись в адресной книге
  friend1: {street: "8th street", zip: "90210", city: "Beverley Hills", state: "CA"},

  // это запись для John'а в адресной книге phatduckk'а
  John: {street: "Howard street", zip: "94404", city: "FC", state: "CA"},
  Kim: {street: "X street", zip: "87876", city: "Balls", state: "VA"},
  Tod: {street: "Jerry street", zip: "54556", city: "Cartoon", state: "CO"},
  Bob: {street: "Q Blvd", zip: "24252", city: "Nowhere", state: "MN"},
  ...
  // у нас может быть неограниченное число суперстолбцов в этой строке
 }, // конец строки
 ieure: { // это ключ другой строки внутри семейства суперстолбцов
  joey: {street: "A ave", zip: "55485", city: "Hell", state: "NV"},
  William: {street: "Armpit Dr", zip: "93301", city: "Bakersfield", state: "CA"},
 },
}

Пространство ключей

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

Итак, пространство ключей может содержать несколько семейств столбцов, но это не значит, что они как-то будут зависеть друг от друга. Например, их нельзя JOIN'ить, как таблицы в MySQL. Также, только потому, что ColumnFamily_1 содержит строку с ключом «phatduckk», это не значит, что ColumnFamily_2 тоже её содержит.

Сортировка


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

Столбцы внутри строк всегда отсортированы по имени столбца. Это важно, так что повторю: столбцы всегда сортируются по имени! Как именно сравниваются имена зависит от параметра CompareWith семейства столбцов. По-умолчанию у вас есть следующие варианты: BytesType, UTF8Type, LexicalUUIDType, TimeUUIDType, AsciiType, и LongType. Каждый из этих вариантов рассматривает имена столбцов как различные типы данных, обеспечивая некоторую гибкость. Например: использование LongType будет трактовать имена столбцов как 64-битные целые числа. Давайте попробуем и проясним это, взглянув на данные до и после сортировки:
// Это список всех столбцов из одной строки, в случайном порядке
// Cassandra "никогда" не хранит данные в случайном порядке. Это просто пример
// Также, можно игнорировать значения - они не играют вообще никакой роли в сортировке

{name: 123, value: "hello there"},
{name: 832416, value: "kjjkbcjkcbbd"},
{name: 3, value: "101010101010"},
{name: 976, value: "kjjkbcjkcbbd"}

Так, учитывая, что мы используем вариант LongType, эти столбцы будут выглядеть так после сортировки:
<!-- определение семейства столбцов в storage-conf.xml -->
<ColumnFamily CompareWith="LongType" Name="CF_NAME_HERE"/>
// Заметьте, столбцы рассматриваются как целые числа
// фактически, наши столбцы идут в числовом порядке
{name: 3, value: "101010101010"},
{name: 123, value: "hello there"},
{name: 976, value: "kjjkbcjkcbbd"},
{name: 832416, value: "kjjkbcjkcbbd"}

Как видите, имена столбцов сравнивались, как если бы они были 64-битными целыми. Если бы мы сейчас использовали другой вариант CompareWith, мы бы получили другой результат. Если бы мы установили CompareWith как UTF8Type, имена столбцов трактовались бы как строки в кодировке UTF8 и образовали такой порядок:
<!-- определение семейства столбцов в storage-conf.xml -->
<ColumnFamily CompareWith="UTF8Type" Name="CF_NAME_HERE"/>
// имена столбцов рассматриваются как строки в кодировке UTF8
{name: 123, value: "hello there"},
{name: 3, value: "101010101010"},
{name: 832416, value: "kjjkbcjkcbbd"},
{name: 976, value: "kjjkbcjkcbbd"}

Совершенно другой результат!

Этот принцип сортировки применим и к суперстолбцам, но у нас появляется ещё одно измерение, с которым надо работать: мы определяем не только то, как должны сортироваться суперстолбцы, но также и то, как должны сортиваться столбцы внутри суперстолбцов. Сортировка столбцов внутри суперстолбцов определяется значением параметра CompareSubcolumnsWith. Вот пример:
// Это строка с двумя суперстолбцами в ней
// в данный момент они в случайном порядке

{ // первый суперстолбец в строке
 name: "workAddress",
 // и столбцы в нём
 value: {
  street: {name: "street", value: "1234 x street"},
  city: {name: "city", value: "san francisco"},
  zip: {name: "zip", value: "94107"}
 }
},
{ // другой суперстолбец в той же строке
 name: "homeAddress",
 // и столбцы в нём
 value: {
  street: {name: "street", value: "1234 x street"},
  city: {name: "city", value: "san francisco"},
  zip: {name: "zip", value: "94107"}
 }
}

Теперь, если мы решим установить для CompareSubcolumnsWith и CompareWith значение UTF8Type, мы получим следующий результат:
// Теперь они отсортированы

{
 name: "homeAddress",
 value: {
  city: {name: "city", value: "san francisco"},
  street: {name: "street", value: "1234 x street"},
  zip: {name: "zip", value: "94107"}
 }
},
{
 name: "workAddress",
 value: {
  city: {name: "city", value: "san francisco"},
  street: {name: "street", value: "1234 x street"},
  zip: {name: "zip", value: "94107"}
 }
}

Хочу заметить, что в последнем примере CompareSubcolumnsWith и CompareWith оба установлены в UTF8Type, но это не обязательно. Вы можете сочетать значения параметров CompareSubcolumnsWith и CompareWith как вам угодно.

И последнее, о чём я хочу упомянуть в связи с сортировкой — это то, что вы можете написать собственный класс для выполнения сортировки. Сортирующий механизм подключается независимо… вы можете установить для CompareSubcolumnsWith и/или CompareWith любое подходящее имя класса, как только этот класс будет реализовывать интерфейс org.apache.cassandra.db.marshal.IType (то есть, вы можете создать свою схему сравнения для сортировки).

 

Пример схемы


Ладно, теперь у нас есть все кусочки паззла, так что давайте соберём их вместе и смоделируем простое приложение для блога. Будем моделировать приложение со следующими спецификациями:
  • поддержка одного блога
  • может быть несколько авторов
  • записи содержат заголовок, тело, уникальную метку и дату публикации
  • записи могут быть ассоциированы с любым чисом тегов
  • люди могут оставлять комментарии, но не могут регистрироваться: они вводят информацию о себе каждый раз заново (просто упрощаем)
  • комментарии содержат текст, время, когда были оставлены, и имя комментатора
  • должна быть возможность показать все посты в порядке, обратном хронологическому (самое новое вверху)
  • должна быть возможность показать все посты по тегу в порядке, обратном хронологическому
Каждый из следующих разделов будет описывать семейство столбцов, которое мы будем определять в пространстве ключей нашего приложения, показывать определение в xml, говорить, почему мы выбрали тот или иной вариант(ы) сортировки, а так же показывать данные семейства столбцов в виде JSON.

Семейство столбцов Authors

Моделирование семейства столбцов авторов — это достаточно базово; мы не будем здесь делать ничего крутого. Мы назначим каждому автору по строке и ключу и это будет полное имя автора. Каждый столбец в строке будет представлять собой определённый параметр профиля автора.

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

Мы будем получать строки из нашего семейства столбцов с помощью ключа и выбирать все столбцы для каждой строки (то есть, например, мы не будем выбирать только первые 3 столбца из строки с ключом «foo»). Это означает, что для нас не имеет значения, как будут отсортированы столбцы, так что будем использовать сортировку BytesType, потому что она не требует никакой валидации для имён столбцов.
<!--
  ColumnFamily: Authors
  Мы будем хранить тут данные об авторах.

  Ключ строки => имя автора (подразумевается, что имена уникальны)
  Имя столбца: параметр записи (email, bio и т.д.)
  Значение столбца: соответствующее значение параметра

  Выборка: получение автора по имени (выбираем все столбцы из соответствующей имени строки)

  Authors : { // семейство столбцов
    Arin Sarkissian : { // ключ строки
      // столбцы, параметры профиля
      numPosts: 11,
      twitter: phatduckk,
      email: arin@example.com,
      bio: "bla bla bla"
    },
    // и другие авторы
    Author 2 {
      ...
    }
  }
-->
<ColumnFamily CompareWith="BytesType" Name="Authors"/>

Семейство столбцов BlogEntries

И снова семейство столбцов будет вести себя как простое хранилище ключ/значение. Мы будем хранить одну запись в одной строке. Столбцы в строках будут служить параметрами записи: заголовок, тело, и т.д. (так же как и в предыдущем примере). Небольшой оптимизацией мы денормализуем теги в один столбец как строку, разделённую запятыми. На выводе мы будем разбивать значение столбца, чтобы получить список тегов.

Ключом каждой строки будет уникальная метка (slug). Так что, для выборки единственной записи мы будем искать её по этой метке.
<!--
  ColumnFamily: BlogEntries
  Все записи в блоге будут храниться тут.

  Ключ строки => уникальная метка поста (она будет использоваться в урлах)
  Имя столбца: параметр записи (заголовок, тело, и т.д.)
  Значение столбца: соответствующее значение параметра

  Выборка: получение записи по уникальной метке (всегда выбираем все столбцы в строке)

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

  BlogEntries : { // семейство столбцов
    i-got-a-new-guitar : { // ключ строки - уникальная метка записи (slug)
      title: This is a blog entry about my new, awesome guitar,
      body: this is a cool entry. etc etc yada yada
      author: Arin Sarkissian // ключ строки в семействе столбцов Authors
      tags: life,guitar,music
      pubDate: 1250558004   // дата публикации в формате unixtime
      slug: i-got-a-new-guitar
    },
    // другие записи
    another-cool-guitar : {
      ...
      tags: guitar,
      slug: another-cool-guitar
    },
    scream-is-the-best-movie-ever : {
      ...
      tags: movie,horror,
      slug: scream-is-the-best-movie-ever
    }
  }
-->
<ColumnFamily CompareWith="BytesType" Name="BlogEntries"/>

Семейство столбцов TaggedPosts

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

Особенность решения, которую я хочу отметить, в том, что логика нашего приложения должна прикреплять к каждой записи BlogEntry тег "__notag__" (я его только что придумал). Такой тег позволит нам использовать это семейство стобцов также и для хранения списка всех записей в блоге в отсортированном виде. Это небольшой трюк, который даст возможность использовать только одно семейство столбцов для двух выборок: «показать все последние посты» и «показать все последние посты с тегом `foo`».

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

Так как мы решили, что будем отображать списки записей в хронологическом порядке, нам надо сделать так, чтобы имена столбцов были типа TimeUUID и установить для параметра CompareWith значение TimeUUIDType. Это отсортирует столбцы по времени. Так что использование запросов типа «получить последние 10 записей с тегом `foo`» будет очень эффективной операцией.

Теперь, когда мы захотим отобразить последние 10 записей (на главной, например), нам нужно будет:
  1. взять последние 10 столбцов по ключу "__notag__" (тег «все посты»)
  2. пройтись циклом по этому набору столбцов
  3. в цикле, мы знаем, что значение каждого столбца — это ключ строки в семействе столбцов BlogEntries
  4. так что мы используем этот ключ, чтобы получить строку для этой записи из семейства столбцов BlogEntries. так мы получаем все данные о записи
  5. один из столбцов в строке BlogEntries назван author и его значение — это ключ в семействе столбцов Authors, и мы используем его, чтобы получить данные профиля автора
  6. итак, у нас есть данные поста и данные автора
  7. дальше мы разбиваем столбец с тегами, чтобы получить список тегов
  8. теперь у нас есть всё, чтобы отобразить этот пост (пока без комментариев — это страница списка постов, а не конкретного поста)

Мы можем проделать эту процедуру используя любой тег… она работает и для «всех записей», и для «записей с тегом `foo`». Вроде неплохо.
<!--
  ColumnFamily: TaggedPosts
  Вспомогательный индекс для определения, какие записи в BlogEntries соответствуют тегу

  Ключ строки => тег
  Имена столбцов: TimeUUIDType
  Значения столбцов: ключ строки в семействе столбцов BlogEntries

  Выборка: получение среза записей с тегом "foo"

  Мы используем это семейство столбцов чтобы определить, какие записи блога показывать по тегу
  Мы будем чуточку гетто и используем строку __notag__, подразумевая "тег не имеет значения".
  Каждой записи будет соответствовать столбец в этой строке...
  Это значит, что у нас будет "кол-во тегов + 1" столбцов на каждый пост.

  TaggedPosts : { // семейство столбцов
    // записи блога с тегом "guitar"
    guitar : {
      timeuuid_1 : i-got-a-new-guitar,
      timeuuid_2 : another-cool-guitar,
    },
    // все записи в блоге
    __notag__ : {
      timeuuid_1b : i-got-a-new-guitar,

      // заметьте, что такой столбец есть и в строке "guitar"
      timeuuid_2b : another-cool-guitar,

      // а такой - в строке "movie"
      timeuuid_2b : scream-is-the-best-movie-ever,
    },
    // записи блога с тегом "movie"
    movie: {
      timeuuid_1c: scream-is-the-best-movie-ever
    }
  }
-->
<ColumnFamily CompareWith="TimeUUIDType" Name="TaggedPosts"/>

Семейство столбцов Comments

Последнее, что мы должны выяснить — как смоделировать комменты. И тут, наконец, нам понадобятся суперстолбцы.

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

Итак, это довольно просто до сих пор… ничего сверхъестесственного.
<!--
  ColumnFamily: Comments
  Здесь мы храним комментарии

  Ключ строки => ключ строки в BlogEntries
  Имя суперстолбца: TimeUUIDType

  Выборка: получение всех комментариев к записи

  Comments : {
    // комментарии для scream-is-the-best-movie-ever
    scream-is-the-best-movie-ever : {
      // вначале старые комментарии
      timeuuid_1 : { // имя суперстолбца
        // все столбцы в суперстолбце - данные комментария
        commenter: Joe Blow,
        email: joeb@example.com,
        comment: you're a dumb douche, the godfather is the best movie ever
        commentTime: 1250438004
      },

      ... ещё комментарии к scream-is-the-best-movie-ever

      // в конце - последние комментарии
      timeuuid_2 : {
        commenter: Some Dude,
        email: sd@example.com,
        comment: be nice Joe Blow this isnt youtube
        commentTime: 1250557004
      },
    },

    // комментарии для i-got-a-new-guitar
    i-got-a-new-guitar : {
      timeuuid_1 : {
        commenter: Johnny Guitar,
        email: guitardude@example.com,
        comment: nice axe dawg...
        commentTime: 1250438004
      },
    }

    ..
    // ещё строки для других записей
  }
-->
<ColumnFamily CompareWith="TimeUUIDType" ColumnType="Super"
  CompareSubcolumnsWith="BytesType" Name="Comments"/>


Woot!


Это всё. Наше маленькое приложение блога смоделировано и готово к эксплуатации. Совсем немного переварить, и окончите вы с очень маленьким куском XML в вашем storage-conf.xml:
<Keyspace Name="BloggyAppy">
 <!-- ... -->
 <!-- CF definitions -->
 <ColumnFamily CompareWith="BytesType" Name="Authors"/>
 <ColumnFamily CompareWith="BytesType" Name="BlogEntries"/>
 <ColumnFamily CompareWith="TimeUUIDType" Name="TaggedPosts"/>
 <ColumnFamily CompareWith="TimeUUIDType" Name="Comments"
  CompareSubcolumnsWith="BytesType" ColumnType="Super"/>
</Keyspace>

Теперь всё, что вам нужно сделать — разобраться, как записывать и считывать данные из Cassandra. Это можно осуществлять с помощью Thrift Interface. На wiki-странице API Cassandra сделана приличная работа по объяснению, как с этим работать, так что я не буду вдаваться во все эти детали. Но, вообще, вы можете просто скомпилировать файл cassandra.thrift и использовать сгенерированный код для доступа к API. Также вы можете воспользоваться преимуществами клиента Ruby или клиента для Python.

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

От переводчика: старался перевести максимально близко к оригиналу. Надеюсь на конструктивную критику.

Update: спасибо Honeyman за исправление и ссылку


24.08.2010: Перенесено в блог NoSQL
Tags:
Hubs:
+58
Comments23

Articles