Пример моделирования схемы в Cassandra 2.0 на CQL3

  • Tutorial
В предыдущей статье я доходчиво рассказал как Cassandra хранит данные. Настоятельно рекомендую хотя бы пробежаться глазами. В этой статье мы создадим простенькую БД, чтобы использовать её в следующей статье, которая будет полностью посвящена выборке/поиску данных.

Задача


Допустим у нас есть ad network, который откручивает рекламу. Люди кликают на баннеры, заказчик рекламы платит, мы (сеть), реселлеры (распространители) и хостеры рекламного места имеем на этом доход. Реселлеры рекламного места работают за 20%. Этот процент растёт из-за различных факторов, самое главное, что он не постоянен и новый процент может применяться, например, на клики месячной давности.

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


Disclaimer

  • Статья НЕ служит единственно верным путеводителем для моделирования ad network задач.
  • Каждый конкретный случай требует своего конкретного моделирования. Серебряной пули не существует.


Ликбез


  • Cassandra (далее C*) — распределённая NoSQL БД.
  • cqlsh — коммандная строка для C* CQL.
  • CQL — SQL-подобный язык запросов. Аббревиатура от Cassandra Query Language.
  • Keyspace — аналог базы данных в реляционных СУБД.
  • Основная единица хранения — строка. Строка может динамически раширяться до 2 миллиардов колонок. Это важно.
  • Главный ключ (Primary Key) — уникальный идентификатор строки. Обязан быть у каждой строки. Именно он используется для поиска той ноды, в которой хранится строка, а также для поиска внутри самой строки.
  • Распределительный ключ — первая часть главного ключа.
  • Кластерный ключ — вторая часть главного ключа.
  • Нода (node) — java процесс Кассандры. Все ноды равнозначны. Нет master-slave зависимостей.
  • Кластер — несколько нод работающих друг с другом как единое целое.
  • Во время записи нет возможности прочитать текущее значение записываемой ячейки (это почти правда).

Давайте считать, что у нас 6 нод.

cqlsh

Запустим cqlsh.
Connected to Test Cluster at localhost:9160.
[cqlsh 4.1.0 | Cassandra 2.0.2 | CQL spec 3.1.1 | Thrift protocol 19.38.0]
cqlsh>

Создадим keyspace

Создадим keyspace (базу данных).
CREATE KEYSPACE ad_network WITH replication = {
  'class': 'SimpleStrategy',
  'replication_factor': '3'
};

USE ad_network;

replication_factor — это количество нод, которые будут хранить строку.

Создаём модель



Таблица reseller

Создадим таблицу реселлеров и заполним данными. В таблице будем хранить историю изменений процентной ставки реселлера.
CREATE TABLE reseller (
  id text,
  effective_since text, -- day in the format of 'YYYY-MM-DD'
  reward_percent float, -- value from 0.0 to 1.0
  PRIMARY KEY (id, effective_since) -- распределительный ключ (id) и кластерный ключ (effective_since)
)
WITH CLUSTERING ORDER BY (effective_since DESC); -- обратная сортировка для LIMIT 1 запросов

INSERT INTO reseller (id, effective_since, reward_percent) VALUES ('supaboobs', '2011-02-13', 0.2);
INSERT INTO reseller (id, effective_since, reward_percent) VALUES ('supaboobs', '2012-01-22', 0.25);
INSERT INTO reseller (id, effective_since, reward_percent) VALUES ('supaboobs', '2013-11-30', 0.3);

Кажется, что мы создали три строки. Но те, кто читал предыдущую статью, знают, что мы создали одну строку с распределительным ключом 'supaboobs' и тремя кластерными ключами: '2011-02-13', '2012-01-22' и '2013-11-30'. Эта строка, и все последующие, будет храниться на трёх из наших шести нод.

Посмотрим содержимое:
cqlsh:ad_network> SELECT * FROM reseller WHERE id='supaboobs';

 id        | effective_since | reward_percent
-----------+-----------------+----------------
 supaboobs |      2013-11-30 |            0.3
 supaboobs |      2012-01-22 |           0.25
 supaboobs |      2011-02-13 |            0.2


В дальнейшем, когда нам понадобится текущая процентная ставка, мы выполним следующее:
cqlsh:ad_network> SELECT * FROM reseller WHERE id = 'supaboobs' LIMIT 1;

 id        | effective_since | reward_percent
