Кассандра: список 10 самых последних измененных записей

У меня возникают проблемы с попыткой моделирования моих данных, чтобы я мог эффективно запросить Cassandra для последних 10 (на самом деле) записей, которые были недавно изменены. Каждая запись имеет столбец last_modified_date, который задается приложением при вставке/обновлении записи.

Я исключил столбцы данных из этого примера кода.

Таблица основных данных (содержит только одну строку на запись):

CREATE TABLE record (
    record_id int,
    last_modified_by text,
    last_modified_date timestamp,
    PRIMARY KEY (record_id)
);

Решение 1 (сбой)

Я попытался создать отдельную таблицу, в которой использовался порядок ключей кластеризации.

Таблица (одна строка для каждой записи, только вставка последней измененной даты):

CREATE TABLE record_by_last_modified_index (
    record_id int,
    last_modified_by text,
    last_modified_date timestamp,
    PRIMARY KEY (record_id, last_modified_date)
) WITH CLUSTERING ORDER BY (last_modified_date DESC);

Query:

SELECT * FROM record_by_last_modified_index LIMIT 10

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

Решение 2 (неэффективно)

Другое решение, которое я пробовал, - это просто запросить Cassandra для всех значений record_id и last_modified_date, отсортировать их и выбрать первые 10 записей в моем приложении. Это явно неэффективно и не будет хорошо масштабироваться.

Решение 3

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

Ответ 1

Я думаю, что вы пытаетесь сделать больше из модели реляционной базы данных и являетесь частью анти-шаблона в Cassandra.

Cassandra только сортирует вещи, основываясь на столбцах кластеризации, но порядок сортировки не ожидается. Это связано с тем, что когда memtables записываются на диск в виде SSTables (Sorted String Tables), SSTables являются неизменяемыми и не могут быть повторно отсортированы эффективно. Вот почему вам не разрешено обновлять значение столбца кластеризации.

Если вы хотите повторно сортировать кластерные строки, единственный способ, которым я знаю, - удалить старую строку и вставить новую в пакет. Чтобы сделать это еще более неэффективным, вам, вероятно, потребуется сначала прочитать, чтобы узнать, что last_modified_date было для record_id, чтобы вы могли его удалить.

Итак, я бы поискал другой подход, например, просто записывал обновления в виде новых кластеризованных строк и оставлял там старые (возможно, их очистку с течением времени с помощью TTL). Таким образом, ваши новейшие обновления всегда будут сверху, когда вы выполнили запрос LIMIT.

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

Update:

Размышляя об этом еще немного, вы сможете сделать это в Cassandra 3.0, используя материализованные представления. Представление позаботится о беспорядочном удалении и вставке для вас, чтобы переупорядочить кластерные строки. Итак, как это выглядит в версии 3.0 alpha:

Сначала создайте базовую таблицу:

CREATE TABLE record_ids (
    record_type int,
    last_modified_date timestamp,
    record_id int,
    PRIMARY KEY(record_type, record_id));

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

CREATE MATERIALIZED VIEW last_modified AS
    SELECT record_type FROM record_ids
    WHERE record_type IS NOT NULL AND last_modified_date IS NOT NULL AND record_id IS NOT NULL
    PRIMARY KEY (record_type, last_modified_date, record_id)
    WITH CLUSTERING ORDER BY (last_modified_date DESC);

Теперь вставьте несколько записей:

insert into record_ids (record_type, last_modified_date, record_id) VALUES ( 1, dateof(now()), 100);
insert into record_ids (record_type, last_modified_date, record_id) VALUES ( 1, dateof(now()), 200);
insert into record_ids (record_type, last_modified_date, record_id) VALUES ( 1, dateof(now()), 300);

SELECT * FROM record_ids;

 record_type | record_id | last_modified_date
-------------+-----------+--------------------------
           1 |       100 | 2015-08-14 19:41:10+0000
           1 |       200 | 2015-08-14 19:41:25+0000
           1 |       300 | 2015-08-14 19:41:41+0000

SELECT * FROM last_modified;

 record_type | last_modified_date       | record_id
-------------+--------------------------+-----------
           1 | 2015-08-14 19:41:41+0000 |       300
           1 | 2015-08-14 19:41:25+0000 |       200
           1 | 2015-08-14 19:41:10+0000 |       100

Теперь мы обновляем запись в базовой таблице и видим, что она перемещается в верхнюю часть списка в представлении:

UPDATE record_ids SET last_modified_date = dateof(now()) 
WHERE record_type=1 AND record_id=200;

Итак, в базовой таблице мы увидели временную метку для record_id = 200:

SELECT * FROM record_ids;

 record_type | record_id | last_modified_date
-------------+-----------+--------------------------
           1 |       100 | 2015-08-14 19:41:10+0000
           1 |       200 | 2015-08-14 19:43:13+0000
           1 |       300 | 2015-08-14 19:41:41+0000

И в представлении мы видим:

 SELECT * FROM last_modified;

 record_type | last_modified_date       | record_id
-------------+--------------------------+-----------
           1 | 2015-08-14 19:43:13+0000 |       200
           1 | 2015-08-14 19:41:41+0000 |       300
           1 | 2015-08-14 19:41:10+0000 |       100

Итак, вы видите, что record_id = 200 перемещается вверх в представлении, и если вы делаете ограничение N в этой таблице, вы получите N последних измененных строк.

Ответ 2

