Translate

пʼятниця, 9 жовтня 2015 р.

Cassandra. Part I: Overview

Apache Cassandra — розподілена система керування базами даних, що відноситься до класу noSQL-систем і розрахована на створення високомасштабованих і надійних сховищ величезних масивів даних, представлених у вигляді хеша.

Проект стартував у надрах компанії Facebook і в 2009 році був переданий фонду Apache. В основу Cassandra лягли 2 архітектурні ідеї: Amazon Dynamo та Google Big Table, опис яких був опублікований в 2007 та 2006 роках відповідно.

Цю базу данних використовують такі компанії як IBM, Apple, Netflix, Twitter, Yandex, CERN, Reddit, SoundCloud, Rackspace та інші.

Cassandra проектувалась для великих географічно розподілених систем, тому на відміну від традиційних баз не має багатьох недоліків: масштабування, висока доступність і т.п. тут реалізовано простіше ніж, скажімо, в MySQL. Звісно Cassandra не срібна куля, і при проектуванні інфраструктури треба добре розуміти як архітектору так і розробнику, які запити буде обслуговувати база та інші особливості.

Що ж таке Cassandra? Cassandra - це гібрид key-value и column-oriented баз даних. Термінологія структури данних дещо інша ніж в реліційній моделі, проте не варто на цьому особливо зациклюватись адже це не так важливо.


Тож в Cassandra є бази (keyspace), в котрих знаходяться сімейства колонок (аналог таблиць у реляційній системі), а в самому сімействі, вже майже як і у звичайній таблиці, колонки і рядки. Майже, тому що для кожного рядка може бути визначений свій набір колонок (розріджені таблиці). Як це виглядає в реальному житті? Ну щось схоже на це:


До кожного запису в сімействі колонок також привязане значення timestamp, але воно приховане від очей і використовується для реплікацій та вирішення можливих конфліктів: найбільш актуальні дані - дані з найостаннішим timestamp.

Installation of one-node cluster


Схоже, що показати певні особливості краще на реальному прикладі. Тому встановимо Cassandra. Cassandra написана на Java, тому без неї ніяк:

# add-apt-repository ppa:webupd8team/java
# apt-get update
# apt-get install oracle-java8-installer
# apt-get install oracle-java8-set-default

Перевіряємро версію:

# java -version
java version "1.8.0_60"
Java(TM) SE Runtime Environment (build 1.8.0_60-b27)
Java HotSpot(TM) 64-Bit Server VM (build 25.60-b23, mixed mode)

Наскільки мені відомо з версією Java 1.7 також все коректно працює. Тепер можна перейти до установки Cassandra:

# echo "deb http://debian.datastax.com/community stable main" | sudo tee -a /etc/apt/sources.list.d/cassandra.sources.list
# curl -L http://debian.datastax.com/debian/repo_key | sudo apt-key add -
# apt-get update
# aptitude install cassandra
The following NEW packages will be installed:
  cassandra libopts25{a} ntp{a} python-support{a} 
0 packages upgraded, 4 newly installed, 0 to remove and 0 not upgraded.
Need to get 24.9 MB of archives. After unpacking 34.0 MB will be used.
Do you want to continue? [Y/n/?] 

По-замовчуванню Cassandra резервує досить багато місця в оперативній пам’яті для свого процесу, тому обмежимо його:

# vim /etc/cassandra/cassandra-env.sh
...
MAX_HEAP_SIZE="350M"
HEAP_NEWSIZE="150M"
...

Звісно, це лише для тестового оточення. Для реального цього чіпати не варто, так як cassandra-env.sh розраховує ці значення автоматично на основі доступних потужностей сервера.

Рестартуємо сервер:

# service cassandra restart

Все, можна користуватись. Заходимо в консоль cqlsh:

# cqlsh
Connected to localhost:9042.
[cqlsh 5.0.1 | Cassandra 2.2.1 | CQL spec 3.3.0 | Native protocol v4]
Use HELP for help.
cqlsh>

CQL basics


Для управління вмістом баз використовується Cassandra Query Language (CQL). CQL - мова формування структурованих запитів, яка на перший погляд нагадує SQL, але істотно урізана в функціональності.

Коротко опишу базові операції. Переглянемо які keyspaces присутні:

cqlsh> DESCRIBE keyspaces;

system_auth  system  system_distributed  system_traces

Створимо новий keyspace:

cqlsh> create keyspace dev 
      with replication = {'class':'SimpleStrategy','replication_factor':1};

'class':'SimpleStrategy' - вказання стратегії реплікації. Є дакілька варінтів, детальніше про це - далі. Суть 'SimpleStrategy' в тому що для реплікації буде обрано перший-ліпший сервер, не залежно від топології.
'replication_factor':1 - в кластері (хоч і з однієї ноди) keyspace dev буде знаходитись в одній копії.

Отже, стратегію реплікації та фактор реплікації можна вказувати окремо для кожної бази.

Наразі можна створювати сімейство колонок (CF).