-----------+-----------------+----------------
 supaboobs |      2013-11-30 |            0.3


Таблица ad_click

В этой будем хранить клики на наши баннеры.
Колонки: ID реселлера, день (для ускорения поиска), дата+время клика, ID баннера, полная стоимость клика.
CREATE TABLE ad_click (
  reseller_id text,
  day text, -- day in the format of 'YYYY-MM-DD'
  time timestamp,
  ad_id text,
  amount float,
  PRIMARY KEY ((reseller_id, day), time, ad_id) -- распределительный ключ (reseller_id, day) и кластерные ключи (time, ad_id)
)
WITH CLUSTERING ORDER BY (time DESC); -- обратная сортировка данных в строке

Добавим немного данных.
INSERT INTO ad_click (reseller_id, day, time, ad_id, amount) VALUES ('supaboobs', '2013-11-28', '2013-11-28 02:16:52', '890_567_234', 0.005);
INSERT INTO ad_click (reseller_id, day, time, ad_id, amount) VALUES ('supaboobs', '2013-11-28', '2013-11-28 07:17:35', '890_567_234', 0.005);
INSERT INTO ad_click (reseller_id, day, time, ad_id, amount) VALUES ('supaboobs', '2013-11-29', '2013-11-29 17:18:51', '890_567_211', 0.0075);
INSERT INTO ad_click (reseller_id, day, time, ad_id, amount) VALUES ('supaboobs', '2013-11-29', '2013-11-29 22:20:37', '890_567_211', 0.0075);
INSERT INTO ad_click (reseller_id, day, time, ad_id, amount) VALUES ('supaboobs', '2013-11-30', '2013-11-30 11:21:56', '890_567_234', 0.005);
INSERT INTO ad_click (reseller_id, day, time, ad_id, amount) VALUES ('supaboobs', '2013-12-01', '2013-12-01 12:21:59', '890_567_010', 0.01);

Посмотрим на них.
cqlsh:ad_network> SELECT * FROM ad_click;

 reseller_id | day        | time                | ad_id       | amount
-------------+------------+---------------------+-------------+--------
   supaboobs | 2013-12-01 | 2013-12-01 12:21:59 | 890_567_010 |   0.01
   supaboobs | 2013-11-30 | 2013-11-30 11:21:56 | 890_567_234 |  0.005
   supaboobs | 2013-11-28 | 2013-11-28 07:17:35 | 890_567_234 |  0.005
   supaboobs | 2013-11-28 | 2013-11-28 02:16:52 | 890_567_234 |  0.005
   supaboobs | 2013-11-29 | 2013-11-29 22:20:37 | 890_567_211 | 0.0075
   supaboobs | 2013-11-29 | 2013-11-29 17:18:51 | 890_567_211 | 0.0075

Так как распределительный ключ у нас составной — (reseller_id, day), то здесь фактически создалось 4 строки (если сложно понять почему, то прочтите предыдущую статью, и всё встанет на свои места). Получается, что для каждого реселлера мы будем создавать каждый день новую строку и заполнять её данными. Кластерный ключ тоже составной — time, ad_id.

Таблица amount_by_day

Так как процентная ставка не может меняться чаще, чем раз в день, то можно на этом выиграть немного миллисекунд и процессорного времени. Создадим ещё одну таблицу, которая будет хранить те же деньги, но без разбивки на клики:
CREATE TABLE amount_by_day (
  reseller_id text,
  day text, -- day in the format of 'YYYY-MM-DD'
  amount double,
  PRIMARY KEY (reseller_id, day) -- распределительный ключ (reseller_id) и кластерный ключ (day)
)
WITH CLUSTERING ORDER BY (day DESC); -- обратная сортировка дней строки

Она должна будет заполнятся раз в сутуки. Отдельный код в нашей системе будет собирать данные из ad_click, суммировать и записывать в amount_by_day .

Taблицы подсчета количества кликов

Естественно, нам важно знать сколько раз кликнули на какой баннер. Но так как запускать SELECT COUNT(0) FROM ad_click WHERE ad_id='...' по всем шести нодам — это было бы слишком накладно (да и не существует операции COUNT в CQL), то в C* есть такая вещь как counter-ы.

