Upload
planet-cassandra
View
119
Download
4
Embed Size (px)
Citation preview
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 © 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 © 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 © 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 © 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 ?
● 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 © 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 © 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 © 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 © 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