cqlsh> use dev;
cqlsh> create table some_table (
      key text PRIMARY KEY,
      data text      
  );

Виведемо всі доступні таблиці:

cqlsh:dev> describe tables;

some_table

Виглядає як SQL, чи не так? Далі - ще більше подібностей. Заповнимо CF якимись даними:

cqlsh:dev> insert into some_table (key, data) values ('some key 1','some data 1');
cqlsh:dev> insert into some_table (key, data) values ('some key 2','some data 2');
cqlsh:dev> insert into some_table (key, data) values ('some key 3','some data 3');

cqlsh:dev> select * from some_table;

 key        | data
------------+-------------
 some key 1 | some data 1
 some key 3 | some data 3
 some key 2 | some data 2

(3 rows)

Cassandra, на відміну від SQL, не підтримує multiply INSERT, тому кожен рядок необхідно вставляти окремою інструкцією.

Вибірки з умовами WHERE також доступні, проте лише на колонках з PRIMARY ключами або проіндексованими:

cqlsh:dev> select * from some_table where key='some key 1';

 key        | data        
------------+-------------
 some key 1 | some data 2 

(1 rows)

Тому в цьому разі нас очікує навдача:

cqlsh:dev> select * from some_table where data='some data 2';
InvalidRequest: code=2200 [Invalid query] message="No secondary indexes on the restricted columns support the provided operators: "

Додаємо secondary index на колонку. Чим більше індексів - тим повільніше працюватимуть запити, тому розставляти їх необхідно з розумом:

cqlsh:dev> create index idx_data on some_table(data);

Запускаємо попередній SELECT знову:

cqlsh:dev> select * from some_table where data='some data 2';

 key        | data        | text
------------+-------------+------
 some key 1 | some data 2 | null
 some key 2 | some data 2 | null

(2 rows)

Додамо додаткову колонку text:

cqlsh:dev> ALTER TABLE some_table ADD text text;
cqlsh:dev> select * from some_table;

 key        | data        | text
------------+-------------+------
 some key 1 | some data 1 | null
 some key 3 | some data 3 | null
 some key 2 | some data 2 | null

(3 rows)

В Cassandra дії UPDATE/INSERT по суті дублюють одна одну.

cqlsh:dev> update some_table set text='text 4' where key='some key 4';
cqlsh:dev> select * from some_table;

 key        | data        | text
------------+-------------+--------
 some key 1 | some data 1 |   null
 some key 4 | null        |   text 4
 some key 3 | some data 3 |   null
 some key 2 | some data 2 |   null

(4 rows)

Та INSERT:

cqlsh:dev> insert into some_table (key, data) values ('some key 1','some data 2');
cqlsh:dev> select * from some_table;

 key        | data        | text
------------+-------------+--------
 some key 1 | some data 2 |   null
 some key 4 |        null | text 4
 some key 3 | some data 3 |   null
 some key 2 | some data 2 |   null

(4 rows)

Тобто INSERT замість попередження про вже існуючий аналогічний рядок просто перезапише дані.
Також в Cassandra відсутні операції JOIN над таблицями.

Одна із приємних особливостей - можливість встановлення часу життя для запису (TTL):

cqlsh:dev> insert into some_table (key, data) values ('some key 5','some data 5') using ttl 15;

Через 15 секунд запис буде витерто. TTL може бути корисним для зберігання данних сесій чи іншої інформації для якої важливий строк давності.

Keys


Із базовими запитами CQL закінчимо. Розповім трохи про ключі та особливості їх зберігання, їх досить багато, часом їх по-різному називають, тому не дивно, що можна трішки заплутатись.

При оголошенні структури таблиці (CF) одну колонку чи групу колонок можна оголосити як колонку з PRIMARY ключем. PRIMARY - це ключ, що використовується для отримання данних з таблиці, тому власне їх треба встановлювати виходячи з логіки роботи програми, яка буде використовувати таблицю. Як це робити я вже показував, але нагадаю ще раз:

cqlsh:dev> CREATE TABLE object_coordinates (
object_id int PRIMARY KEY,
coordinate text
);

object_coordinates - ім'я колоночного сімейства (таблиця з координатами об'єкта);
object_id - ідентифікатор об'єкта;
coordinate - координати об'єкта

Тож object_id - колонка з PRIMARY ключем. Це простий ключ, адже він встановлений лише для однієї колонки.


Увесь PRIMARY ключ у цьому випадку буде виступати в якості PARTITION ключа, по якому Cassandra визначатиме на якому вузлі/вузлах (node) буде зберігатись даний запис. Точніше, PARTITION ключ (часом його ще називають ROW KEY) буде виступати аргументом до хеш-функції, результатом якої буде маркер, який вказуватиме на положення кожного із записів в кластері (в т.ч. на якому фізичному вузлі він лежатиме).

Логічне представлення кожного нового запису в таблиці наведеній вище буде виглядати так:


Як я вже згадував на початку, до кожного рядка таблиці прив'язана також мітка часу Timestamp, по котрій Cassandra визначить найновіший запис у всіх наявних вузлів, якщо дані раптом будуть тичасово розсинхронізовані. Тобто остання мітка Timestamp - найбільш актуальні дані, все досить просто.

А ось приклад і його графічна ілюстрація з реальними даними:

cqlsh:dev> insert into object_coordinates (object_id, coordinate) values (564682,'59.8505,34.0035');
cqlsh:dev> insert into object_coordinates (object_id, coordinate) values (1235,'61.7814,40.3316');
cqlsh:dev> select object_id, coordinate, writetime(coordinate) from object_coordinates;

 object_id | coordinate      | writetime(coordinate)
-----------+-----------------+-----------------------
    564682 | 59.8505,34.0035 |      1424942875
      1235 | 61.7814,40.3316 |      1401096475


(2 rows)



PRIMARY KEY також може бути складним (COMPOUND/COMPOSITE). У цьому разі PRIMARY KEY може складатись із двох і більше колонок. Продемонструю це на прикладі:

cqlsh:dev> drop table object_coordinates;
cqlsh:dev> CREATE TABLE object_coordinates (
object_id int,
time timestamp,
coordinate text,
PRIMARY KEY (object_id, time) );

cqlsh:dev> insert into object_coordinates (object_id, time, coordinate) values (564682, 1424945419000,'59.8505,34.0035');
cqlsh:dev> insert into object_coordinates (object_id, time, coordinate) values (1235, 1393416901000,'59.8505,34.0035');
cqlsh:dev> insert into object_coordinates (object_id, time, coordinate) values (564682, 1424949019000,'59.9609,35.2340');
cqlsh:dev> insert into object_coordinates (object_id, time, coordinate) values (564682, 1424952901000,'60.7232,36.2447');

cqlsh:dev> select * from object_coordinates;

 object_id | time                     | coordinate
-----------+--------------------------+-----------------
      1235 | 2014-02-26 12:15:01+0000 | 59.8505,34.0035
    564682 | 2015-02-26 10:10:19+0000 | 59.8505,34.0035
    564682 | 2015-02-26 11:10:19+0000 | 59.9609,35.2340
    564682 | 2015-02-26 12:15:01+0000 | 60.7232,36.2447

(4 rows)

Час, завдяки маппінгу timestamp, перетвориться в чітку дату. У запитах його задано в мілісекундах, звідси і три додаткові нулі вкінці.

У цьому разі з COMPOSITE PRIMARY KEY, object_id буде PARTITION KEY (перша частина PRIMARY KEY), а time - CLUSTERING KEY:


Отже  наразі, якщо один із ключів, PARTITION KEY (object_id) або CLUSTERING KEY (time), буде відрізнятись - то фактично це вже буде інший запис в таблиці. Але насправді ситуація буде трохи складнішою: якщо PARTITION KEY буде у двох записів один і той же (той самий object_id), а CLUSTERING KEY відрізнятися (різний time), то це в підсумку буде один великий запис, що в свою чергу складається з двох інших. Без малюнка тут не зрозумієш:


Головна відмінність від простого PRIMARY KEY (в котрому PRIMARY KEY=PARTITION KEY) полягає в тому, що записи із різними значеннями CLUSTERING KEY додаються до вже існуючого запису з PARTITION KEY. Записи впорядковані по CLUSTERING KEY, що дозволяє оптимізувати пошук в межах записів з одним PARTITION KEY.

Наш приклад в даному наразі буде виглядати так (додамо кілька значень, щоб ілюстрація була більш наочною):


Розглянемо останній приклад створення складного PRIMARY KEY:

cqlsh:dev> drop table object_coordinates;
cqlsh:dev> CREATE TABLE object_coordinates (
object_id int,
date text,
time timestamp,
coordinate text,
PRIMARY KEY ((object_id, date), time)
);

cqlsh:dev> insert into object_coordinates (object_id, date, time, coordinate) values (564682, '2015-02-25', 1424878103000,'59.8505,34.0035');
cqlsh:dev> insert into object_coordinates (object_id, date, time, coordinate) values (1235, '2015-12-26',   1451129003000,'59.8505,34.0035');
cqlsh:dev> insert into object_coordinates (object_id, date, time, coordinate) values (564682, '2015-02-25', 1424869397000,'59.9609,35.2340');
cqlsh:dev> insert into object_coordinates (object_id, date, time, coordinate) values (564682, '2015-02-27', 1425047461000,'60.7232,36.2447');

cqlsh:dev> select * from object_coordinates;

 object_id | date       | time                     | coordinate
-----------+------------+--------------------------+-----------------
    564682 | 2015-02-27 | 2015-02-27 14:31:01+0000 | 60.7232,36.2447
      1235 | 2015-12-26 | 2015-12-26 11:23:23+0000 | 59.8505,34.0035
    564682 | 2015-02-25 | 2015-02-25 13:03:17+0000 | 59.9609,35.2340
    564682 | 2015-02-25 | 2015-02-25 15:28:23+0000 | 59.8505,34.0035

