ippon: doing multi-criteria queries on a cassandra application (français)

49
Doing multi-criteria queries on a Cassandra application

Upload: planet-cassandra

Post on 30-Jul-2015

119 views

Category:

Technology


4 download

TRANSCRIPT

Doing multi-criteria queries on a Cassandra application

Ippon Technologies © 2015

Qui sommes-nous?

Jérôme Mainaud○ Architecte Java chez Ippon Technologies○ DataStax Solution Architect Certifié

Julien Dubois○ 15 ans d’expérience en Java○ Directeur de l’innovation chez Ippon Technologies

Ippon Technologies○ Expertise Java & Big Data: consulting, formation, hébergement○ 200 personnes○ Paris, Bordeaux, Nantes, Richmond (USA)

Ippon Technologies © 2015

Sommaire

1. Intro2. Stack technique3. Configuration du cluster4. Application d’exemple5. Recherche multi-critère6. Utilisation de CQL3 avec Java 87. Limitations8. Résultats

Ippon Technologies © 2014

Stack technique

Ippon Technologies © 2015

Stack technique

● JHipster○ Générateur d’applications Spring Boot + AngularJS○ Supporte JPA, MongoDB et… Cassandra!

● Nous a permis de générer l’application très rapidement○ Squelette d’application prêt en 5 minutes○ Ajout de tables avec leur mapping○ Configuration, build, gestion des logs, etc.○ Tests Gatling prêts à l’emploi

Plus d’informations sur http://jhipster.github.io/

Ippon Technologies © 2015

Stack technique

● Spring Boot○ Basé sur Spring○ Convention over configuration○ Nombreux “starters” prêts à l’emploi

● Services Web○ CXF vs Spring MVC REST

● Cassandra○ DataStax Enterprise

Ippon Technologies © 2015

Paramétrage du Driver

● Configuration Spring Boot○ Nous avons réalisé notre propre configuration du DataStax Java

Driver○ Intégration dans la configuration standard de Spring Boot, utilisant

un fichier YAML ● Intégrée dans Spring Boot 1.3

○ Ce code a été proposé à Pivotal, et intégré dans Spring Boot 1.3● Améliorée par PR

○ JHipster a été amélioré depuis, et propose toujours une configuration plus complète que celle de Spring Boot

Ippon Technologies © 2015

Développement des Repositories

● DataStax Java driver utilisé dans un Repository Spring@Repository

