I. Introduction▲
François Ostyn (@ostynf), Khanh Tuong Maudoux (@jetoile) ont écrit un premier billet sur Cassandra (ici) suite à la présentation faite pendant Devoxx France 2012. Aujourd'hui, nous allons voir à travers cet article comment utiliser Cassandra, l'installer, configurer un cluster, utiliser une API client, et implémenter des tests d'intégration avec Cassandra. Mais, avant cela, nous vous expliquerons les cas d'utilisation les plus adaptés à Cassandra, nous allons entrer en détail pour comprendre les problèmes liés au modèle relationnel et les solutions proposées par les bases de données NoSql.
II. Les cas d'utilisation typiques de Cassandra▲
Bien que Cassandra soit dotée d'une architecture passionnante, supporte la scalabilité, la tolérance aux pannes, et bénéficie d'une haute disponibilité, ce n'est pas la meilleure solution pour tout type de problème. Voyons quels sont les cas d'utilisation les plus adaptés avec Cassandra.
II-A. Systèmes distribués▲
Vous êtes sans doute comblés par l'architecture passionnante de Cassandra (réplication peer-to-peer, niveau de consistance, scalabilité, haute disponibilité, etc.). Aucune de ces qualités n'est vraiment nécessaire lorsque l'on souhaite installer une BD "single-node". Dans beaucoup de situations, une base de données installée sur une seule machine suffira. Par contre, si votre besoin est d'avoir une base de données distribuée sur plusieurs nœuds (même des dizaines), Cassandra est un des meilleurs choix.
II-B. Beaucoup d'opérations d'écriture▲
Dans une base de données relationnelle, l'écriture des données est coûteuse. Ceci est dû à la manière dont les données sont structurées : le SGBDR doit assurer l'intégrité des données dans les différentes tables au moment de chaque écriture. Cassandra a été conçue pour offrir une meilleure optimisation d'écriture de données. La stratégie d'écriture adoptée par Cassandra ainsi que son incompatibilité avec les transactions ACID permettent de donner plus de performance au niveau de l'écriture des données. Une base de données qui fait beaucoup d'opérations d'écriture est plus performante avec Cassandra.
II-C. Données dynamiques, trop volumineuses▲
Cassandra est une base de données orientée colonne, elle s'inspire du modèle BigTable de google. Sur chaque ligne on peut insérer un grand nombre de colonnes (jusqu'à deux milliards). Cassandra offre un plus par rapport au modèle BigTable, car elle propose un concept de conteneur de colonnes ou SuperColumns. Le modèle orienté colonne offre une disposition différente du modèle relationnel. Les colonnes sur Cassandra sont dynamiques et peuvent changer d'une ligne à l'autre.
III. Différences entre SGBDR et Cassandra▲
Il y a plusieurs différences entre les méthodes d'interrogation d'une base de données Cassandra et d'un SGBDR. Connaître les points de divergences entre Cassandra et un SGBDR vous aidera à choisir la base de données la plus adaptée à vos besoins.
III-A. Pas d'intégrité référentielle▲
Cassandra ne définit aucune notion d'intégrité référentielle, il n'y a donc pas de jointures. Dans les bases de données relationnelles, vous pouvez spécifier dans une table la clé étrangère qui référence la clé primaire d'un enregistrement d'une autre table. Mais Cassandra ne supporte pas cette règle, donc les opérations telles que le cascading n'existent pas.
III-B. Pas de formes normales▲
Il est souvent recommandé pour les bases de données relationnelles de respecter la normalisation. Mais ce n'est pas conseillé lorsque l'on souhaite augmenter la performance de la BD. C'est souvent le cas d'entreprises qui finissent par dénormaliser leurs bases de données relationnelles. Dans le monde relationnel, la dénormalisation viole les formes normales et c'est à éviter. Dans Cassandra, la dénormalisation est tout à fait logique. C'est un avantage supplémentaire car le modèle de données est plus simple, donc plus facile à comprendre.
III-C. Pas de clause "ORDER BY"▲
Dans un SGBDR, on peut facilement changer l'ordre dans lequel les résultats d'une requête SELECT sont retournés, à l'aide de la clause ORDER BY. Mais il n'y a pas d'ordre de tri par défaut, les enregistrements sont retournés dans l'ordre dans lequel ils ont été écrits. Dans Cassandra, le tri est traité différemment : lors de la création de votre ColumnFamily, vous devez définir la stratégie de comparaison qui détermine l'ordre dans lequel les lignes et les colonnes seront triées, mais ce n'est pas définissable dans la requête.
À partir de la version 1.1 d'Apache Cassandra (sortie en septembre 2012), il est possible d'utiliser la clause ORDER BY pour trier le résultat d'une requête sur une colonne donnée (à condition d'utiliser la version 3.0 du langage d'interrogation CQL).
IV. Passons à la pratique▲
IV-A. Installation et configuration▲
Le site d'Apache Cassandra fournit une explication suffisante pour vous aider à installer, configurer, lancer un cluster Cassandra avec un ou plusieurs nœuds. Voyons de plus près ce que contient la distribution binaire Cassandra :
- bin : contient les exécutables permettant l'exécution de Cassandra et l'interface de ligne de commande cassandra-cli. Ce répertoire contient aussi des scripts pour convertir les fichiers de données (SSTables) en format JSON ou l'inverse ;
- conf : comprend l'ensemble des fichiers nécessaires pour la configuration d'un cluster Cassandra. cassandra.yaml : fichier de configuration principal de Cassandra. Ce fichier contient la configuration du nœud courant, mais aussi celle du cluster et des autres nœuds ;
- interface : le fichier cassandra.thrift est l'interface qui contient la spécification IDL des services offerts pour les API clients Cassandra. Thrift assure l'interopérabilité entre Cassandra et les API implémentant les services décrits dans l'IDL. Ainsi Cassandra peut supporter des clients en Java, C++, PHP, Ruby, Python, C#, via cette interface Thrift ;
- lib : contient toutes les bibliothèques dont Cassandra a besoin pour s'exécuter. On trouve aussi les bibliothèques RPC de Thrift et Avro pour communiquer avec Cassandra.
IV-B. Cassandra-cli▲
Cassandra offre depuis les premières versions une interface en ligne de commande très basique : Cassandra-cli. Vous pouvez vous connecter au cluster pour créer ou mettre à jour votre schéma puis définir et récupérer des données de votre base.
Pour se connecter à un cluster Cassandra, c'est simple : il suffit de renseigner le port et l'adresse pour accéder au cluster dont le nom est dans le fichier cassandra.yaml.
cassandra-
cli -
host
120
.0
.0
.1
-
port
9160
#ou
cassandra-
cli
[default]
connect
127
.0
.0
.1
/
9160
Pour la création et la manipulation d'un keyspace, d'un column Family…
-- creation d'un keyspace
[default-unknown]
create
keyspace keyspace1
[default-unknown]
use
keyspace1
-- création d'un ColumnFamily
[default@keyspace1]
CREATE
COLUMN
FAMILY users
WITH
comparator =
UTF8Type
AND
key_validation_class=
UTF8Type
AND
column_metadata =
[
{column_name
: full_name, validation_class : UTF8Type}
{column_name
: email, validation_class : UTF8Type}
{column_name
: age, validation_class : LongType}
];
-- insertion
[default@keyspace1]
set
User
['user1']['first']
=
'Mouhcine'
;
[default@keyspace1]
set
User
['user1']['last']
=
'MOULOUUU'
;
[default@keyspace1]
set
User
['user1']['age']
=
27
;
-- pour modifier un champ, on utilise la même syntaxe
[default@keyspace1]
set
User
['user1']['last']
=
'MOULOU'
;
-- Récupération d'une ligne entière par sa clé
[default@keyspace1]
get
User
['user1']
-- ou par autres critères
[default@keyspace1]
get
User
where
age >
18
;
IV-C. Cassandra Query Langage (CQL)▲
Avant la version 0.8 de Cassandra, les développeurs Apache n'ont pas trouvé nécessaire d'avoir un langage d'interrogation de données comme SQL. Les clients voulant interroger Cassandra depuis une interface de ligne de commande utilisaient Cassandra-cli.
Avec la sortie de la version 0.8, le langage d'interrogation de données pour Cassandra (CQL) est apparu, dont la syntaxe est basée sur SQL. Par contre, cela ne modifie pas le modèle Cassandra : il n'y a pas de support des jointures, de tri, ni de Group By.
Ci-dessous quelques exemples de requêtes écrites avec CQL :
-- Create a new Keyspace
cqlsh>
CREATE
KEYSPACE keyspace1
WITH
strategy_class =
'org.apache.cassandra.locator.SimpleStrategy'
AND
strategy_options :replication_factor=
'1'
;
cqlsh>
USE
keyspace1;
-- Create d'un ColumnFamily
cqlsh>
CREATE
TABLE
USERS(
USER_NAME varchar
,
PASSWORD
varchar
,
AGE long
,
PRIMARY
KEY
(
USER_NAME)
)
;
-- insertion
cqlsh>
INSERT
INTO
USERS (
USER_NAME, PASSWORD
)
VALUES
(
'user1'
, 'p@ssw0rd'
)
;
-- Récupération d'une ligne entière par critères
cqlsh>
SELECT
*
FROM
users WHERE
user_name=
'user1'
;
IV-D. API Cliente▲
Au cours des premières versions Cassandra, l'interface Thrift était utilisée pour l'implémentation de l'API cliente entière. Malgré ses limitations, Thrift continue d'être supportée dans les nouvelles versions Cassandra.
L'API Thrift offre des fonctionnalités de bas niveau dont bon nombre sont manquantes. Plusieurs implémentations ont vu le jour afin de proposer des API clients Cassandra avec des interfaces de plus haut niveau.
IV-D-1. Hector▲
C'est une API client Cassandra qui offre des caractéristiques de haut niveau (tolérance aux pannes, pool de connexions, support CQL, support JMX, etc.). Hector est parmi les API clients Cassandra les plus utilisées. Nous présentons ci-dessous quelques exemples d'utilisation de cette API.
L'entité persistante est une simple classe "Product" avec des attributs de différents types :
public
class
Product {
private
String ref;
private
String name;
private
int
quantity;
private
double
unitPrice;
// Constructeur, getters et Setters
}
IV-D-1-a. Déclarer une dépendance Hector▲
<dependency>
<groupId>
org.hectorclient</groupId>
<artifactId>
hector-core</artifactId>
<version>
1.1-1</version>
</dependency>
IV-D-1-b. Créer un cluster , un Keyspace, un ColumnFamilyTemplate▲
Un objet de la classe Cluster représente un cluster Cassandra. Un objet Keyspace représente une base de données ou un schéma Cassandra.
Une instance de la classe ColumnFamilyTemplate conserve de nombreux attributs communs entre les différentes requêtes, de sorte que les développeurs n'ont pas besoin de renseigner les valeurs de ces attributs sur chaque requête. Il s'agit notamment du keyspace, du columnFamily, des sérialiseurs, etc.
import
me.prettyprint.hector.api.Cluster;
import
me.prettyprint.hector.api.Keyspace;
import
me.prettyprint.cassandra.service.template.ColumnFamilyTemplate;
import
me.prettyprint.cassandra.service.template.ThriftColumnFamilyTemplate;
// ...
StringSerializer stringSerializer =
StringSerializer.get
(
);
DoubleSerializer doubleSerializer =
DoubleSerializer.get
(
);
IntegerSerializer integerSerializer =
IntegerSerializer.get
(
);
BytesArraySerializer bytesArraySerializer =
BytesArraySerializer.get
(
);
Cluster cluster =
HFactory.getOrCreateCluster
(
"clusterName"
, "host"
);
Keyspace keyspace =
HFactory.createKeyspace
(
"keyspaceName"
, cluster);
ColumnFamilyTemplate<
String, String>
columnFamilyTemplate =
new
ThriftColumnFamilyTemplate<
String, String>(
keyspace, COLUMN_Family_NAME, stringSerializer, stringSerializer);
IV-D-1-c. Insérer un nouvel objet "Product"▲
La classe Mutator permet de faire des opérations d'insertion sur une columnFamily. Mutator est une classe générique qui reçoit le type de la clé d'une columnFamily :
Mutator<
String>
mutator =
HFactory.createMutator
(
keyspace, stringSerializer);
HColumn<
String, String>
nameColumn =
HFactory.createStringColumn
(
"NAME"
, product.getName
(
));
HColumn<
String, Integer>
quantityColumn =
HFactory.createColumn
(
"QUANTITY"
, product.getQuantity
(
), stringSerializer, integerSerializer);
HColumn<
String, Double>
unitPriceColumn =
HFactory.createColumn
(
"UNIT_PRICE"
, product.getUnitPrice
(
), stringSerializer, doubleSerializer);
mutator.addInsertion
(
product.getRef
(
), COLUMN_Family_NAME,nameColumn)
.addInsertion
(
product.getRef
(
), COLUMN_Family_NAME, quantityColumn)
.addInsertion
(
product.getRef
(
), COLUMN_Family_NAME, unitPriceColumn);
mutator.execute
(
);
IV-D-1-d. Récupérer un objet par sa clé▲
public
Product getByRef
(
String ref){
checkNotNull
(
ref, "ref must not be null."
);
logger.info
(
"Getting product its ref where ref= "
+
ref);
// les colonnes ont différents datatypes, on utilise donc byte[] pour les valeurs des colonnes avec BytesArraySerializer comme sérialiseur
RangeSlicesQuery<
String, String, byte
[]>
rangeSlicesQuery =
HFactory.createRangeSlicesQuery
(
getKeyspace
(
), stringSerializer,stringSerializer, bytesArraySerializer);
rangeSlicesQuery.setColumnFamily
(
COLUMN_Family_NAME);
rangeSlicesQuery.setKeys
(
ref, ""
)
rangeSlicesQuery.setRange
(
""
, ""
, false
, Integer.MAX_VALUE);
rangeSlicesQuery.setRowCount
(
1
);
QueryResult<
OrderedRows<
String, String, byte
[]>>
result =
range SlicesQuery.execute
(
);
OrderedRows<
String, String, byte
[]>
orderedRows =
result.get
(
);
Row<
String, String, byte
[]>
row =
orderedRows.peekLast
(
);
if
(
row.getColumnSlice
(
) ==
null
||
row.getColumnSlice
(
).getColum ns
(
).isEmpty
(
)){
return
null
;
}
return
createProduct
(
ref, row.getColumnSlice
(
));
}
public
Product getByRefCQL
(
String ref) {
checkNotNull
(
ref, "ref must not be null."
);
logger.info
(
"Getting product its ref where ref= "
+
ref +
", using CQL query."
);
CqlQuery<
String, String, byte
[]>
cqlQuery =
new
CqlQuery<
String, String, byte
[]>
(
getKeyspace
(
), stringSerializer, stringSerializer, bytesArraySerializer);
String query =
String.format
(
"Select * from %s where key = '%s' "
, COLUMN_Family_NAME, ref);
cqlQuery.setQuery
(
query);
QueryResult<
CqlRows<
String, String, byte
[]>>
queryResult =
cqlQuery.execute
(
);
// use 'checkNotNull' (google.guava) to avoid null test with 'if else' clause
CqlRows<
String, String, byte
[]>
cqlRows =
checkNotNull
(
queryResult.get
(
), "empty result"
);
Row<
String, String, byte
[]>
row =
checkNotNull
(
cqlRows.getByKey
(
ref), "product with ref :"
+
ref +
"not found"
);
return
createProduct
(
ref, row.getColumnSlice
(
));
}
private
Product createProduct
(
String ref, ColumnSlice<
String, byte
[]>
columnSlice) {
checkNotNull
(
ref, "ref must not be null."
);
checkArgument
((
columnSlice!=
null
&
amp;&
amp; !
columnSlice.getColumns
(
).isEmpty
(
)), "columnsSlice must not be null or empty"
);
String name =
new
String
(
columnSlice.getColumnByName
(
"NAME"
).getValue
(
));
int
quantity =
ByteBuffer.wrap
(
columnSlice.getColumnByName
(
"QUANTITY"
).getValue
(
)).getInt
(
);
double
unitPrice =
ByteBuffer.wrap
(
columnSlice.getColumnByName
(
"UNIT_PRICE"
).getValue
(
)).getDouble
(
);
logger.info
(
"initializing new product : [REF=' "
+
ref +
"', NAME=' "
+
name +
"', QUANTITY="
+
quantity +
", UNIT_PRICE="
+
unitPrice);
return
new
Product
(
ref, name, quantity, unitPrice);
}
IV-D-1-e. Modifier un objet Product▲
public
void
update
(
Product product) {
checkNotNull
(
product.getRef
(
), "product.ref must not be null."
);
logger.info
(
"updating row with ref : "
+
product.getRef
(
));
ColumnFamilyUpdater<
String, String>
updater =
columnFamilyTemplate.createUpdater
(
product.getRef
(
));
updater.setString
(
"NAME"
, product.getName
(
));
updater.setInteger
(
"QUANTITY"
, product.getQuantity
(
));
updater.setDouble
(
"UNIT_PRICE"
, product.getUnitPrice
(
));
try
{
columnFamilyTemplate.update
(
updater);
}
catch
(
HectorException e) {
e.printStackTrace
(
);
throw
new
RuntimeException
(
"Unable to update product with key : "
+
product.getRef
(
), e);
}
}
IV-D-1-f. Supprimer un objet Product▲
public
void
deleteByRef
(
String ref) {
checkNotNull
(
ref, "ref must not be null."
);
logger.info
(
"Deleting product with ref= "
+
ref);
columnFamilyTemplate.deleteRow
(
ref);
}
public
void
deleteByRefCQL
(
String ref) {
checkNotNull
(
ref, "ref must not be null."
);
logger.info
(
"Deleting product with ref= "
+
ref +
", using CQL query"
);
CqlQuery<
String, String, byte
[]>
cqlQuery =
new
CqlQuery<
String, String, byte
[]>(
getKeyspace
(
), stringSerializer, stringSerializer, bytesArraySerializer);
String query =
String.format
(
"Delete from %s where key = '%s' "
, COLUMN_Family_NAME, ref);
cqlQuery.setQuery
(
query);
cqlQuery.execute
(
);
}
IV-D-2. Astyanax▲
Astyanax est une API client Java pour Cassandra développée et utilisée par Netflix. Astyanax s'inspire beaucoup des concepts d'Hector mais se démarque en offrant une latence plus faible et de meilleures performances.
Astyanax offre une API simple et riche en fonctionnalités par rapport à Hector. Ces fonctionnalités incluent :
- la non-nécessité d'utiliser les sérialiseurs ;
- la possibilité d'avoir plusieurs types de clés de columnFamily par keyspace ;
- un support des annotations ;
- une pagination automatique ;
- la possibilité de configurer le niveau de consistance par opération.
Ci-dessous des exemples basiques d'utilisation de Astyanax :
private
static
final
String COLUMN_FAMILY_NAME =
"PRODUCT"
;
private
static
final
String NAME_COLUMN =
"NAME"
;
private
static
final
String QUANTITY_COLUMN =
"QUANTITY"
;
private
static
final
String UNIT_PRICE_COLUMN =
"UNIT_PRICE"
;
private
static
final
ColumnFamily<
String, String>
ProductColumnFamily =
getColumnFamily
(
);
public
ProductNoSqlRepository
(
String clusterName, String host, int
port,
String keyspaceName) {
AstyanaxContext<
Keyspace>
context =
new
AstyanaxContext.Builder
(
)
.forCluster
(
clusterName)
.forKeyspace
(
keyspaceName)
.withAstyanaxConfiguration
(
new
AstyanaxConfigurationImpl
(
)
.setDiscoveryType
(
NodeDiscoveryType.NONE))
.withConnectionPoolConfiguration
(
new
ConnectionPoolConfigurationImpl
(
"samplePool"
)
.setPort
(
port)
.setMaxConnsPerHost
(
1
)
.setSeeds
(
host +
" :"
+
port))
.withConnectionPoolMonitor
(
new
CountingConnectionPoolMonitor
(
))
.buildKeyspace
(
ThriftFamilyFactory.getInstance
(
));
context.start
(
);
keyspace =
context.getEntity
(
);
}
private
static
ColumnFamily<
String, String>
getColumnFamily
(
) {
return
new
ColumnFamily<
String, String>(
COLUMN_FAMILY_NAME, StringSerializer.get
(
), StringSerializer.get
(
));
}
/**
*
@param
product
*/
public
void
insert
(
Product product) {
checkNotNull
(
product.getRef
(
), "product.ref must not be null."
);
logger.info
(
"insert new product : "
+
product.toString
(
));
try
{
MutationBatch mutationBatch =
getKeyspace
(
).prepareMutationBatch
(
);
mutationBatch.withRow
(
ProductColumnFamily, product.getRef
(
))
.putColumn
(
NAME_COLUMN, product.getName
(
))
.putColumn
(
QUANTITY_COLUMN, product.getQuantity
(
))
.putColumn
(
UNIT_PRICE_COLUMN, product.getUnitPrice
(
));
OperationResult<
Void>
result =
mutationBatch.execute
(
);
}
catch
(
ConnectionException e) {
logger.error
(
"Unexpected Exception while trying to insert new Product : "
+
e.getMessage
(
));
}
}
/**
*
@param
ref
*
@return
*/
public
Product getByRef
(
String ref) {
checkNotNull
(
ref, "ref must not be null."
);
logger.info
(
"Getting product its ref where ref= "
+
ref);
Product product =
null
;
try
{
OperationResult<
ColumnList<
String>>
result;
result =
getKeyspace
(
).prepareQuery
(
ProductColumnFamily)
.getKey
(
ref)
.execute
(
);
ColumnList<
String>
columns =
result.getResult
(
);
checkArgument
(!
columns.isEmpty
(
), "Unable to find product with key : "
+
ref);
product =
createProduct
(
ref, columns);
}
catch
(
ConnectionException e) {
logger.error
(
"Unexpected Exception while searching for Product with key : "
+
ref +
". : -"
+
e.getMessage
(
));
}
return
product;
}
public
void
delete
(
String ref) {
checkNotNull
(
ref, "ref must not be null."
);
logger.info
(
"deleting product with ref= "
+
ref);
try
{
MutationBatch mutationBatch =
getKeyspace
(
).prepareMutationBatch
(
);
mutationBatch.withRow
(
ProductColumnFamily, ref).delete
(
);
mutationBatch.execute
(
);
}
catch
(
ConnectionException e) {
logger.error
(
"Unexpected Exception while trying to delete row with product ref : "
+
ref +
". -"
+
e);
}
}
/**
*
@param
StartFrom
*
@param
maxResut
*
@return
*/
public
List<
Product>
getProducts
(
String StartFrom, int
maxResut){
List<
Product>
products =
Lists.newArrayList
(
);
try
{
IndexQuery<
String, String>
query =
getKeyspace
(
)
.prepareQuery
(
ProductColumnFamily).searchWithIndex
(
).setStartKey
(
StartFrom)
.setRowLimit
(
maxResut).autoPaginateRows
(
true
);
Rows<
String, String>
result =
query.execute
(
).getResult
(
);
for
(
Iterator<
Row<
String, String>>
iterator =
result.iterator
(
); iterator.hasNext
(
);) {
Row<
String, String>
row =
iterator.next
(
);
products.add
(
createProduct
(
row.getKey
(
), row.getColumns
(
)));
}
}
catch
(
Exception e) {
logger.error
(
""
+
e);
}
return
products;
}
/**
*
@param
ref
*
@param
columns
*
@return
*/
private
Product createProduct
(
String ref, ColumnList<
String>
columns) {
Product product =
new
Product
(
ref);
product.setName
(
columns.getColumnByName
(
NAME_COLUMN).getStringValue
(
));
product.setQuantity
(
columns.getColumnByName
(
QUANTITY_COLUMN).getIntegerValue
(
));
product.setUnitPrice
(
columns.getColumnByName
(
UNIT_PRICE_COLUMN).getDoubleValue
(
));
return
product;
}
IV-E. Les tests d'intégration avec Cassandra▲
Nous avons souvent l'habitude de créer une BD qui sert à la fois pour les tests unitaires et les tests d'intégration. Chaque poste développeur a donc un cluster Cassandra dédié pour les tests, ou bien les développeurs partagent un même cluster distant (risque de résultat inattendu en cas d'accès concurrent à la même ressource).
Comme les tests SQL en Java utilisent souvent HSQLDB pour mocker la base de données, sur Cassandra il existe un certain nombre de services qui permettent de charger un cluster Cassandra dans la JVM au moment de l'exécution des tests.
IV-E-1. Cassandra-unit▲
Ce service offre un moyen de mocker les tests interrogeant un cluster Cassandra. Pour le mettre en place, il faut :
IV-E-1-a. Configurer les dépendances▲
Pour ceux qui utilisent Maven, cassandra-unit est disponible dans le référentiel central de Maven. Ainsi, vous pouvez simplement modifier votre fichier pom.xml et ajouter la dépendance :
<dependency>
<groupId>
org.cassandraunit</groupId>
<artifactId>
cassandra-unit</artifactId>
<version>
1.0.3.1</version>
<scope>
test</scope>
</dependency>
IV-E-1-b. Configurer le cluster , les métadonnées du Keyspace▲
- cassandra.yaml : configuration utilisée pour charger Cassandra dans la JVM. Il s'agit du même fichier de la distribution binaire Cassandra.
- Dataset.json : description du Keyspace en Format JSON (définition des columnFamily et des colonnes, etc.)
Pour notre exemple nous avons le code présenté ci-dessous
{
"name"
:
"StockKS"
,
"replicationFactor"
:
1
,
"strategy"
:
"org.apache.cassandra.locator.SimpleStrategy"
,
"columnFamilies"
:
[
{
"name"
:
"PRODUCT"
,
"keyType"
:
"UTF8Type"
,
"comparatorType"
:
"UTF8Type"
,
"columnsMetadata"
:
[
{
"name"
:
"NAME"
,
"validationClass"
:
"UTF8Type"
},
{
"name"
:
"QUANTITY"
,
"validationClass"
:
"IntegerType"
},
{
"name"
:
"UNIT_PRICE"
,
"validationClass"
:
"BytesType"
}
]
}
]
}
IV-E-1-c. Mise en place, implémentation des tests▲
Nous pouvons définir une TestSuite pour les tests utilisant Cassandra, afin de charger la base en une seule fois pendant l'exécution des tests. Une autre solution consiste à créer une classe abstraite qui sera étendue par chaque classe de test :
package
fr.soat.it;
import
java.io.IOException;
import
org.apache.cassandra.config.ConfigurationException;
import
org.apache.thrift.transport.TTransportException;
import
org.cassandraunit.DataLoader;
import
org.cassandraunit.dataset.json.ClassPathJsonDataSet;
import
org.cassandraunit.utils.EmbeddedCassandraServerHelper;
import
org.junit.After;
import
org.junit.AfterClass;
import
org.junit.Before;
import
org.junit.BeforeClass;
import
com.netflix.astyanax.AstyanaxContext;
import
com.netflix.astyanax.Keyspace;
import
com.netflix.astyanax.connectionpool.NodeDiscoveryType;
import
com.netflix.astyanax.connectionpool.impl.ConnectionPoolConfigurationImpl;
import
com.netflix.astyanax.connectionpool.impl.CountingConnectionPoolMonitor;
import
com.netflix.astyanax.impl.AstyanaxConfigurationImpl;
import
com.netflix.astyanax.thrift.ThriftFamilyFactory;
public
abstract
class
AbstractNoSqlRepositoryIntegrationIT {
@BeforeClass
public
static
void
startCassandra
(
) throws
TTransportException, IOException, ConfigurationException {
EmbeddedCassandraServerHelper.startEmbeddedCassandra
(
"cassandra.yaml"
);
}
@Before
public
void
setUp
(
) throws
IOException,
TTransportException, ConfigurationException, InterruptedException
{
DataLoader dataLoader =
new
DataLoader
(
"TestCluster"
,
"localhost :9160"
);
dataLoader.load
(
new
ClassPathJsonDataSet
(
"dataset.json"
));
AstyanaxContext<
Keyspace>
context =
new
AstyanaxContext.Builder
(
)
.forCluster
(
"TestCluster"
)
.forKeyspace
(
"test_keyspace"
)
.withAstyanaxConfiguration
(
new
AstyanaxConfigurationImpl
(
)
.setDiscoveryType
(
NodeDiscoveryType.NONE))
.withConnectionPoolConfiguration
(
new
ConnectionPoolConfigurationImpl
(
"testConnectionPool"
).setPort
(
9160
)
.setMaxConnsPerHost
(
1
)
.setSeeds
(
"localhost :9160"
))
.withConnectionPoolMonitor
(
new
CountingConnectionPoolMonitor
(
))
.buildKeyspace
(
ThriftFamilyFactory.getInstance
(
));
context.start
(
);
}
@After
public
void
clearCassandra
(
) {
EmbeddedCassandraServerHelper.cleanEmbeddedCassandra
(
);
}
@AfterClass
public
static
void
stopCassandra
(
) {
EmbeddedCassandraServerHelper.stopEmbeddedCassandra
(
);
}
}
V. Quelques astuces et conseils▲
V-A. Tuning Cassandra▲
En utilisant la configuration par défaut Cassandra, vous pouvez rapidement rencontrer des problèmes de performance et d'instabilité (consommation de beaucoup de mémoire pour ne faire que des opérations GC par exemple). Il est nécessaire de configurer correctement votre cluster en fonction de vos besoins. Le fichier cassandra.yaml contient tous les attributs nécessaires pour donner plus de performance à votre base. Vous pouvez définir la stratégie et la taille de cache (par clé, par ligne), configurer la taille de la pile Java, l'accès concurrent, etc. Cassandra demande un peu plus de configuration et de surveillance par rapport aux bases de données plus matures.
V-B. Fragmentation des lignes Cassandra▲
Bien que Cassandra soit orientée colonne, elle donne plus de performance si on conçoit notre schéma pour stocker plusieurs centaines de colonnes par ligne (exemple : tous les tweets d'un utilisateur stockés dans une seule ligne, avec l'identifiant de l'utilisateur comme clé de la ligne). Mais vous devez être prudent avec cette stratégie d'écriture car elle peut devenir très coûteuse en temps. Une opération d'écriture/lecture peut prendre quelques secondes (et non millisecondes) si votre ligne contient des dizaines de milliers de colonnes, d'où la nécessité de fragmenter les lignes (on peut stocker sur chaque ligne les tweets de l'année en cours, la clé sera donc la concaténation de l'id utilisateur et de l'année).
V-C. Time-to-Live▲
Nous avons souvent tendance à implémenter des programmes standalone pour les traitements batch afin de supprimer les données obsolètes de notre base. Cassandra offre une alternative qui consiste à définir une valeur TTL (Time-to-live) au moment de la création de la colonne pour préciser la durée pendant laquelle celle-ci sera pérenne. Attention, une fois qu'une valeur TTL est définie, vous ne pouvez plus la modifier.
V-D. Nodetool▲
Cassandra dispose d'une interface de ligne de commande qui repose sur JMX pour vous aider à gérer vos clusters : Nodetool. Il est situé dans le répertoire bin de la distribution binaire Cassandra. Vous pouvez consulter les informations sur votre cluster, faire des snapshots ou afficher les statistiques sur les keyspaces et les columnFamilies.
VI. Conclusion et remerciements▲
Cet article montre essentiellement comment démarrer avec l'utilisation de Cassandra. Cependant, les exemples sont basiques et se basent sur une ColumnFamily de quatre colonnes, ce qui ne permet pas d'exploiter toutes les fonctionnalités disponibles (SuperColumnFamily, Dynamic columns, consistency level, etc.). Cassandra est un outil très puissant et finalement peu complexe grâce à son langage d'interrogation proche de SQL et ses API Clients de haut niveau comme Hector ou Astyanax (pour Java).
Cet article a été publié avec l'aimable autorisation de la société SoatSoat.
Nous tenons à remercier ClaudeLELOUP pour sa relecture orthographique attentive de cet article et Mickael Baron pour la mise au gabarit.