(4 rows)

Отже ми додали також колонку date, що буде зберігати дату (число/місяць/рік) зміни координат.


У цьому випадку:

PRIMARY KEY ((object_id, date), time) - це COMPAUND/COMPOSITE PRIMARY KEY;
(object_id, date) - PARTITION KEY (його також часом називають складним)
time - CLUSTERING KEY

Логічне представлення даних буде виглядати наступним чином:


Sstabledump з пакету cassandra-tools може підтвердити, що дані органіовані подібним чином. Установимо sstabledump:

# aptitude install cassandra-tools

Виконаємо примусове збереження всіх даних на диск:

# nodetool flush

Відкриємо директорію зі збереженою таблицею (ім'я може бути дещо іншим):

# cd /var/lib/cassandra/data/dev/object_coordinates-333a62d0b70611e68059fd2bb0e8312f

Проаналізуємо sstable, що відноситься до піддослідної таблиці:

# sstabledump -t mc-1-big-Data.db
[
  {
    "partition" : {
      "key" : [ "564682", "2015-02-27" ],
      "position" : 0
    },
    "rows" : [
      {
        "type" : "row",
        "position" : 34,
        "clustering" : [ "2015-02-27 14:31Z" ],
        "liveness_info" : { "tstamp" : "1480522192421853" },
        "cells" : [
          { "name" : "coordinate", "value" : "60.7232,36.2447" }
        ]
      }
    ]
  },
  {
    "partition" : {
      "key" : [ "1235", "2015-12-26" ],
      "position" : 68
    },
    "rows" : [
      {
        "type" : "row",
        "position" : 102,
        "clustering" : [ "2015-12-26 11:23Z" ],
        "liveness_info" : { "tstamp" : "1480522181589879" },
        "cells" : [
          { "name" : "coordinate", "value" : "59.8505,34.0035" }
        ]
      }
    ]
  },
  {
    "partition" : {
      "key" : [ "564682", "2015-02-25" ],
      "position" : 136
    },
    "rows" : [
      {
        "type" : "row",
        "position" : 170,
        "clustering" : [ "2015-02-25 13:03Z" ],
        "liveness_info" : { "tstamp" : "1480522187670011" },
        "cells" : [
          { "name" : "coordinate", "value" : "59.9609,35.2340" }
        ]
      },
      {
        "type" : "row",
        "position" : 203,
        "clustering" : [ "2015-02-25 15:28Z" ],
        "liveness_info" : { "tstamp" : "1480522162309550" },
        "cells" : [
          { "name" : "coordinate", "value" : "59.8505,34.0035" }
        ]
      }
    ]
  }
]

Тобто значення з однаковим PARTITION KEY (однакові дата та object_id) знаходяться в одній групі (партиції) з вже різними відсортованими (по даті) CLUSTERING ключами всередині.

Часом зручніше переглядати sstable в построковому вигляді, особливо якщо даних в таблиці забагато:

# sstabledump -d mc-1-big-Data.db
[564682:2015-02-27]@0 Row[info=[ts=1480523693233986] ]: 2015-02-27 14:31Z | [coordinate=60.7232,36.2447 ts=1480523693233986]
[1235:2015-12-26]@66 Row[info=[ts=1480523693228801] ]: 2015-12-26 11:23Z | [coordinate=59.8505,34.0035 ts=1480523693228801]
[564682:2015-02-25]@132 Row[info=[ts=1480523693231429] ]: 2015-02-25 13:03Z | [coordinate=59.9609,35.2340 ts=1480523693231429]
[564682:2015-02-25]@197 Row[info=[ts=1480523693225653] ]: 2015-02-25 15:28Z | [coordinate=59.8505,34.0035 ts=1480523693225653]

CLUSTERING KEY, як і PARTITION KEY, також може складатись з багатьох колонок. Розглянемо наступний випадок:

cqlsh:dev> drop table object_coordinates;
cqlsh:dev> create table object_coordinates(
      k_part_one text,
      k_part_two int,
      k_clust_one text,
      k_clust_two int,
      k_clust_three uuid,
      data text,
      PRIMARY KEY((k_part_one,k_part_two), k_clust_one, k_clust_two, k_clust_three)    
  );

Складовими PRIMARY KEY((k_part_one,k_part_two), k_clust_one, k_clust_two, k_clust_three) будуть:

(k_part_one,k_part_two) - PARTITION KEY
(k_clust_one, k_clust_two, k_clust_three) - CLUSTERING KEY (все що лишилось від PARTITION KEY в PRIMARY KEY)

Отже, підсумуємо:

PARTITION/ROW KEY - відповідальний за знаходження/розподіл данних по нодах (про ноди - далі)
CLUSTERING KEY - відповідальний за сортування даних в межах одного PARTITION/ROW KEY
PRIMARY KEY - ключ, який встановлюється на колонки або їх комбінації задля можливості шукати по даних колонкового сімейства. PRIMARY KEY = PARTITION KEY у випадку простого PRIMARY KEY.
COMPAUND/COMPOSITE - складний ключ, що складається з більше ніж однієї колонки.

Узагальнено це звучить так: дані з однаковим PARTITION KEY утворюють єдину партицію, в котрій знаходяться дані, що вже відсортовані по CLUSTERING KEY. PARTITION KEY та CLUSTERING KEY разом утворюють PRIMARY KEY.

PRIMARY KEY важливо виставляти в залежності від того які запити необхідно буде виконувати. Наприклад, нехай існує така схема:

cqlsh:dev> create table employees (
  name text,
  surname text,
  profession text,
  PRIMARY KEY(name, surname)    
);

cqlsh:dev> insert into employees (name, surname, profession) VALUES ('Vasya', 'Pupkin', 'football player');
cqlsh:dev> insert into employees (name, surname, profession) VALUES ('John', 'Doe', 'football player');
cqlsh:dev> insert into employees (name, surname, profession) VALUES ('Jane', 'Doe', 'manager');
cqlsh:dev> insert into employees (name, surname, profession) VALUES ('Thom', 'Yorke', 'hr');
cqlsh:dev> insert into employees (name, surname, profession) VALUES ('John', 'Pupkin', 'blogger');

cqlsh:dev> select * from employees;

 name  | surname | profession
-------+---------+-----------------
  John |     Doe | football player
  John |  Pupkin |         blogger
 Vasya |  Pupkin | football player
  Jane |     Doe |         manager
  Thom |   Yorke |              hr

(5 rows)

Виходячи з установленого PRIMARY KEY можна виконувати такі запити:

cqlsh:dev> select * from employees where name='John';

 name | surname | profession
------+---------+-----------------
 John |     Doe | football player
 John |  Pupkin |         blogger

cqlsh:dev> select * from employees where name='John' and surname='Doe';

 name | surname | profession
------+---------+-----------------
 John |     Doe | football player

І не можна без додаткових secondary index таких:

cqlsh:dev> select * from employees where surname='Doe';
InvalidRequest: code=2200 [Invalid query] message="Cannot execute this query as it might involve data filtering and thus may have unpredictable performance. If you want to execute this query despite the performance unpredictability, use ALLOW FILTERING"

cqlsh:dev> select * from employees where name='John' and profession='manager';
InvalidRequest: code=2200 [Invalid query] message="No secondary indexes on the restricted columns support the provided operators: "

cqlsh:dev> select * from employees where profession='manager';
InvalidRequest: code=2200 [Invalid query] message="No secondary indexes on the restricted columns support the provided operators: "

Тобто PARTITION KEY (складова PRIMARY KEY) обов’язково має бути присутня в запиті. Запити лише з CLUSTERING KEY можуть бути виконані, але якщо таких записів, прив’язаних до PARTITION KEY, забагато - то це може викликати високі навантаження і, як наслідок, довгий час їх виконання. Про що власне і попереджує СQLSH.

cqlsh:dev> select * from employees where surname='Doe' ALLOW FILTERING;

 name | surname | profession
------+---------+-----------------
 John |     Doe | football player
 Jane |     Doe |         manager

Як наслідок 'ALLOW FILTERING' використовувати небажано.

Більш того, якщо CLUSTERING KEY складається з двох і більше колонок - то в інструкціях можуть бути запитані лише складові CLUSTERING KEY по черзі. Наприклад для такого PRIMARY KEY

PRIMARY KEY((col1, col2), col10, col4))

