A fost o vreme când tehnologiile NoSQL erau considerate un trend, un termen la modă, în esență ceva care nu era aplicabil în viața de zi cu zi. În ziua de azi însă, cred că NoSQL nu mai reprezintă o moda și chiar și companiile de dimensiune medie se confruntă cu problema volumului crescător de date. În această situație, scenariul migrării de la o bază de date relațională la una de tip NoSQL devine tot mai comun datorită avantajului principal al acestor tehnologii: posibilitatea de a scala aplicațiile în clustere.
Acest articol va prezenta cum se instalează o bază de date de tip Cassandra și cum se poate crea un cluster Cassandra prin adăugarea mai multor noduri. Vom vedea cum se utilizează linia de comandă, cum arată limbajul de interogare (CQL) și un exemplu de clasă Java care folosește un client API Cassandra. În final vom discuta despre limitările acestei soluții NoSQL, modul cum acestea pot fi depășite și câteva recomandări generale.
Baza de date Cassandra este una din cele mai utilizate baze de date NoSQL oferind cele mai bune rezultate pentru scalarea performanței și posibilitatea de a distribui în mod partiționat setul de date pe nodurile din cluster în mod gratuit (licența Apache 2.0). Următorul grafic creat de compania DataStax compară 5 din cele mai utilizate soluții NoSQL din care reiese performanța superioară a bazei de date Cassandra în ceea ce privește creșterea volumului de date procesate odată cu creșterea numărului de noduri din cluster.
Această bază de date poate fi instalată atât pe Linux cât și pe Windows, iar procesul de instalare este unul simplu: descărcarea și decomprimarea arhivei software, configurarea a câteva adrese pe disc folosite pentru stocarea datelor în fișierului cassandra.yaml și rularea executabilului cassandra.
După ce linia de comandă este pornită totul ar trebui să pară foarte familiar. Limbajul de interogare, CQL (Cassandra Query Language) e aproape identic cu SQL.
cqlsh:demo> create keyspace demo with replication =
... {'class':'SimpleStrategy', 'replication_factor':1};
cqlsh:demo> create table users (
... id varchar primary key,
... name varchar
... );
cqlsh:demo> insert into users (id, name) values
... ('1', 'John');
cqlsh:demo>
cqlsh:demo> select * from users where id = '1';
id | name
----+------
1 | John
(1 rows)
Singurul aspect necunoscut din exemplul de mai sus ar trebui să fie conceptul de keyspace care se aseamănă cu schema din baza de date Oracle și conține informații despre cum trebuie replicate datele în cluster-ul de Cassandra.
Pentru a putea scala performanța unei aplicații, Cassandra permite configurarea de clustere de noduri Cassandra. Prin utilizarea cluster-elor o aplicație poate beneficia de un grad ridicat de disponibilitate, o scalare liniară a performanței și de o interfață simplă de acces către o multitudine de servere de cost redus fără un punct unic de eșec (single point of failure).
În cadrul unui context de cluster se pot defini cantitatea de date pe care un anumit nod poate să o proceseze (prin intermediul token-ilor) și pe care nod din cluster să ajungă o anumită înregistrare. Gradul de disponibilitate al cluster-ului se poate regla prin intermediul strategiei de replicare care indică numărul de copii (replica) al fiecărui rând dintr-un tabel ce trebuie să existe în cluster la un moment dat.
Pentru a crea un cluster Cassandra simplu, editați fișierul cassandra.yaml și completați următoarele valori:
cluster_name: 'Test Cluster'
seed_provider:
# Addresses of hosts that are deemed contact points.
# Cassandra nodes use this list of hosts to find each other and learn
# the topology of the ring. You must change this if you are running
# multiple nodes!
- class_name: org.apache.cassandra.locator.SimpleSeedProvider
parameters:
- seeds: "16.22.70.184"
listen_address: 16.22.70.184
După ce fișierul de configurare este finalizat, instalați pachetul Cassandra și copiați fișierul cassandra.yaml pe toate nodurile din cluster. Valoarea cluster_name trebuie să fie identică pe toate mașinile. Nodurile seed sunt utilizate de către fiecare nod din cluster pentru a descoperi topologia cluster-ului (Pentru acest exemplu alegeți un nod ca și nod seed și completați IP-ul acestuia pe restul mașinilor). Setați câmpul listen_address la adresa IP a fiecărei mașină, aceasta va fi folosită pentru a comunica cu celelalte noduri.
# Node 1 Configuration
cluster_name: 'Test Cluster'
seed_provider:
- class_name: org.apache.cassandra.locator.SimpleSeedProvider
parameters:
- seeds: "16.22.70.184"
listen_address: 16.22.70.184
# Node 2 Configuration
cluster_name: 'Test Cluster'
seed_provider:
- class_name: org.apache.cassandra.locator.SimpleSeedProvider
parameters:
- seeds: "16.22.70.184"
listen_address: 16.22.70.193
După ce toate nodurile au fost configurate și pornite, putem verifica pornirea cu succes a fiecărei mașini căutând mesajul "Listening for thrift clients..." în consola de pornire Cassandra. Pentru a verifica starea cluster-ului, putem utiliza comanda de mai jos:
e:\programs\apache-cassandra-2.1.2\bin> nodetool status
Starting NodeTool
Datacenter: datacenter1
========================
Status=Up/Down
|/ State=Normal/Leaving/Joining/Moving
-- Address Load Tokens Owns Host ID Rack
DN 16.59.61.100 ? 256 ? 9ae29fc3-b218-4ee2-904a-0db4ff5e28a3 rack1
DN 16.60.161.225 ? 256 ? 48045f86-26a4-4e1f-80fc-12dc16480319 rack1
UN 16.22.70.184 330.11 KB 256 ? 8b42a1e4-abe3-406a-96d8-12c12382b075 rack1
DN 16.22.69.37 ? 256 ? c3b3feef-41e2-4346-a86e-ec443fbd2fa5 rack1
Utilitarul nodetool folosit mai sus permite adăugarea/ștergerea de noduri în/din cluster precum și executarea unor operații de întreținere și management.
În ceea ce privește clienții de API, Cassandra oferă o gamă largă de posibilități în funcție de limbajul de programare utilizat. Cel puțin 6 din acești clienți sunt scriși în limbajul Java, iar DataStax, Astyanax și Hector sunt printre cei mai utilizați. Pentru exemplul de mai jos vom folosi API-ul DataStax deoarece este unul din alegerile mature care dispune de o documentație clară și detaliată. (Compania DataStax este una ce investeste activ în ecosistemul Cassandra, oferind documentație pentru această bază de date ca și soluție open-source dar și dezvoltând în paralel o soluție comercială bazată pe Cassandra).
Pentru a scrie o aplicație Java simplă pentru Cassandra, începem prin a adauga dependința cassandra-driver-core în fișierul de configurare Maven, pom.xml.
<dependency>
<groupId>com.datastax.cassandra</groupId>
<artifactId>cassandra-driver-core</artifactId>
<version>2.1.3</version>
</dependency>
Clasa de mai jos se conectează la instanța locală de Cassandra și interoghează utilizatorii din tabelul creat anterior prin linia de comandă.
import com.datastax.driver.core.Cluster;
import com.datastax.driver.core.ResultSet;
import com.datastax.driver.core.Row;
import com.datastax.driver.core.Session;
import com.datastax.driver.core.querybuilder.QueryBuilder;
import com.datastax.driver.core.querybuilder.Select;
import java.util.List;
public class CassandraTestClient {
private Cluster cluster;
private Session session;
public void connect(String node) {
cluster = Cluster.builder().addContactPoint(node).
build();
System.out.printf("Connected to cluster: %s\n",
cluster.getMetadata().getClusterName());
session = cluster.connect();
}
public List getUsers() {
ResultSet execute = session.execute("select * "+
"from demo.users;");
return execute.all();
// don't do this outside a demo
}
public List getUsersUsingQueryBuilder() {
Select select = QueryBuilder.select().all().
from("demo", "users");
return session.execute(select).all();
// don't do this outside a demo
}
public void close() {
session.close();
cluster.close();
}
public static void main(String[] args) {
CassandraTestClient client =
new CassandraTestClient();
client.connect("127.0.0.1");
System.out.println(client.getUsers());
System.out.println(client.
getUsersUsingQueryBuilder());
client.close();
}
}
În exemplul de mai sus, codul client se conectează la cluster-ul Cassandra utilizând nodul contact primit (în acest caz, mașina locală) care are rolul de a-i descoperi clientului topologia cluster-ului. După ce obiectul Session a fost obținut putem construi și executa interogări într-un mod foarte asemănător cum am face cu o bază de date Oracle.
Metoda getUsers execută interogarea CQL și returnează toate rândurile ca și rezultat (a se folosi doar pentru un număr mic de rânduri în tabel deoarece forțează aducerea tuturor rândurilor din tabel odată). API-ul DataStax permite și mecanismele avansate de execuție a interogărilor prezente în clienții tradiționali ai bazelor de date cum ar fi prepared și batch statements (cu toate acestea aveți grijă, batch-urile nu trebuiesc utilizate pentru performanță) și setări cum ar fi fetch size și selectarea unor politici de reconectare și reîncărcare.
Pe lângă construcția de statement-uri CQL, DataStax oferă API-ul QueryBuilder care permite crearea dinamică a interogărilor cu beneficiul de a elimina atacurile de tip injection. Metoda getUsersUsingQueryBuilder ilustrează acest mecanism.
Rezultatul rulării metodei CassandraTestClient.main() trebuie să fie:
Connected to cluster: Test Cluster
[Row[1, John]]
[Row[1, John]]
Cu toate că baza de date Cassandra oferă multe avantaje comparativ cu soluțiile clasice de tip relațional, prezintă de asemenea și câteva dezavantaje semnificative. Punctele slabe ale acestei soluții sunt:
Cassandra nu oferă garanția atomicității și izolării la nivel de tranzacție (de fapt, conceptul de tranzacție nu există), cu toate acestea, asigură aceste proprietăți la nivelul unui rând de tabel. Avînd în vedere natura bazei de date Cassandra (column-oriented key-value store), garanția la nivel de rând are sens din punct de vedere al design-ului deoarece într-o baza de date NoSQL de acest tip fiecare rând ar trebui să reprezinte o entitate complexa întreagă (iar accesul atomic și izolat la acea entitate ar trebuie să fie suficient pentru a asigura consistența datelor).
În ceea ce privește consistența datelor la nivel de cluster, Cassandra oferă posibilitatea de a folosi diferite nivele de consistență pentru citire și scriere. Aceste nivele pot fi rezumate la următoarea idee: Câte noduri din cluster trebuie să confirme operația de citire/scriere?
De asemenea, pentru a ajuta programatorii în a asigura consistența datelor, începând cu Cassandra 2.0, s-a adăugat conceptul de tranzacție lightweight. Cu toate că numele este promițător, functionalitatea permite operații de tip compare and set cum ar fi crearea unei inregistrări noi doar dacă nu există deja sau actualizarea unei inregistrari dacă o anumită condiție e indeplinită.
Despre problema lipsei tranzacțiilor, cunoscutul programator și autor, Martin Fowler, spune că nu trebuie privită ca și un dezavantaj major fiindcă în majoritatea cazurilor utilizarea tranzacțiilor pe perioade mari de timp poate afecta serios performanța. Astfel în [9] el propune soluția de offline lock în care setul de date este versionat iar fiecare commit necesită verificarea informației de versiune pentru setul de date implicat în operație.
Următoarea diagramă prezintă un exemplu pentru acest concept.
Această binecunoscută limitare a bazelor de date NoSQL este una cu care trebuie să începeți. Crearea (sau migrarea) unei aplicații cu ajutorul Cassandra va necesita gândire NoSQL. Aceasta înseamnă remodelarea structurii bazei de date în jurul ideii de modele agregate (nu stoca un tabel cu utilizatori și un tabel cu adrese, ci stochează un singur tabel pentru utilizatori în care înregistrarea pentru utilizator include adresele necesare) chiar dacă presupune un anumit nivel de redundanță a datelor în db sau o rescriere majoră a data acces layer-ului. Cu toate aceastea, în funcție de modelul de business al aplicației, utilizarea de modele agregate ar putea simplica mult codul de persistență.
Dacă o aplicație necesită multă flexibilitate în ceea ce privește condițiile interogărilor, Cassandra nu va fi de mare ajutor. De fapt, Cassandra nu va permite
setarea unei condiții pe o coloană care nu face parte din cheia primară sau nu e indexată
select * from users where name = 'John';
fără a crea explicit un index pe coloana namesetarea unei condiții folosind unul din operatorii IN, =, >, >=, <, or <=
fără a adauga o condiție de egalitate pe cel puțin una din coloanele ce fac parte din cheie,
create index users_name_index on users(name);
putem executa interogarea select * from users where name ='John';
însă nu putem executa interogarea similară select * from users where name in ('John');
deoarece interogarea nu contine o condiție de egalitateÎn aceasta situație suntem forțați să implementăm unele interogari manual. Însă, din fericire majoritatea soluțiilor NoSQL permit integrări Hadoop iar în acest caz specific, Cassandra poate fi integrată cu 2 framwork-uri ce permit execuția de interogări, Apache Hive [5] și Apache Pig [6] (din păcate documentația despre integrarea propriu-zisă lipsește cu desăvîrșire, însă vă recomand să căutați câteva cărți cu Cassandra care includ aceste informații). Apache Hive oferă o interfață foarte similară cu SQL și CQL care nu necesită învățarea de concepte noi însă poate avea penalizări de performanță, pe când Apache Pig dispune de un limbaj de programare care permite execuția de interogări într-o manieră programatică și cu un grad mai mare de control.
NoSQL reprezintă un pas important în dezvoltarea mecanismelor de persistare a datelor care necesită o schimbare în modul care trebuie să gândim aplicațiile. Fie că o facem din necesitate sau din curiozitate programatică, consider că NoSQL este o tehnologie ce merită învățată.
Bibliografie
datastax.com/documentation/developer/java-driver/2.1/java-driver/whatsNew2.html
Cassandra High Performance Cookbook by Edward Capriolo