Coherence Part V : optimisations des requêtes avec des index
Dans l’article précédent, nous évoquions la possibilité de créer un index sur le cache afin d’accélérer les recherches en évitant de de-sérialiser l’ensemble des entrées du cache pour appliquer le filtre. Nous allons voir comment faire !
Anatomie d’un index
Un index Coherence est en réalité une map qui fait correspondre les éléments indexés aux entrées du cache qui les contiennent. Un parcours de l’index est beaucoup plus rapide que de parcourir le cache en de-sérialisant toutes les entrées. Dans le cas d’un cache distribué, l’index est répartit sur le cluster. Chaque nœud du cluster est responsable de l’indexation des données qu’il porte, le parcours de l’index est donc distribué.
Un index peut être ordonné ou non. Un index ordonné sera plus efficace pour faire des requêtes sur un intervalle ou ensemble de valeurs telles que “toutes les consultations vétérinaires entre deux dates” ou “tous les animaux dont le nom commence par ‘A'”, alors qu’un index non ordonné sera préférable pour des requêtes d’égalité. L’index non ordonné sera aussi plus rapide à mettre à jour puisque les modifications ne nécessitent pas de maintenir l’ordre.
Il existe deux types d’index, les index “reverse” et les index “forward”. Un index reverse va faire correspondre les valeurs de l’index aux clés des entrées qui portent la valeur. L’index forward fait correspondre des clés aux valeurs. Notez que dans ces phrases, valeur fait référence au champs qui est indexé, et pas à la value au sens de Map. L’index reverse permet de retrouver les entrées du cache pour lesquelles l’attribut indexé correspond au critère donné, c’est cet index qui va nous intéresser pour le moment. L’index forward permet de garder de-sérialisé l’ensemble des valeurs d’un attribut, nous l’utiliserons dans un prochain article.
Création et utilisation d’un index
Un index est très simple à créer, il suffit d’appeler la méthode addIndex() de QueryMap, qui est implémentée par les NamedCaches de Coherence. Cette méthode prend en paramètre un extracteur, un booléen indiquant si l’index doit être trié et un Comparator pour effectuer le tri. Un index peut être ajouté plusieurs fois sans problèmes, l’ajout un index avec le même extracteur, ordonné ou non et le même Comparator, qui existe déjà sera détecté et l’index ne sera créé qu’une seule fois, il n’y a pas d’inquiétudes à avoir là dessus. Tous les noeuds peuvent donc déclarer les index.
Dans le DataInitializer, nous allons créer une méthode pour ajouter les index et l’appeler dans la méthode contextInitialized() qui pour rappel est appelée une fois que le ServletContext a démarré.
private void initializeIndices() { ownerCache.addIndex(new PetIdsExtractor(), false, null); } public void contextInitialized(ServletContextEvent sce) { ... initializeIndices(); }
Pour utiliser un index, il faut faire une requête sur le cluster avec un filtre et ce filtre doit utiliser l’extracteur ayant servi à créer l’index. Pensez à implémenter equals() et hashCode() dans vos extracteurs.
@Override public int hashCode() { return 31; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() == obj.getClass()) return true; return false; }
La requête de loadPet(), qui utilise un PetIdsExtractor va automatiquement faire appel à l’index.
L’index est mis à jour de façon synchrone, chaque ajout, modification ou suppression des entrées du cache va donc déclencher une mise à jour des données concernées dans l’index avec le retour de la fonction. Cela permet de garantir la consistance de l’index mais si il y a de nombreux index à maintenir ou si les extracteurs sont coûteux, il peut y avoir une dégradation des performances. L’ajout d’un index sur un cache contenant déjà des données va déclencher leur indexation de façon synchrone là aussi, la méthode addIndex() termine après l’indexation complète du cache.
Un index partiel
Dans certains cas il est préférable de ne pas indexer l’ensemble des entrées d’un cache, par exemple dans le cas où un petit nombre d’entrées ont un attribut que nous souhaitons utiliser dans les requêtes tandis que pour les autres entrées cet attribut ne nous intéresse pas. On peut par exemple vouloir indexer uniquement les Owner qui ont une adresse, en évitant de maintenir un ensemble d’entrées associées à une adresse null. Pour cela, il est possible d’utiliser un ConditionalIndex, qui prend un filtre et un extracteur en paramètre. Les seules entrées indexées seront celles pour lesquelles le filtre a retourné true.
ValueExtractor extractor = new ReflectionExtractor("getAddress"); Filter filter = new NotEqualsFilter("getAddress", null); ConditionalExtractor conditionalExtractor = new ConditionalExtractor(filter, extractor, false); ownerCache.addIndex(conditionalExtractor, false, null);
Dans notre cas, l’extracteur utilisé pour le filtre et pour générer l’index sont les mêmes, mais on pourrait aussi utiliser des extracteurs différents.
Traps and pitfalls
L’index utilisé par Coherence étant de type HashSet, dans le cas de l’utilisation de plusieurs index dans une même requête, le résultat peut être peu efficace, voir contre productif. En effet, si la recherche se fait sur deux ensembles qui peuvent contenir beaucoup d’éléments, par exemple dans le cas de PetClinic, la recherche de tous les Owners qui ont une adresse et un numéro de téléphone sur deux index différents, la recherche va créer un set contenant tous les Owners ayant une adresse, un autre set contenant tous les Owners ayant un numéro de téléphone puis réaliser l’intersection en parcourant toutes les entrées de l’un des sets et en comparant chacune de ces entrées avec l’ensemble de l’autre set. Si la requête est lancé sans index, Coherence va simplement de-sérialiser chaque entrée du cache, appliquer le filtre sur les adresses, le cas échéant appliquer le filtre sur les numéros. Comme pour une base de données, il est donc important de mesurer l’efficacité de l’index.
De plus, Coherence ne fait pas d’optimisation globale de la requête. Pour les requêtes sur de multiples critères, l’ordre dans lequel les filtres sont appliqués est donc important. Le résultat du premier filtre est passé au suivant, mesurer l’impact du chan
gement d’ordre des filtres est donc une piste d’optimisation si vous avez des requêtes lentes.
Les sources sont téléchargeables : Sur GitHub
git clone git://github.com/obourgain/petclinic-coherence.git git checkout article5-end
En zip
Index des articles de la série Coherence :
- Introduction à Coherence: Part I
- Introduction à Coherence: Part II
- Coherence Part III : Filtres
- Coherence Part IV : extracteurs et recherches distribuées sur le cluster
- Coherence Part V : optimisations des requêtes avec des index
- Coherence Part VI : traitement de données distribuées, concurrence et in-place processing