Counter — это тип колонки, т.е. синтаксически используется точно так же как timestamp, text, double, и пр. Но есть ограничения. Если в таблице есть хоть один counter, то все остальные колонки тоже должны быть типа counter (исключая PRIMARY KEY, конечно же). Создадим же таблицу:
CREATE TABLE clicks_per_ad (
  ad_id text,
  clicks counter,
  PRIMARY KEY (ad_id)); -- распределительный ключ (ad_id), кластерного ключа нет

А вот так в таблице можно изменять значение колонки clicks.
cqlsh:ad_network> SELECT * FROM clicks_per_ad;

(0 rows)

cqlsh:ad_network> UPDATE clicks_per_ad SET clicks = clicks + 1 WHERE ad_id = '890_567_234';
cqlsh:ad_network> SELECT * FROM clicks_per_ad;

 ad_id       | clicks
-------------+--------
 890_567_234 |      1

(1 rows)

cqlsh:ad_network> UPDATE clicks_per_ad SET clicks = clicks + 1 WHERE ad_id = '890_567_234';
cqlsh:ad_network> SELECT * FROM clicks_per_ad;

 ad_id       | clicks
-------------+--------
 890_567_234 |      2

(1 rows)

cqlsh:ad_network> UPDATE clicks_per_ad SET clicks = 0 WHERE ad_id = '890_567_234';
cqlsh:ad_network> SELECT * FROM clicks_per_ad;

 ad_id       | clicks
-------------+--------
 890_567_234 |      0

(1 rows)


Таким образом можно считать что угодно, если оно signed int. Т.е. можно считать исключительное целые числа, но зато в диапазоне -2^63 — +2^63.

Примечательно, что сначала не было строк в таблице, но после команды UPDATE одна вдруг появилась. Это особенность CQL. INSERT и UPDATE — суть одна и та же комманда. Оговорюсь, что в С* есть возможность не обновлять/вставлять данные если они уже (или 'ещё не') существуют. Она называется «лёгкие транзакции» (lightweight transactions), которые работают медленно относительно обычной операции записи данных.

Конечно, количество кликов можно собирать по любым критериям (ключам). Например:
CREATE TABLE clicks_per_reseller_per_day (
  reseller_id text,
  day text, -- day in the format of 'YYYY-MM-DD'
  clicks counter,
  PRIMARY KEY ((reseller_id, day))); -- распределительный ключ (reseller_id, day), кластерного ключа нет
CREATE TABLE clicks_per_reseller (
  reseller_id text,
  clicks counter,
  PRIMARY KEY (reseller_id)); -- распределительный ключ (reseller_id), кластерного ключа нет


Немного о текстовых ID

В RDBMS мы привыкли назначать строкам уникальный идентификатор типа int. Почему бы не делать идентификаторы текстовыми, которые бы означали что-либо осмысленное? Да потому, что производительность тогда пострадает. Лично меня это сильно угнетало. Мы привыкли, что ID наших водительских прав — цифры, ID страхового полиса — цифры. Но ведь часто приходится примешивать буквы, например ID паспорта — две буквы и 6 цифр, номера домов часто с буквами или дефисами, и пр.

В С* не принято использовать сухие цифры как ключи, потому что они не несут ускорения работы в отличие от RDBMS. Да и auto increment в С* отсутствует (зато есть timeuuid, если вдруг понадобится уникальный ID). Непривычным может показаться text как тип колонки reseller_id. В С* распределительный ключ (partition key) ищется путём сравнения хешей. Т.е. не происходит прямое сравнение строк, а значит не проседает производительность.

Запись данных


Чтобы записать один клик нам надо будет делать аж 4 UPDATE операций. Я не сошел с ума. Запись в C* чересчур быстаря операция, производительность не пострадает. Скорость записи при 6-ти нодах будет примерно в 100 раз быстрее, чем в MongoDB или в 2-5 раз быстрее, чем в HBase, не говоря уж об RDBMS. Последние версии С* умеют самостоятельно оптимизировать место на диске (compaction, compression), поэтому и с местом на жестком диске всё будет хорошо.

Чтобы убедиться, что все INSERT-ы сработали, существует такое понятние как BATCH-и. К сожалению они работают в пределах одного распределительного ключа (в пределах одной строки).
BEGIN BATCH
  -- INSERT, UPDATE, DELETE ...
APPLY BATCH;