Единственный способ запроса CQL для всей таблицы/вида, отсортированного по полю, - сделать ключ раздела постоянным. Точно одна машина (коэффициент репликации по времени) будет удерживать всю таблицу. Например. с ключом раздела partition INT, который всегда равен нулю, а ключ кластеризации - как поле, которое требует сортировки. Вы должны наблюдать за чтением/записью/производительностью, подобной одной-единственной node базе данных с индексом в отсортированном поле, даже если у вас больше узлов в вашем кластере. Это не полностью разрушает цель Кассандры, потому что она может помочь масштабировать в будущем.

Если производительность недостаточна, вы можете решить масштабировать, увеличивая разнообразие разделов. Например. случайный выбор из 0, 1, 2, 3 для вставок будет до четырехкратного чтения/записи/пропускной способности, когда используются 4 узла. Затем, чтобы найти "10 самых последних" элементов, вам придется вручную запросить все 4 раздела и объединить сортировку результатов.

В теории Cassandra может предоставить этот объект динамического ключа node -count-max-modulo для INSERT и сортировки слияния для SELECT (с ALLOW FILTERING).

Цели проекта Cassandra Disallow Global Sort

Чтобы позволить возможности записи, чтения и хранения масштабироваться линейно с помощью node count, Cassandra требует:

  • Каждая вставка помещается на один node.
  • Каждая выбираемая земля на одном node.
  • Клиенты распределяют рабочую нагрузку аналогично между всеми узлами.

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

Примечание. Материализованные представления эквивалентны таблицам, они не обладают магическим свойством, которое делает их лучше при глобальном сортировке. См. http://www.datastax.com/dev/blog/we-shall-have-order, где Аарон Плоэц соглашается, что cassandra и cql не могут сортировать по одному полю без разбиения и масштаба.

Пример решения

CREATE KEYSPACE IF NOT EXISTS
    tmpsort
WITH REPLICATION =
    {'class':'SimpleStrategy', 'replication_factor' : 1};

USE tmpsort;

CREATE TABLE record_ids (
    partition int,
    last_modified_date timestamp,
    record_id int,
    PRIMARY KEY((partition), last_modified_date, record_id))
    WITH CLUSTERING ORDER BY (last_modified_date DESC);

INSERT INTO record_ids (partition, last_modified_date, record_id) VALUES ( 1, DATEOF(NOW()), 100);
INSERT INTO record_ids (partition, last_modified_date, record_id) VALUES ( 2, DATEOF(NOW()), 101);
INSERT INTO record_ids (partition, last_modified_date, record_id) VALUES ( 3, DATEOF(NOW()), 102);
INSERT INTO record_ids (partition, last_modified_date, record_id) VALUES ( 1, DATEOF(NOW()), 103);
INSERT INTO record_ids (partition, last_modified_date, record_id) VALUES ( 2, DATEOF(NOW()), 104);
INSERT INTO record_ids (partition, last_modified_date, record_id) VALUES ( 3, DATEOF(NOW()), 105);
INSERT INTO record_ids (partition, last_modified_date, record_id) VALUES ( 3, DATEOF(NOW()), 106);
INSERT INTO record_ids (partition, last_modified_date, record_id) VALUES ( 3, DATEOF(NOW()), 107);
INSERT INTO record_ids (partition, last_modified_date, record_id) VALUES ( 2, DATEOF(NOW()), 108);
INSERT INTO record_ids (partition, last_modified_date, record_id) VALUES ( 3, DATEOF(NOW()), 109);
INSERT INTO record_ids (partition, last_modified_date, record_id) VALUES ( 1, DATEOF(NOW()), 110);
INSERT INTO record_ids (partition, last_modified_date, record_id) VALUES ( 1, DATEOF(NOW()), 111);

SELECT * FROM record_ids;

-- Note the results are only sorted in their partition
-- To try again:
-- DROP KEYSPACE tmpsort;

Обратите внимание, что без предложения WHERE вы получаете результаты в порядке маркера (раздела). См. https://dba.stackexchange.com/questions/157537/querying-cassandra-without-a-partition-key

Другие модели распространения базы данных

Если бы я правильно понял, CockroachDB аналогичным образом выполнил бы чтение/запись на бутылочной горловине на монотонном приращении данных на один node в любой момент времени, но емкость памяти будет масштабироваться линейно. Также другие запросы диапазона, такие как "старейшие 10" или "между датой X и датой Y", будут распределять нагрузку на большее количество узлов, в отличие от Cassandra. Это связано с тем, что база данных CockroachDB представляет собой один гигантский сортированный хранилище ключей, где всякий раз, когда диапазон отсортированных данных достигает определенного размера, он перераспределяется.

Ответ 3

Есть еще одна проблема с принятым решением, я думаю. Если у вас есть несколько реплик, вставки не обязательно будут в порядке.

Из документов Dasastax:

now() - В узле координатора генерирует новый уникальный timeuuid в миллисекундах при выполнении оператора. Часть временной метки timeuuid соответствует стандарту UTC (универсальное время). Этот метод полезен для вставки значений. Значение, возвращаемое now(), гарантированно будет уникальным.

Когда у вас есть несколько реплик, у вас также есть несколько узлов-координаторов, поскольку любой узел может быть выбран в качестве узла-координатора. Это означает, что ваши вставки не в порядке из-за небольших колебаний времени на узлах. Таким образом, одна вставка, которая произошла на самом деле позже в вашем ссылочном фрейме, может быть отсортирована перед предыдущей вставленной записью, потому что now() просто генерирует дату на узле координатора, который немного позади.

Вы пытаетесь получить какое-то непротиворечивое (или единственную ссылку на правду) представление о ваших данных. К сожалению, в распределенной среде нет единой ссылки на правду.