можуть бути виконані такі запити з WHERE (без secondary index):

* WHERE col1 and col2
* WHERE col1 and col2 and col10
* WHERE col1 and col2 and col10 and col4

і не можуть:

* WHERE col1 and col2 and col4
* будь-які, які не включають в себе col1 та col2 одночасно.

Тобто PARTITION KEY (у цьому випадку - це (col1, col2)) не може бути розірваний у запитах з WHERE.

Secondary індекси, на відміну від Primary, зберігаються локально на диску кожного вузла кластеру і тому, у разі пошуку з їх використанням, можуть досить серйозно навантажуватись усі ноди Cassandra кластеру. У випадку великих кластерів такі запити можуть займати значну кількість часу.

Сassandra Cluster


Добре, завершимо на цьому етапі щодо ключів. Розповім тепер про те, як спроектований Сassandra Cluster. Проте спочатку опишу структурні компоненти архітектури:

Вузол (Node) - базовий компонент архітектури. На практиці - це віртуальна машина, або справжній сервер з Cassandra.


Стійка (Rack) - група серверів в одній стійці, віртуальний об’єкт (умовна логічна одиниця). За можливістю, якщо сервера і справді стоять в Rack-стійці, краще про це повідомляти Касандру для забезпечення високої доступності та ефективної реплікації. Насправді, стійкою можна назвати будь-яку групу серверів, що стоять наприклад в одній кімнаті і користуються однією мережею: як струму так і інтернет.