Batch-и — не замена транзакциям в RDBMS. С помощью них С* передаёт все команды одним пакетом, а не несколькими коммандами, таким образом оптимизируя работу сети. Существует два вида batch-ей.
  1. Unlogged — BEGIN UNLOGGED BATCH — обычный batch. Но если координирущая нода (coordinator node — та, которая ответственна за пришедшую к ней CQL комманду и за связь с другими нодами) умрёт посреди batch-а, то может испортиться целостность (consistency) данных.
  2. Atomic (атомарные) — BEGIN BATCH — в этом случае С* удостоверится, что или все данные записаны, или ничего. Но эта операция примерно на 30% медленнее.


Чтение данных


Этой теме посвящена следующая статья. Операция SELECT ... FROM ... WHERE имеет множество ограничений по сравнению с RDBMS, поэтому на этом следует акцентировать особое внимание.

Заключение


Задачей этого поста было показать на сколько отличается подход к моделированию БД в Кассандре от RDBMS. Подход отличается кардинально, как можно заметить. И пусть вас не смущает схожесть CQL и SQL, на самом деле сопадает только синтаксис. Здесь было показано только несколько приёмов и отличительных особенностей, но их гораздо-гораздо больше.

Предыдущая статья цикла.
Следующая статья цикла.
Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 21

    0
    По counter-ам, вот к примеру нужно следить за тем чтобы счетчик при уменьшении не опускался ниже нуля, как лучше реализовать такую задачу? Есть идеи?
      0
      Есть.
      Нужно больше информации. Почему счетчик нельзя ниже нуля? Почему нужен счетчик вообще? Что этим счетчиком считается?
        0
        > поче нельзя ниже нуля?
        Это показатель кол-во записей по первичным ключам

        > почему нужен?
        Каждый раз считать долго

        > что и как считается?
        Кол-во записей в бд по различным ключам, к примеру, операция удаления записи, от которой зависит показатель счетчика, удаляется из бд несколько раз

        Потом у меня есть тот-же counter а записи по TTL удаляются и нужно счетчик уменьшать, есть способ как то детектировать это событие. Поможет-ли тут триггер и на скока они медленные?
          0
          Я плохо задал вопрос. Извиняюсь.
          Какие бизнес требования заставляют вас считать количество-записей?

          Удаление по TTL невозможно отследить триггерами (могу ошибаться, ибо триггеры появились только в этом году). Если нужно считать кол-во записей, то TTL лучше не использовать. Это факт.
            0
            Есть граффик на него надо выводит кол-во записей оп определенным условиям, вот условия у меня как primary keys а counter это их кол-во в другой таблице, а в таблице источник эти записи могут протухать из-за не надобности, срок их действия истек, зачем держать ненужные данные, вот я их и и удаляю.

            А если и не удалять по TTL то надо удалять ручками, пробегаться раз в какое-то время и сносить ненужные записи тем самым уменшая счетчик.
              0
              Ясно.
              Удалять придётся только ручками.

              Если большое количество записей (миллионы), то можно и принебречь высокой точностью. Но вам виднее.
              Если количество записей небольшое, то используйте Lightweight Transactions для изменения счётчиков.

              Жаль, что вы не можете поделится-таки бизнесс требованиями.
                0
                > Если количество записей небольшое, то используйте Lightweight Transactions для изменения счётчиков.

                Не могли-бы привести пример )) Мне почему-то казалось что Lightweight Transactions это что-то типа IF EXISTS ( а тут относительно данной таблицы) вроде как ссылатся на другие таблицы нельзя, иначе бы получилась каша %)

                Есть 6 primary ключей и поле с бинарными данным, где харнится полные данные
                Есть таблица с каунтером с 4 ключами, по тем-же ключам за исключением 2-х ключей, по которому и происходит увеличение/уменшение счетчика.
                  0
                  Пример:
                  INSERT INTO USERS (login, email, name, login_count)
                  values ('jbellis', 'jbellis@datastax.com', 'Jonathan Ellis', 1)
                  IF NOT EXISTS;
                  
                  UPDATE users
                  SET reset_token = null, password = ‘newpassword’
                  WHERE login = ‘jbellis’
                  IF reset_token = ‘some-generated-reset-token’;
                  


                  Без CREATE TABLE ваше словесное «Есть 6 primary ключей» плохо объясняет вашу модель. Мне не понятна ваша терминология.
                  У меня есть ощущение, что вы не читали (недопоняли) первую статью цикла. Прочитайте её, плз.
      0
      По replication factory, оно как-то может влиять на производительность.
      Логично предположить что чем больше это значение тем больше места и ресурсов надо.

      Сколько нод нужно иметь для replication factory 3, 3 и более? А если у нас нод меньше чем replication factory, тогда данные будут хранится несколько копий на одной из нод или нет?
        0
        Replication Factor — это то количество копий ваших данных, сколько вы считаете нужным. Это не Factory, а Factor. Переводится как «коэффициент рекпликации». — Это количетсво нод, на которых будет хранится одна строка.
        Т.е., чтобы replication factor 3 имел смысл, вам нужно иметь три ноды.
          0
          Если, например, я поставил 3 ноды и сделал коэффициент репликации тоже 3. Затем у меня одна нода упала.
          1. Как будет вести себя C* пока я собираю для нее новую ноду?
          2. Где-то давно читал, что если появилась проблема с одной нодой, то лучше ее просто «убить» и добавить в кластер «чистую» ноду. Это так?
            0
            1. Я думал это очевидно. С* будет ждать появления третей ноды. Сама работа не изменится.
            2. Это не так. :) Чистой воды бред. :) Если нода сгорела в пожаре, или умер диск, — тогда надо менять. Это очевидно. Но если она всего-лишь недоступна по причине отсутствия электричества или связи, то оптимальнее её вернуть в строй. Она же хранит наши данные. Мы ж не хотим, чтобы две живые ноды заново пересылали весь keyspace на новую ноду.
              0
              На счет первого вопроса согласен. Не подумал что-то. Действительно, очевидная ситуация.
              На счет второго вопроса:
              Нашел я статью, где читал про это: http://habrahabr.ru/post/114160/
              Там рассказывается про семинар, на котором авторы C* говорили что «проблемную» ноду лучше очистить полностью и вернуть в строй уже «новенькой». Мой вопрос был «не изменилось-ли что за пару лет?».
                0
                Ага. Ты меня запутал словами «появилась проблема», я предположил, что это уход ноды в офлайн.
                Тогда как «проблемная нода» — это нода с множествами внтренних проблем.
                Вашей ссылке 3 года почти. С* ушла далеко вперёд. Проблемы сами собой больше не возникают.
                В той старой статье была проблема с кофигурированием кольца. В версии С* 1.2 это делать не обязательно. Там по-умолчанию кольцо сконфигурировано за вас. В большинстве случаем этой конфигурации хватает с головой.
        0
        У меня 2 небольших замечания:

        1. В одном из примеров вроде бы описка: вместо SELECT * FROM reseller WHERE reseller='supaboobs'; должно быть SELECT * FROM reseller WHERE id='supaboobs';

        2. BEGIN BATCH является атомарным только по одной физической строке и задумывался для того, чтобы можно было делать несколько вставок записей в CQL 3 одним махом. При комбинировании записи в разные физические строки в одном батче атомарность никто не гарантирует (иначе это были бы транзакции как в RDBMS).
          0
          Спасибо.
          1 — исправил.
          2 — не знал.
          0
          Чтобы убедиться, что все INSERT-ы сработали, существует такое понятние как BATCH-и. К сожалению они работают в пределах одного распределительного ключа (в пределах одной строки).
          BEGIN BATCH
          — INSERT, UPDATE, DELETE…
          APPLY BATCH;

          Можете разъяснить, что значит «в пределах одного распределительного ключа»?
          Если в BATCH засунуть INSERTы с разными кластерными и распределительными ключами, что случится?
            0
            Распределительный ключ — это ключ одной строки.
            Более подробно что такое «строка» см. в первой статье: habrahabr.ru/post/203200/
            Раздел Второй пример. Конкретно в том примере распрделительные ключи это OSC и RKG.
              0
              BATCH с двумя INSERT в разные строки просто не сработает. C* вернёт ошибку, типа «неправильный запрос».
                0
                Вот у меня есть такая схема CREATE TABLE deviceInfo (
                deviceId bigint,
                hour int,
                date bigint,
                data blob,
                PRIMARY KEY (deviceId, hour ,date)
                );
                Я делаю батчи
                BEGIN BATCH
                INSERT INTO deviceInfo(deviceId, hour, date, data) VALUES (?,?,?,?)
                … еще 1000 разных инсертов…
                APPLY BATCH
                deviceInfo везде чаще всего разный.

                Ошибок не возвращает, кассандра 2.0.4.
                  0
                  Хм. С попробую разобраться где натупил. Спасибо.

            Only users with full accounts can post comments. Log in, please.