public class UserRepository {

@Inject private Session session;

private PreparedStatement findOneByEmailStmt;

@PostConstruct

public void init() {

findOneByEmailStmt = session.prepare(

"SELECT id FROM user_by_email WHERE email = :email");

}

public Optional<User> findOneByEmail(String email) { … }

Ippon Technologies © 2014

Configuration du cluster

Ippon Technologies © 2015

Hardware

● Hébergement chez Ippon Hosting● 8 noeuds équivalents

○ 16 Go de RAM○ Deux disques durs SSD de 256 Go en RAID 0

● 6 noeuds pour le cluster Cassandra, 2 noeuds pour l’application

Ippon Technologies © 2015

DataStax Enterprise

● Utilisation de DataStax Enterprise● OpsCenter nous a été d’une très grande aide

○ Monitoring○ Services automatisés○ Gestion du cluster

Ippon Technologies © 2014

Application exempleSystème de gestion de factures

Ippon Technologies © 2015

Modèle conceptuel

Ippon Technologies © 2015

Modèle physique

Ippon Technologies © 2015

create table invoice (

invoice_id timeuuid,

user_id uuid static,

firstname text static,

lastname text static,

invoice_date timestamp static,

payment_date timestamp static,

total_amount decimal static,

delivery_address text static,

delivery_city text static,

delivery_zipcode text static,

item_id timeuuid,

item_label text,

item_price decimal,

item_qty int,

item_total decimal,

primary key (invoice_id, item_id)

);

Table

Ippon Technologies © 2014

Recherche multi-critère

Ippon Technologies © 2015

Recherche multi-critères

Critères obligatoires○ User (implicite)○ Date de la facture (plage de dates)

Critères supplémentaires○ Nom du client○ Prénom du client○ Ville○ Code postal

Ippon Technologies © 2015

Utiliser Solr ?

Ippon Technologies © 2015

Utiliser Solr ?

● Intégré dans DataStax Enterprise● Mise à jour atomique et automatique● Recherche documentaire

Ippon Technologies © 2015

Utiliser Solr ?

On cherche sur des colonnes statiquesSolr ne les gère pas

On cherche des partitionsSolr retourne des lignes

Ippon Technologies © 2015

Utiliser Solr ?

On cherche sur des colonnes statiquesSolr ne les gère pas

On cherche des partitionsSolr retourne des lignes

Ippon Technologies © 2015

Index secondaires ?

● Ne répondent qu’aux cas de recherche sur un seul champ● Délicats à utiliser avec de bonnes performances

Ippon Technologies © 2015

Tables d’index

Utilisation de tables d’index○ Clé de partition : Les critères primaire et un critère secondaire

■ user_id■ date de facturation (tronqué à la date)■ le critère secondaire

○ Clustering columns : l’identifiant de la facture

Ippon Technologies © 2015

Recherche

Q1

Q2

A D J M

A C J L M

A J M

Fusion applicativeen mémoire

Recherches en parallèle

Ippon Technologies © 2015

Recherche

Une page de résultat (id)8f5b69ee-0ad0-11e5-a6c0-1697f925ec7b8f5b6d4a-0ad0-11e5-a6c0-1697f925ec7b8f5b6e9e-0ad0-11e5-a6c0-1697f925ec7bb3db1a30-0ad0-11e5-a6c0-1697f925ec7bb3db1c88-0ad0-11e5-a6c0-1697f925ec7bb3db202a-0ad0-11e5-a6c0-1697f925ec7bb3db219c-0ad0-11e5-a6c0-1697f925ec7bcac5be94-0ad0-11e5-a6c0-1697f925ec7bcac5c006-0ad0-11e5-a6c0-1697f925ec7bcac5c150-0ad0-11e5-a6c0-1697f925ec7b

N recherches unitaires en parallèle

Ippon Technologies © 2015

Recherche

Recherche sur une plage de dates○ boucle sur les jours en s’arrêtant

dès qu’on a une page de résultat

Ippon Technologies © 2015

Recherche

Nombre de requêtes○ Pour chaque jour dans la plage de dates

■ 1 requête par critère secondaire (partition by query)○ 1 requête par élément trouvé (partition by query)

Complexité de la recherche○ partitions by query

Exemple: 3 critères, 3 jours, 100 par pages○ nombre de requêtes ≤ 3 × 3 + 100 = 109

Ippon Technologies © 2014

JAVA

Ippon Technologies © 2015

Index — instances@Repository

public class InvoiceByLastNameRepository extends IndexRepository<String> {

public InvoiceByLastNameRepository() {

super("invoice_by_lastname", "lastname",

Invoice::getLastName, Criteria::getLastName);

}

}

@Repository

public class InvoiceByFirstNameRepository extends IndexRepository<String> {

public InvoiceByFirstNameRepository() {

super("invoice_by_firstname", "firstname",

Invoice::getFirstName, Criteria::getFirstName);

}

}

Ippon Technologies © 2015

Index — classe parentepublic class IndexRepository<T> {

@Inject

private Session session;

private final String tableName;

private final String valueName;

private final Function<Invoice, T> valueGetter;

private final Function<Criteria, T> criteriumGetter;

private PreparedStatement insertStmt;

private PreparedStatement findStmt;

private PreparedStatement findWithOffsetStmt;

@PostConstruct

public void init() { /* initialise les PreparedStatements */ }

Ippon Technologies © 2015

Index — Écriture @Override

public void insert(Invoice invoice) {

T value = valueGetter.apply(invoice);

if (value != null) {

session.execute(

insertStmt.bind(

invoice.getUserId(),

Dates.toDate(invoice.getInvoiceDay()),

value,

invoice.getId()));

}

}

Ippon Technologies © 2015

Index — Écriture

insertStmt = session.prepare(

QueryBuilder.insertInto(tableName)

.value("user_id", bindMarker())

.value("invoice_day", bindMarker())

.value(valueName, bindMarker())

.value("invoice_id", bindMarker())

);

public static Date toDate(LocalDate date) {

return date == null ? null :

Date.from(date.atStartOfDay().atZone(ZoneOffset.systemDefault()).toInstant());

}

Ippon Technologies © 2015

Index — Recherche@Override

public CompletableFuture<Iterator<UUID>> find(Criteria criteria, LocalDate day, UUID offset) {

T criterium = criteriumGetter.apply(criteria);

if (criterium == null) {

return CompletableFuture.completedFuture(null);

}

BoundStatement stmt;

if (invoiceIdOffset == null) {

stmt = findStmt.bind(criteria.getUserId(), Dates.toDate(day), criterium);

} else {

stmt = findWithOffsetStmt.bind(criteria.getUserId(), Dates.toDate(day), criterium, offset);

}

return Jdk8.completableFuture(session.executeAsync(stmt))

.thenApply(rs -> Iterators.transform(rs.iterator(), row -> row.getUUID(0)));

}

Ippon Technologies © 2015

Index — Recherche

findWithOffsetStmt = session.prepare(

QueryBuilder.select()

.column("invoice_id")

.from(tableName)

.where(eq("user_id", bindMarker()))

.and(eq("invoice_day", bindMarker()))

.and(eq(valueName, bindMarker()))

.and(lte("invoice_id", bindMarker()))

);

Ippon Technologies © 2015

Index — Recherche (Guava to Java 8)public static <T> CompletableFuture<T> completableFuture(ListenableFuture<T> guavaFuture) {

CompletableFuture<T> future = new CompletableFuture<>();

Futures.addCallback(guavaFuture, new FutureCallback<T>() {

@Override

public void onSuccess(V result) {

future.complete(result);

}

@Override

public void onFailure(Throwable t) {

future.completeExceptionally(t);

}

});

return future;

}

Ippon Technologies © 2014

JAVAService de recherche

Ippon Technologies © 2015

Service — Class@Service

public class InvoiceSearchService {

@Inject

private InvoiceRepository invoiceRepository;

@Inject

private InvoiceByDayRepository byDayRepository;

@Inject

private InvoiceByLastNameRepository byLastNameRepository;

@Inject

private InvoiceByFirstNameRepository byLastNameRepository;

@Inject

private InvoiceByCityRepository byCityRepository;

@Inject

private InvoiceByZipCodeRepository byZipCodeRepository;

Ippon Technologies © 2015

Service — recherchepublic ResultPage findByCriteria(Criteria criteria) {

return byDateInteval(criteria, (crit, day, offset) -> {

CompletableFuture<Iterator<UUID>> futureUuidIt;

if (crit.hasIndexedCriteria()) {

/*

* ... Recherche multi-critère à voir dans la prochaine diapo ...

*/

} else {

futureUuidIt = byDayRepository.find(crit.getUserId(), day, offset);

}

return futureUuidIt;

});

}

Ippon Technologies © 2015

Service — rechercheCompletableFuture<Iterator<UUID>>[] futures = Stream.<IndexRepository> of(

byLastNameRepository, byFirstNameRepository,

byCityRepository, byZipCodeRepository)

.map(repo -> repo.find(crit, day, offset))

.toArray(CompletableFuture[]::new);

futureUuidIt = CompletableFuture.allOf(futures).thenApply(v ->

Iterators.intersection(TimeUUIDComparator.desc,

Stream.of(futures)

.map(CompletableFuture::join)

.filter(Objects::nonNull)

.collect(Collectors.toList())));

Ippon Technologies © 2015

Service — Comparaison des UUIDs/**

* Comparateur de TimeUUID équivalent à celui de Cassandra:

* @see org.apache.cassandra.db.marshal.TimeUUIDType#compare()

*/

public enum TimeUUIDComparator implements Comparator<UUID> {

desc {

@Override

public int compare(UUID o1, UUID o2) {

long delta = o2.timestamp() - o1.timestamp();

if (delta != 0)

return Ints.saturatedCast(delta);

return o2.compareTo(o1);

}

};

}

Ippon Technologies © 2015

Service — Boucle sur les jours@FunctionalInterface

private static interface DayQuery {

CompletableFuture<Iterator<UUID>> find(Criteria criteria, LocalDate day, UUID invoiceIdOffset);

}

private ResultPage byDateInteval(Criteria criteria, DayQuery dayQuery) {

int limit = criteria.getLimit();

List<Invoice> resultList = new ArrayList<>(limit);

LocalDate dayOffset = criteria.getDayOffset();

UUID invoiceIdOffset = criteria.getInvoiceIdOffset();

/* ... Boucle sur les jours ; à voir dans la prochaine diapo ... */

return new ResultPage(resultList);

}

Ippon Technologies © 2015

Service — Boucle sur les jours LocalDate day = criteria.getLastDay();

do {

Iterator<UUID> uuidIt = dayQuery.find(criteria, day, invoiceIdOffset).join();

limit -= loadInvoices(resultList, uuidIt, criteria, limit);

if (uuidIt.hasNext()) {

return new ResultPage(resultList, day, uuidIt.next());

}

day = day.minusDays(1);

invoiceIdOffset = null;

} while (!day.isBefore(criteria.getFirstDay()));

Ippon Technologies © 2015

Service — chargement des facturesprivate int loadInvoices(List<Invoice> resultList, Iterator<UUID> uuidIt,

Criteria criteria, int limit) {

List<CompletableFuture<Invoice>> futureList = new ArrayList<>(limit);

for (int i = 0; i < limit && uuidIt.hasNext(); ++i) {

futureList.add(invoiceRepository.findOne(uuidIt.next()));

}

futureList.stream()

.map(CompletableFuture::join)

.forEach(resultList::add);

return futureList.size();

}

Ippon Technologies © 2014

Limitations

Ippon Technologies © 2015

Limitations

La recherche ne fonctionne que sur un texte précis○ Pas de recherche “plein texte”○ Comme dans une base de données classique

La pagination ne donne pas le nombre total de pages de résultats

Ce mécanisme ne peut fonctionner que s’il existe des critères obligatoires fortement discriminants (ici: user_id et invoice_day)

Ippon Technologies © 2014

Résultats

Ippon Technologies © 2015

Résultats métier

● Gestion d’un an de données, sans limite○ Nous comptons monter à 3 ans○ Ancien système: limité à 3 mois

● Obtention des résultats en “temps réel”○ Les données sont immédiatement disponibles○ Ancien système: 24h de retard

● Coûts nettement plus bas

Ippon Technologies © 2015

Résultats techniques

● Les tests Gatling ont montré que nous pouvions tenir 5000 utilisateurs concurrents○ Sur des requêtes complexes, avec multiples critères et pagination

● Nous avons aussi démontré que le cluster était scalable linéairement○ En termes de volumétrie: ajout de nouveaux disques (passage en

JBOD)○ En termes de performance: ajout de nouveaux noeuds à chaud

Ippon Technologies © 2014

Merci à tous!