Дата Центр (Data Center) - група Rack-стійок в одній локації. Також віртуальне поняття і ідея його введення приблизно така ж як і Rack-стійки: при варіанті реплікації PropertyFileSnitch Cassandra буде використовувати інформацію, щодо описаної топології задля мінімізації можливих простоїв та убезпечення даних. Наприклад, для реплікації даних серверу, що знаходиться в одному датацентрі, може бути обраний сервер, що фізично розміщується в іншому дата центрі і, отже, після інциденту в першому дата центрі, дані будуть доступними.

Кластер (Cluster) - сукупність всіх датацентрів, що працюють як одне ціле.


У Cassandra загальний обсяг даних, що обслуговується кластером, представлений у вигляді кільця (Token Ring). Кільце розділене на діапазони, к-ть яких рівна кількості вузлів (Nodes):


Проте сам вузол може відповідати за декілька сегментів (у випадку, якщо він також зберігає реплікацію).

Під час приєднання кожного вузла до кластеру йому призначається маркер. Маркер - це 64-бітне хеш-число (виправте, якщо я помиляюсь). Якщо Касандрі буде необхідно записати новий рядок, хеш-функція розподільника даних (partitioner) перетворить PARTITION KEY (ROW KEY) запису в маркер і почнеться обхід кільця по часовій стрілці для того, щоб знайти місце де дані мають розміщуватись. Обхід продовжується до того часу поки не буде знайдено маркер вузла в кільці, що буде більше ніж маркер запису. Кожен вузол відповідальний за ділянку кільця між своїм маркером (включаючи його) та своїм попередником (виключаючи його маркер). Іншими словами, кожен вузол відповідає за діапазон маркерів в кільці і записаний в цей діапазон буде запис, значення маркера якого лежить в межах його значень. У свою чергу, вузли відсортовані в порядку зростання значень маркерів, які їм призначені.

Читання відбувається по-суті аналогічно.

Можливо більш зрозуміло логіку демонструють ці зображення. Кожен сектор кільця відповідає за діапазон значень маркерів. Потім вираховується маркер запису (значення навпроти записів в таблиці) на основі PRIMARY KEY (береться PARTITION key від нього) запису:


На основі вирахуваного значення маркеру, у разі зміни/створення/читання, кожен запит потрапляє у відповідний сектор круга/кільця, в залежності чи потрапляє маркер запису в діапазон маркерів кільця:


Розподільник записів називається Partitioner і в останніх версіях по замовчуванню - це Murmur3Partitioner. І це власне все про його логіку роботи. Є і інші функції-розподільники даних, наприклад, RandomPartitioner та ByteOrderedPartitioner.

Replication


В залежності від обраного 'replication_factor' (на малюнку нижче він рівний значенню 3), копії також можуть зберігатись на інших секторах:


Механізм розподілу реплік загалом поділяється на два види:

SimpleStrategy - репліки розміщуються без врахування топології вузлів кластеру і будуть знаходитись на наступних вузлах після вузла, який вираховується розподільником як основний, по алгоритму наведеному вище. Це значення по-замовчуванню і реалізовується за допомогою опції "endpoint_snitch: SimpleSnitch". Є сенс лишити, якщо всі вузли кластера знаходяться в одному датацентрі.

NetworkTopologyStrategy - репліки розміщуються з врахуванням топології кластеру. Ця стратегія розміщує кожну наступну репліку на вузол, що знаходиться на іншій Rack-стійці. Це, вочевидь, робиться задля зменшення ймовірності простою: якщо вийде з ладу вся стійка - репліка не буде повністю втрачена у разі рознесення реплік по датацентрах. Можливі опції endpoint_snitch для реалізації такого типу репліки: GossipingPropertyFileSnitch, PropertyFileSnitch, Ec2Snitch, Ec2MultiRegionSnitch, RackInferringSnitch.

Важливим механізмом роботи реплік є рівень узгодженості (consistency level), який відповідає за забезпечення ступеня узгодженості даних в репліках. Рівень узгодженості визначається для читання і запису даних:

* Рівень узгодженості на запис даних визначає від скількох вузлів повинні бути отримані підтвердження про успішний запис, щоб клієнту було надіслано підтвердження про успішну операцію запису вцілому.
* Рівень узгодженості на читання даних визначає від скількох вузлів повинні бути отримані відповіді на запит читання, щоб відправити результат клієнту.

Рівні бувають такими: ANY, ONE, TWO, THREE, QUORUM, SERIAL, ALL і т.д. Наприклад, рівень ONE вказує, що для того щоб запит вважався успішним необхідна лише одна відповідь від найближчої репліки, QUORUM - що запит має успішно обробити більшість серверів в кластері, наприклад, 2 з 3 і т.п.  Таким чином, можна завжди обирати між швидкістю виконання запитів і надійністю. По замовчуванню в фоні ноди Cassandra працює read repair процес, що приводить всі ноди в консистентний стан, тому для багатьох завдань ONE є цілком підходящим рівнем.

Для обміну сервісною інформацією про стан кластеру вузли використовують одноранговий протокол Gossip. Щосекунди кожен вузол кластеру відсилає сервісні дані про себе та своїх сусідів, що відправили дані таким же чином, 3-м іншим вузлам. У наслідок чого інформація про стан кластеру поширюється на всі вузли. З кожним подальшим обміном Gossip старі повідомлення перезаписуються новішими даними.

Для коректної роботи Gossip у Cassandra необхідно для кожного вузла кластеру описати seeds list - список серверів, які використовуються як еталонні ноди з сервісними даними на початку старту нових вузлів кластеру. Seeds list має бути однаковий для всіх вузлів. Це має бути декілька серверів, що знаходяться в різних дата центрах/стійках для мінімізації можливого простою.

Описувати в seeds list всі вузли не варто, так як це може вплинути на швидкість роботи служби.

Розглянемо як відбувається запис данних в Cassandra на нищому рівні.

Under the hood: Commit Log, Memtable, SStable


Отже запит на запис прийшов від клієнта на будь-яку ноду кластеру Cassandra. Вузол, який прийняв запит стає координатором процесу і надсилає дані основній репліці відповідальній за обробку запиту.



На цільовому вузлі запит одразу потрапить до кешу операційної системи (Page Cache) а вже потім до структури Commit Log на диску.

Commit Log представляє з себе структуру даних, яка розташовується на жорсткому диску і логує всі операції запису для можливості відновлення у випадку збою, коли дані з наступних структур не встигнуть скинутись на жорсткий диск і т.п. (щось схоже на binlog в MySQL). Commit Log може складатись з множини файлів.

Скидання данних з Page Cache до Commit Log може відбуватись в декількох режимах:

* periodic - кожний певний проміжок часу Commit Log буде синхронізуватись з кешем операційної системи, до того ж підтвердження успішного запису в Commit Log буде відправлятись вже на етапі потрапляння запиту на запис в Page Cache операційної системи.

* batch - підтвердження запису запиту буде лише у випадку, коли запит запишеться в Commit Log на диск. По замовчуванню синхронізації відбуваються кожні 50 мілісекунд. Цей режим більш вимогливий до дискової підсистеми за попередній, адже дрібних операцій запису буде значно більше. Проте він більш безпечний, адже в першому випадку існує можливість втратити дані на протязі 10 секунд.

Керування режимами роботи Commit Log здійснюється за допомогою зміни параметрів commitlog_sync та commitlog_sync_period_in_ms.

Commit Log може запитувати синхронізацію данних на диск з Memtable у випадку його повного заповнення (commitlog_total_space_in_mb та commitlog_segment_size_in_mb регулює об’єм Commit Log та кількість файлів-сегментів).

Після успішного запису команд до кешу ОС чи Commit Log дані виконуються в Memtable. Memtable - структура в оперативній пам’яті, що характеризується такими особливостями:

* елементи в ній відсортовані по PARTITION KEYs.
* дані всередині PARTITION KEY відсортовані по CLUSTERING KEYs.
* для кожного колонкового сімейства існує окрема Memtable.

Memtable - це структура, яка тримає в пам’яті лише найсвіжіші дані. Для пошуку і вставки нових елементів у Memtable використовується алгоритм Skip List, вірніше його реалізація на Java - ConcurrentSkipListMap.

Коли розмір Memtable досягає максимального об’єму відбувається скидання данних в структуру SSTable, що лежить на диску (проте підструктири SSTable є такоє в оперативній памяті, про це - далі). Регулювати максимальний об’єм Memtable для збросу на диск можна за допомогою опції "memtable_total_space_in_mb", проте вона достатньо ефективно розраховується автоматично.

Основні особливості SSTable:

* незмінність(immutable);
* існує на кожна колонкове сімейство (таблицю);
* до кожної SSTable будується індекс розділу (partition index), зміст розділу (partition summary) та прив’язується bloom-фільтр;

SSTable складається з таких основних компонентів:

* дані (data) - упорядкований список записів та колонок
* інформація для розшифровки (CompressionInfo)
* фільтр Блума (bloom filter)
* зміст розділу (partition summary)
* індекс розділу (partition index)
* файл статистики (statistics) - містить статистичні дані: об’єми записів, к-ть колонок та ін.
* TOC-файл - містить список компонентів для конкретної SSTable.

Всі перераховані компоненти представлені у вигляді файлів на диску, що знаходяться в окремій директорії, ім'я якої аналогічно імені простору ключів. Проте зміст розділу та фільтр Блума також завантажуються в оперативну пам’ять для прискорення доступу до даних.

Індекс розділу - "карта" даних певної SSTable. Це список ключів та відповідних стартових позицій для кожного із них в SSTable. Тобто список зсувів в байтах, який необхідно зробити задля доступу до відповідного ключа.


Індекс розділу розміщується на диску, та задля прискорення доступу до позицій кожного із PARTITION ключів (даних) створено структуру Partition Summary (змістр розділу), що працює в оперативній пам'яті. Суть в тому, що індекс розділу може мати досить пристойний розмір і, як наслідок, розміщувати одразу ввесь індекс в RAM не дуже доцільно. Зміст розділу - це список, де описано розміщення кожного 128-го PARTITION/ROW ключа (по замовчуванню) в індексі розділу. Тобто зі змістом немає необхідності кожен раз перечитувати індекс розділу з початку, а стартувати одразу з певної позиції на яку і вкаже ця структура.


До кожного SSTable-файлу також прив’язаний окремий Bloomfilter. Це структура ймовірності, яка відповідає на запит про те, чи належить даний ключ множині ключів, що зберігаються в файлі даних (data file). Bloomfilter може із 100%-ймовірностю відповісти, що конекретний ключ не належить SSTable-файлу, але може помилитись, відповідаючи на питання до якої саме SSTable належить ключ. Проте помиляється Bloomfilter достатньо рідко.

Якщо ми хочемо отримати ключ по якомусь значенню, ми просто опитуємо Bloomfilter кожного з SSTable-файлів, і знаходимо серед них файл, якому даний ключ належить. Або не знаходимо, якщо Bloomfilter таки помилився, тоді відбудеться знову пошук з виключенням цього SSTable. І нарешті в SSTable-файлі ми шукаємо цей ключ або інтервал ключів за допомогою змісту та індексу розділу.

Суть роботи Bloomfilter полягає у розрахунку хеш-сум двома функціями перед кожним записом ключа та порівнянні її з, розрахованою тими ж функціями, хеш-сумами після кожного нового запиту ключа. Якщо хеш сума до і після не співпадає - ключ лежить не в цьому SSTable, якщо ж співпадає - то, можливо, лежить, адже іноді відбуваються колізії. Це дуже базове пояснення, детальніше про Bloomfilter можна почитати тут чи тут.

SSTable - незмінна структура, тобто файли даних, що лежать на дисках не можуть змінюватись на протязі роботи. Надалі при кожній зміни в записах, які зберігає SSTable буде створюватись додатковий SSTable, котрий відображатиме ці зміни.

Проте з часом може відбуватись ущільненння SSTable в одну результуючу файл-таблицю для пришвидшення пошуків та економії місця. Цей процес злиття називається compaction.

Цікаво, що після видалення значень в таблиці (операція DELETE в CQL), Cassandra також сворить нову SSTable, проте вже зі значенням null навроти видаленого значення, котре часом називають tombstone.  Тому логіки, котра потребує часті видалення значень в таблицях, краще уникати.

Читання відбувається за допомогою порівняння Timestamp кожної SSTable, останній Timestamp - найбільш актуальні дані. Звісно на таке порівняння необхідно затратити більший обсяг IO-операцій, ніж на запис, тому і запити на запис в Cassandra відбувають швидше за запити на читання. Така от досить нестандартна особливість.

PS. Думаю, має сенс час від часу перевіряти статус розвитку ScyllaDB. Це переписана Cassandra на мові C++, яка, як говорять розробники, працює до 10 разів швидше. Проект поки в стадії бета тестування і не підтримує деяких можливостей Cassandra, проте реліз вже обіцяють на початку 2016 року.

Посилання:
http://blog.bissquit.com/?cat=72&paged=2
http://profyclub.ru/docs/172
http://eax.me/cassandra/
http://docs.datastax.com/en/cassandra/2.0/cassandra/architecture/architectureIntro_c.html
http://docs.datastax.com/en/cassandra/2.0/cassandra/install/installDeb_t.html
http://docs.datastax.com/en/cassandra/2.2/cassandra/dml/dmlAboutReads.html
http://www.datastax.com/2012/01/getting-started-with-cassandra
http://jonathanhui.com/how-cassandra-read-persists-data-and-maintain-consistency
http://habrahabr.ru/post/114160/
http://thelastpickle.com/blog/2013/01/11/primary-keys-in-cql.html
http://www.slideshare.net/nickmbailey/introduction-to-cassandra-27664234
http://www.tutorialspoint.com/cassandra/cassandra_shell_commands.htm
http://intellidzine.blogspot.co.uk/2013/11/cassandra-data-modelling-tables.html
http://intellidzine.blogspot.com/2014/01/cassandra-data-modelling-primary-keys.html
http://stackoverflow.com/questions/24949676/difference-between-partition-key-composite-key-and-clustering-key-in-cassandra/24953331#comment53716152_24953331
http://thelastpickle.com/blog/2016/03/04/introductiont-to-the-apache-cassandra-3-storage-engine.html
https://dzone.com/articles/introduction-apache-cassandras
https://www.packtpub.com/books/content/cassandra-architecture

Немає коментарів:

Дописати коментар